Merge pull request #368 from MikaylaFischler/145-graphical-configure-utilities

RTU configurator and other updates
This commit is contained in:
Mikayla 2023-11-12 18:37:23 -05:00 committed by GitHub
commit 69680a53a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2391 additions and 706 deletions

View File

@ -18,7 +18,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function println(message) print(tostring(message)) end local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.11a" local CCMSI_VERSION = "v1.11c"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
@ -32,6 +32,7 @@ local function red() term.setTextColor(colors.red) end
local function orange() term.setTextColor(colors.orange) end local function orange() term.setTextColor(colors.orange) end
local function yellow() term.setTextColor(colors.yellow) end local function yellow() term.setTextColor(colors.yellow) end
local function green() term.setTextColor(colors.green) end local function green() term.setTextColor(colors.green) end
local function cyan() term.setTextColor(colors.cyan) end
local function blue() term.setTextColor(colors.blue) end local function blue() term.setTextColor(colors.blue) end
local function white() term.setTextColor(colors.white) end local function white() term.setTextColor(colors.white) end
local function lgray() term.setTextColor(colors.lightGray) end local function lgray() term.setTextColor(colors.lightGray) end
@ -199,7 +200,7 @@ if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>") println("usage: ccmsi <mode> <app> <branch>")
println("<mode>") println("<mode>")
lgray() lgray()
println(" check - check latest versions avilable") println(" check - check latest versions available")
yellow() yellow()
println(" ccmsi check <branch> for target") println(" ccmsi check <branch> for target")
lgray() lgray()
@ -266,18 +267,16 @@ if mode == "check" then
blue();print(local_manifest.versions[key]) blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then if value ~= local_manifest.versions[key] then
white();print(" (") white();print(" (")
term.setTextColor(colors.cyan) cyan();print(value);white();println(" available)")
print(value);white();println(" available)")
else green();println(" (up to date)") end else green();println(" (up to date)") end
else else
lgray();print("not installed");white();print(" (latest ") lgray();print("not installed");white();print(" (latest ")
term.setTextColor(colors.cyan) cyan();print(value);white();println(")")
print(value);white();println(")")
end end
end end
if manifest.versions.installer ~= local_manifest.versions.installer then if manifest.versions.installer ~= local_manifest.versions.installer then
yellow();println("\nA newer version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white() yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end end
elseif mode == "install" or mode == "update" then elseif mode == "install" or mode == "update" then
local update_installer = app == "installer" local update_installer = app == "installer"
@ -315,7 +314,7 @@ elseif mode == "install" or mode == "update" then
end end
if manifest.versions.installer ~= CCMSI_VERSION then if manifest.versions.installer ~= CCMSI_VERSION then
if not update_installer then yellow();println("A newer version of the installer is available, it is recommended to update to it.");white() end if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now") then if update_installer or ask_y_n("Would you like to update now") then
lgray();println("GET ccmsi.lua") lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path .. "ccmsi.lua") local dl, err = http.get(repo_path .. "ccmsi.lua")

View File

@ -2,8 +2,8 @@ print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
if fs.exists("reactor-plc/configure.lua") then if fs.exists("reactor-plc/configure.lua") then
require("reactor-plc.configure").configure() require("reactor-plc.configure").configure()
elseif fs.exists("rtu/startup.lua") then elseif fs.exists("rtu/configure.lua") then
print("CONFIGURE> RTU CONFIGURATOR NOT YET IMPLEMENTED IN BETA") require("rtu.configure").configure()
elseif fs.exists("supervisor/startup.lua") then elseif fs.exists("supervisor/startup.lua") then
print("CONFIGURE> SUPERVISOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA") print("CONFIGURE> SUPERVISOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
elseif fs.exists("coordinator/startup.lua") then elseif fs.exists("coordinator/startup.lua") then

View File

@ -469,7 +469,7 @@ function iocontrol.record_unit_builds(builds)
-- note: if not all units and RTUs are connected, some will be nil -- note: if not all units and RTUs are connected, some will be nil
for id, build in pairs(builds) do for id, build in pairs(builds) do
local unit = io.units[id] ---@type ioctl_unit local unit = io.units[id] ---@type ioctl_unit
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ") local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
@ -694,8 +694,8 @@ function iocontrol.update_facility_status(status)
for id, sps in pairs(rtu_statuses.sps) do for id, sps in pairs(rtu_statuses.sps) do
if type(fac.sps_data_tbl[id]) == "table" then if type(fac.sps_data_tbl[id]) == "table" then
local data = fac.sps_data_tbl[id] ---@type sps_session_db local data = fac.sps_data_tbl[id] ---@type sps_session_db
local ps = fac.sps_ps_tbl[id] ---@type psil local ps = fac.sps_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(sps, data, ps) local rtu_faulted = _record_multiblock_status(sps, data, ps)
@ -732,8 +732,8 @@ function iocontrol.update_facility_status(status)
for id, tank in pairs(rtu_statuses.tanks) do for id, tank in pairs(rtu_statuses.tanks) do
if type(fac.tank_data_tbl[id]) == "table" then if type(fac.tank_data_tbl[id]) == "table" then
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = fac.tank_ps_tbl[id] ---@type psil local ps = fac.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps) local rtu_faulted = _record_multiblock_status(tank, data, ps)
@ -760,20 +760,34 @@ function iocontrol.update_facility_status(status)
end end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.envds) == "table" then
if #rtu_statuses.rad_mon > 0 then local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false
local rad_mon = rtu_statuses.rad_mon[1]
local rtu_faulted = rad_mon[1] ---@type boolean
fac.radiation = rad_mon[2] ---@type number
fac.ps.publish("rad_computed_status", util.trinary(rtu_faulted, 2, 3)) for _, envd in pairs(rtu_statuses.envds) do
fac.ps.publish("radiation", fac.radiation) local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
any_conn = true
any_faulted = any_faulted or rtu_faulted
if rad_raw > max_rad then
max_rad = rad_raw
max_reading = radiation
end
end
if any_conn then
fac.radiation = max_reading
fac.ps.publish("rad_computed_status", util.trinary(any_faulted, 2, 3))
else else
fac.radiation = types.new_zero_radiation_reading() fac.radiation = types.new_zero_radiation_reading()
fac.ps.publish("rad_computed_status", 1) fac.ps.publish("rad_computed_status", 1)
end end
fac.ps.publish("radiation", fac.radiation)
else else
log.debug(log_header .. "radiation monitor list not a table") log.debug(log_header .. "environment detector list not a table")
valid = false valid = false
end end
else else
@ -917,8 +931,8 @@ function iocontrol.update_unit_statuses(statuses)
for id, boiler in pairs(rtu_statuses.boilers) do for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then if type(unit.boiler_data_tbl[id]) == "table" then
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil local ps = unit.boiler_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(boiler, data, ps) local rtu_faulted = _record_multiblock_status(boiler, data, ps)
@ -960,8 +974,8 @@ function iocontrol.update_unit_statuses(statuses)
for id, turbine in pairs(rtu_statuses.turbines) do for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then if type(unit.turbine_data_tbl[id]) == "table" then
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil local ps = unit.turbine_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(turbine, data, ps) local rtu_faulted = _record_multiblock_status(turbine, data, ps)
@ -1033,9 +1047,9 @@ function iocontrol.update_unit_statuses(statuses)
-- solar neutron activator status info -- solar neutron activator status info
if type(rtu_statuses.sna) == "table" then if type(rtu_statuses.sna) == "table" then
unit.num_snas = rtu_statuses.sna[1] ---@type integer unit.num_snas = rtu_statuses.sna[1] ---@type integer
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number
unit.unit_ps.publish("sna_count", unit.num_snas) unit.unit_ps.publish("sna_count", unit.num_snas)
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate) unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
@ -1048,16 +1062,28 @@ function iocontrol.update_unit_statuses(statuses)
end end
-- environment detector status -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.envds) == "table" then
if #rtu_statuses.rad_mon > 0 then local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false
local rad_mon = rtu_statuses.rad_mon[1]
-- local rtu_faulted = rad_mon[1] ---@type boolean
unit.radiation = rad_mon[2] ---@type number
unit.unit_ps.publish("radiation", unit.radiation) for _, envd in pairs(rtu_statuses.envds) do
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
any_conn = true
if rad_raw > max_rad then
max_rad = rad_raw
max_reading = radiation
end
end
if any_conn then
unit.radiation = max_reading
else else
unit.radiation = types.new_zero_radiation_reading() unit.radiation = types.new_zero_radiation_reading()
end end
unit.unit_ps.publish("radiation", unit.radiation)
else else
log.debug(log_header .. "radiation monitor list not a table") log.debug(log_header .. "radiation monitor list not a table")
valid = false valid = false

View File

@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v1.0.16" local COORDINATOR_VERSION = "v1.0.17"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {} local core = {}
core.version = "2.0.2" core.version = "2.0.7"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events
@ -173,7 +173,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
if e.enabled then if e.enabled then
e.w_set_bkg(fg_bg.bkg) e.w_set_bkg(fg_bg.bkg)
e.w_set_fgd(fg_bg.fgd) e.w_set_fgd(fg_bg.fgd)
else elseif dis_fg_bg ~= nil then
e.w_set_bkg(dis_fg_bg.bkg) e.w_set_bkg(dis_fg_bg.bkg)
e.w_set_fgd(dis_fg_bg.fgd) e.w_set_fgd(dis_fg_bg.fgd)
end end

View File

