Merge branch 'devel' into pocket-alpha-dev

This commit is contained in:
Mikayla Fischler 2025-05-10 17:51:17 -04:00
commit ce92fd15ef
27 changed files with 890 additions and 539 deletions

View File

@ -240,6 +240,7 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"} TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"} TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}

View File

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

View File

@ -62,6 +62,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."} TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."}
TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"} TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg} TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}

View File

@ -314,7 +314,7 @@ function iorx.record_unit_data(data)
local function blue(text) return { text = text, color = colors.blue } end local function blue(text) return { text = text, color = colors.blue } end
-- if unit.reactor_data.rps_status then -- if unit.reactor_data.rps_status then
-- for k, v in pairs(unit.alarms) do -- for k, _ in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED -- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end -- end
-- end -- end

View File

@ -269,8 +269,8 @@ function pocket.init_nav(smem)
-- open an app -- open an app
---@param app_id POCKET_APP_ID ---@param app_id POCKET_APP_ID
---@param on_loaded? function ---@param on_ready? function
function nav.open_app(app_id, on_loaded) function nav.open_app(app_id, on_ready)
-- reset help return on navigating out of an app -- reset help return on navigating out of an app
if app_id == APP_ID.ROOT then self.help_return = nil end if app_id == APP_ID.ROOT then self.help_return = nil end
@ -283,7 +283,7 @@ function pocket.init_nav(smem)
app = self.apps[app_id] app = self.apps[app_id]
else self.loader_return = nil end else self.loader_return = nil end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_loaded }) end if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_ready }) end
self.cur_app = app_id self.cur_app = app_id
self.pane.set_value(app_id) self.pane.set_value(app_id)
@ -291,6 +291,8 @@ function pocket.init_nav(smem)
if #app.sidebar_items > 0 then if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items) self.sidebar.update(app.sidebar_items)
end end
if app.loaded and on_ready then on_ready() end
else else
log.debug("tried to open unknown app") log.debug("tried to open unknown app")
end end

View File

@ -22,7 +22,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local threads = require("pocket.threads") local threads = require("pocket.threads")
local POCKET_VERSION = "v0.13.2-beta" local POCKET_VERSION = "v0.13.4-beta"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -136,23 +136,46 @@ local function self_check()
self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones") self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones")
-- check redstone configurations -- check redstone configurations
local ifaces = {}
local bundled_sides = {} local phys = {} ---@type rtu_rs_definition[][]
local inputs = { [0] = {}, {}, {}, {}, {} }
for i = 1, #cfg.Redstone do for i = 1, #cfg.Redstone do
local entry = cfg.Redstone[i] local entry = cfg.Redstone[i]
local name = entry.relay or "local"
if phys[name] == nil then phys[name] = {} end
table.insert(phys[entry.relay or "local"], entry)
end
for name, entries in pairs(phys) do
TextBox{parent=self.sc_log,text="> checking redstone @ "..name.."...",fg_bg=cpair(colors.blue,colors.white)}
local ifaces = {}
local bundled_sides = {}
for i = 1, #entries do
local entry = entries[i]
local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "") local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "")
local dupe = util.table_contains(ifaces, ident)
local sc_dupe = util.table_contains(ifaces, ident)
local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil)) local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil))
local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is") local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is")
self.self_check_msg("> check redstone " .. ident .. " unique...", not dupe, "only one port should be set to a side/color combination") self.self_check_msg("> check redstone " .. ident .. " unique...", not sc_dupe, "only one port should be set to a side/color combination")
self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work") self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work")
self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry") self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry")
if rsio.get_io_dir(entry.port) == rsio.IO_DIR.IN then
local in_dupe = util.table_contains(inputs[entry.unit or 0], entry.port)
self.self_check_msg("> check redstone " .. ident .. " input...", not in_dupe, "you cannot have multiple of the same input for a given unit or the facility ("..rsio.to_string(entry.port)..")")
end
bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil
table.insert(ifaces, ident) table.insert(ifaces, ident)
end end
end
-- check peripheral configurations -- check peripheral configurations
for i = 1, #cfg.Peripherals do for i = 1, #cfg.Peripherals do
@ -245,7 +268,7 @@ function check.create(main_pane, settings_cfg, check_sys, style)
TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg} TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg}
self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=500,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local last_check = { nil, nil } local last_check = { nil, nil }

View File