@ -91,6 +91,8 @@ function element.new(args, child_offset_x, child_offset_y)
p_window = nil, ---@type table p_window = nil, ---@type table
position = events.new_coord_2d(1, 1), position = events.new_coord_2d(1, 1),
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
offset_x = 0,
offset_y = 0,
next_y = 1, -- next child y coordinate next_y = 1, -- next child y coordinate
next_id = 0, -- next child ID next_id = 0, -- next child ID
subscriptions = {}, subscriptions = {},
@ -105,6 +107,7 @@ function element.new(args, child_offset_x, child_offset_y)
value = nil, ---@type any value = nil, ---@type any
window = nil, ---@type table window = nil, ---@type table
content_window = nil, ---@type table|nil content_window = nil, ---@type table|nil
mouse_window_shift = { x = 0, y = 0 },
fg_bg = core.cpair(colors.white, colors.black), fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1), frame = core.gframe(1, 1, 1, 1),
children = {}, children = {},
@ -193,6 +196,10 @@ function element.new(args, child_offset_x, child_offset_y)
---@param offset_y integer y offset for mouse events ---@param offset_y integer y offset for mouse events
---@param next_y integer next line if no y was provided ---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y) function protected.prepare_template(offset_x, offset_y, next_y)
-- record offsets in case there is a reposition
self.offset_x = offset_x
self.offset_y = offset_y
-- get frame coordinates/size -- get frame coordinates/size
if args.gframe ~= nil then if args.gframe ~= nil then
protected.frame.x = args.gframe.x protected.frame.x = args.gframe.x
@ -344,6 +351,10 @@ function element.new(args, child_offset_x, child_offset_y)
-- handle this element having been unfocused -- handle this element having been unfocused
function protected.on_unfocused() end function protected.on_unfocused() end
-- handle this element having had a child focused
---@param child graphics_element
function protected.on_child_focused(child) end
-- handle this element having been shown -- handle this element having been shown
function protected.on_shown() end function protected.on_shown() end
@ -520,6 +531,13 @@ function element.new(args, child_offset_x, child_offset_y)
else args.parent.__focus_child(child) end else args.parent.__focus_child(child) end
end end
-- a child was focused, used to make sure it is actually visible to the user in the content frame
---@param child graphics_element
function public.__child_focused(child)
protected.on_child_focused(child)
if not self.is_root then args.parent.__child_focused(public) end
end
-- get a child element -- get a child element
---@nodiscard ---@nodiscard
---@param id element_id ---@param id element_id
@ -652,6 +670,7 @@ function element.new(args, child_offset_x, child_offset_y)
if args.can_focus and protected.enabled and not self.focused then if args.can_focus and protected.enabled and not self.focused then
self.focused = true self.focused = true
protected.on_focused() protected.on_focused()
if not self.is_root then args.parent.__child_focused(public) end
end end
end end
@ -666,7 +685,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- unfocus this element and all its children -- unfocus this element and all its children
function public.unfocus_all() function public.unfocus_all()
public.unfocus() public.unfocus()
for _, child in pairs(protected.children) do child.get().unfocus() end for _, child in pairs(protected.children) do child.get().unfocus_all() end
end end
-- custom recolor command, varies by element if implemented -- custom recolor command, varies by element if implemented
@ -681,7 +700,22 @@ function element.new(args, child_offset_x, child_offset_y)
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner -- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
---@param x integer x position relative to parent frame ---@param x integer x position relative to parent frame
---@param y integer y position relative to parent frame ---@param y integer y position relative to parent frame
function public.reposition(x, y) protected.window.reposition(x, y) end function public.reposition(x, y)
protected.window.reposition(x, y)
-- record position
self.position.x, self.position.y = protected.window.getPosition()
-- shift per parent child offset
self.position.x = self.position.x + self.offset_x
self.position.y = self.position.y + self.offset_y
-- calculate mouse event bounds
self.bounds.x1 = self.position.x
self.bounds.x2 = self.position.x + protected.frame.w - 1
self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + protected.frame.h - 1
end
-- FUNCTION CALLBACKS -- -- FUNCTION CALLBACKS --
@ -704,10 +738,11 @@ function element.new(args, child_offset_x, child_offset_y)
end end
local event_T = events.mouse_transposed(event, self.position.x, self.position.y) local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
-- handle the mouse event then pass to children
protected.handle_mouse(event_T) protected.handle_mouse(event_T)
for _, child in pairs(protected.children) do child.get().handle_mouse(event_T) end
-- shift child event if the content window has moved then pass to children
local c_event_T = events.mouse_transposed(event_T, protected.mouse_window_shift.x + 1, protected.mouse_window_shift.y + 1)
for _, child in pairs(protected.children) do child.get().handle_mouse(c_event_T) end
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
-- clicked out, unfocus this element and children -- clicked out, unfocus this element and children
public.unfocus_all() public.unfocus_all()

View File

@ -5,6 +5,8 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local ALIGN = core.ALIGN
local MOUSE_CLICK = core.events.MOUSE_CLICK local MOUSE_CLICK = core.events.MOUSE_CLICK
local KEY_CLICK = core.events.KEY_CLICK local KEY_CLICK = core.events.KEY_CLICK
@ -12,6 +14,7 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field text string button text ---@field text string button text
---@field callback function function to call on touch ---@field callback function function to call on touch
---@field min_width? integer text length if omitted ---@field min_width? integer text length if omitted
---@field alignment? ALIGN text align if min width > length
---@field active_fg_bg? cpair foreground/background colors when pressed ---@field active_fg_bg? cpair foreground/background colors when pressed
---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element ---@field parent graphics_element
@ -31,6 +34,7 @@ local function push_button(args)
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
local text_width = string.len(args.text) local text_width = string.len(args.text)
local alignment = args.alignment or ALIGN.CENTER
-- set automatic settings -- set automatic settings
args.can_focus = true args.can_focus = true
@ -41,9 +45,15 @@ local function push_button(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1 local h_pad = 1
local v_pad = math.floor(e.frame.h / 2) + 1 local v_pad = math.floor(e.frame.h / 2) + 1
if alignment == ALIGN.CENTER then
h_pad = math.floor((e.frame.w - text_width) / 2) + 1
elseif alignment == ALIGN.RIGHT then
h_pad = (e.frame.w - text_width) + 1
end
-- draw the button -- draw the button
function e.redraw() function e.redraw()
e.window.clear() e.window.clear()

View File

@ -133,6 +133,9 @@ local function number_field(args)
elseif type(args.min) == "number" and val < min then elseif type(args.min) == "number" and val < min then
e.value = "" .. min e.value = "" .. min
ifield.nav_start() ifield.nav_start()
else
e.value = "" .. val
ifield.nav_end()
end end
else else
e.value = "" e.value = ""

View File

@ -5,6 +5,7 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class listbox_args ---@class listbox_args
@ -33,6 +34,8 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@param args listbox_args ---@param args listbox_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function listbox(args) local function listbox(args)
args.can_focus = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
@ -128,7 +131,7 @@ local function listbox(args)
end end
e.w_set_cur(e.frame.w, i) e.w_set_cur(e.frame.w, i)
e.w_write(" ") if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
end end
e.w_set_bkg(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
@ -158,6 +161,9 @@ local function listbox(args)
scroll_frame.reposition(1, 1 + scroll_offset) scroll_frame.reposition(1, 1 + scroll_offset)
scroll_frame.setVisible(true) scroll_frame.setVisible(true)
-- shift mouse events
e.mouse_window_shift.y = scroll_offset
draw_bar() draw_bar()
end end
@ -219,6 +225,32 @@ local function listbox(args)
end end
end end
-- handle focus
e.on_focused = draw_bar
e.on_unfocused = draw_bar
-- handle a child in the list being focused, make sure it is visible
function e.on_child_focused(child)
for i = 1, #list do
local item = list[i] ---@type listbox_item
if item.e == child then
if (item.y + scroll_offset) <= 0 then
scroll_offset = 1 - item.y
update_positions()
draw_bar()
elseif (item.y + scroll_offset) == 1 then
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
update_positions()
draw_bar()
end
return
end
end
end
-- handle mouse interaction -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
function e.handle_mouse(event) function e.handle_mouse(event)
@ -226,23 +258,27 @@ local function listbox(args)
if event.type == MOUSE_CLICK.TAP then if event.type == MOUSE_CLICK.TAP then
if event.current.x == e.frame.w then if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up() scroll_up()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end if event.current.y == 1 then
draw_arrows(1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down() scroll_down()
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end if event.current.y == e.frame.h then
draw_arrows(-1)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end
end end
end end
elseif event.type == MOUSE_CLICK.DOWN then elseif event.type == MOUSE_CLICK.DOWN then
if event.current.x == e.frame.w then if event.current.x == e.frame.w then
if event.current.y == 1 or event.current.y < bar_bounds[1] then if event.current.y == 1 or event.current.y < bar_bounds[1] then
draw_arrows(1)
scroll_up() scroll_up()
if event.current.y == 1 then draw_arrows(1) end
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
draw_arrows(-1)
scroll_down() scroll_down()
if event.current.y == e.frame.h then draw_arrows(-1) end
else else
-- clicked on bar -- clicked on bar
holding_bar = true holding_bar = true
@ -274,6 +310,24 @@ local function listbox(args)
end end
end end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if event.key == keys.up then
scroll_up()
elseif event.key == keys.down then
scroll_down()
elseif event.key == keys.home then
scroll_offset = 0
update_positions()
elseif event.key == keys["end"] then
scroll_offset = max_down_scroll
update_positions()
end
end
end
-- element redraw -- element redraw
function e.redraw() function e.redraw()
draw_arrows(0) draw_arrows(0)

View File

@ -60,7 +60,7 @@ def make_manifest(size):
}, },
"files" : { "files" : {
# common files # common files
"system" : [ "initenv.lua", "startup.lua", "configure.lua" ], "system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
"common" : list_files("./scada-common"), "common" : list_files("./scada-common"),
"graphics" : list_files("./graphics"), "graphics" : list_files("./graphics"),
"lockbox" : list_files("./lockbox"), "lockbox" : list_files("./lockbox"),

View File

@ -5,6 +5,7 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local rsio = require("scada-common.rsio")
local core = require("graphics.core") local core = require("graphics.core")
@ -23,6 +24,7 @@ local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field") local TextField = require("graphics.elements.form.text_field")
local println = util.println local println = util.println
local tri = util.trinary
local cpair = core.cpair local cpair = core.cpair
@ -104,21 +106,24 @@ local tmp_cfg = {
---@class plc_config ---@class plc_config
local ini_cfg = {} local ini_cfg = {}
---@class plc_config
local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = { local fields = {
{ "Networked", "Networked" }, { "Networked", "Networked", false },
{ "UnitID", "Unit ID" }, { "UnitID", "Unit ID", 1 },
{ "EmerCoolEnable", "Emergency Coolant" }, { "EmerCoolEnable", "Emergency Coolant", false },
{ "EmerCoolSide", "Emergency Coolant Side" }, { "EmerCoolSide", "Emergency Coolant Side", nil },
{ "EmerCoolColor", "Emergency Coolant Color" }, { "EmerCoolColor", "Emergency Coolant Color", nil },
{ "SVR_Channel", "SVR Channel" }, { "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel" }, { "PLC_Channel", "PLC Channel", 16241 },
{ "ConnTimeout", "Connection Timeout" }, { "ConnTimeout", "Connection Timeout", 5 },
{ "TrustedRange", "Trusted Range" }, { "TrustedRange", "Trusted Range", 0 },
{ "AuthKey", "Facility Auth Key" }, { "AuthKey", "Facility Auth Key" , ""},
{ "LogMode", "Log Mode" }, { "LogMode", "Log Mode", log.MODE.APPEND },
{ "LogPath", "Log Path" }, { "LogPath", "Log Path", "/log.txt" },
{ "LogDebug","Log Debug Messages" } { "LogDebug","Log Debug Messages", false }
} }
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" } local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
@ -126,25 +131,6 @@ local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" } local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown } local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
local color_name_map = {
[colors.red] = "red",
[colors.orange] = "orange",
[colors.yellow] = "yellow",
[colors.lime] = "lime",
[colors.green] = "green",
[colors.cyan] = "cyan",
[colors.lightBlue] = "lightBlue",
[colors.blue] = "blue",
[colors.purple] = "purple",
[colors.magenta] = "magenta",
[colors.pink] = "pink",
[colors.white] = "white",
[colors.lightGray] = "lightGray",
[colors.gray] = "gray",
[colors.black] = "black",
[colors.brown] = "brown"
}
-- convert text representation to index -- convert text representation to index
---@param side string ---@param side string
local function side_to_idx(side) local function side_to_idx(side)
@ -163,20 +149,15 @@ end
-- load data from the settings file -- load data from the settings file
---@param target plc_config ---@param target plc_config
local function load_settings(target) ---@param raw boolean? true to not use default values
target.Networked = settings.get("Networked", false) local function load_settings(target, raw)
target.UnitID = settings.get("UnitID", 1) for _, v in pairs(fields) do settings.unset(v[1]) end
target.EmerCoolEnable = settings.get("EmerCoolEnable", false)
target.EmerCoolSide = settings.get("EmerCoolSide", nil) local loaded = settings.load("/reactor-plc.settings")
target.EmerCoolColor = settings.get("EmerCoolColor", nil)
target.SVR_Channel = settings.get("SVR_Channel", 16240) for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
target.PLC_Channel = settings.get("PLC_Channel", 16241)
target.ConnTimeout = settings.get("ConnTimeout", 5) return loaded
target.TrustedRange = settings.get("TrustedRange", 0)
target.AuthKey = settings.get("AuthKey", "")
target.LogMode = settings.get("LogMode", log.MODE.APPEND)
target.LogPath = settings.get("LogPath", "/log.txt")
target.LogDebug = settings.get("LogDebug", false)
end end
-- create the config view -- create the config view
@ -203,16 +184,16 @@ local function config_view(display)
local y_start = 5 local y_start = 5
TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."} TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
if tool_ctl.ask_config then if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text_align=CENTER,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)} TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 5 y_start = y_start + 5
end end
local function view_config() local function view_config()
tool_ctl.viewing_config = true tool_ctl.viewing_config = true
tool_ctl.gen_summary(ini_cfg) tool_ctl.gen_summary(settings_cfg)
tool_ctl.settings_apply.hide(true) tool_ctl.settings_apply.hide(true)
main_pane.set_value(5) main_pane.set_value(5)
end end
@ -239,10 +220,10 @@ local function config_view(display)
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}} local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
TextBox{parent=plc_cfg,x=1,y=2,height=1,text_align=CENTER,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)} TextBox{parent=plc_cfg,x=1,y=2,height=1,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
TextBox{parent=plc_c_1,x=1,y=1,height=1,text_align=CENTER,text="Would you like to set this PLC as networked?"} TextBox{parent=plc_c_1,x=1,y=1,height=1,text="Would you like to set this PLC as networked?"}
TextBox{parent=plc_c_1,x=1,y=3,height=4,text_align=CENTER,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg} TextBox{parent=plc_c_1,x=1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg}
local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)} local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
@ -254,13 +235,13 @@ local function config_view(display)
PushButton{parent=plc_c_1,x=1,y=14,min_width=6,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=plc_c_1,x=1,y=14,min_width=6,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=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_2,x=1,y=1,height=1,text_align=CENTER,text="Please enter the reactor unit ID for this PLC."} TextBox{parent=plc_c_2,x=1,y=1,height=1,text="Please enter the reactor unit ID for this PLC."}
TextBox{parent=plc_c_2,x=1,y=3,height=3,text_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg} TextBox{parent=plc_c_2,x=1,y=3,height=3,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg}
TextBox{parent=plc_c_2,x=1,y=6,height=1,text_align=CENTER,text="Unit #"} TextBox{parent=plc_c_2,x=1,y=6,height=1,text="Unit #"}
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=ini_cfg.UnitID,min=1,fg_bg=bw_fg_bg} local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=ini_cfg.UnitID,min=1,fg_bg=bw_fg_bg}
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_id() local function submit_id()
local unit_id = tonumber(u_id.get_value()) local unit_id = tonumber(u_id.get_value())
@ -274,8 +255,8 @@ local function config_view(display)
PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_3,x=1,y=1,height=4,text_align=CENTER,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC. "} TextBox{parent=plc_c_3,x=1,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC. "}
TextBox{parent=plc_c_3,x=1,y=6,height=5,text_align=CENTER,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg} TextBox{parent=plc_c_3,x=1,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg}
local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)} local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
@ -291,17 +272,17 @@ local function config_view(display)
PushButton{parent=plc_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=plc_c_4,x=1,y=1,height=1,text_align=CENTER,text="Emergency Coolant Redstone Output Side"} TextBox{parent=plc_c_4,x=1,y=1,height=1,text="Emergency Coolant Redstone Output Side"}
local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange} local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
TextBox{parent=plc_c_4,x=1,y=5,height=1,text_align=CENTER,text="Bundled Redstone Configuration"} TextBox{parent=plc_c_4,x=1,y=5,height=1,text="Bundled Redstone Configuration"}
local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end} local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end}
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),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} local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),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}
if ini_cfg.EmerCoolColor == nil then color.disable() end if ini_cfg.EmerCoolColor == nil then color.disable() end
local function submit_emcool() local function submit_emcool()
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()] tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
tmp_cfg.EmerCoolColor = color_options_map[color.get_value()] tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
next_from_plc() next_from_plc()
end end
@ -316,19 +297,19 @@ local function config_view(display)
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}} local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
TextBox{parent=net_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please set the network channels below."} TextBox{parent=net_c_1,x=1,y=1,height=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,text="Supervisor Channel"} TextBox{parent=net_c_1,x=1,y=8,height=1,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=9,height=4,text_align=CENTER,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=11,height=1,text_align=CENTER,text="PLC Channel"} TextBox{parent=net_c_1,x=1,y=11,height=1,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg} local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=12,height=4,text_align=CENTER,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_1,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels() local function submit_channels()
local svr_c = tonumber(svr_chan.get_value()) local svr_c = tonumber(svr_chan.get_value())
@ -350,16 +331,16 @@ local function config_view(display)
PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,height=1,text_align=CENTER,text="Connection Timeout"} TextBox{parent=net_c_2,x=1,y=1,height=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,fg_bg=bw_fg_bg} local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=2,height=2,text_align=CENTER,text="seconds (default 5)",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=3,height=4,text_align=CENTER,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,height=1,text_align=CENTER,text="Trusted Range"} TextBox{parent=net_c_2,x=1,y=8,height=1,text="Trusted Range"}
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_digits=20,allow_decimal=true,fg_bg=bw_fg_bg} local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_digits=20,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,height=4,text_align=CENTER,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_ct_tr() local function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value()) local timeout_val = tonumber(timeout.get_value())
@ -381,10 +362,10 @@ local function config_view(display)
PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,height=2,text_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_3,x=1,y=4,height=6,text_align=CENTER,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg} TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"} TextBox{parent=net_c_3,x=1,y=11,height=1,text="Facility Auth Key"}
local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
@ -394,7 +375,7 @@ local function config_view(display)
hide_key.set_value(true) hide_key.set_value(true)
censor_key(true) censor_key(true)
local key_err = TextBox{parent=net_c_3,x=8,y=14,height=1,width=35,text_align=LEFT,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local key_err = TextBox{parent=net_c_3,x=8,y=14,height=1,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth() local function submit_auth()
local v = key.get_value() local v = key.get_value()
@ -412,20 +393,20 @@ local function config_view(display)
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
TextBox{parent=log_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please configure logging below."} TextBox{parent=log_c_1,x=1,y=1,height=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,height=1,text_align=CENTER,text="Log File Mode"} TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"} TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log() local function submit_log()
if path.get_value() ~= "" then if path.get_value() ~= "" then
@ -457,7 +438,7 @@ local function config_view(display)
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,height=1,text_align=CENTER,text=" Summary",fg_bg=cpair(colors.black,colors.green)} TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
@ -483,6 +464,7 @@ local function config_view(display)
if settings.save("reactor-plc.settings") then if settings.save("reactor-plc.settings") then
load_settings(ini_cfg) load_settings(ini_cfg)
load_settings(settings_cfg, true)
try_set(networked, ini_cfg.Networked) try_set(networked, ini_cfg.Networked)
try_set(u_id, ini_cfg.UnitID) try_set(u_id, ini_cfg.UnitID)
@ -499,6 +481,8 @@ local function config_view(display)
try_set(path, ini_cfg.LogPath) try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug) try_set(en_dbg, ini_cfg.LogDebug)
tool_ctl.view_cfg.enable()
if tool_ctl.importing_legacy then if tool_ctl.importing_legacy then
tool_ctl.importing_legacy = false tool_ctl.importing_legacy = false
sum_pane.set_value(3) sum_pane.set_value(3)
@ -514,7 +498,7 @@ local function config_view(display)
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,height=1,text_align=CENTER,text="Settings saved!"} TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
local function go_home() local function go_home()
main_pane.set_value(1) main_pane.set_value(1)
@ -526,7 +510,7 @@ local function config_view(display)
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=2,text_align=CENTER,text="The old config.lua file will now be deleted, then the configurator will exit."} TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
local function delete_legacy() local function delete_legacy()
fs.delete("/reactor-plc/config.lua") fs.delete("/reactor-plc/config.lua")
@ -536,8 +520,7 @@ local function config_view(display)
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=5,text_align=CENTER,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=sum_c_4,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=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
@ -545,7 +528,7 @@ local function config_view(display)
local cl = Div{parent=changelog,x=2,y=4,width=49} local cl = Div{parent=changelog,x=2,y=4,width=49}
TextBox{parent=changelog,x=1,y=2,height=1,text_align=CENTER,text=" Config Change Log",fg_bg=bw_fg_bg} TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
@ -627,8 +610,8 @@ local function config_view(display)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end
if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
if f[1] == "EmerCoolColor" and raw ~= nil then val = color_name_map[raw] end if f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw) end
if val == "nil" then val = "n/a" end if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate alternate = not alternate
@ -665,9 +648,8 @@ end
---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration ---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration
function configurator.configure(ask_config) function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true tool_ctl.ask_config = ask_config == true
tool_ctl.has_config = settings.load("/reactor-plc.settings") tool_ctl.has_config = load_settings(ini_cfg)
load_settings(settings_cfg, true)
load_settings(ini_cfg)
reset_term() reset_term()
@ -685,18 +667,14 @@ function configurator.configure(ask_config)
-- handle event -- handle event
if event == "timer" then if event == "timer" then
-- notify timer callback dispatcher
tcd.handle(param1) tcd.handle(param1)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
-- handle a mouse event
local m_e = core.events.new_mouse_event(event, param1, param2, param3) local m_e = core.events.new_mouse_event(event, param1, param2, param3)
if m_e then display.handle_mouse(m_e) end if m_e then display.handle_mouse(m_e) end
elseif event == "char" or event == "key" or event == "key_up" then elseif event == "char" or event == "key" or event == "key_up" then
-- handle a key event
local k_e = core.events.new_key_event(event, param1, param2) local k_e = core.events.new_key_event(event, param1, param2)
if k_e then display.handle_key(k_e) end if k_e then display.handle_key(k_e) end
elseif event == "paste" then elseif event == "paste" then
-- handle a paste event
display.handle_paste(param1) display.handle_paste(param1)
end end

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.6.2" local R_PLC_VERSION = "v1.6.4"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -1,73 +0,0 @@
local rsio = require("scada-common.rsio")
local config = {}
-- supervisor comms channel
config.SVR_CHANNEL = 16240
-- RTU/MODBUS comms channel
config.RTU_CHANNEL = 16242
-- max trusted modem message distance (0 to disable check)
config.TRUSTED_RANGE = 0
-- time in seconds (>= 2) before assuming a remote device is no longer active
config.COMMS_TIMEOUT = 5
-- facility authentication key (do NOT use one of your passwords)
-- this enables verifying that messages are authentic
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
config.SOUNDER_VOLUME = 1.0
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0
-- true to log verbose debug messages
config.LOG_DEBUG = false
-- RTU peripheral devices (named: side/network device name)
config.RTU_DEVICES = {
{
name = "boilerValve_0",
index = 1,
for_reactor = 1
},
{
name = "turbineValve_0",
index = 1,
for_reactor = 1
}
}
-- RTU redstone interface definitions
config.RTU_REDSTONE = {
-- {
-- for_reactor = 1,
-- io = {
-- {
-- port = rsio.IO.WASTE_PO,
-- side = "top",
-- bundled_color = colors.red
-- },
-- {
-- port = rsio.IO.WASTE_PU,
-- side = "top",
-- bundled_color = colors.orange
-- },
-- {
-- port = rsio.IO.WASTE_POPL,
-- side = "top",
-- bundled_color = colors.yellow
-- },
-- {
-- port = rsio.IO.WASTE_AM,
-- side = "top",
-- bundled_color = colors.lime
-- }
-- }
-- }
}
return config