@ -1,4 +1,5 @@
local constants = require("scada-common.constants") local constants = require("scada-common.constants")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -18,6 +19,7 @@ local NumberField = require("graphics.elements.form.NumberField")
---@class rtu_rs_definition ---@class rtu_rs_definition
---@field unit integer|nil ---@field unit integer|nil
---@field port IO_PORT ---@field port IO_PORT
---@field relay string|nil
---@field side side ---@field side side
---@field color color|nil ---@field color color|nil
---@field invert true|nil ---@field invert true|nil
@ -33,6 +35,7 @@ local IO_MODE = rsio.IO_MODE
local LEFT = core.ALIGN.LEFT local LEFT = core.ALIGN.LEFT
local self = { local self = {
rs_cfg_phy = false, ---@type string|nil|false
rs_cfg_port = 1, ---@type IO_PORT rs_cfg_port = 1, ---@type IO_PORT
rs_cfg_editing = false, ---@type integer|false rs_cfg_editing = false, ---@type integer|false
@ -108,6 +111,23 @@ local function color_to_idx(color)
end end
end end
-- select the subset of redstone entries assigned to the given phy
---@param cfg rtu_rs_definition[] the full redstone entry list
---@param phy string|nil which phy to get redstone entries for
---@param invert boolean? true to get all except this phy
---@return rtu_rs_definition[]
local function redstone_subset(cfg, phy, invert)
local subset = {}
for i = 1, #cfg do
if ((not invert) and cfg[i].relay == phy) or (invert and cfg[i].relay ~= phy) then
table.insert(subset, cfg[i])
end
end
return subset
end
local redstone = {} local redstone = {}
-- validate a redstone entry -- validate a redstone entry
@ -145,13 +165,81 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_9 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_10 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8}} local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8,rs_c_9,rs_c_10}}
TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} local header = TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} --#region Interface Selection
local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
TextBox{parent=rs_c_1,x=1,y=1,text="Configure this computer or a redstone relay."}
local iface_list = ListBox{parent=rs_c_1,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
-- update relay interface list
function tool_ctl.update_relay_list()
local mounts = ppm.list_mounts()
iface_list.remove_all()
-- assemble list of configured relays
local relays = {}
for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i]
if def.relay and not util.table_contains(relays, def.relay) then
table.insert(relays, def.relay)
end
end
-- add unconfigured connected relays
for name, entry in pairs(mounts) do
if entry.type == "redstone_relay" and not util.table_contains(relays, name) then
table.insert(relays, name)
end
end
local function config_rs(name)
header.set_value(" Redstone Connections (" .. name .. ")")
self.rs_cfg_phy = tri(name == "local", nil, name)
tool_ctl.gen_rs_summary()
rs_pane.set_value(2)
end
local line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ local",fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="This Computer",fg_bg=cpair(colors.gray,colors.white)}
local count = #redstone_subset(ini_cfg.Redstone, nil)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs("local")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
for i = 1, #relays do
local name = relays[i]
line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ "..name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="Redstone Relay",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=line,x=18,y=2,text=tri(mounts[name],"ONLINE","OFFLINE"),fg_bg=cpair(tri(mounts[name],colors.green,colors.red),colors.white)}
count = #redstone_subset(ini_cfg.Redstone, name)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs(name)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end
end
tool_ctl.update_relay_list()
PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_1,x=27,y=14,min_width=23,text="I don't see my relay!",callback=function()rs_pane.set_value(10)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Configuration List
TextBox{parent=rs_c_2,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
local rs_list = ListBox{parent=rs_c_2,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function rs_revert() local function rs_revert()
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
@ -159,40 +247,47 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
end end
local function rs_apply() local function rs_apply()
settings.set("Redstone", tmp_cfg.Redstone) -- add the changed data to the existing saved data
local new_data = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local new_save = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy, true)
for i = 1, #new_data do table.insert(new_save, new_data[i]) end
settings.set("Redstone", new_save)
if settings.save("/rtu.settings") then if settings.save("/rtu.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)
load_settings(ini_cfg) load_settings(ini_cfg)
rs_pane.set_value(4) rs_pane.set_value(5)
-- for return to list from saved screen -- for return to list from saved screen
-- this will delete unsaved changes for other phy's, which is acceptable
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
tool_ctl.gen_rs_summary() tool_ctl.gen_rs_summary()
tool_ctl.update_relay_list()
else else
rs_pane.set_value(5) rs_pane.set_value(6)
end end
end end
PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} local function rs_back()
local rs_revert_btn = PushButton{parent=rs_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} self.rs_cfg_phy = false
PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} rs_pane.set_value(1)
local rs_apply_btn = PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} header.set_value(" Redstone Connections")
end
TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."} PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=rs_back,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local rs_revert_btn = PushButton{parent=rs_c_2,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_2,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local rs_apply_btn = PushButton{parent=rs_c_2,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} --#endregion
--#region Port Selection
TextBox{parent=rs_c_3,x=1,y=1,text="Select one of the below ports to use."}
local rs_ports = ListBox{parent=rs_c_3,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function new_rs(port) local function new_rs(port)
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port then
rs_pane.set_value(6)
return
end
end
end
self.rs_cfg_editing = false self.rs_cfg_editing = false
local text local text
@ -249,7 +344,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_selection.set_value(text) self.rs_cfg_selection.set_value(text)
self.rs_cfg_port = port self.rs_cfg_port = port
rs_pane.set_value(3) rs_pane.set_value(4)
end end
-- add entries to redstone option list -- add entries to redstone option list
@ -270,38 +365,43 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
end end
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""} --#endregion
--#region Port Configuration
PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} self.rs_cfg_selection = TextBox{parent=rs_c_4,x=1,y=1,height=2,text=""}
self.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"} PushButton{parent=rs_c_4,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
self.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"} self.rs_cfg_side_l = TextBox{parent=rs_c_4,x=1,y=4,width=11,text="Output Side"}
self.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} local side = Radio2D{parent=rs_c_4,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
self.rs_cfg_unit_l = TextBox{parent=rs_c_4,x=25,y=7,width=7,text="Unit ID"}
self.rs_cfg_unit = NumberField{parent=rs_c_4,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local function set_bundled(bundled) local function set_bundled(bundled)
if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end
end end
self.rs_cfg_shortcut = TextBox{parent=rs_c_3,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."} self.rs_cfg_shortcut = TextBox{parent=rs_c_4,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
self.rs_cfg_shortcut.hide(true) self.rs_cfg_shortcut.hide(true)
self.rs_cfg_bundled = Checkbox{parent=rs_c_3,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg} self.rs_cfg_bundled = Checkbox{parent=rs_c_4,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} self.rs_cfg_color = Radio2D{parent=rs_c_4,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color.disable() self.rs_cfg_color.disable()
local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local rs_err = TextBox{parent=rs_c_4,x=8,y=14,width=30,text="Unit ID invalid.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
rs_err.hide(true) rs_err.hide(true)
local function back_from_rs_opts() local function back_from_rs_opts()
rs_err.hide(true) rs_err.hide(true)
if self.rs_cfg_editing ~= false then rs_pane.set_value(1) else rs_pane.set_value(2) end if self.rs_cfg_editing ~= false then rs_pane.set_value(2) else rs_pane.set_value(3) end
end end
local function save_rs_entry() local function save_rs_entry()
assert(self.rs_cfg_phy ~= false, "tried to save a redstone entry without a phy")
local port = self.rs_cfg_port local port = self.rs_cfg_port
local u = tonumber(self.rs_cfg_unit.get_value()) local u = tonumber(self.rs_cfg_unit.get_value())
@ -313,12 +413,23 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
local def = { local def = {
unit = tri(PORT_DSGN[port] == 1, u, nil), unit = tri(PORT_DSGN[port] == 1, u, nil),
port = port, port = port,
relay = self.rs_cfg_phy,
side = side_options_map[side.get_value()], side = side_options_map[side.get_value()],
color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil), color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil),
invert = self.rs_cfg_inverted.get_value() or nil invert = self.rs_cfg_inverted.get_value() or nil
} }
if self.rs_cfg_editing == false then if self.rs_cfg_editing == false then
-- check for duplicate inputs for this unit/facility
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port and tmp_cfg.Redstone[i].unit == def.unit then
rs_pane.set_value(7)
return
end
end
end
table.insert(tmp_cfg.Redstone, def) table.insert(tmp_cfg.Redstone, def)
else else
def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port
@ -331,13 +442,14 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
table.insert(tmp_cfg.Redstone, { table.insert(tmp_cfg.Redstone, {
unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
port = IO.WASTE_PU + i, port = IO.WASTE_PU + i,
relay = self.rs_cfg_phy,
side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil) color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil)
}) })
end end
end end
rs_pane.set_value(1) rs_pane.set_value(2)
tool_ctl.gen_rs_summary() tool_ctl.gen_rs_summary()
side.set_value(1) side.set_value(1)
@ -349,30 +461,35 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
else rs_err.show() end else rs_err.show() end
end end
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_advanced = PushButton{parent=rs_c_3,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(8)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} self.rs_cfg_advanced = PushButton{parent=rs_c_4,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(9)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"} --#endregion
PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_5,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} TextBox{parent=rs_c_5,x=1,y=1,text="Settings saved!"}
PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} TextBox{parent=rs_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"} TextBox{parent=rs_c_7,x=1,y=1,height=6,text="You already configured this input for this facility/unit assignment. There can only be one entry for each input per each unit or the facility (for facility inputs).\n\nPlease select a different port."}
TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_8,x=1,y=1,height=5,text="Advanced Options"} TextBox{parent=rs_c_8,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
self.rs_cfg_inverted = Checkbox{parent=rs_c_8,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg} TextBox{parent=rs_c_8,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_8,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=rs_c_8,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_9,x=1,y=1,height=5,text="Advanced Options"}
self.rs_cfg_inverted = Checkbox{parent=rs_c_9,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg}
TextBox{parent=rs_c_9,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)}
PushButton{parent=rs_c_9,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_10,x=1,y=1,height=10,text="Make sure your relay is either touching the RTU gateway or connected via wired modems. There should be a wired modem on a side of the RTU gateway then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
PushButton{parent=rs_c_10,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
@ -422,7 +539,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
side.set_value(side_to_idx(def.side)) side.set_value(side_to_idx(def.side))
self.rs_cfg_color.set_value(value) self.rs_cfg_color.set_value(value)
self.rs_cfg_inverted.set_value(def.invert or false) self.rs_cfg_inverted.set_value(def.invert or false)
rs_pane.set_value(3) rs_pane.set_value(4)
end end
local function delete_rs_entry(idx) local function delete_rs_entry(idx)
@ -432,13 +549,19 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
-- generate the redstone summary list -- generate the redstone summary list
function tool_ctl.gen_rs_summary() function tool_ctl.gen_rs_summary()
assert(self.rs_cfg_phy ~= false, "tried to generate a summary without a phy set")
rs_list.remove_all() rs_list.remove_all()
local modified = #ini_cfg.Redstone ~= #tmp_cfg.Redstone local ini = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy)
local tmp = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local modified = #ini ~= #tmp
for i = 1, #tmp_cfg.Redstone do for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i] local def = tmp_cfg.Redstone[i]
if def.relay == self.rs_cfg_phy then
local name = rsio.to_string(def.port) local name = rsio.to_string(def.port)
local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple) local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple)
@ -459,7 +582,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
local a = ini_cfg.Redstone[i] local a = ini_cfg.Redstone[i]
local b = tmp_cfg.Redstone[i] local b = tmp_cfg.Redstone[i]
modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert) modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.relay ~= b.relay) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert)
end
end end
end end

View File

@ -36,7 +36,8 @@ local changes = {
{ "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } }, { "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } }, { "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } }, { "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } },
{ "v1.11.8", { "Added advanced option to invert digital redstone signals" } } { "v1.11.8", { "Added advanced option to invert digital redstone signals" } },
{ "v1.12.0", { "Added support for redstone relays" } }
} }
---@class rtu_configurator ---@class rtu_configurator
@ -76,6 +77,7 @@ local tool_ctl = {
gen_summary = nil, ---@type function gen_summary = nil, ---@type function
load_legacy = nil, ---@type function load_legacy = nil, ---@type function
update_peri_list = nil, ---@type function update_peri_list = nil, ---@type function
update_relay_list = nil, ---@type function
gen_peri_summary = nil, ---@type function gen_peri_summary = nil, ---@type function
gen_rs_summary = nil, ---@type function gen_rs_summary = nil, ---@type function
} }
@ -128,7 +130,7 @@ end
---@param data rtu_rs_definition[] ---@param data rtu_rs_definition[]
function tool_ctl.deep_copy_rs(data) function tool_ctl.deep_copy_rs(data)
local array = {} local array = {}
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color, invert = d.invert }) end for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, relay = d.relay, side = d.side, color = d.color, invert = d.invert }) end
return array return array
end end
@ -208,7 +210,6 @@ local function config_view(display)
end end
local function show_rs_conns() local function show_rs_conns()
tool_ctl.gen_rs_summary()
main_pane.set_value(9) main_pane.set_value(9)
end end
@ -348,10 +349,12 @@ function configurator.configure(ask_config)
---@diagnostic disable-next-line: discard-returns ---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1) ppm.handle_unmount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
elseif event == "peripheral" then elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns ---@diagnostic disable-next-line: discard-returns
ppm.mount(param1) ppm.mount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
end end
if event == "terminate" then return end if event == "terminate" then return end