1505
rtu/configure.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -109,12 +109,10 @@ local function init(panel, units)
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 name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.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 name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=name,height=1} local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),height=1}
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end)
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
end)
-- assignment (unit # or facility) -- assignment (unit # or facility)
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)

View File

@ -5,7 +5,6 @@ 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 config = require("rtu.config")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
@ -17,6 +16,51 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
---@type rtu_config
local config = {}
rtu.config = config
-- load the RTU configuration
function rtu.load_config()
if not settings.load("/rtu.settings") then return false end
config.Peripherals = settings.get("Peripherals")
config.Redstone = settings.get("Redstone")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.SVR_Channel = settings.get("SVR_Channel")
config.RTU_Channel = settings.get("RTU_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
local cfv = util.new_validator()
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.RTU_Channel)
cfv.assert_type_int(config.ConnTimeout)
cfv.assert_min(config.ConnTimeout, 2)
cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey)
if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true)
end
cfv.assert_type_int(config.LogMode)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
return cfv.valid()
end
-- create a new RTU unit -- create a new RTU unit
---@nodiscard ---@nodiscard
function rtu.init_unit() function rtu.init_unit()
@ -175,7 +219,7 @@ function rtu.init_sounder(speaker)
function spkr_ctl.continue() function spkr_ctl.continue()
if spkr_ctl.playing then if spkr_ctl.playing then
if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then
local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SOUNDER_VOLUME) local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SpeakerVolume)
if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end
end end
end end
@ -203,11 +247,8 @@ end
---@nodiscard ---@nodiscard
---@param version string RTU version ---@param version string RTU version
---@param nic nic network interface device ---@param nic nic network interface device
---@param rtu_channel integer PLC comms channel
---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) function rtu.comms(version, nic, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
@ -218,13 +259,13 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
local insert = table.insert local insert = table.insert
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
nic.closeAll() nic.closeAll()
nic.open(rtu_channel) nic.open(config.RTU_Channel)
-- send a scada management packet -- send a scada management packet
---@param msg_type MGMT_TYPE ---@param msg_type MGMT_TYPE
@ -236,7 +277,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
m_pkt.make(msg_type, msg) m_pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(svr_channel, rtu_channel, s_pkt) nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@ -280,7 +321,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
function public.send_modbus(m_pkt) function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
nic.transmit(svr_channel, rtu_channel, s_pkt) nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@ -365,7 +406,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
if l_chan == rtu_channel then if l_chan == config.RTU_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num()

View File

@ -15,7 +15,7 @@ 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 config = require("rtu.config") local configure = require("rtu.configure")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
@ -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.6.6" local RTU_VERSION = "v1.7.0"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@ -40,27 +40,26 @@ local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
---------------------------------------- ----------------------------------------
-- config validation -- get configuration
---------------------------------------- ----------------------------------------
local cfv = util.new_validator() if not rtu.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
assert(rtu.load_config(), "failed to load valid RTU configuration")
else
assert(success, "RTU configuration error: " .. error)
end
end
cfv.assert_channel(config.SVR_CHANNEL) local config = rtu.config
cfv.assert_channel(config.RTU_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
cfv.assert_type_table(config.RTU_DEVICES)
cfv.assert_type_table(config.RTU_REDSTONE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
---------------------------------------- ----------------------------------------
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================") log.info("========================================")
log.info("BOOTING rtu.startup " .. RTU_VERSION) log.info("BOOTING rtu.startup " .. RTU_VERSION)
@ -85,8 +84,8 @@ local function main()
ppm.mount_all() ppm.mount_all()
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" then
network.init_mac(config.AUTH_KEY) network.init_mac(config.AuthKey)
end end
-- get modem -- get modem
@ -139,173 +138,174 @@ local function main()
local units = __shared_memory.rtu_sys.units local units = __shared_memory.rtu_sys.units
local rtu_redstone = config.RTU_REDSTONE local rtu_redstone = config.Redstone
local rtu_devices = config.RTU_DEVICES local rtu_devices = config.Peripherals
-- configure RTU gateway based on config file definitions -- configure RTU gateway based on settings file definitions
local function configure() local function sys_config()
-- redstone interfaces -- redstone interfaces
local rs_rtus = {}
-- go through redstone definitions list
for entry_idx = 1, #rtu_redstone do for entry_idx = 1, #rtu_redstone do
local rs_rtu = redstone_rtu.new() local entry = rtu_redstone[entry_idx] ---@type rtu_rs_definition
local io_table = rtu_redstone[entry_idx].io ---@type table local assignment
local io_reactor = rtu_redstone[entry_idx].for_reactor ---@type integer local for_reactor = entry.unit
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
-- CHECK: reactor ID must be >= to 1 if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
if (not util.is_int(io_reactor)) or (io_reactor < 0) then ---@cast for_reactor integer
local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0") assignment = "reactor unit " .. entry.unit
println(message) if rs_rtus[for_reactor] == nil then
log.fatal(message) log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit))
return false rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
-- CHECK: io table exists
if type(io_table) ~= "table" then
local message = util.c("configure> redstone entry #", entry_idx, " no IO table found")
println(message)
log.fatal(message)
return false
end
local capabilities = {}
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
local continue = true
-- CHECK: no duplicate entries
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then
-- duplicate entry
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
" with already defined redstone I/O")
println(message)
log.warning(message)
continue = false
break
end end
elseif entry.unit == nil then
assignment = "facility"
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
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
println(message)
log.fatal(message)
return false
end end
-- not a duplicate -- verify configuration
if continue then local valid = false
for i = 1, #io_table do if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
local valid = false valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
local conf = io_table[i] end
-- verify configuration local rs_rtu = rs_rtus[for_reactor].rtu
if rsio.is_valid_port(conf.port) and rsio.is_valid_side(conf.side) then local capabilities = rs_rtus[for_reactor].capabilities
if conf.bundled_color then
valid = rsio.is_color(conf.bundled_color)
else
valid = true
end
end
if not valid then if not valid then
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx, local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
" (for reactor ", io_reactor, ")") println(message)
log.fatal(message)
return false
else
-- link redstone in RTU
local mode = rsio.get_io_mode(entry.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
println(message) println(message)
log.fatal(message) log.warning(message)
return false
else else
-- link redstone in RTU rs_rtu.link_di(entry.side, entry.color)
local mode = rsio.get_io_mode(conf.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.port) then
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_di(conf.side, conf.bundled_color)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(conf.side, conf.bundled_color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.port) then
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_ai(conf.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(conf.side)
else
-- should be unreachable code, we already validated ports
log.error("configure> fell through if chain attempting to identify IO mode", true)
println("configure> encountered a software error, check logs")
return false
end
table.insert(capabilities, conf.port)
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.port),
" (", conf.side, ") for reactor ", io_reactor))
end end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(entry.side, entry.color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
println(message)
log.warning(message)
else
rs_rtu.link_ai(entry.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(entry.side)
else
-- 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)
println("sys_config> encountered a software error, check logs")
return false
end end
---@class rtu_unit_registry_entry table.insert(capabilities, entry.port)
local unit = {
uid = 0, ---@type integer
name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = entry_idx, ---@type integer
reactor = io_reactor, ---@type integer
device = capabilities, ---@type table use device field for redstone ports
is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
}
table.insert(units, unit) log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
local for_message = "facility"
if io_reactor > 0 then
for_message = util.c("reactor ", io_reactor)
end
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end end
end end
-- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do
---@class rtu_unit_registry_entry
local unit = {
uid = 0, ---@type integer
name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = false, ---@type integer|false
reactor = for_reactor, ---@type integer
device = def.capabilities, ---@type table use device field for redstone ports
is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
rtu = def.rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
}
table.insert(units, unit)
local for_message = "facility"
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))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end
-- mounted peripherals -- mounted peripherals
for i = 1, #rtu_devices do for i = 1, #rtu_devices do
local name = rtu_devices[i].name local entry = rtu_devices[i] ---@type rtu_peri_definition
local index = rtu_devices[i].index local name = entry.name
local for_reactor = rtu_devices[i].for_reactor local index = entry.index
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
-- CHECK: name is a string -- CHECK: name is a string
if type(name) ~= "string" then if type(name) ~= "string" then
local message = util.c("configure> device entry #", i, ": device ", name, " isn't a string") local message = util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
end end
-- CHECK: index is an integer >= 1 -- CHECK: index type
if (not util.is_int(index)) or (index <= 0) then if (index ~= nil) and (not util.is_int(index)) then
local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1") local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
end end
-- CHECK: index range
local function validate_index(min, max)
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min)
if max ~= nil then message = util.c(message, " and <= ", max) end
println(message)
log.fatal(message)
return false
else return true end
end
-- CHECK: reactor is an integer >= 0 -- CHECK: reactor is an integer >= 0
if (not util.is_int(for_reactor)) or (for_reactor < 0) then local function validate_assign(for_facility)
local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0") if for_facility and for_reactor ~= 0 then
println(message) local message = util.c("sys_config> device entry #", i, ": must only be for the facility")
log.fatal(message) println(message)
return false log.fatal(message)
return false
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
local message = util.c("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
println(message)
log.fatal(message)
return false
else return true end
end end
local device = ppm.get_periph(name) local device = ppm.get_periph(name)
@ -318,7 +318,7 @@ local function main()
local faulted = nil ---@type boolean|nil local faulted = nil ---@type boolean|nil
if device == nil then if device == nil then
local message = util.c("configure> '", name, "' not found, using placeholder") local message = util.c("sys_config> '", name, "' not found, using placeholder")
println(message) println(message)
log.warning(message) log.warning(message)
@ -330,70 +330,93 @@ local function main()
if type == "boilerValve" then if type == "boilerValve" then
-- boiler multiblock -- boiler multiblock
if not validate_index(1, 2) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface, faulted = boilerv_rtu.new(device) rtu_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
return false return false
end end
elseif type == "turbineValve" then elseif type == "turbineValve" then
-- turbine multiblock -- turbine multiblock
if not validate_index(1, 3) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface, faulted = turbinev_rtu.new(device) rtu_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
return false return false
end end
elseif type == "dynamicValve" then elseif type == "dynamicValve" then
-- dynamic tank multiblock -- dynamic tank multiblock
if entry.unit == nil then
if not validate_index(1, 4) then return false end
if not validate_assign(true) then return false end
else
if not validate_index(1, 1) then return false end
if not validate_assign() then return false end
end
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
rtu_iface, faulted = dynamicv_rtu.new(device) rtu_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed dynamic tank multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
return false return false
end end
elseif type == "inductionPort" then elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface, faulted = imatrix_rtu.new(device) rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
return false return false
end end
elseif type == "spsPort" then elseif type == "spsPort" then
-- SPS multiblock -- SPS multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.SPS rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface, faulted = sps_rtu.new(device) rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock")) log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
return false return false
end end
elseif type == "solarNeutronActivator" then elseif type == "solarNeutronActivator" then
-- SNA -- SNA
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.SNA rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device) rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" then elseif type == "environmentDetector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
if not validate_index(1) then return false end
if not validate_assign(entry.unit == nil) then return false end
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface, faulted = envd_rtu.new(device) rtu_iface, faulted = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then elseif type == ppm.VIRTUAL_DEVICE_TYPE then
@ -401,7 +424,7 @@ local function main()
rtu_type = RTU_UNIT_TYPE.VIRTUAL rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface() rtu_iface = rtu.init_unit().interface()
else else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")") local message = util.c("sys_config> device '", name, "' is not a known type (", type, ")")
println_ts(message) println_ts(message)
log.fatal(message) log.fatal(message)
return false return false
@ -409,12 +432,12 @@ local function main()
if is_multiblock then if is_multiblock then
if not formed then if not formed then
log.info(util.c("configure> device '", name, "' is not formed")) log.info(util.c("sys_config> device '", name, "' is not formed"))
elseif faulted then elseif faulted then
-- sometimes there is a race condition on server boot where it reports formed, but -- sometimes there is a race condition on server boot where it reports formed, but
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later -- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
formed = false formed = false
log.warning(util.c("configure> device '", name, "' is formed, but initialization had one or more faults: marked as unformed")) log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
end end
end end
@ -423,7 +446,7 @@ local function main()
uid = 0, ---@type integer uid = 0, ---@type integer
name = name, ---@type string name = name, ---@type string
type = rtu_type, ---@type RTU_UNIT_TYPE type = rtu_type, ---@type RTU_UNIT_TYPE
index = index, ---@type integer index = index or false, ---@type integer|false
reactor = for_reactor, ---@type integer reactor = for_reactor, ---@type integer
device = device, ---@type table device = device, ---@type table
is_multiblock = is_multiblock, ---@type boolean is_multiblock = is_multiblock, ---@type boolean
@ -444,7 +467,7 @@ local function main()
for_message = util.c("reactor ", for_reactor) for_message = util.c("reactor ", for_reactor)
end end
log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message)) log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
rtu_unit.uid = #units rtu_unit.uid = #units
@ -465,7 +488,6 @@ 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
-- we made it through all that trusting-user-to-write-a-config-file chaos
return true return true
end end
@ -475,9 +497,9 @@ local function main()
local rtu_state = __shared_memory.rtu_state local rtu_state = __shared_memory.rtu_state
log.debug("boot> running configure()") log.debug("boot> running sys_config()")
if configure() then if sys_config() then
-- start UI -- start UI
local message local message
rtu_state.fp_ok, message = renderer.try_start_ui(units) rtu_state.fp_ok, message = renderer.try_start_ui(units)
@ -502,12 +524,11 @@ local function main()
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders) databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
-- start connection watchdog -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, config.RTU_CHANNEL, config.SVR_CHANNEL, smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
config.TRUSTED_RANGE, smem_sys.conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
-- init threads -- init threads