View File

@ -11,10 +11,14 @@ local digital_write = rsio.digital_write
-- create new redstone device -- create new redstone device
---@nodiscard ---@nodiscard
---@param relay? table optional redstone relay to use instead of the computer's redstone interface
---@return rtu_rs_device interface, boolean faulted ---@return rtu_rs_device interface, boolean faulted
function redstone_rtu.new() function redstone_rtu.new(relay)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- physical interface to use
local phy = relay or rs
-- get RTU interface -- get RTU interface
local interface = unit.interface() local interface = unit.interface()
@ -30,104 +34,114 @@ function redstone_rtu.new()
write_holding_reg = interface.write_holding_reg write_holding_reg = interface.write_holding_reg
} }
-- change the phy in use (a relay or rs)
---@param new_phy table
function public.remount_phy(new_phy) phy = new_phy end
-- NOTE: for runtime speed, inversion logic results in extra code here but less code when functions are called
-- link digital input -- link digital input
---@param side string ---@param side string
---@param color integer ---@param color integer
---@param invert boolean|nil ---@param invert boolean|nil
---@return integer count count of digital inputs
function public.link_di(side, color, invert) function public.link_di(side, color, invert)
local f_read ---@type function local f_read ---@type function
if color then if color then
if invert then if invert then
f_read = function () return digital_read(not rs.testBundledInput(side, color)) end f_read = function () return digital_read(not phy.testBundledInput(side, color)) end
else else
f_read = function () return digital_read(rs.testBundledInput(side, color)) end f_read = function () return digital_read(phy.testBundledInput(side, color)) end
end end
else else
if invert then if invert then
f_read = function () return digital_read(not rs.getInput(side)) end f_read = function () return digital_read(not phy.getInput(side)) end
else else
f_read = function () return digital_read(rs.getInput(side)) end f_read = function () return digital_read(phy.getInput(side)) end
end end
end end
unit.connect_di(f_read) return unit.connect_di(f_read)
end end
-- link digital output -- link digital output
---@param side string ---@param side string
---@param color integer ---@param color integer
---@param invert boolean|nil ---@param invert boolean|nil
---@return integer count count of digital outputs
function public.link_do(side, color, invert) function public.link_do(side, color, invert)
local f_read ---@type function local f_read ---@type function
local f_write ---@type function local f_write ---@type function
if color then if color then
if invert then if invert then
f_read = function () return digital_read(not colors.test(rs.getBundledOutput(side), color)) end f_read = function () return digital_read(not colors.test(phy.getBundledOutput(side), color)) end
f_write = function (level) f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = rs.getBundledOutput(side) local output = phy.getBundledOutput(side)
-- inverted conditions -- inverted conditions
if digital_write(level) then if digital_write(level) then
output = colors.subtract(output, color) output = colors.subtract(output, color)
else output = colors.combine(output, color) end else output = colors.combine(output, color) end
rs.setBundledOutput(side, output) phy.setBundledOutput(side, output)
end end
end end
else else
f_read = function () return digital_read(colors.test(rs.getBundledOutput(side), color)) end f_read = function () return digital_read(colors.test(phy.getBundledOutput(side), color)) end
f_write = function (level) f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = rs.getBundledOutput(side) local output = phy.getBundledOutput(side)
if digital_write(level) then if digital_write(level) then
output = colors.combine(output, color) output = colors.combine(output, color)
else output = colors.subtract(output, color) end else output = colors.subtract(output, color) end
rs.setBundledOutput(side, output) phy.setBundledOutput(side, output)
end end
end end
end end
else else
if invert then if invert then
f_read = function () return digital_read(not rs.getOutput(side)) end f_read = function () return digital_read(not phy.getOutput(side)) end
f_write = function (level) f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
rs.setOutput(side, not digital_write(level)) phy.setOutput(side, not digital_write(level))
end end
end end
else else
f_read = function () return digital_read(rs.getOutput(side)) end f_read = function () return digital_read(phy.getOutput(side)) end
f_write = function (level) f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
rs.setOutput(side, digital_write(level)) phy.setOutput(side, digital_write(level))
end end
end end
end end
end end
unit.connect_coil(f_read, f_write) return unit.connect_coil(f_read, f_write)
end end
-- link analog input -- link analog input
---@param side string ---@param side string
---@return integer count count of analog inputs
function public.link_ai(side) function public.link_ai(side)
unit.connect_input_reg(function () return rs.getAnalogInput(side) end) return unit.connect_input_reg(function () return phy.getAnalogInput(side) end)
end end
-- link analog output -- link analog output
---@param side string ---@param side string
---@return integer count count of analog outputs
function public.link_ao(side) function public.link_ao(side)
unit.connect_holding_reg( return unit.connect_holding_reg(
function () return rs.getAnalogOutput(side) end, function () return phy.getAnalogOutput(side) end,
function (value) rs.setAnalogOutput(side, value) end function (value) phy.setAnalogOutput(side, value) end
) )
end end

View File

@ -399,43 +399,41 @@ function modbus.new(rtu_dev, use_parallel_read)
return public return public
end end
-- create an error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@param code MODBUS_EXCODE exception code
---@return modbus_packet reply
local function excode_reply(packet, code)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
reply.make(packet.txn_id, packet.unit_id, fcode, { code })
return reply
end
-- return a SERVER_DEVICE_FAIL error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_fail(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_FAIL) end
-- return a SERVER_DEVICE_BUSY error reply -- return a SERVER_DEVICE_BUSY error reply
---@nodiscard ---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet) function modbus.reply__srv_device_busy(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_BUSY) end
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a NEG_ACKNOWLEDGE error reply -- return a NEG_ACKNOWLEDGE error reply
---@nodiscard ---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__neg_ack(packet) function modbus.reply__neg_ack(packet) return excode_reply(packet, MODBUS_EXCODE.NEG_ACKNOWLEDGE) end
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a GATEWAY_PATH_UNAVAILABLE error reply -- return a GATEWAY_PATH_UNAVAILABLE error reply
---@nodiscard ---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet) function modbus.reply__gw_unavailable(packet) return excode_reply(packet, MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE) end
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
return modbus return modbus

View File

@ -20,6 +20,7 @@ local LEDPair = require("graphics.elements.indicators.LEDPair")
local RGBLED = require("graphics.elements.indicators.RGBLED") local RGBLED = require("graphics.elements.indicators.RGBLED")
local LINK_STATE = types.PANEL_LINK_STATE local LINK_STATE = types.PANEL_LINK_STATE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
@ -129,31 +130,46 @@ local function init(panel, units)
-- show routine statuses -- show routine statuses
for i = 1, list_length do for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)} TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn} local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=util.trinary(units[i].type~=RTU_UNIT_TYPE.REDSTONE,ind_grn,cpair(style.ind_bkg,style.ind_bkg))}
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update) rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end end
local unit_hw_statuses = Div{parent=panel,height=term_h-3,x=25,y=3} local unit_hw_statuses = Div{parent=panel,height=term_h-3,x=25,y=3}
local relay_counter = 0
-- show hardware statuses -- show hardware statuses
for i = 1, list_length do for i = 1, list_length do
local unit = units[i] local unit = units[i]
local is_rs = unit.type == RTU_UNIT_TYPE.REDSTONE
-- hardware status -- hardware status
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}} local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update) unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + index) -- unit name identifier (type + index)
local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end local function get_name()
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15} if is_rs then
local is_local = unit.name == "redstone_local"
relay_counter = relay_counter + util.trinary(is_local, 0, 1)
return util.c("REDSTONE", util.trinary(is_local, "", " RELAY " .. relay_counter))
else
return util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", util.trinary(util.is_int(unit.index), unit.index, ""))
end
end
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end) local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(),width=util.trinary(is_rs,24,15)}
name_box.register(databus.ps, "unit_type_" .. i, function () name_box.set_value(get_name()) end)
-- assignment (unit # or facility) -- assignment (unit # or facility)
if unit.reactor then
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg} TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg}
end end
end
end end
return init return init

View File

@ -338,13 +338,7 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[i] local unit = units[i]
if unit.type ~= nil then if unit.type ~= nil then
local advert = { unit.type, unit.index, unit.reactor } insert(advertisement, { unit.type, unit.index, unit.reactor or -1, unit.rs_conns })
if unit.type == RTU_UNIT_TYPE.REDSTONE then
insert(advert, unit.device)
end
insert(advertisement, advert)
end end
end end
@ -477,9 +471,10 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[packet.unit_id] local unit = units[packet.unit_id]
local unit_dbg_tag = " (unit " .. packet.unit_id .. ")" local unit_dbg_tag = " (unit " .. packet.unit_id .. ")"
if unit.name == "redstone_io" then if unit.type == RTU_UNIT_TYPE.REDSTONE then
-- immediately execute redstone RTU requests -- immediately execute redstone RTU requests
return_code, reply = unit.modbus_io.handle_packet(packet) return_code, reply = unit.modbus_io.handle_packet(packet)
if not return_code then if not return_code then
log.warning("requested MODBUS operation failed" .. unit_dbg_tag) log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end end
@ -496,7 +491,7 @@ function rtu.comms(version, nic, conn_watchdog)
unit.pkt_queue.push_packet(packet) unit.pkt_queue.push_packet(packet)
end end
else else
log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag) log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end end
end end
else else

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.11.8" local RTU_VERSION = "v1.12.1"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE local RTU_HW_STATE = databus.RTU_HW_STATE
@ -140,32 +140,36 @@ local function main()
local rtu_redstone = config.Redstone local rtu_redstone = config.Redstone
local rtu_devices = config.Peripherals local rtu_devices = config.Peripherals
-- get a string representation of a port interface
---@param entry rtu_rs_definition
---@return string
local function entry_iface_name(entry)
return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
end
-- configure RTU gateway based on settings file definitions -- configure RTU gateway based on settings file definitions
local function sys_config() local function sys_config()
-- redstone interfaces --#region Redstone Interfaces
local rs_rtus = {} ---@type { rtu: rtu_rs_device, capabilities: IO_PORT[] }[]
local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[]
local all_conns = { [0] = {}, {}, {}, {}, {} }
-- go through redstone definitions list -- go through redstone definitions list
for entry_idx = 1, #rtu_redstone do for entry_idx = 1, #rtu_redstone do
local entry = rtu_redstone[entry_idx] local entry = rtu_redstone[entry_idx]
local assignment local assignment
local for_reactor = entry.unit local for_reactor = entry.unit
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) local phy = entry.relay or 0
local phy_name = entry.relay or "local"
local iface_name = entry_iface_name(entry)
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
---@cast for_reactor integer ---@cast for_reactor integer
assignment = "reactor unit " .. entry.unit assignment = "reactor unit " .. entry.unit
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
elseif entry.unit == nil then elseif entry.unit == nil then
assignment = "facility" assignment = "facility"
for_reactor = 0 for_reactor = 0
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> allocated redstone RTU for the facility"))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
else else
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx) local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
println(message) println(message)
@ -173,14 +177,44 @@ local function main()
return false return false
end end
-- create the appropriate RTU if it doesn't exist and check relay name validity
if entry.relay then
if type(entry.relay) ~= "string" then
local message = util.c("sys_config> invalid redstone relay '", entry.relay, '"')
println(message)
log.fatal(message)
return false
elseif not rs_rtus[entry.relay] then
log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay))
local hw_state = RTU_HW_STATE.OK
local relay = ppm.get_periph(entry.relay)
if not relay then
hw_state = RTU_HW_STATE.OFFLINE
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not connected"))
local _, v_device = ppm.mount_virtual()
relay = v_device
elseif ppm.get_type(entry.relay) ~= "redstone_relay" then
hw_state = RTU_HW_STATE.FAULTED
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay"))
end
rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } }
end
elseif rs_rtus[0] == nil then
log.debug(util.c("sys_config> allocated local redstone RTU"))
rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } }
end
-- verify configuration -- verify configuration
local valid = false local valid = false
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color)) valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
end end
local rs_rtu = rs_rtus[for_reactor].rtu local bank = rs_rtus[phy].banks[for_reactor]
local capabilities = rs_rtus[for_reactor].capabilities local conns = all_conns[for_reactor]
if not valid then if not valid then
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx) local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
@ -192,73 +226,105 @@ local function main()
local mode = rsio.get_io_mode(entry.port) local mode = rsio.get_io_mode(entry.port)
if mode == rsio.IO_MODE.DIGITAL_IN then if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs -- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name) local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message) println(message)
log.warning(message) log.warning(message)
else else
rs_rtu.link_di(entry.side, entry.color, entry.invert) table.insert(bank, entry)
end end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(entry.side, entry.color, entry.invert)
elseif mode == rsio.IO_MODE.ANALOG_IN then elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs -- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name) local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message) println(message)
log.warning(message) log.warning(message)
else else
rs_rtu.link_ai(entry.side) table.insert(bank, entry)
end end
elseif mode == rsio.IO_MODE.ANALOG_OUT then elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then
rs_rtu.link_ao(entry.side) table.insert(bank, entry)
else else
-- should be unreachable code, we already validated ports -- should be unreachable code, we already validated ports
log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true) log.fatal("sys_config> failed to identify IO mode at block index #" .. entry_idx)
println("sys_config> encountered a software error, check logs") println("sys_config> encountered a software error, check logs")
return false return false
end end
table.insert(capabilities, entry.port) table.insert(conns, entry.port)
log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment)) log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
end end
end end
-- create unit entries for redstone RTUs -- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do for _, def in pairs(rs_rtus) do
---@class rtu_registry_entry local rtu_conns = { [0] = {}, {}, {}, {}, {} }
-- connect the IO banks
for for_reactor = 0, #def.banks do
local bank = def.banks[for_reactor]
local conns = rtu_conns[for_reactor]
local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility")
-- link redstone to the RTU
for i = 1, #bank do
local conn = bank[i]
local phy_name = conn.relay or "local"
local mode = rsio.get_io_mode(conn.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
def.rtu.link_di(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
def.rtu.link_do(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.ANALOG_IN then
def.rtu.link_ai(conn.side)
elseif mode == rsio.IO_MODE.ANALOG_OUT then
def.rtu.link_ao(conn.side)
else
log.fatal(util.c("sys_config> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
println("sys_config> encountered a software error, check logs")
return false
end
table.insert(conns, conn.port)
log.debug(util.c("sys_config> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign))
end
end
---@type rtu_registry_entry
local unit = { local unit = {
uid = 0, ---@type integer uid = 0,
name = "redstone_io", ---@type string name = def.name,
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE type = RTU_UNIT_TYPE.REDSTONE,
index = false, ---@type integer|false index = false,
reactor = for_reactor, ---@type integer reactor = nil,
device = def.capabilities, ---@type IO_PORT[] use device field for redstone ports device = def.phy,
is_multiblock = false, ---@type boolean rs_conns = rtu_conns,
formed = nil, ---@type boolean|nil is_multiblock = false,
hw_state = RTU_HW_STATE.OK, ---@type RTU_HW_STATE formed = nil,
rtu = def.rtu, ---@type rtu_device|rtu_rs_device hw_state = def.hw_state,
rtu = def.rtu,
modbus_io = modbus.new(def.rtu, false), modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil pkt_queue = nil,
thread = nil ---@type parallel_thread|nil thread = nil
} }
table.insert(units, unit) table.insert(units, unit)
local for_message = "facility" local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
if util.is_int(for_reactor) then
for_message = util.c("reactor unit ", for_reactor)
end
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message)) log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
unit.uid = #units unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state) databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end end
-- mounted peripherals --#endregion
--#region Mounted Peripherals
for i = 1, #rtu_devices do for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name local name = entry.name
@ -439,19 +505,20 @@ local function main()
---@class rtu_registry_entry ---@class rtu_registry_entry
local rtu_unit = { local rtu_unit = {
uid = 0, ---@type integer uid = 0, ---@type integer RTU unit ID
name = name, ---@type string name = name, ---@type string unit name
type = rtu_type, ---@type RTU_UNIT_TYPE type = rtu_type, ---@type RTU_UNIT_TYPE unit type
index = index or false, ---@type integer|false index = index or false, ---@type integer|false device index
reactor = for_reactor, ---@type integer reactor = for_reactor, ---@type integer|nil unit/facility assignment
device = device, ---@type table peripheral reference device = device, ---@type table peripheral reference
is_multiblock = is_multiblock, ---@type boolean rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections
formed = formed, ---@type boolean|nil is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE formed = formed, ---@type boolean|nil if this peripheral is currently formed
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status
modbus_io = modbus.new(rtu_iface, true), rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface
pkt_queue = mqueue.new(), ---@type mqueue|nil modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface
thread = nil ---@type parallel_thread|nil pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue
thread = nil ---@type parallel_thread|nil associated RTU thread
} }
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
@ -485,6 +552,8 @@ local function main()
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state) databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end end
--#endregion
return true return true
end end
@ -495,17 +564,6 @@ local function main()
log.debug("boot> running sys_config()") log.debug("boot> running sys_config()")
if sys_config() then if sys_config() then
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- check modem -- check modem
if smem_dev.modem == nil then if smem_dev.modem == nil then
println("startup> wireless modem not found") println("startup> wireless modem not found")
@ -527,6 +585,17 @@ local function main()
databus.tx_hw_spkr_count(#smem_dev.sounders) databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- start connection watchdog -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
@ -553,7 +622,7 @@ local function main()
-- run threads -- run threads
parallel.waitForAll(table.unpack(_threads)) parallel.waitForAll(table.unpack(_threads))
else else
println("configuration failed, exiting...") println("system initialization failed, exiting...")
end end
renderer.close_ui() renderer.close_ui()

View File

@ -132,6 +132,8 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
unit.rtu, faulted = sna_rtu.new(device) unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device) unit.rtu, faulted = envd_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.REDSTONE then
unit.rtu.remount_phy(device)
else else
unknown = true unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)

View File

@ -17,7 +17,7 @@ local max_distance = nil
local comms = {} local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version) -- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "3.0.5" comms.version = "3.0.6"
comms.api_version = "0.0.9" comms.api_version = "0.0.9"
---@enum PROTOCOL ---@enum PROTOCOL

View File

@ -125,7 +125,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@field type RTU_UNIT_TYPE ---@field type RTU_UNIT_TYPE
---@field index integer|false ---@field index integer|false
---@field reactor integer ---@field reactor integer
---@field rsio IO_PORT[]|nil ---@field rs_conns IO_PORT[][]|nil
-- create a new reactor database -- create a new reactor database
---@nodiscard ---@nodiscard
@ -465,7 +465,8 @@ types.ALARM = {
ReactorHighWaste = 9, ReactorHighWaste = 9,
RPSTransient = 10, RPSTransient = 10,
RCSTransient = 11, RCSTransient = 11,
TurbineTrip = 12 TurbineTrip = 12,
FacilityRadiation = 13
} }
types.ALARM_NAMES = { types.ALARM_NAMES = {
@ -480,7 +481,8 @@ types.ALARM_NAMES = {
"ReactorHighWaste", "ReactorHighWaste",
"RPSTransient", "RPSTransient",
"RCSTransient", "RCSTransient",
"TurbineTrip" "TurbineTrip",
"FacilityRadiation"
} }
---@enum ALARM_PRIORITY ---@enum ALARM_PRIORITY

View File

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

137
supervisor/alarm_ctl.lua Normal file
View File

@ -0,0 +1,137 @@
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ALARM_STATE = types.ALARM_STATE
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
local alarm_ctl = {}
alarm_ctl.AISTATE = AISTATE
alarm_ctl.AISTATE_NAMES = AISTATE_NAMES
-- update an alarm state based on its current status and if it is tripped
---@param caller_tag string tag to use in log messages
---@param alarm_states { [ALARM]: ALARM_STATE } unit instance
---@param tripped boolean if the alarm condition is sti ll active
---@param alarm alarm_def alarm table
---@param no_ring_back boolean? true to skip the ring back state, returning to inactive instead
---@return boolean new_trip if the alarm just changed to being tripped
function alarm_ctl.update_alarm_state(caller_tag, alarm_states, tripped, alarm, no_ring_back)
local int_state = alarm.state
local ext_state = alarm_states[alarm.id]
-- alarm inactive
if int_state == AISTATE.INACTIVE then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.TRIPPING
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
else
alarm.trip_time = util.time_ms()
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm condition met, but not yet for required hold time
elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then
if tripped then
local elapsed = util.time_ms() - alarm.trip_time
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
else
alarm.trip_time = 0
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm tripped and alarming
elseif int_state == AISTATE.TRIPPED then
if tripped then
if ext_state == ALARM_STATE.ACKED then
-- was acked by coordinator
alarm.state = AISTATE.ACKED
end
elseif no_ring_back then
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm acknowledged but still tripped
elseif int_state == AISTATE.ACKED then
if not tripped then
if no_ring_back then
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
end
-- alarm no longer tripped, operator must reset to clear
elseif int_state == AISTATE.RING_BACK then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.RING_BACK_TRIPPING
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
end
elseif ext_state == ALARM_STATE.INACTIVE then
-- was reset by coordinator
alarm.state = AISTATE.INACTIVE
alarm.trip_time = 0
end
else
log.error(util.c(caller_tag, " invalid alarm state for alarm ", alarm.id), true)
end
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
log.debug(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
return alarm.state == AISTATE.TRIPPED
else return false end
end
return alarm_ctl

View File

@ -2,13 +2,19 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local alarm_ctl = require("supervisor.alarm_ctl")
local unit = require("supervisor.unit") local unit = require("supervisor.unit")
local fac_update = require("supervisor.facility_update") local fac_update = require("supervisor.facility_update")
local rsctl = require("supervisor.session.rsctl") local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local AISTATE = alarm_ctl.AISTATE
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local AUTO_GROUP = types.AUTO_GROUP local AUTO_GROUP = types.AUTO_GROUP
local PRIO = types.ALARM_PRIORITY
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
@ -138,7 +144,17 @@ function facility.new(config)
imtx_last_charge = 0, imtx_last_charge = 0,
imtx_last_charge_t = 0, imtx_last_charge_t = 0,
-- track faulted induction matrix update times to reject -- track faulted induction matrix update times to reject
imtx_faulted_times = { 0, 0, 0 } imtx_faulted_times = { 0, 0, 0 },
-- facility alarms
---@type { [string]: alarm_def }
alarms = {
-- radiation monitor alarm for the facility
FacilityRadiation = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.FacilityRadiation, tier = PRIO.CRITICAL },
},
---@type { [ALARM]: ALARM_STATE }
alarm_states = {
[ALARM.FacilityRadiation] = ALARM_STATE.INACTIVE
}
} }
--#region SETUP --#region SETUP
@ -157,7 +173,7 @@ function facility.new(config)
self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd } self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd }
-- init redstone RTU I/O controller -- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone) self.io_ctl = rsctl.new(self.redstone, 0)
-- fill blank alarm/tone states -- fill blank alarm/tone states
for _ = 1, 12 do table.insert(self.test_alarm_states, false) end for _ = 1, 12 do table.insert(self.test_alarm_states, false) end
@ -335,6 +351,9 @@ function facility.new(config)
-- unit tasks -- unit tasks
f_update.unit_mgmt() f_update.unit_mgmt()
-- update alarm states right before updating the audio
f_update.update_alarms()
-- update alarm tones -- update alarm tones
f_update.alarm_audio() f_update.alarm_audio()
end end
@ -404,10 +423,14 @@ function facility.new(config)
end end
end end
-- ack all alarms on all reactor units -- ack all alarms on all reactor units and the facility
function public.ack_all() function public.ack_all()
for i = 1, #self.units do -- unit alarms
self.units[i].ack_all() for i = 1, #self.units do self.units[i].ack_all() end
-- facility alarms
for id, state in pairs(self.alarm_states) do
if state == ALARM_STATE.TRIPPED then self.alarm_states[id] = ALARM_STATE.ACKED end
end end
end end