View File

@ -28,6 +28,147 @@ local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
---@param smem rtu_shared_memory
---@param println_ts function
---@param iface string
---@param type string
---@param device table
---@param unit rtu_unit_registry_entry
local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
local sys = smem.rtu_sys
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == iface then
local resend_advert, faulted, unknown, invalid = false, false, false, false
local function fail(msg)
invalid = true
log.error(msg .. " in config")
end
-- found, re-link
unit.device = device
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("boiler '", unit.name, "' cannot init, not assigned to a valid unit")) end
if (unit.index == false) or unit.index < 1 or unit.index > 2 then fail(util.c("boiler '", unit.name, "' cannot init, invalid index provided")) end
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("turbine '", unit.name, "' cannot init, not assigned to a valid unit")) end
if (unit.index == false) or unit.index < 1 or unit.index > 3 then fail(util.c("turbine '", unit.name, "' cannot init, invalid index provided")) end
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "dynamicValve" then
-- dynamic tank multiblock
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("dynamic tank '", unit.name, "' cannot init, no valid assignment provided")) end
if (unit.reactor == 0 and ((unit.index == false) or unit.index < 1 or unit.index > 4)) or
(unit.reactor > 0 and unit.index ~= 1) then
fail(util.c("dynamic tank '", unit.name, "' cannot init, invalid index provided"))
end
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end
unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
if unit.reactor ~= 0 then fail(util.c("SPS '", unit.name, "' cannot init, not assigned to facility")) end
unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end
if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
databus.tx_unit_hw_type(unit.uid, unit.type)
end
-- if disconnected on startup, config wouldn't have been validated
-- checking now that it has connected; the config isn't valid, so don't connect it
if invalid then
unit.hw_state = UNIT_HW_STATE.OFFLINE
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
return
end
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end
if unit.is_multiblock then
unit.hw_state = UNIT_HW_STATE.UNFORMED
if unit.formed == false then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
end
elseif faulted then
unit.hw_state = UNIT_HW_STATE.FAULTED
elseif not unknown then
unit.hw_state = UNIT_HW_STATE.OK
else
unit.hw_state = UNIT_HW_STATE.OFFLINE
end
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
if not unknown then
unit.modbus_io = modbus.new(unit.rtu, true)
local type_name = types.rtu_type_to_string(unit.type)
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
println_ts(message)
log.info(message)
if resend_advert then
sys.rtu_comms.send_advertisement(sys.units)
else
sys.rtu_comms.send_remounted(unit.uid)
end
end
end
end
-- main thread -- main thread
---@nodiscard ---@nodiscard
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
@ -180,102 +321,7 @@ function threads.thread__main(smem)
else else
-- relink lost peripheral to correct unit entry -- relink lost peripheral to correct unit entry
for i = 1, #units do for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry handle_unit_mount(smem, println_ts, param1, type, device, units[i])
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == param1 then
local resend_advert = false
local faulted = false
local unknown = false
-- found, re-link
unit.device = device
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
databus.tx_unit_hw_type(unit.uid, unit.type)
end
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end
if unit.is_multiblock then
unit.hw_state = UNIT_HW_STATE.UNFORMED
if unit.formed == false then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
end
elseif faulted then
unit.hw_state = UNIT_HW_STATE.FAULTED
elseif not unknown then
unit.hw_state = UNIT_HW_STATE.OK
else
unit.hw_state = UNIT_HW_STATE.OFFLINE
end
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
if not unknown then
unit.modbus_io = modbus.new(unit.rtu, true)
local type_name = types.rtu_type_to_string(unit.type)
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
println_ts(message)
log.info(message)
if resend_advert then
rtu_comms.send_advertisement(units)
else
rtu_comms.send_remounted(unit.uid)
end
end
end
end end
end end
end end

View File

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

View File