View File

@ -5,6 +5,8 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local alarm_ctl = require("supervisor.alarm_ctl")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
@ -643,7 +645,7 @@ function update.auto_safety()
end end
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.radiation or astatus.gen_fault
if scram and not self.ascram then if scram and not self.ascram then
-- SCRAM all units -- SCRAM all units
@ -714,11 +716,17 @@ function update.post_auto()
self.mode = next_mode self.mode = next_mode
end end
-- update facility alarm states
function update.update_alarms()
-- Facility Radiation
alarm_ctl.update_alarm_state("FAC", self.alarm_states, self.ascram_status.radiation, self.alarms.FacilityRadiation, true)
end
-- update alarm audio control -- update alarm audio control
function update.alarm_audio() function update.alarm_audio()
local allow_test = self.allow_testing and self.test_tone_set local allow_test = self.allow_testing and self.test_tone_set
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } local alarms = { false, false, false, false, false, false, false, false, false, false, false, false, false }
-- reset tone states before re-evaluting -- reset tone states before re-evaluting
for i = 1, #self.tone_states do self.tone_states[i] = false end for i = 1, #self.tone_states do self.tone_states[i] = false end
@ -734,8 +742,11 @@ function update.alarm_audio()
end end
end end
if not self.test_tone_reset then -- record facility alarms
alarms[ALARM.FacilityRadiation] = self.alarm_states[ALARM.FacilityRadiation] == ALARM_STATE.TRIPPED
-- clear testing alarms if we aren't using them -- clear testing alarms if we aren't using them
if not self.test_tone_reset then
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
end end
end end
@ -774,7 +785,7 @@ function update.alarm_audio()
end end
-- radiation is a big concern, always play this CRITICAL level alarm if active -- radiation is a big concern, always play this CRITICAL level alarm if active
if alarms[ALARM.ContainmentRadiation] then if alarms[ALARM.ContainmentRadiation] or alarms[ALARM.FacilityRadiation] then
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled -- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one -- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one

View File

@ -9,7 +9,8 @@ local rsctl = {}
-- create a new redstone RTU I/O controller -- create a new redstone RTU I/O controller
---@nodiscard ---@nodiscard
---@param redstone_rtus redstone_session[] redstone RTU sessions ---@param redstone_rtus redstone_session[] redstone RTU sessions
function rsctl.new(redstone_rtus) ---@param bank integer I/O bank (unit/facility assignment) to interface with
function rsctl.new(redstone_rtus, bank)
---@class rs_controller ---@class rs_controller
local public = {} local public = {}
@ -18,7 +19,7 @@ function rsctl.new(redstone_rtus)
---@return boolean ---@return boolean
function public.is_connected(port) function public.is_connected(port)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
if redstone_rtus[i].get_db().io[port] ~= nil then return true end if redstone_rtus[i].get_db().io[bank][port] ~= nil then return true end
end end
return false return false
@ -29,7 +30,7 @@ function rsctl.new(redstone_rtus)
---@param value boolean ---@param value boolean
function public.digital_write(port, value) function public.digital_write(port, value)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port] local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then io.write(value) end if io ~= nil then io.write(value) end
end end
end end
@ -40,7 +41,7 @@ function rsctl.new(redstone_rtus)
---@return boolean|nil ---@return boolean|nil
function public.digital_read(port) function public.digital_read(port)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port] local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then return io.read() --[[@as boolean|nil]] end if io ~= nil then return io.read() --[[@as boolean|nil]] end
end end
end end
@ -52,7 +53,7 @@ function rsctl.new(redstone_rtus)
---@param max number maximum value for scaling 0 to 15 ---@param max number maximum value for scaling 0 to 15
function public.analog_write(port, value, min, max) function public.analog_write(port, value, min, max)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port] local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then io.write(rsio.analog_write(value, min, max)) end if io ~= nil then io.write(rsio.analog_write(value, min, max)) end
end end
end end

View File

@ -93,7 +93,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
type = self.advert[i][1], type = self.advert[i][1],
index = self.advert[i][2], index = self.advert[i][2],
reactor = self.advert[i][3], reactor = self.advert[i][3],
rsio = self.advert[i][4] rs_conns = self.advert[i][4]
} }
local u_type = unit_advert.type ---@type RTU_UNIT_TYPE|boolean local u_type = unit_advert.type ---@type RTU_UNIT_TYPE|boolean
@ -104,14 +104,17 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false)) advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false))
advert_validator.assert_type_int(unit_advert.reactor) advert_validator.assert_type_int(unit_advert.reactor)
if u_type == RTU_UNIT_TYPE.REDSTONE then
advert_validator.assert_type_table(unit_advert.rsio)
end
if advert_validator.valid() then if advert_validator.valid() then
if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end
if (unit_advert.reactor == -1) or (u_type == RTU_UNIT_TYPE.REDSTONE) then
advert_validator.assert((unit_advert.reactor == -1) and (u_type == RTU_UNIT_TYPE.REDSTONE))
advert_validator.assert_type_table(unit_advert.rs_conns)
else
advert_validator.assert_min(unit_advert.reactor, 0) advert_validator.assert_min(unit_advert.reactor, 0)
advert_validator.assert_max(unit_advert.reactor, #self.fac_units) advert_validator.assert_max(unit_advert.reactor, #self.fac_units)
end
if not advert_validator.valid() then u_type = false end if not advert_validator.valid() then u_type = false end
else else
u_type = false u_type = false
@ -126,15 +129,34 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
-- validation fail -- validation fail
log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure") log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure")
else else
if unit_advert.reactor > 0 then if unit_advert.reactor == -1 then
local target_unit = self.fac_units[unit_advert.reactor] -- redstone RTUs can be used in multiple different assignments
-- unit RTUs
if u_type == RTU_UNIT_TYPE.REDSTONE then if u_type == RTU_UNIT_TYPE.REDSTONE then
-- redstone -- redstone
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end
elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then -- link this to any subsystems this RTU provides connections for
if type(unit) ~= "nil" then
for assignment, conns in pairs(unit_advert.rs_conns) do
if #conns > 0 then
if assignment == 0 then
facility.add_redstone(unit)
elseif assignment > 0 and assignment <= #self.fac_units then
self.fac_units[assignment].add_redstone(unit)
else
log.warning(util.c(log_tag, "_handle_advertisement(): invalid redstone RTU assignment ", assignment))
end
end
end
end
else
log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported multi-assignment RTU type ", type_string))
end
elseif unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor]
-- unit RTUs
if u_type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler -- boiler
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q) unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end if type(unit) ~= "nil" then target_unit.add_boiler(unit) end

View File