@ -13,6 +13,7 @@ local MODE = { APPEND = 0, NEW = 1 }
log.MODE = MODE log.MODE = MODE
local logger = { local logger = {
not_ready = true,
path = "/log.txt", path = "/log.txt",
mode = MODE.APPEND, mode = MODE.APPEND,
debug = false, debug = false,
@ -32,6 +33,8 @@ local free_space = fs.getFreeSpace
-- private log write function -- private log write function
---@param msg string ---@param msg string
local function _log(msg) local function _log(msg)
if logger.not_ready then return end
local out_of_space = false local out_of_space = false
local time_stamp = os.date("[%c] ") local time_stamp = os.date("[%c] ")
local stamped = time_stamp .. util.strval(msg) local stamped = time_stamp .. util.strval(msg)
@ -94,6 +97,8 @@ function log.init(path, write_mode, include_debug, dmesg_redirect)
else else
logger.dmesg_out = term.current() logger.dmesg_out = term.current()
end end
logger.not_ready = false
end end
-- close the log file handle -- close the log file handle

View File

@ -161,10 +161,10 @@ local function peri_init(iface)
setmetatable(self.device, mt) setmetatable(self.device, mt)
return { ---@class ppm_entry
type = self.type, local entry = { type = self.type, dev = self.device }
dev = self.device
} return entry
end end
---------------------- ----------------------
@ -310,7 +310,11 @@ function ppm.list_avail() return peripheral.getNames() end
-- list mounted peripherals -- list mounted peripherals
---@nodiscard ---@nodiscard
---@return table mounts ---@return table mounts
function ppm.list_mounts() return ppm_sys.mounts end function ppm.list_mounts()
local list = {}
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
return list
end
-- get a mounted peripheral side/interface by device table -- get a mounted peripheral side/interface by device table
---@nodiscard ---@nodiscard

View File

@ -82,46 +82,59 @@ rsio.IO_LVL = IO_LVL
rsio.IO_DIR = IO_DIR rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE rsio.IO_MODE = IO_MODE
rsio.IO = IO_PORT rsio.IO = IO_PORT
rsio.NUM_PORTS = IO_PORT.U_EMER_COOL
-- self checks
local dup_chk = {}
for _, v in pairs(IO_PORT) do
assert(dup_chk[v] ~= true, "duplicate in port list")
dup_chk[v] = true
end
assert(#dup_chk == rsio.NUM_PORTS, "port list malformed")
--#endregion --#endregion
--#region Utility Functions --#region Utility Functions
local PORT_NAMES = {
"F_SCRAM",
"F_ACK",
"R_SCRAM",
"R_RESET",
"R_ENABLE",
"U_ACK",
"F_ALARM",
"F_ALARM_ANY",
"WASTE_PU",
"WASTE_PO",
"WASTE_POPL",
"WASTE_AM",
"R_ACTIVE",
"R_AUTO_CTRL",
"R_SCRAMMED",
"R_AUTO_SCRAM",
"R_HIGH_DMG",
"R_HIGH_TEMP",
"R_LOW_COOLANT",
"R_EXCESS_HC",
"R_EXCESS_WS",
"R_INSUFF_FUEL",
"R_PLC_FAULT",
"R_PLC_TIMEOUT",
"U_ALARM",
"U_EMER_COOL"
}
assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect")
-- port to string -- port to string
---@nodiscard ---@nodiscard
---@param port IO_PORT ---@param port IO_PORT
function rsio.to_string(port) function rsio.to_string(port)
local names = { if util.is_int(port) and port > 0 and port <= #PORT_NAMES then
"F_SCRAM", return PORT_NAMES[port]
"F_ACK",
"R_SCRAM",
"R_RESET",
"R_ENABLE",
"U_ACK",
"F_ALARM",
"F_ALARM_ANY",
"WASTE_PU",
"WASTE_PO",
"WASTE_POPL",
"WASTE_AM",
"R_ACTIVE",
"R_AUTO_CTRL",
"R_SCRAMMED",
"R_AUTO_SCRAM",
"R_HIGH_DMG",
"R_HIGH_TEMP",
"R_LOW_COOLANT",
"R_EXCESS_HC",
"R_EXCESS_WS",
"R_INSUFF_FUEL",
"R_PLC_FAULT",
"R_PLC_TIMEOUT",
"U_ALARM",
"U_EMER_COOL"
}
if util.is_int(port) and port > 0 and port <= #names then
return names[port]
else else
return "UNKNOWN" return "UNKNOWN"
end end
@ -266,12 +279,24 @@ end
-- check if a color is a valid single color -- check if a color is a valid single color
---@nodiscard ---@nodiscard
---@param color integer ---@param color any
---@return boolean valid ---@return boolean valid
function rsio.is_color(color) function rsio.is_color(color)
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0) return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0)
end end
-- color to string
---@nodiscard
---@param color color
---@return string
function rsio.color_name(color)
local color_name_map = { [colors.red] = "red", [colors.orange] = "orange", [colors.yellow] = "yellow", [colors.lime] = "lime", [colors.green] = "green", [colors.cyan] = "cyan", [colors.lightBlue] = "lightBlue", [colors.blue] = "blue", [colors.purple] = "purple", [colors.magenta] = "magenta", [colors.pink] = "pink", [colors.white] = "white", [colors.lightGray] = "lightGray", [colors.gray] = "gray", [colors.black] = "black", [colors.brown] = "brown" }
if rsio.is_color(color) then
return color_name_map[color]
else return "unknown" end
end
--#endregion --#endregion
--#region Digital I/O --#region Digital I/O

View File

@ -63,7 +63,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@class rtu_advertisement ---@class rtu_advertisement
---@field type RTU_UNIT_TYPE ---@field type RTU_UNIT_TYPE
---@field index integer ---@field index integer|false
---@field reactor integer ---@field reactor integer
---@field rsio table|nil ---@field rsio table|nil
@ -252,6 +252,14 @@ types.ALARM_STATE_NAMES = {
-- STRING TYPES -- -- STRING TYPES --
--#region --#region
---@alias side
---|"top"
---|"bottom"
---|"left"
---|"right"
---|"front"
---|"back"
---@alias os_event ---@alias os_event
---| "alarm" ---| "alarm"
---| "char" ---| "char"

View File

@ -14,11 +14,15 @@ local print = print
local tostring = tostring local tostring = tostring
local type = type local type = type
local t_concat = table.concat
local t_insert = table.insert
local t_unpack = table.unpack
---@class util ---@class util
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.1.5" util.version = "1.1.9"
util.TICK_TIME_S = 0.05 util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50 util.TICK_TIME_MS = 50
@ -70,7 +74,7 @@ function util.strval(val)
if t == "string" then return val end if t == "string" then return val end
-- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables) -- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables)
if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then
return table.concat{"[", tostring(val), "]"} return t_concat{"[", tostring(val), "]"}
else return tostring(val) end else return tostring(val) end
end end
@ -90,7 +94,7 @@ function util.pad(str, n)
local lpad = math.floor((n - len) / 2) local lpad = math.floor((n - len) / 2)
local rpad = (n - len) - lpad local rpad = (n - len) - lpad
return table.concat{util.spaces(lpad), str, util.spaces(rpad)} return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
end end
-- wrap a string into a table of lines -- wrap a string into a table of lines
@ -109,8 +113,9 @@ function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
---@diagnostic disable-next-line: unused-vararg ---@diagnostic disable-next-line: unused-vararg
function util.concat(...) function util.concat(...)
local strings = {} local strings = {}
for i = 1, #arg do strings[i] = util.strval(arg[i]) end ---@diagnostic disable-next-line: undefined-field
return table.concat(strings) for i = 1, arg.n do strings[i] = util.strval(arg[i]) end
return t_concat(strings)
end end
-- alias -- alias
@ -121,7 +126,7 @@ util.c = util.concat
---@param format string ---@param format string
---@vararg any ---@vararg any
---@diagnostic disable-next-line: unused-vararg ---@diagnostic disable-next-line: unused-vararg
function util.sprintf(format, ...) return string.format(format, table.unpack(arg)) end function util.sprintf(format, ...) return string.format(format, t_unpack(arg)) end
-- luacheck: unused args -- luacheck: unused args
@ -185,7 +190,7 @@ function util.mov_avg(length, default)
---@param x number value ---@param x number value
function public.reset(x) function public.reset(x)
data = {} data = {}
for _ = 1, length do table.insert(data, x) end for _ = 1, length do t_insert(data, x) end
end end
-- record a new value -- record a new value
@ -482,6 +487,7 @@ function util.new_validator()
function public.assert_type_str(value) valid = valid and type(value) == "string" end function public.assert_type_str(value) valid = valid and type(value) == "string" end
function public.assert_type_table(value) valid = valid and type(value) == "table" end function public.assert_type_table(value) valid = valid and type(value) == "table" end
function public.assert(check) valid = valid and (check == true) end
function public.assert_eq(check, expect) valid = valid and check == expect end function public.assert_eq(check, expect) valid = valid and check == expect end
function public.assert_min(check, min) valid = valid and check >= min end function public.assert_min(check, min) valid = valid and check >= min end
function public.assert_min_ex(check, min) valid = valid and check > min end function public.assert_min_ex(check, min) valid = valid and check > min end

View File

@ -1,30 +1,28 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local BOOTLOADER_VERSION = "0.3"
local println = util.println local println = util.println
local println_ts = util.println_ts
local BOOTLOADER_VERSION = "0.4"
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION) println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
println("BOOT> SCANNING FOR APPLICATIONS...")
local exit_code ---@type boolean local exit_code
println_ts("BOOT> SCANNING FOR APPLICATIONS...")
if fs.exists("reactor-plc/startup.lua") then if fs.exists("reactor-plc/startup.lua") then
println("BOOT> FOUND REACTOR PLC CODE: EXEC STARTUP") println("BOOT> EXEC REACTOR PLC STARTUP")
exit_code = shell.execute("reactor-plc/startup") exit_code = shell.execute("reactor-plc/startup")
elseif fs.exists("rtu/startup.lua") then elseif fs.exists("rtu/startup.lua") then
println("BOOT> FOUND RTU CODE: EXEC STARTUP") println("BOOT> EXEC RTU STARTUP")
exit_code = shell.execute("rtu/startup") exit_code = shell.execute("rtu/startup")
elseif fs.exists("supervisor/startup.lua") then elseif fs.exists("supervisor/startup.lua") then
println("BOOT> FOUND SUPERVISOR CODE: EXEC STARTUP") println("BOOT> EXEC SUPERVISOR STARTUP")
exit_code = shell.execute("supervisor/startup") exit_code = shell.execute("supervisor/startup")
elseif fs.exists("coordinator/startup.lua") then elseif fs.exists("coordinator/startup.lua") then
println("BOOT> FOUND COORDINATOR CODE: EXEC STARTUP") println("BOOT> EXEC COORDINATOR STARTUP")
exit_code = shell.execute("coordinator/startup") exit_code = shell.execute("coordinator/startup")
elseif fs.exists("pocket/startup.lua") then elseif fs.exists("pocket/startup.lua") then
println("BOOT> FOUND POCKET CODE: EXEC STARTUP") println("BOOT> EXEC POCKET STARTUP")
exit_code = shell.execute("pocket/startup") exit_code = shell.execute("pocket/startup")
else else
println("BOOT> NO SCADA STARTUP FOUND") println("BOOT> NO SCADA STARTUP FOUND")
@ -32,6 +30,6 @@ else
return false return false
end end
if not exit_code then println_ts("BOOT> APPLICATION CRASHED") end if not exit_code then println("BOOT> APPLICATION CRASHED") end
return exit_code return exit_code

View File

@ -232,9 +232,7 @@ function facility.new(num_reactors, cooling_conf)
-- link a redstone RTU session -- link a redstone RTU session
---@param rs_unit unit_session ---@param rs_unit unit_session
function public.add_redstone(rs_unit) function public.add_redstone(rs_unit) table.insert(self.redstone, rs_unit) end
table.insert(self.redstone, rs_unit)
end
-- link an induction matrix RTU session -- link an induction matrix RTU session
---@param imatrix unit_session ---@param imatrix unit_session
@ -258,23 +256,11 @@ function facility.new(num_reactors, cooling_conf)
-- link a dynamic tank RTU session -- link a dynamic tank RTU session
---@param dynamic_tank unit_session ---@param dynamic_tank unit_session
---@return boolean linked dynamic tank accepted (max 1) function public.add_tank(dynamic_tank) table.insert(self.tanks, dynamic_tank) end
function public.add_tank(dynamic_tank)
if #self.tanks == 0 then
table.insert(self.tanks, dynamic_tank)
return true
else return false end
end
-- link an environment detector RTU session -- link an environment detector RTU session
---@param envd unit_session ---@param envd unit_session
---@return boolean linked environment detector accepted (max 1) function public.add_envd(envd) table.insert(self.envd, envd) end
function public.add_envd(envd)
if #self.envd == 0 then
table.insert(self.envd, envd)
return true
else return false end
end
-- purge devices associated with the given RTU session ID -- purge devices associated with the given RTU session ID
---@param session integer RTU session ID ---@param session integer RTU session ID
@ -643,11 +629,16 @@ function facility.new(num_reactors, cooling_conf)
end end
-- check for facility radiation -- check for facility radiation
if self.envd[1] ~= nil then if #self.envd > 0 then
local envd = self.envd[1] ---@type unit_session local max_rad = 0
local e_db = envd.get_db() ---@type envd_session_db
astatus.radiation = e_db.radiation_raw > ALARM_LIMS.FAC_HIGH_RAD for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session
local e_db = envd.get_db() ---@type envd_session_db
if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end
end
astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD
else else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming -- don't clear, if it is true then we lost it with high radiation, so just keep alarming
-- operator can restart the system or hit the stop/reset button -- operator can restart the system or hit the stop/reset button
@ -1093,22 +1084,22 @@ function facility.new(num_reactors, cooling_conf)
build.induction = {} build.induction = {}
for i = 1, #self.induction do for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session local matrix = self.induction[i] ---@type unit_session
build.induction[matrix.get_device_idx()] = { matrix.get_db().formed, matrix.get_db().build } build.induction[i] = { matrix.get_db().formed, matrix.get_db().build }
end end
end end
if all or type == RTU_UNIT_TYPE.SPS then if all or type == RTU_UNIT_TYPE.SPS then
build.sps = {} build.sps = {}
for i = 1, #self.sps do for i = 1, #self.sps do
local sps = self.sps[i] ---@type unit_session local sps = self.sps[i] ---@type unit_session
build.sps[sps.get_device_idx()] = { sps.get_db().formed, sps.get_db().build } build.sps[i] = { sps.get_db().formed, sps.get_db().build }
end end
end end
if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
build.tanks = {} build.tanks = {}
for i = 1, #self.tanks do for i = 1, #self.tanks do
local tank = self.tanks[i] ---@type unit_session local tank = self.tanks[i] ---@type unit_session
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build } build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
end end
end end
@ -1160,7 +1151,7 @@ function facility.new(num_reactors, cooling_conf)
for i = 1, #self.induction do for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session local matrix = self.induction[i] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db local db = matrix.get_db() ---@type imatrix_session_db
status.induction[matrix.get_device_idx()] = { matrix.is_faulted(), db.formed, db.state, db.tanks } status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
end end
-- status of sps -- status of sps
@ -1168,7 +1159,7 @@ function facility.new(num_reactors, cooling_conf)
for i = 1, #self.sps do for i = 1, #self.sps do
local sps = self.sps[i] ---@type unit_session local sps = self.sps[i] ---@type unit_session
local db = sps.get_db() ---@type sps_session_db local db = sps.get_db() ---@type sps_session_db
status.sps[sps.get_device_idx()] = { sps.is_faulted(), db.formed, db.state, db.tanks } status.sps[i] = { sps.is_faulted(), db.formed, db.state, db.tanks }
end end
-- status of dynamic tanks -- status of dynamic tanks
@ -1180,10 +1171,11 @@ function facility.new(num_reactors, cooling_conf)
end end
-- radiation monitors (environment detectors) -- radiation monitors (environment detectors)
status.rad_mon = {} status.envds = {}
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation } local db = envd.get_db() ---@type envd_session_db
status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw }
end end
return status return status