@ -10,7 +10,6 @@ local redstone = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_FCODE = types.MODBUS_FCODE
local IO_PORT = rsio.IO
local IO_LVL = rsio.IO_LVL local IO_LVL = rsio.IO_LVL
local IO_MODE = rsio.IO_MODE local IO_MODE = rsio.IO_MODE
@ -39,6 +38,9 @@ local PERIODICS = {
OUTPUT_SYNC = 200 OUTPUT_SYNC = 200
} }
-- create a new block of IO banks (facility, then each unit)
local function new_io_block() return { [0] = {}, {}, {}, {}, {} } end
---@class dig_phy_entry ---@class dig_phy_entry
---@field phy IO_LVL actual value ---@field phy IO_LVL actual value
---@field req IO_LVL commanded value ---@field req IO_LVL commanded value
@ -74,27 +76,27 @@ function redstone.new(session_id, unit_id, advert, out_queue)
next_ir_req = 0, next_ir_req = 0,
next_hr_sync = 0 next_hr_sync = 0
}, },
---@class rs_io_list ---@class rs_io_map
io_list = { io_map = {
digital_in = {}, ---@type IO_PORT[] discrete inputs digital_in = {}, ---@type { bank: integer, port: IO_PORT }[] discrete inputs
digital_out = {}, ---@type IO_PORT[] coils digital_out = {}, ---@type { bank: integer, port: IO_PORT }[] coils
analog_in = {}, ---@type IO_PORT[] input registers analog_in = {}, ---@type { bank: integer, port: IO_PORT }[] input registers
analog_out = {} ---@type IO_PORT[] holding registers analog_out = {} ---@type { bank: integer, port: IO_PORT }[] holding registers
}, },
phy_trans = { coils = -1, hold_regs = -1 }, phy_trans = { coils = -1, hold_regs = -1 },
-- last set/read ports (reflecting the current state of the RTU) -- last set/read ports (reflecting the current state of the RTU)
---@class rs_io_states ---@class rs_io_states
phy_io = { phy_io = {
digital_in = {}, ---@type dig_phy_entry[] discrete inputs digital_in = new_io_block(), ---@type dig_phy_entry[][] discrete inputs
digital_out = {}, ---@type dig_phy_entry[] coils digital_out = new_io_block(), ---@type dig_phy_entry[][] coils
analog_in = {}, ---@type ana_phy_entry[] input registers analog_in = new_io_block(), ---@type ana_phy_entry[][] input registers
analog_out = {} ---@type ana_phy_entry[] holding registers analog_out = new_io_block() ---@type ana_phy_entry[][] holding registers
}, },
---@class redstone_session_db ---@class redstone_session_db
db = { db = {
-- read/write functions for connected I/O -- read/write functions for connected I/O
---@type (rs_db_dig_io|rs_db_ana_io)[] ---@type (rs_db_dig_io|rs_db_ana_io)[][]
io = {} io = new_io_block()
} }
} }
@ -103,106 +105,104 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- INITIALIZE -- -- INITIALIZE --
-- create all ports as disconnected
for _ = 1, #IO_PORT do
table.insert(self.db, IO_LVL.DISCONNECT)
end
-- setup I/O -- setup I/O
for i = 1, #advert.rsio do for bank = 0, 4 do
local port = advert.rsio[i] for i = 1, #advert.rs_conns[bank] do
local port = advert.rs_conns[bank][i]
if rsio.is_valid_port(port) then if rsio.is_valid_port(port) then
local mode = rsio.get_io_mode(port) local mode = rsio.get_io_mode(port)
local io_entry = { bank = bank, port = port }
if mode == IO_MODE.DIGITAL_IN then if mode == IO_MODE.DIGITAL_IN then
self.has_di = true self.has_di = true
table.insert(self.io_list.digital_in, port) table.insert(self.io_map.digital_in, io_entry)
self.phy_io.digital_in[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } self.phy_io.digital_in[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
---@class rs_db_dig_io ---@class rs_db_dig_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end, read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[bank][port].phy) end,
write = function () end write = function () end
} }
self.db.io[port] = io_f self.db.io[bank][port] = io_f
elseif mode == IO_MODE.DIGITAL_OUT then elseif mode == IO_MODE.DIGITAL_OUT then
self.has_do = true self.has_do = true
table.insert(self.io_list.digital_out, port) table.insert(self.io_map.digital_out, io_entry)
self.phy_io.digital_out[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } self.phy_io.digital_out[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
---@class rs_db_dig_io ---@class rs_db_dig_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end, read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[bank][port].phy) end,
---@param active boolean ---@param active boolean
write = function (active) write = function (active)
local level = rsio.digital_write_active(port, active) local level = rsio.digital_write_active(port, active)
if level ~= nil then self.phy_io.digital_out[port].req = level end if level ~= nil then self.phy_io.digital_out[bank][port].req = level end
end end
} }
self.db.io[port] = io_f self.db.io[bank][port] = io_f
elseif mode == IO_MODE.ANALOG_IN then elseif mode == IO_MODE.ANALOG_IN then
self.has_ai = true self.has_ai = true
table.insert(self.io_list.analog_in, port) table.insert(self.io_map.analog_in, io_entry)
self.phy_io.analog_in[port] = { phy = 0, req = 0 } self.phy_io.analog_in[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io ---@class rs_db_ana_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
---@return integer ---@return integer
read = function () return self.phy_io.analog_in[port].phy end, read = function () return self.phy_io.analog_in[bank][port].phy end,
write = function () end write = function () end
} }
self.db.io[port] = io_f self.db.io[bank][port] = io_f
elseif mode == IO_MODE.ANALOG_OUT then elseif mode == IO_MODE.ANALOG_OUT then
self.has_ao = true self.has_ao = true
table.insert(self.io_list.analog_out, port) table.insert(self.io_map.analog_out, io_entry)
self.phy_io.analog_out[port] = { phy = 0, req = 0 } self.phy_io.analog_out[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io ---@class rs_db_ana_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
---@return integer ---@return integer
read = function () return self.phy_io.analog_out[port].phy end, read = function () return self.phy_io.analog_out[bank][port].phy end,
---@param value integer ---@param value integer
write = function (value) write = function (value)
if value >= 0 and value <= 15 then if value >= 0 and value <= 15 then
self.phy_io.analog_out[port].req = value self.phy_io.analog_out[bank][port].req = value
end end
end end
} }
self.db.io[port] = io_f self.db.io[bank][port] = io_f
else else
-- should be unreachable code, we already validated ports -- should be unreachable code, we already validated ports
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", port, ")"), true) log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", bank, ":", port, ")"), true)
return nil return nil
end end
else else
log.error(util.c(log_tag, "invalid advertisement port (", port, ")"), true) log.error(util.c(log_tag, "invalid advertisement port (", bank, ":", port, ")"), true)
return nil return nil
end end
end end
end
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- query discrete inputs -- query discrete inputs
local function _request_discrete_inputs() local function _request_discrete_inputs()
self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in }) self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_map.digital_in })
end end
-- query input registers -- query input registers
local function _request_input_registers() local function _request_input_registers()
self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_map.analog_in })
end end
-- write all coil outputs -- write all coil outputs
@ -210,9 +210,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
local params = { 1 } local params = { 1 }
local outputs = self.phy_io.digital_out local outputs = self.phy_io.digital_out
for i = 1, #self.io_list.digital_out do for i = 1, #self.io_map.digital_out do
local port = self.io_list.digital_out[i] local entry = self.io_map.digital_out[i]
table.insert(params, outputs[port].req) table.insert(params, outputs[entry.bank][entry.port].req)
end end
self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params) self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params)
@ -220,7 +220,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- read all coil outputs -- read all coil outputs
local function _read_coils() local function _read_coils()
self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_list.digital_out }) self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_map.digital_out })
end end
-- write all holding register outputs -- write all holding register outputs
@ -228,9 +228,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
local params = { 1 } local params = { 1 }
local outputs = self.phy_io.analog_out local outputs = self.phy_io.analog_out
for i = 1, #self.io_list.analog_out do for i = 1, #self.io_map.analog_out do
local port = self.io_list.analog_out[i] local entry = self.io_map.analog_out[i]
table.insert(params, outputs[port].req) table.insert(params, outputs[entry.bank][entry.port].req)
end end
self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params) self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params)
@ -238,7 +238,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- read all holding register outputs -- read all holding register outputs
local function _read_holding_registers() local function _read_holding_registers()
self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_list.analog_out }) self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_map.analog_out })
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@ -259,24 +259,24 @@ function redstone.new(session_id, unit_id, advert, out_queue)
end end
elseif txn_type == TXN_TYPES.DI_READ then elseif txn_type == TXN_TYPES.DI_READ then
-- discrete input read response -- discrete input read response
if m_pkt.length == #self.io_list.digital_in then if m_pkt.length == #self.io_map.digital_in then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.digital_in[i] local entry = self.io_map.digital_in[i]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.digital_in[port].phy = value self.phy_io.digital_in[entry.bank][entry.port].phy = value
end end
else else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end end
elseif txn_type == TXN_TYPES.INPUT_REG_READ then elseif txn_type == TXN_TYPES.INPUT_REG_READ then
-- input register read response -- input register read response
if m_pkt.length == #self.io_list.analog_in then if m_pkt.length == #self.io_map.analog_in then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.analog_in[i] local entry = self.io_map.analog_in[i]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.analog_in[port].phy = value self.phy_io.analog_in[entry.bank][entry.port].phy = value
end end
else else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
@ -288,15 +288,14 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- update phy I/O table -- update phy I/O table
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical) -- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done -- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
if m_pkt.length == #self.io_list.digital_out then if m_pkt.length == #self.io_map.digital_out then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.digital_out[i] local entry = self.io_map.digital_out[i]
local state = self.phy_io.digital_out[entry.bank][entry.port]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.digital_out[port].phy = value state.phy = value
if self.phy_io.digital_out[port].req == IO_LVL.FLOATING then if state.req == IO_LVL.FLOATING then state.req = value end
self.phy_io.digital_out[port].req = value
end
end end
self.phy_trans.coils = TXN_READY self.phy_trans.coils = TXN_READY
@ -310,12 +309,12 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- update phy I/O table -- update phy I/O table
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical) -- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done -- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
if m_pkt.length == #self.io_list.analog_out then if m_pkt.length == #self.io_map.analog_out then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.analog_out[i] local entry = self.io_map.analog_out[i]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.analog_out[port].phy = value self.phy_io.analog_out[entry.bank][entry.port].phy = value
end end
else else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
@ -343,8 +342,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- sync digital outputs -- sync digital outputs
if self.has_do then if self.has_do then
if (self.periodics.next_cl_sync <= time_now) and (self.phy_trans.coils == TXN_READY) then if (self.periodics.next_cl_sync <= time_now) and (self.phy_trans.coils == TXN_READY) then
for _, entry in pairs(self.phy_io.digital_out) do for bank = 0, 4 do
local changed = false
for _, entry in pairs(self.phy_io.digital_out[bank]) do
if entry.phy ~= entry.req then if entry.phy ~= entry.req then
changed = true
break
end
end
if changed then
_write_coils() _write_coils()
break break
end end
@ -365,8 +373,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- sync analog outputs -- sync analog outputs
if self.has_ao then if self.has_ao then
if (self.periodics.next_hr_sync <= time_now) and (self.phy_trans.hold_regs == TXN_READY) then if (self.periodics.next_hr_sync <= time_now) and (self.phy_trans.hold_regs == TXN_READY) then
for _, entry in pairs(self.phy_io.analog_out) do for bank = 0, 4 do
local changed = false
for _, entry in pairs(self.phy_io.analog_out[bank]) do
if entry.phy ~= entry.req then if entry.phy ~= entry.req then
changed = true
break
end
end
if changed then
_write_holding_registers() _write_holding_registers()
break break
end end
@ -379,9 +396,10 @@ function redstone.new(session_id, unit_id, advert, out_queue)
self.session.post_update() self.session.post_update()
end end
-- invalidate build cache -- force a re-read of cached outputs
function public.invalidate_cache() function public.invalidate_cache()
-- no build cache for this device _read_coils()
_read_holding_registers()
end end
-- get the unit session database -- get the unit session database

View File

@ -23,7 +23,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.6.8" local SUPERVISOR_VERSION = "v1.7.0"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -3,20 +3,23 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local logic = require("supervisor.unitlogic") local alarm_ctl = require("supervisor.alarm_ctl")
local unit_logic = require("supervisor.unit_logic")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local rsctl = require("supervisor.session.rsctl") local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local WASTE_MODE = types.WASTE_MODE local AISTATE = alarm_ctl.AISTATE
local WASTE = types.WASTE_PRODUCT
local ALARM = types.ALARM local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL local PRIO = types.ALARM_PRIORITY
local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local TRI_FAIL = types.TRI_FAIL
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
@ -37,23 +40,6 @@ local DT_KEYS = {
TurbinePower = "TPR" TurbinePower = "TPR"
} }
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
-- burn rate to idle at -- burn rate to idle at
local IDLE_RATE = 0.01 local IDLE_RATE = 0.01
@ -81,7 +67,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
num_boilers = num_boilers, num_boilers = num_boilers,
num_turbines = num_turbines, num_turbines = num_turbines,
aux_coolant = aux_coolant, aux_coolant = aux_coolant,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, types = { DT_KEYS = DT_KEYS },
-- rtus -- rtus
rtu_list = {}, ---@type unit_session[][] rtu_list = {}, ---@type unit_session[][]
redstone = {}, ---@type redstone_session[] redstone = {}, ---@type redstone_session[]
@ -258,7 +244,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd } self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd }
-- init redstone RTU I/O controller -- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone) self.io_ctl = rsctl.new(self.redstone, reactor_id)
-- init boiler table fields -- init boiler table fields
for _ = 1, num_boilers do for _ = 1, num_boilers do
@ -597,20 +583,20 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
_dt__compute_all() _dt__compute_all()
-- update annunciator logic -- update annunciator logic
logic.update_annunciator(self) unit_logic.update_annunciator(self)
-- update alarm status -- update alarm status
logic.update_alarms(self) unit_logic.update_alarms(self)
-- if in auto mode, SCRAM on certain alarms -- if in auto mode, SCRAM on certain alarms
logic.update_auto_safety(public, self) unit_logic.update_auto_safety(self, public)
-- update status text -- update status text
logic.update_status_text(self) unit_logic.update_status_text(self)
-- handle redstone I/O -- handle redstone I/O
if #self.redstone > 0 then if #self.redstone > 0 then
logic.handle_redstone(self) unit_logic.handle_redstone(self)
elseif not self.plc_cache.rps_trip then elseif not self.plc_cache.rps_trip then
self.em_cool_opened = false self.em_cool_opened = false
end end
@ -775,10 +761,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
-- acknowledge all alarms (if possible) -- acknowledge all alarms (if possible)
function public.ack_all() function public.ack_all()
for i = 1, #self.db.alarm_states do for id, state in pairs(self.db.alarm_states) do
if self.db.alarm_states[i] == ALARM_STATE.TRIPPED then if state == ALARM_STATE.TRIPPED then self.db.alarm_states[id] = ALARM_STATE.ACKED end
self.db.alarm_states[i] = ALARM_STATE.ACKED
end
end end
end end

View File

@ -4,10 +4,14 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local alarm_ctl = require("supervisor.alarm_ctl")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local qtypes = require("supervisor.session.rtu.qtypes") local qtypes = require("supervisor.session.rtu.qtypes")
local AISTATE = alarm_ctl.AISTATE
local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
local TRI_FAIL = types.TRI_FAIL local TRI_FAIL = types.TRI_FAIL
local CONTAINER_MODE = types.CONTAINER_MODE local CONTAINER_MODE = types.CONTAINER_MODE
@ -22,19 +26,9 @@ local IO = rsio.IO
local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
local ALARM_LIMS = const.ALARM_LIMITS local ALARM_LIMS = const.ALARM_LIMITS
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local RS_THRESH = const.RS_THRESHOLDS local RS_THRESH = const.RS_THRESHOLDS
---@class unit_logic_extension ---@class unit_logic_extension
@ -179,12 +173,8 @@ function logic.update_annunciator(self)
annunc.EmergencyCoolant = 1 annunc.EmergencyCoolant = 1
for i = 1, #self.redstone do if self.io_ctl.is_connected(IO.U_EMER_COOL) then
local io = self.redstone[i].get_db().io[IO.U_EMER_COOL] annunc.EmergencyCoolant = util.trinary(self.io_ctl.digital_read(IO.U_EMER_COOL), 3, 2)
if io ~= nil then
annunc.EmergencyCoolant = util.trinary(io.read(), 3, 2)
break
end
end end
--#endregion --#endregion
@ -426,97 +416,16 @@ function logic.update_annunciator(self)
end end
-- update an alarm state given conditions -- update an alarm state given conditions
---@param self _unit_self unit instance ---@param self _unit_self
---@param tripped boolean if the alarm condition is still active ---@param tripped boolean if the alarm condition is still active
---@param alarm alarm_def alarm table ---@param alarm alarm_def alarm table
---@return boolean new_trip if the alarm just changed to being tripped ---@return boolean new_trip if the alarm just changed to being tripped
local function _update_alarm_state(self, tripped, alarm) local function _update_alarm_state(self, tripped, alarm)
local AISTATE = self.types.AISTATE return alarm_ctl.update_alarm_state("UNIT " .. self.r_id, self.db.alarm_states, tripped, alarm)
local int_state = alarm.state
local ext_state = self.db.alarm_states[alarm.id]
-- alarm inactive
if int_state == AISTATE.INACTIVE then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.TRIPPING
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
else
alarm.trip_time = util.time_ms()
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm condition met, but not yet for required hold time
elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then
if tripped then
local elapsed = util.time_ms() - alarm.trip_time
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
else
alarm.trip_time = 0
alarm.state = AISTATE.INACTIVE
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm tripped and alarming
elseif int_state == AISTATE.TRIPPED then
if tripped then
if ext_state == ALARM_STATE.ACKED then
-- was acked by coordinator
alarm.state = AISTATE.ACKED
end
else
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm acknowledged but still tripped
elseif int_state == AISTATE.ACKED then
if not tripped then
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm no longer tripped, operator must reset to clear
elseif int_state == AISTATE.RING_BACK then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.RING_BACK_TRIPPING
else
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
end
elseif ext_state == ALARM_STATE.INACTIVE then
-- was reset by coordinator
alarm.state = AISTATE.INACTIVE
alarm.trip_time = 0
end
else
log.error(util.c("invalid alarm state for unit ", self.r_id, " alarm ", alarm.id), true)
end
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
return alarm.state == AISTATE.TRIPPED
else return false end
end end
-- evaluate alarm conditions -- evaluate alarm conditions
---@param self _unit_self unit instance ---@param self _unit_self
function logic.update_alarms(self) function logic.update_alarms(self)
local annunc = self.db.annunciator local annunc = self.db.annunciator
local plc_cache = self.plc_cache local plc_cache = self.plc_cache
@ -629,11 +538,9 @@ function logic.update_alarms(self)
end end
-- update the internal automatic safety control performed while in auto control mode -- update the internal automatic safety control performed while in auto control mode
---@param self _unit_self
---@param public reactor_unit reactor unit public functions ---@param public reactor_unit reactor unit public functions
---@param self _unit_self unit instance function logic.update_auto_safety(self, public)
function logic.update_auto_safety(public, self)
local AISTATE = self.types.AISTATE
if self.auto_engaged then if self.auto_engaged then
local alarmed = false local alarmed = false
@ -660,9 +567,8 @@ function logic.update_auto_safety(public, self)
end end
-- update the two unit status text messages -- update the two unit status text messages
---@param self _unit_self unit instance ---@param self _unit_self
function logic.update_status_text(self) function logic.update_status_text(self)
local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator local annunc = self.db.annunciator
-- check if an alarm is active (tripped or ack'd) -- check if an alarm is active (tripped or ack'd)
@ -824,9 +730,8 @@ function logic.update_status_text(self)
end end
-- handle unit redstone I/O -- handle unit redstone I/O
---@param self _unit_self unit instance ---@param self _unit_self
function logic.handle_redstone(self) function logic.handle_redstone(self)
local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator local annunc = self.db.annunciator
local cache = self.plc_cache local cache = self.plc_cache
local rps = cache.rps_status local rps = cache.rps_status
@ -906,7 +811,7 @@ function logic.handle_redstone(self)
if enable_emer_cool and not self.em_cool_opened then if enable_emer_cool and not self.em_cool_opened then
log.debug(util.c(">> Emergency Coolant Enable Detail Report (Unit ", self.r_id, ") <<")) log.debug(util.c(">> Emergency Coolant Enable Detail Report (Unit ", self.r_id, ") <<"))
log.debug(util.c("| CoolantLevelLow[", annunc.CoolantLevelLow, "] CoolantLevelLowLow[", rps.low_cool, "] ExcessHeatedCoolant[", rps.ex_hcool, "]")) log.debug(util.c("| CoolantLevelLow[", annunc.CoolantLevelLow, "] CoolantLevelLowLow[", rps.low_cool, "] ExcessHeatedCoolant[", rps.ex_hcool, "]"))
log.debug(util.c("| ReactorOverTemp[", AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]")) log.debug(util.c("| ReactorOverTemp[", alarm_ctl.AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]"))
for i = 1, #annunc.WaterLevelLow do for i = 1, #annunc.WaterLevelLow do
log.debug(util.c("| WaterLevelLow(", i, ")[", annunc.WaterLevelLow[i], "]")) log.debug(util.c("| WaterLevelLow(", i, ")[", annunc.WaterLevelLow[i], "]"))