View File

@ -100,7 +100,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- validate unit advertisement -- validate unit advertisement
local advert_validator = util.new_validator() local advert_validator = util.new_validator()
advert_validator.assert_type_int(unit_advert.index) 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 if u_type == RTU_UNIT_TYPE.REDSTONE then
@ -108,7 +108,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
end end
if advert_validator.valid() then if advert_validator.valid() then
advert_validator.assert_min(unit_advert.index, 1) if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end
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)
if not advert_validator.valid() then u_type = false end if not advert_validator.valid() then u_type = false end

View File

@ -37,13 +37,16 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function boilerv.new(session_id, unit_id, advert, out_queue) function boilerv.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
log.error("attempt to instantiate boilerv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate boilerv RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate boilerv RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").boilerv(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").boilerv(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -49,13 +49,16 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function dynamicv.new(session_id, unit_id, advert, out_queue) function dynamicv.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
log.error("attempt to instantiate dynamicv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate dynamicv RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate dynamicv RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").dynamicv(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").dynamicv(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -28,13 +28,16 @@ local PERIODICS = {
---@param advert rtu_advertisement ---@param advert rtu_advertisement
---@param out_queue mqueue ---@param out_queue mqueue
function envd.new(session_id, unit_id, advert, out_queue) function envd.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
log.error("attempt to instantiate envd RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate envd RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate envd RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").envd(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").envd(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -37,13 +37,13 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function imatrix.new(session_id, unit_id, advert, out_queue) function imatrix.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
log.error("attempt to instantiate imatrix RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate imatrix RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").imatrix(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").imatrix[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -52,12 +52,11 @@ local PERIODICS = {
function redstone.new(session_id, unit_id, advert, out_queue) function redstone.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
log.error("attempt to instantiate redstone RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate redstone RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
-- for redstone, use unit ID not device index local log_tag = util.c("session.rtu(", session_id, ").redstone[@", unit_id, "]: ")
local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. unit_id .. "): "
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -36,11 +36,11 @@ local PERIODICS = {
function sna.new(session_id, unit_id, advert, out_queue) function sna.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.SNA then if advert.type ~= RTU_UNIT_TYPE.SNA then
log.error("attempt to instantiate sna RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate sna RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").sna(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").sna[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -39,11 +39,11 @@ local PERIODICS = {
function sps.new(session_id, unit_id, advert, out_queue) function sps.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.SPS then if advert.type ~= RTU_UNIT_TYPE.SPS then
log.error("attempt to instantiate sps RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate sps RTU for type " .. types.rtu_type_to_string(advert.type))
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").sps(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").sps[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -49,13 +49,16 @@ local PERIODICS = {
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function turbinev.new(session_id, unit_id, advert, out_queue) function turbinev.new(session_id, unit_id, advert, out_queue)
-- type check -- checks
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
log.error("attempt to instantiate turbinev RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.") log.error("attempt to instantiate turbinev RTU for type " .. types.rtu_type_to_string(advert.type))
return nil
elseif not util.is_int(advert.index) then
log.error("attempt to instantiate turbinev RTU without index")
return nil return nil
end end
local log_tag = "session.rtu(" .. session_id .. ").turbinev(" .. advert.index .. "): " local log_tag = util.c("session.rtu(", session_id, ").turbinev(", advert.index, ")[@", unit_id, "]: ")
local self = { local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),

View File

@ -152,7 +152,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
function public.get_unit_id() return unit_id end function public.get_unit_id() return unit_id end
-- get the device index -- get the device index
---@nodiscard ---@nodiscard
function public.get_device_idx() return self.device_index end function public.get_device_idx() return self.device_index or 0 end
-- get the reactor ID -- get the reactor ID
---@nodiscard ---@nodiscard
function public.get_reactor() return self.reactor end function public.get_reactor() return self.reactor end

View File

@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.0.9" local SUPERVISOR_VERSION = "v1.1.0"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -864,10 +864,11 @@ function unit.new(reactor_id, num_boilers, num_turbines)
status.sna = { #self.snas, public.get_sna_rate(), total_peak } status.sna = { #self.snas, public.get_sna_rate(), total_peak }
-- radiation monitors (environment detectors) -- radiation monitors (environment detectors)
status.rad_mon = {} status.envds = {}
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation } local db = envd.get_db() ---@type envd_session_db
status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw }
end end
return status return status

View File

@ -47,8 +47,9 @@ function logic.update_annunciator(self)
local num_boilers = self.num_boilers local num_boilers = self.num_boilers
local num_turbines = self.num_turbines local num_turbines = self.num_turbines
local annunc = self.db.annunciator
self.db.annunciator.RCSFault = false annunc.RCSFault = false
-- variables for boiler, or reactor if no boilers used -- variables for boiler, or reactor if no boilers used
local total_boil_rate = 0.0 local total_boil_rate = 0.0
@ -57,14 +58,14 @@ function logic.update_annunciator(self)
-- REACTOR -- -- REACTOR --
------------- -------------
self.db.annunciator.AutoControl = self.auto_engaged annunc.AutoControl = self.auto_engaged
-- check PLC status -- check PLC status
self.db.annunciator.PLCOnline = self.plc_i ~= nil annunc.PLCOnline = self.plc_i ~= nil
local plc_ready = self.db.annunciator.PLCOnline local plc_ready = annunc.PLCOnline
if self.db.annunciator.PLCOnline then if plc_ready then
local plc_db = self.plc_i.get_db() local plc_db = self.plc_i.get_db()
-- update ready state -- update ready state
@ -110,29 +111,29 @@ function logic.update_annunciator(self)
-- heartbeat blink about every second -- heartbeat blink about every second
if self.last_heartbeat + 1000 < plc_db.last_status_update then if self.last_heartbeat + 1000 < plc_db.last_status_update then
self.db.annunciator.PLCHeartbeat = not self.db.annunciator.PLCHeartbeat annunc.PLCHeartbeat = not annunc.PLCHeartbeat
self.last_heartbeat = plc_db.last_status_update self.last_heartbeat = plc_db.last_status_update
end end
local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O) local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O)
-- update other annunciator fields -- update other annunciator fields
self.db.annunciator.ReactorSCRAM = plc_db.rps_tripped annunc.ReactorSCRAM = plc_db.rps_tripped
self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL annunc.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL
self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC annunc.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC
self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool) annunc.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool)
self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low annunc.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low
self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow annunc.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh annunc.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT annunc.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow annunc.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh annunc.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000) local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000)
local high_rate = plc_db.mek_status.burn_rate >= (plc_db.mek_status.ccool_amnt * 0.27 / heating_rate_conv) local high_rate = plc_db.mek_status.burn_rate >= (plc_db.mek_status.ccool_amnt * 0.27 / heating_rate_conv)
-- this advisory applies when no coolant is buffered (which we can't easily determine)<br> -- this advisory applies when no coolant is buffered (which we can't easily determine)<br>
-- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation -- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation
self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate annunc.HighStartupRate = not plc_db.mek_status.status and high_rate
-- if no boilers, use reactor heating rate to check for boil rate mismatch -- if no boilers, use reactor heating rate to check for boil rate mismatch
if num_boilers == 0 then if num_boilers == 0 then
@ -146,21 +147,25 @@ function logic.update_annunciator(self)
-- MISC RTUs -- -- MISC RTUs --
--------------- ---------------
self.db.annunciator.RadiationMonitor = 1 local max_rad, any_faulted = 0, false
self.db.annunciator.RadiationWarning = false
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3) local db = envd.get_db() ---@type envd_session_db
self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw >= ANNUNC_LIMS.RadiationWarning any_faulted = any_faulted or envd.is_faulted()
break if db.radiation_raw > max_rad then max_rad = db.radiation_raw end
end end
self.db.annunciator.EmergencyCoolant = 1 annunc.RadiationMonitor = util.trinary(#self.envd == 0, 1, util.trinary(any_faulted, 2, 3))
annunc.RadiationWarning = max_rad >= ANNUNC_LIMS.RadiationWarning
annunc.EmergencyCoolant = 1
for i = 1, #self.redstone do for i = 1, #self.redstone do
local db = self.redstone[i].get_db() ---@type redstone_session_db local db = self.redstone[i].get_db() ---@type redstone_session_db
local io = db.io[IO.U_EMER_COOL] ---@type rs_db_dig_io|nil local io = db.io[IO.U_EMER_COOL] ---@type rs_db_dig_io|nil
if io ~= nil then if io ~= nil then
self.db.annunciator.EmergencyCoolant = util.trinary(io.read(), 3, 2) annunc.EmergencyCoolant = util.trinary(io.read(), 3, 2)
break break
end end
end end
@ -172,7 +177,7 @@ function logic.update_annunciator(self)
local boilers_ready = num_boilers == #self.boilers local boilers_ready = num_boilers == #self.boilers
-- clear boiler online flags -- clear boiler online flags
for i = 1, num_boilers do self.db.annunciator.BoilerOnline[i] = false end for i = 1, num_boilers do annunc.BoilerOnline[i] = false end
-- aggregated statistics -- aggregated statistics
local boiler_steam_dt_sum = 0.0 local boiler_steam_dt_sum = 0.0
@ -185,7 +190,7 @@ function logic.update_annunciator(self)
local boiler = session.get_db() ---@type boilerv_session_db local boiler = session.get_db() ---@type boilerv_session_db
local idx = session.get_device_idx() local idx = session.get_device_idx()
self.db.annunciator.RCSFault = self.db.annunciator.RCSFault or (not boiler.formed) or session.is_faulted() annunc.RCSFault = annunc.RCSFault or (not boiler.formed) or session.is_faulted()
-- update ready state -- update ready state
-- - must be formed -- - must be formed
@ -199,8 +204,8 @@ function logic.update_annunciator(self)
boiler_steam_dt_sum = _get_dt(DT_KEYS.BoilerSteam .. idx) boiler_steam_dt_sum = _get_dt(DT_KEYS.BoilerSteam .. idx)
boiler_water_dt_sum = _get_dt(DT_KEYS.BoilerWater .. idx) boiler_water_dt_sum = _get_dt(DT_KEYS.BoilerWater .. idx)
self.db.annunciator.BoilerOnline[idx] = true annunc.BoilerOnline[idx] = true
self.db.annunciator.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow annunc.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow
end end
-- check heating rate low -- check heating rate low
@ -209,14 +214,14 @@ function logic.update_annunciator(self)
-- check for inactive boilers while reactor is active -- check for inactive boilers while reactor is active
for i = 1, #self.boilers do for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session local boiler = self.boilers[i] ---@type unit_session
local idx = boiler.get_device_idx() local idx = boiler.get_device_idx()
local db = boiler.get_db() ---@type boilerv_session_db local db = boiler.get_db() ---@type boilerv_session_db
if r_db.mek_status.status then if r_db.mek_status.status then
self.db.annunciator.HeatingRateLow[idx] = db.state.boil_rate == 0 annunc.HeatingRateLow[idx] = db.state.boil_rate == 0
else else
self.db.annunciator.HeatingRateLow[idx] = false annunc.HeatingRateLow[idx] = false
end end
end end
end end
@ -234,9 +239,9 @@ function logic.update_annunciator(self)
if num_boilers > 0 then if num_boilers > 0 then
for i = 1, #self.boilers do for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session local boiler = self.boilers[i] ---@type unit_session
local idx = boiler.get_device_idx() local idx = boiler.get_device_idx()
local db = boiler.get_db() ---@type boilerv_session_db local db = boiler.get_db() ---@type boilerv_session_db
local gaining_hc = _get_dt(DT_KEYS.BoilerHCool .. idx) > 10.0 or db.tanks.hcool_fill == 1 local gaining_hc = _get_dt(DT_KEYS.BoilerHCool .. idx) > 10.0 or db.tanks.hcool_fill == 1
@ -256,7 +261,7 @@ function logic.update_annunciator(self)
cfmismatch = cfmismatch or _get_dt(DT_KEYS.ReactorCCool) < -10.0 or (gaining_hc and r_db.mek_status.ccool_fill == 0) cfmismatch = cfmismatch or _get_dt(DT_KEYS.ReactorCCool) < -10.0 or (gaining_hc and r_db.mek_status.ccool_fill == 0)
end end
self.db.annunciator.CoolantFeedMismatch = cfmismatch annunc.CoolantFeedMismatch = cfmismatch
-------------- --------------
-- TURBINES -- -- TURBINES --
@ -265,7 +270,7 @@ function logic.update_annunciator(self)
local turbines_ready = num_turbines == #self.turbines local turbines_ready = num_turbines == #self.turbines
-- clear turbine online flags -- clear turbine online flags
for i = 1, num_turbines do self.db.annunciator.TurbineOnline[i] = false end for i = 1, num_turbines do annunc.TurbineOnline[i] = false end
-- aggregated statistics -- aggregated statistics
local total_flow_rate = 0 local total_flow_rate = 0
@ -277,10 +282,10 @@ function logic.update_annunciator(self)
-- go through turbines for stats and online -- go through turbines for stats and online
for i = 1, #self.turbines do for i = 1, #self.turbines do
local session = self.turbines[i] ---@type unit_session local session = self.turbines[i] ---@type unit_session
local turbine = session.get_db() ---@type turbinev_session_db local turbine = session.get_db() ---@type turbinev_session_db
self.db.annunciator.RCSFault = self.db.annunciator.RCSFault or (not turbine.formed) or session.is_faulted() annunc.RCSFault = annunc.RCSFault or (not turbine.formed) or session.is_faulted()
-- update ready state -- update ready state
-- - must be formed -- - must be formed
@ -295,59 +300,44 @@ function logic.update_annunciator(self)
max_water_return_rate = max_water_return_rate + turbine.build.max_water_output max_water_return_rate = max_water_return_rate + turbine.build.max_water_output
self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades
self.db.annunciator.TurbineOnline[session.get_device_idx()] = true annunc.TurbineOnline[session.get_device_idx()] = true
end end
-- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine -- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine
self.db.annunciator.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate) annunc.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate)
-- check for steam feed mismatch and max return rate -- check for steam feed mismatch and max return rate
local steam_dt_max = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MaxSteamDT_H20, ANNUNC_LIMS.SFM_MaxSteamDT_NA) local steam_dt_max = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MaxSteamDT_H20, ANNUNC_LIMS.SFM_MaxSteamDT_NA)
local water_dt_min = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MinWaterDT_H20, ANNUNC_LIMS.SFM_MinWaterDT_NA) local water_dt_min = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MinWaterDT_H20, ANNUNC_LIMS.SFM_MinWaterDT_NA)
local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch
sfmismatch = sfmismatch or boiler_steam_dt_sum > steam_dt_max or boiler_water_dt_sum < water_dt_min sfmismatch = sfmismatch or boiler_steam_dt_sum > steam_dt_max or boiler_water_dt_sum < water_dt_min
self.db.annunciator.SteamFeedMismatch = sfmismatch annunc.SteamFeedMismatch = sfmismatch
self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0 annunc.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0
-- turbine safety checks -- turbine safety checks
for i = 1, #self.turbines do for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session local turbine = self.turbines[i] ---@type unit_session
local db = turbine.get_db() ---@type turbinev_session_db local db = turbine.get_db() ---@type turbinev_session_db
local idx = turbine.get_device_idx() local idx = turbine.get_device_idx()
-- check if steam dumps are open -- check if steam dumps are open
if db.state.dumping_mode == DUMPING_MODE.IDLE then if db.state.dumping_mode == DUMPING_MODE.IDLE then
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.OK annunc.SteamDumpOpen[idx] = TRI_FAIL.OK
elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.PARTIAL annunc.SteamDumpOpen[idx] = TRI_FAIL.PARTIAL
else else
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.FULL annunc.SteamDumpOpen[idx] = TRI_FAIL.FULL
end end
-- check if turbines are at max speed but not keeping up -- check if turbines are at max speed but not keeping up
self.db.annunciator.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0) annunc.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0)
--[[ -- see notes at cc-mek-scada/wiki/Annunciator-Panels#Generator-Trip
Generator Trip annunc.GeneratorTrip[idx] = (_get_dt(DT_KEYS.TurbinePower .. idx) > 0.0) and (db.tanks.energy_fill > 0.05)
a generator trip is when a generator suddenly and unexpectedly loses it's external load, which occurs when a power plant
is disconnected from the grid. in our case, this is when the turbine is disconnected, or what it's connected to becomes
fully charged. this is identified by detecting if:
- the internal power storage of the turbine is increasing AND
- there is at least 5% energy fill (preventing false trips with periodic power extraction from other mods)
this would then mean there is no external load and there will be a turbine trip soon if this is not resolved
]]--
self.db.annunciator.GeneratorTrip[idx] = (_get_dt(DT_KEYS.TurbinePower .. idx) > 0.0) and (db.tanks.energy_fill > 0.05)
--[[ -- see notes at cc-mek-scada/wiki/Annunciator-Panels#Turbine-Trip
Turbine Trip
a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool.
this can be identified by these conditions:
- the current flow rate is 0 mB/t and it should not be
- can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up
- can later identified by presence of steam in tank with a 0 flow rate
]]--
local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01 local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01
self.db.annunciator.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0 annunc.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0
end end
-- update auto control ready state for this unit -- update auto control ready state for this unit
@ -577,6 +567,7 @@ end
---@param self _unit_self unit instance ---@param self _unit_self unit instance
function logic.update_status_text(self) function logic.update_status_text(self)
local AISTATE = self.types.AISTATE local AISTATE = self.types.AISTATE
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)
---@nodiscard ---@nodiscard
@ -666,13 +657,13 @@ function logic.update_status_text(self)
if plc_db.mek_status.status then if plc_db.mek_status.status then
self.status_text[1] = "ACTIVE" self.status_text[1] = "ACTIVE"
if self.db.annunciator.ReactorHighDeltaT then if annunc.ReactorHighDeltaT then
self.status_text[2] = "core temperature rising" self.status_text[2] = "core temperature rising"
elseif self.db.annunciator.ReactorTempHigh then elseif annunc.ReactorTempHigh then
self.status_text[2] = "core temp high, system nominal" self.status_text[2] = "core temp high, system nominal"
elseif self.db.annunciator.FuelInputRateLow then elseif annunc.FuelInputRateLow then
self.status_text[2] = "insufficient fuel input rate" self.status_text[2] = "insufficient fuel input rate"
elseif self.db.annunciator.WasteLineOcclusion then elseif annunc.WasteLineOcclusion then
self.status_text[2] = "insufficient waste output rate" self.status_text[2] = "insufficient waste output rate"
elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then
self.status_text[2] = "awaiting flow stability" self.status_text[2] = "awaiting flow stability"
@ -711,7 +702,7 @@ function logic.update_status_text(self)
end end
self.status_text = { "RPS SCRAM", cause } self.status_text = { "RPS SCRAM", cause }
elseif self.db.annunciator.RadiationWarning then elseif annunc.RadiationWarning then
-- elevated, non-hazardous level of radiation is low priority, so display it now if everything else was fine -- elevated, non-hazardous level of radiation is low priority, so display it now if everything else was fine
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" } self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
else else
@ -726,7 +717,7 @@ function logic.update_status_text(self)
self.status_text[2] = "core hot" self.status_text[2] = "core hot"
end end
end end
elseif self.db.annunciator.RadiationWarning then elseif annunc.RadiationWarning then
-- in case PLC was disconnected but radiation is present -- in case PLC was disconnected but radiation is present
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" } self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
else else
@ -738,6 +729,7 @@ end
---@param self _unit_self unit instance ---@param self _unit_self unit instance
function logic.handle_redstone(self) function logic.handle_redstone(self)
local AISTATE = self.types.AISTATE local AISTATE = self.types.AISTATE
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)
---@nodiscard ---@nodiscard
@ -806,7 +798,7 @@ function logic.handle_redstone(self)
----------------------- -----------------------
local enable_emer_cool = self.plc_cache.rps_status.low_cool or local enable_emer_cool = self.plc_cache.rps_status.low_cool or
(self.auto_engaged and self.db.annunciator.CoolantLevelLow and is_active(self.alarms.ReactorOverTemp)) (self.auto_engaged and annunc.CoolantLevelLow and is_active(self.alarms.ReactorOverTemp))
-- don't turn off emergency coolant on sufficient coolant level since it might drop again -- don't turn off emergency coolant on sufficient coolant level since it might drop again
-- turn off once system is OK again -- turn off once system is OK again
@ -822,7 +814,7 @@ function logic.handle_redstone(self)
end end
end end
if self.db.annunciator.EmergencyCoolant > 1 and self.emcool_opened then if annunc.EmergencyCoolant > 1 and self.emcool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed")) log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed"))
log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam")) log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam"))
end end
@ -849,7 +841,7 @@ function logic.handle_redstone(self)
end end
end end
if self.db.annunciator.EmergencyCoolant > 1 and not self.emcool_opened then if annunc.EmergencyCoolant > 1 and not self.emcool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened")) log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened"))
log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam")) log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam"))
end end