Merge pull request #348 from MikaylaFischler/devel

2023.10.03 Release
This commit is contained in:
Mikayla
2023-10-04 00:11:47 -04:00
committed by GitHub
112 changed files with 3889 additions and 2120 deletions

View File

@ -28,4 +28,4 @@ jobs:
# --no-max-line-length = Disable warnings for long line lengths # --no-max-line-length = Disable warnings for long line lengths
# --exclude-files ... = Exclude lockbox library (external) and config files # --exclude-files ... = Exclude lockbox library (external) and config files
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os' # --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window

View File

@ -5,6 +5,7 @@
"colors", "colors",
"fs", "fs",
"http", "http",
"keys",
"parallel", "parallel",
"periphemu", "periphemu",
"peripheral", "peripheral",
@ -24,6 +25,7 @@
}, },
"Lua.hint.setType": true, "Lua.hint.setType": true,
"Lua.diagnostics.disable": [ "Lua.diagnostics.disable": [
"duplicate-set-field" "duplicate-set-field",
"inject-field"
] ]
} }

122
ccmsi.lua
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.9a" local CCMSI_VERSION = "v1.11a"
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/"
@ -158,7 +158,7 @@ local function _clean_dir(dir, tree)
if fs.isDir(path) then if fs.isDir(path) then
_clean_dir(path, tree[val]) _clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end
elseif not _in_array(val, tree) then elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files
fs.delete(path) fs.delete(path)
println("deleted " .. path) println("deleted " .. path)
end end
@ -172,7 +172,7 @@ local function clean(manifest)
table.insert(tree, "install_manifest.json") table.insert(tree, "install_manifest.json")
table.insert(tree, "ccmsi.lua") table.insert(tree, "ccmsi.lua")
table.insert(tree, "log.txt") table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
lgray() lgray()
@ -181,7 +181,7 @@ local function clean(manifest)
if fs.isDir(val) then if fs.isDir(val) then
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
elseif not _in_array(val, tree) then elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
root_ext = true root_ext = true
yellow();println(val .. " not used") yellow();println(val .. " not used")
end end
@ -203,10 +203,9 @@ if #opts == 0 or opts[1] == "help" then
yellow() yellow()
println(" ccmsi check <branch> for target") println(" ccmsi check <branch> for target")
lgray() lgray()
println(" install - fresh install, overwrites config") println(" install - fresh install, overwrites config.lua")
println(" update - update files EXCEPT for config/logs") println(" update - update files EXCEPT for config.lua")
println(" remove - delete files EXCEPT for config/logs") println(" uninstall - delete files INCLUDING config/logs")
println(" purge - delete files INCLUDING config/logs")
white();println("<app>");lgray() white();println("<app>");lgray()
println(" reactor-plc - reactor PLC firmware") println(" reactor-plc - reactor PLC firmware")
println(" rtu - RTU firmware") println(" rtu - RTU firmware")
@ -218,7 +217,7 @@ if #opts == 0 or opts[1] == "help" then
lgray();println(" main (default) | latest | devel");white() lgray();println(" main (default) | latest | devel");white()
return return
else else
mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" }) mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
if mode == nil then if mode == nil then
red();println("Unrecognized mode.");white() red();println("Unrecognized mode.");white()
return return
@ -310,7 +309,7 @@ elseif mode == "install" or mode == "update" then
ver.lockbox.v_local = lmnf.versions.lockbox ver.lockbox.v_local = lmnf.versions.lockbox
if lmnf.versions[app] == nil then if lmnf.versions[app] == nil then
red();println("Another application is already installed, please purge it before installing a new application.");white() red();println("Another application is already installed, please uninstall it before installing a new application.");white()
return return
end end
end end
@ -389,9 +388,10 @@ elseif mode == "install" or mode == "update" then
-- check space constraints -- check space constraints
if space_available < space_required then if space_available < space_required then
single_file_mode = true single_file_mode = true
yellow();println("WARNING: Insufficient space available for a full download!");white() yellow();println("NOTICE: Insufficient space available for a full cached download!");white()
println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.") lgray();println("Files can instead be downloaded one by one. If you are replacing a current install this may corrupt your install ONLY if it fails (such as a sudden network issue). If that occurs, you can still try again.")
if mode == "update" then println("If installation still fails, delete this device's log file or uninstall the app (not purge) and try again.") end if mode == "update" then println("If installation still fails, delete this device's log file and/or any unrelated files you have on this computer then try again.") end
white();
if not ask_y_n("Do you wish to continue", false) then if not ask_y_n("Do you wish to continue", false) then
println("Operation cancelled.") println("Operation cancelled.")
return return
@ -521,22 +521,19 @@ elseif mode == "install" or mode == "update" then
else println("Update failed, files may have been skipped.") end else println("Update failed, files may have been skipped.") end
end end
end end
elseif mode == "remove" or mode == "purge" then elseif mode == "uninstall" then
local ok, manifest = read_local_manifest() local ok, manifest = read_local_manifest()
if not ok then if not ok then
red();println("Error parsing local installation manifest.");white() red();println("Error parsing local installation manifest.");white()
return return
elseif mode == "remove" and manifest.versions[app] == nil then end
red();println(app .. " is not installed, cannot remove.");white()
if manifest.versions[app] == nil then
red();println("Error: '" .. app .. "' is not installed.")
return return
end end
orange() orange();println("Uninstalling all " .. app .. " files...")
if mode == "remove" then
println("Removing all " .. app .. " files except for config and log...")
elseif mode == "purge" then
println("Purging all " .. app .. " files including config and log...")
end
-- ask for confirmation -- ask for confirmation
if not ask_y_n("Continue", false) then return end if not ask_y_n("Continue", false) then return end
@ -546,76 +543,65 @@ elseif mode == "remove" or mode == "purge" then
local file_list = manifest.files local file_list = manifest.files
local dependencies = manifest.depends[app] local dependencies = manifest.depends[app]
local config_file = app .. "/config.lua"
table.insert(dependencies, app) table.insert(dependencies, app)
-- delete log file if purging -- delete log file
local log_deleted = false
local settings_file = app .. ".settings"
local legacy_config_file = app .. "/config.lua"
lgray() lgray()
if mode == "purge" and fs.exists(config_file) then if fs.exists(legacy_config_file) then
local log_deleted = pcall(function () log_deleted = pcall(function ()
local config = require(app .. ".config") local config = require(app .. ".config")
if fs.exists(config.LOG_PATH) then if fs.exists(config.LOG_PATH) then
fs.delete(config.LOG_PATH) fs.delete(config.LOG_PATH)
println("deleted log file " .. config.LOG_PATH) println("deleted log file " .. config.LOG_PATH)
end end
end) end)
elseif fs.exists(settings_file) and settings.load(settings_file) then
if not log_deleted then local log = settings.get("LogPath")
red();println("Failed to delete log file.") if log ~= nil and fs.exists(log) then
white();println("press any key to continue...") log_deleted = true
any_key();lgray() fs.delete(log)
println("deleted log file " .. log)
end end
end end
-- delete all files except config unless purging if not log_deleted then
red();println("Failed to delete log file.")
white();println("press any key to continue...")
any_key();lgray()
end
-- delete all installed files
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "purge" or file ~= config_file then if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
end
end end
-- delete folders that we should be deleteing local folder = files[1]
if mode == "purge" or dependency ~= app then while true do
local folder = files[1] local dir = fs.getDir(folder)
while true do if dir == "" or dir == ".." then break else folder = dir end
local dir = fs.getDir(folder) end
if dir == "" or dir == ".." then break else folder = dir end
end
if fs.isDir(folder) then if fs.isDir(folder) then
fs.delete(folder) fs.delete(folder)
println("deleted directory " .. folder) println("deleted directory " .. folder)
end
elseif dependency == app then
-- delete individual subdirectories so we can leave the config
for _, folder in pairs(files) do
while true do
local dir = fs.getDir(folder)
if dir == "" or dir == ".." or dir == app then break else folder = dir end
end
if folder ~= app and fs.isDir(folder) then
fs.delete(folder);println("deleted app subdirectory " .. folder)
end
end
end end
end end
-- only delete manifest if purging if fs.exists(settings_file) then
if mode == "purge" then fs.delete(settings_file)
fs.delete("install_manifest.json") println("deleted " .. settings_file)
println("deleted install_manifest.json")
else
-- remove all data from versions list to show nothing is installed
manifest.versions = {}
local imfile = fs.open("install_manifest.json", "w")
imfile.write(textutils.serializeJSON(manifest))
imfile.close()
end end
fs.delete("install_manifest.json")
println("deleted install_manifest.json")
green();println("Done!") green();println("Done!")
end end

16
configure.lua Normal file
View File

@ -0,0 +1,16 @@
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
if fs.exists("reactor-plc/configure.lua") then
require("reactor-plc.configure").configure()
elseif fs.exists("rtu/startup.lua") then
print("CONFIGURE> RTU CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
elseif fs.exists("supervisor/startup.lua") then
print("CONFIGURE> SUPERVISOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
elseif fs.exists("coordinator/startup.lua") then
print("CONFIGURE> COORDINATOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
elseif fs.exists("pocket/startup.lua") then
print("CONFIGURE> POCKET CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
else
print("CONFIGURE> NO CONFIGURATOR FOUND")
print("CONFIGURE> EXIT")
end

View File

@ -17,8 +17,8 @@ local println = util.println
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE local CRDN_TYPE = comms.CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND local FAC_COMMAND = comms.FAC_COMMAND
@ -54,9 +54,9 @@ end
function coordinator.configure_monitors(num_units, disable_flow_view) function coordinator.configure_monitors(num_units, disable_flow_view)
---@class monitors_struct ---@class monitors_struct
local monitors = { local monitors = {
primary = nil, primary = nil, ---@type table|nil
primary_name = "", primary_name = "",
flow = nil, flow = nil, ---@type table|nil
flow_name = "", flow_name = "",
unit_displays = {}, unit_displays = {},
unit_name_map = {} unit_name_map = {}
@ -279,7 +279,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
apisessions.init(nic) apisessions.init(nic)
-- send a packet to the supervisor -- send a packet to the supervisor
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE ---@param msg_type MGMT_TYPE|CRDN_TYPE
---@param msg table ---@param msg table
local function _send_sv(protocol, msg_type, msg) local function _send_sv(protocol, msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
@ -307,7 +307,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack }) m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(pkt_channel, crd_channel, s_pkt) nic.transmit(pkt_channel, crd_channel, s_pkt)
@ -316,13 +316,13 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
-- attempt connection establishment -- attempt connection establishment
local function _send_establish() local function _send_establish()
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN }) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
end end
-- keep alive ack -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) local function _send_keep_alive_ack(srv_time)
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@ -394,20 +394,20 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end end
-- send a facility command -- send a facility command
---@param cmd FAC_COMMAND command ---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode) ---@param option any? optional option options for the optional options (like waste mode)
function public.send_fac_command(cmd, option) function public.send_fac_command(cmd, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option }) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { cmd, option })
end end
-- send the auto process control configuration with a start command -- send the auto process control configuration with a start command
---@param config coord_auto_config configuration ---@param config coord_auto_config configuration
function public.send_auto_start(config) function public.send_auto_start(config)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
}) })
end end
@ -417,7 +417,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
---@param unit integer unit ID ---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate) ---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option) function public.send_unit_command(cmd, unit, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end end
-- parse a packet -- parse a packet
@ -426,7 +426,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
---@param reply_to integer ---@param reply_to integer
---@param message any ---@param message any
---@param distance integer ---@param distance integer
---@return mgmt_frame|crdn_frame|capi_frame|nil packet ---@return mgmt_frame|crdn_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance) local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
@ -444,12 +444,6 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if crdn_pkt.decode(s_pkt) then if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get() pkt = crdn_pkt.get()
end end
-- get as coordinator API packet
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
local capi_pkt = comms.capi_packet()
if capi_pkt.decode(s_pkt) then
pkt = capi_pkt.get()
end
else else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end end
@ -459,7 +453,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
end end
-- handle a packet -- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame|nil ---@param packet mgmt_frame|crdn_frame|nil
---@return boolean close_ui ---@return boolean close_ui
function public.handle_packet(packet) function public.handle_packet(packet)
local was_linked = self.sv_linked local was_linked = self.sv_linked
@ -475,18 +469,18 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
elseif r_chan == pkt_channel then elseif r_chan == pkt_channel then
if not self.sv_linked then if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor") log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.COORD_API then elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet capi_frame ---@cast packet crdn_frame
-- look for an associated session -- look for an associated session
local session = apisessions.find_session(src_addr) local session = apisessions.find_session(src_addr)
-- API packet -- coordinator packet
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
else else
-- any other packet should be session related, discard it -- any other packet should be session related, discard it
log.debug("discarding COORD_API packet without a known session") log.debug("discarding SCADA_CRDN packet without a known session")
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
@ -497,7 +491,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session -- establish a new session
-- validate packet and continue -- validate packet and continue
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
@ -553,7 +547,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if protocol == PROTOCOL.SCADA_CRDN then if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame ---@cast packet crdn_frame
if self.sv_linked then if self.sv_linked then
if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then if packet.type == CRDN_TYPE.INITIAL_BUILDS then
if packet.length == 2 then if packet.length == 2 then
-- record builds -- record builds
local fac_builds = iocontrol.record_facility_builds(packet.data[1]) local fac_builds = iocontrol.record_facility_builds(packet.data[1])
@ -561,31 +555,31 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if fac_builds and unit_builds then if fac_builds and unit_builds then
-- acknowledge receipt of builds -- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {}) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {})
else else
log.debug("received invalid INITIAL_BUILDS packet") log.debug("received invalid INITIAL_BUILDS packet")
end end
else else
log.debug("INITIAL_BUILDS packet length mismatch") log.debug("INITIAL_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then elseif packet.type == CRDN_TYPE.FAC_BUILDS then
if packet.length == 1 then if packet.length == 1 then
-- record facility builds -- record facility builds
if iocontrol.record_facility_builds(packet.data[1]) then if iocontrol.record_facility_builds(packet.data[1]) then
-- acknowledge receipt of builds -- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {}) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
else else
log.debug("received invalid FAC_BUILDS packet") log.debug("received invalid FAC_BUILDS packet")
end end
else else
log.debug("FAC_BUILDS packet length mismatch") log.debug("FAC_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then elseif packet.type == CRDN_TYPE.FAC_STATUS then
-- update facility status -- update facility status
if not iocontrol.update_facility_status(packet.data) then if not iocontrol.update_facility_status(packet.data) then
log.debug("received invalid FAC_STATUS packet") log.debug("received invalid FAC_STATUS packet")
end end
elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then elseif packet.type == CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement -- facility command acknowledgement
if packet.length >= 2 then if packet.length >= 2 then
local cmd = packet.data[1] local cmd = packet.data[1]
@ -613,24 +607,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
else else
log.debug("SCADA_CRDN facility command ack packet length mismatch") log.debug("SCADA_CRDN facility command ack packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
-- record builds -- record builds
if packet.length == 1 then if packet.length == 1 then
if iocontrol.record_unit_builds(packet.data[1]) then if iocontrol.record_unit_builds(packet.data[1]) then
-- acknowledge receipt of builds -- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {}) _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
else else
log.debug("received invalid UNIT_BUILDS packet") log.debug("received invalid UNIT_BUILDS packet")
end end
else else
log.debug("UNIT_BUILDS packet length mismatch") log.debug("UNIT_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
-- update statuses -- update statuses
if not iocontrol.update_unit_statuses(packet.data) then if not iocontrol.update_unit_statuses(packet.data) then
log.debug("received invalid UNIT_STATUSES packet") log.debug("received invalid UNIT_STATUSES packet")
end end
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then elseif packet.type == CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement -- unit command acknowledgement
if packet.length == 3 then if packet.length == 3 then
local cmd = packet.data[1] local cmd = packet.data[1]
@ -672,7 +666,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if self.sv_linked then if self.sv_linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 then if packet.length == 1 then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -690,7 +684,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
else else
log.debug("SCADA keep alive packet length mismatch") log.debug("SCADA keep alive packet length mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST self.sv_addr = comms.BROADCAST
@ -701,7 +695,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
else else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type) log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established -- connection with supervisor established
if packet.length == 2 then if packet.length == 2 then
local est_ack = packet.data[1] local est_ack = packet.data[1]

View File

@ -15,10 +15,12 @@ local panel_view = require("coordinator.ui.layout.front_panel")
local main_view = require("coordinator.ui.layout.main_view") local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view") local unit_view = require("coordinator.ui.layout.unit_view")
local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
---@class coord_renderer
local renderer = {} local renderer = {}
-- render engine -- render engine
@ -131,19 +133,30 @@ function renderer.init_dmesg()
log.direct_dmesg(engine.dmesg_window) log.direct_dmesg(engine.dmesg_window)
end end
-- start the coordinator front panel -- try to start the front panel
function renderer.start_fp() ---@return boolean success, any error_msg
function renderer.try_start_fp()
local status, msg = true, nil
if not engine.fp_ready then if not engine.fp_ready then
-- show front panel view on terminal -- show front panel view on terminal
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root} status, msg = pcall(function ()
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays) engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end)
-- start flasher callback task if status then
flasher.run() -- start flasher callback task and report ready
flasher.run()
-- report front panel as ready engine.fp_ready = true
engine.fp_ready = true else
-- report fail and close front panel
msg = core.extract_assert_msg(msg)
renderer.close_fp()
end
end end
return status, msg
end end
-- close out the front panel -- close out the front panel
@ -176,36 +189,47 @@ function renderer.close_fp()
end end
end end
-- start the coordinator GUI -- try to start the main GUI
function renderer.start_ui() ---@return boolean success, any error_msg
function renderer.try_start_ui()
local status, msg = true, nil
if not engine.ui_ready then if not engine.ui_ready then
-- hide dmesg -- hide dmesg
engine.dmesg_window.setVisible(false) engine.dmesg_window.setVisible(false)
-- show main view on main monitor status, msg = pcall(function ()
if engine.monitors.primary ~= nil then -- show main view on main monitor
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} if engine.monitors.primary ~= nil then
main_view(engine.ui.main_display) engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
main_view(engine.ui.main_display)
end
-- show flow view on flow monitor
if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display)
end
-- show unit views on unit displays
for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
end
end)
if status then
-- start flasher callback task and report ready
flasher.run()
engine.ui_ready = true
else
-- report fail and close ui
msg = core.extract_assert_msg(msg)
renderer.close_ui()
end end
-- show flow view on flow monitor
if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display)
end
-- show unit views on unit displays
for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
end
-- start flasher callback task
flasher.run()
-- report ui as ready
engine.ui_ready = true
end end
return status, msg
end end
-- close out the UI -- close out the UI

View File

@ -8,8 +8,8 @@ local iocontrol = require("coordinator.iocontrol")
local pocket = {} local pocket = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
-- local CAPI_TYPE = comms.CAPI_TYPE -- local CRDN_TYPE = comms.CRDN_TYPE
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
@ -72,22 +72,22 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
iocontrol.fp_pkt_disconnected(id) iocontrol.fp_pkt_disconnected(id)
end end
-- send a CAPI packet -- send a CRDN packet
-----@param msg_type CAPI_TYPE -----@param msg_type CRDN_TYPE
-----@param msg table -----@param msg table
-- local function _send(msg_type, msg) -- local function _send(msg_type, msg)
-- local s_pkt = comms.scada_packet() -- local s_pkt = comms.scada_packet()
-- local c_pkt = comms.capi_packet() -- local c_pkt = comms.crdn_packet()
-- c_pkt.make(msg_type, msg) -- c_pkt.make(msg_type, msg)
-- s_pkt.make(self.seq_num, PROTOCOL.COORD_API, c_pkt.raw_sendable()) -- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
-- out_queue.push_packet(s_pkt) -- out_queue.push_packet(s_pkt)
-- self.seq_num = self.seq_num + 1 -- self.seq_num = self.seq_num + 1
-- end -- end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send_mgmt(msg_type, msg) local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
@ -101,7 +101,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
end end
-- handle a packet -- handle a packet
---@param pkt mgmt_frame|capi_frame ---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
@ -117,17 +117,17 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
self.conn_watchdog.feed() self.conn_watchdog.feed()
-- process packet -- process packet
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
---@cast pkt capi_frame ---@cast pkt crdn_frame
-- handle packet by type -- handle packet by type
if pkt.type == nil then if pkt.type == nil then
else else
log.debug(log_header .. "handler received unsupported CAPI packet type " .. pkt.type) log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
end end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame ---@cast pkt mgmt_frame
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if pkt.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply -- keep alive reply
if pkt.length == 2 then if pkt.length == 2 then
local srv_start = pkt.data[1] local srv_start = pkt.data[1]
@ -146,7 +146,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
else else
@ -174,7 +174,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
-- close the connection -- close the connection
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
log.info(log_header .. "session closed by server") log.info(log_header .. "session closed by server")
end end
@ -229,7 +229,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
periodics.keep_alive = periodics.keep_alive + elapsed periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0 periodics.keep_alive = 0
end end

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.9" local COORDINATOR_VERSION = "v1.0.16"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -182,9 +182,8 @@ local function main()
log_graphics("starting front panel UI...") log_graphics("starting front panel UI...")
local fp_ok, fp_message = pcall(renderer.start_fp) local fp_ok, fp_message = renderer.try_start_fp()
if not fp_ok then if not fp_ok then
renderer.close_fp()
log_graphics(util.c("front panel UI error: ", fp_message)) log_graphics(util.c("front panel UI error: ", fp_message))
println_ts("front panel UI creation failed") println_ts("front panel UI creation failed")
log.fatal(util.c("front panel GUI render failed with error ", fp_message)) log.fatal(util.c("front panel GUI render failed with error ", fp_message))
@ -198,9 +197,8 @@ local function main()
local draw_start = util.time_ms() local draw_start = util.time_ms()
local ui_ok, ui_message = pcall(renderer.start_ui) local ui_ok, ui_message = renderer.try_start_ui()
if not ui_ok then if not ui_ok then
renderer.close_ui()
log_graphics(util.c("main UI error: ", ui_message)) log_graphics(util.c("main UI error: ", ui_message))
log.fatal(util.c("main GUI render failed with error ", ui_message)) log.fatal(util.c("main GUI render failed with error ", ui_message))
else else
@ -358,7 +356,7 @@ local function main()
sounder.stop() sounder.stop()
end end
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
event == "mouse_drag" or event == "mouse_scroll" then event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
-- handle a mouse event -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then elseif event == "speaker_audio_empty" then

View File

@ -12,20 +12,19 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local text_fg_bg = style.text_colors
-- new boiler view -- new boiler view
---@param root graphics_element parent ---@param root graphics_element parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
local function new_view(root, x, y, ps) local function new_view(root, x, y, ps)
local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y} local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12} local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12}
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg} local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=style.lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg} local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
status.register(ps, "computed_status", status.update) status.register(ps, "computed_status", status.update)
temp.register(ps, "temperature", temp.update) temp.register(ps, "temperature", temp.update)

View File

@ -16,7 +16,10 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local text_fg_bg = style.text_colors
local lu_col = style.lu_colors
-- new induction matrix view -- new induction matrix view
---@param root graphics_element parent ---@param root graphics_element parent
@ -31,14 +34,12 @@ local function new_view(root, x, y, data, ps, id)
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y} local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=matrix,text=title,alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=style.lg_gray}
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3} local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local label_fg_bg = cpair(colors.gray, colors.lightGray) local label_fg_bg = cpair(colors.gray, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg} local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}

View File

@ -4,6 +4,8 @@
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
@ -11,10 +13,13 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local text_fg_bg = style.text_colors
local lg_wh = style.lg_white
-- create a pocket list entry -- create a pocket list entry
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer PKT session ID ---@param id integer PKT session ID
@ -23,22 +28,22 @@ local function init(parent, id)
-- root div -- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg}
local ps_prefix = "pkt_" .. id .. "_" local ps_prefix = "pkt_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=text_fg_bg}
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=text_fg_bg,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=text_fg_bg}
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value) pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)} local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=lg_wh}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value) pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1} TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_wh}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_wh}
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update) pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor) pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)

View File

@ -23,7 +23,7 @@ local HazardButton = require("graphics.elements.controls.hazard_button")
local RadioButton = require("graphics.elements.controls.radio_button") local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
@ -33,6 +33,8 @@ local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors local dis_colors = style.dis_colors
local gry_wht = style.gray_white
local ind_grn = style.ind_grn local ind_grn = style.ind_grn
local ind_yel = style.ind_yel local ind_yel = style.ind_yel
local ind_red = style.ind_red local ind_red = style.ind_red
@ -47,6 +49,10 @@ local period = core.flasher.PERIOD
local function new_view(root, x, y) local function new_view(root, x, y)
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local black = cpair(colors.black, colors.black)
local blk_brn = cpair(colors.black, colors.brown)
local blk_pur = cpair(colors.black, colors.purple)
local facility = iocontrol.get_db().facility local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units local units = iocontrol.get_db().units
@ -116,35 +122,35 @@ local function new_view(root, x, y)
local targets = Div{parent=proc,width=31,height=24,x=1,y=1} local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2} TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=gry_wht}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=burn_target,x=18,y=2,text="mB/t"} TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
b_target.register(facility.ps, "process_burn_target", b_target.set_value) b_target.register(facility.ps, "process_burn_target", b_target.set_value)
burn_sum.register(facility.ps, "burn_sum", burn_sum.update) burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2} TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=gry_wht}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=chg_target,x=18,y=2,text="MFE"} TextBox{parent=chg_target,x=18,y=2,text="MFE"}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
c_target.register(facility.ps, "process_charge_target", c_target.set_value) c_target.register(facility.ps, "process_charge_target", c_target.set_value)
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end) cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2} TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=gry_wht}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"} TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
g_target.register(facility.ps, "process_gen_target", g_target.set_value) g_target.register(facility.ps, "process_gen_target", g_target.set_value)
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end) cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
@ -159,10 +165,10 @@ local function new_view(root, x, y)
for i = 1, 4 do for i = 1, 4 do
local unit local unit
local tag_fg_bg = cpair(colors.gray,colors.white) local tag_fg_bg = gry_wht
local lim_fg_bg = cpair(colors.lightGray,colors.white) local lim_fg_bg = style.lg_white
local ctl_fg = colors.lightGray local ctl_fg = colors.lightGray
local cur_fg_bg = cpair(colors.lightGray,colors.white) local cur_fg_bg = style.lg_white
local cur_lu = colors.lightGray local cur_lu = colors.lightGray
if i <= facility.num_units then if i <= facility.num_units then
@ -170,7 +176,7 @@ local function new_view(root, x, y)
tag_fg_bg = cpair(colors.black,colors.lightBlue) tag_fg_bg = cpair(colors.black,colors.lightBlue)
lim_fg_bg = bw_fg_bg lim_fg_bg = bw_fg_bg
ctl_fg = colors.gray ctl_fg = colors.gray
cur_fg_bg = cpair(colors.black,colors.brown) cur_fg_bg = blk_brn
cur_lu = colors.black cur_lu = colors.black
end end
@ -180,7 +186,7 @@ local function new_view(root, x, y)
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2} TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=lim_fg_bg} local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg} local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
@ -203,8 +209,8 @@ local function new_view(root, x, y)
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6} local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
for i = 1, 4 do for i = 1, 4 do
local tag_fg_bg = cpair(colors.gray, colors.white) local tag_fg_bg = gry_wht
local ind_fg_bg = cpair(colors.lightGray, colors.white) local ind_fg_bg = style.lg_white
local ind_off = colors.lightGray local ind_off = colors.lightGray
if i <= facility.num_units then if i <= facility.num_units then
@ -235,18 +241,18 @@ local function new_view(root, x, y)
------------------------- -------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" } local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.purple} local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value) mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg} local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg} local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value) stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value) stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)} local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=gry_wht}
-- save the automatic process control configuration without starting -- save the automatic process control configuration without starting
local function _save_cfg() local function _save_cfg()
@ -321,15 +327,15 @@ local function new_view(root, x, y)
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1} local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cpair(colors.black,colors.brown)} TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=blk_brn}
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=TEXT_ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)} TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3} local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17} local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update) status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown} local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.gray,colors.white),select_color=colors.brown}
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)} local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)

View File

@ -14,6 +14,9 @@ local StateIndicator = require("graphics.elements.indicators.state")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local text_fg_bg = style.text_colors
local lu_col = style.lu_colors
-- create new reactor view -- create new reactor view
---@param root graphics_element parent ---@param root graphics_element parent
---@param x integer top left x ---@param x integer top left x
@ -22,9 +25,6 @@ local border = core.border
local function new_view(root, x, y, ps) local function new_view(root, x, y, ps)
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y} local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16} local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16}
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}

View File

@ -15,16 +15,16 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local text_fg_bg = style.text_colors
local lu_col = style.lu_colors
-- new turbine view -- new turbine view
---@param root graphics_element parent ---@param root graphics_element parent
---@param x integer top left x ---@param x integer top left x
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
local function new_view(root, x, y, ps) local function new_view(root, x, y, ps)
local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y} local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12} local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12}
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg} local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg}

View File

@ -2,6 +2,8 @@
-- Reactor Unit SCADA Coordinator GUI -- Reactor Unit SCADA Coordinator GUI
-- --
local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
@ -26,7 +28,7 @@ local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button") local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
@ -34,6 +36,9 @@ local border = core.border
local bw_fg_bg = style.bw_fg_bg local bw_fg_bg = style.bw_fg_bg
local lu_cpair = style.lu_colors local lu_cpair = style.lu_colors
local hzd_fg_bg = style.hzd_fg_bg local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = style.dis_colors
local gry_wht = style.gray_white
local ind_grn = style.ind_grn local ind_grn = style.ind_grn
local ind_yel = style.ind_yel local ind_yel = style.ind_yel
@ -57,7 +62,7 @@ local function init(parent, id)
local b_ps = unit.boiler_ps_tbl local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl local t_ps = unit.turbine_ps_tbl
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
----------------------------- -----------------------------
-- main stats and core map -- -- main stats and core map --
@ -93,7 +98,7 @@ local function init(parent, id)
waste.register(u_ps, "waste_fill", waste.update) waste.register(u_ps, "waste_fill", waste.update)
ccool.register(u_ps, "ccool_type", function (type) ccool.register(u_ps, "ccool_type", function (type)
if type == "mekanism:sodium" then if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray)) ccool.recolor(cpair(colors.lightBlue, colors.gray))
else else
ccool.recolor(cpair(colors.blue, colors.gray)) ccool.recolor(cpair(colors.blue, colors.gray))
@ -101,7 +106,7 @@ local function init(parent, id)
end) end)
hcool.register(u_ps, "hcool_type", function (type) hcool.register(u_ps, "hcool_type", function (type)
if type == "mekanism:superheated_sodium" then if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray)) hcool.recolor(cpair(colors.orange, colors.gray))
else else
hcool.recolor(cpair(colors.white, colors.gray)) hcool.recolor(cpair(colors.white, colors.gray))
@ -129,8 +134,8 @@ local function init(parent, id)
------------------- -------------------
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg} local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg} local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value) stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value) stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
@ -190,7 +195,7 @@ local function init(parent, id)
-- RPS annunciator panel -- RPS annunciator panel
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=8} TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8}
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9} local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1} local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
@ -218,7 +223,7 @@ local function init(parent, id)
-- cooling annunciator panel -- cooling annunciator panel
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22} TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23} local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1} local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7} local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
@ -341,10 +346,8 @@ local function init(parent, id)
-- reactor controls -- -- reactor controls --
---------------------- ----------------------
local dis_colors = cpair(colors.white, colors.lightGray) local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=gry_wht}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=burn_control,x=9,y=2,text="mB/t"} TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end local set_burn = function () unit.set_burn(burn_rate.get_value()) end
@ -379,7 +382,7 @@ local function init(parent, id)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48} TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1} local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
@ -467,20 +470,20 @@ local function init(parent, id)
-- automatic control settings -- -- automatic control settings --
-------------------------------- --------------------------------
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=TEXT_ALIGN.CENTER,width=13,height=1,x=32,y=36} TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36}
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37} local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1} local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" } local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray} local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end) group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
auto_div.line_break() auto_div.line_break()
local function set_group() unit.set_group(group.get_value() - 1) end local function set_group() unit.set_group(group.get_value() - 1) end
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=cpair(colors.gray,colors.white),callback=set_group} local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=gry_wht,callback=set_group}
auto_div.line_break() auto_div.line_break()

View File

@ -19,11 +19,10 @@ local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.light")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local sprintf = util.sprintf local sprintf = util.sprintf
local cpair = core.cpair
local border = core.border local border = core.border
local pipe = core.pipe local pipe = core.pipe
@ -31,6 +30,7 @@ local wh_gray = style.wh_gray
local bw_fg_bg = style.bw_fg_bg local bw_fg_bg = style.bw_fg_bg
local text_c = style.text_colors local text_c = style.text_colors
local lu_c = style.lu_colors local lu_c = style.lu_colors
local lg_gray = style.lg_gray
local ind_grn = style.ind_grn local ind_grn = style.ind_grn
local ind_wht = style.ind_wht local ind_wht = style.ind_wht
@ -64,15 +64,13 @@ local function make(parent, x, y, wide, unit)
-- bounding box div -- bounding box div
local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height} local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height}
local lg_gray = cpair(colors.lightGray, colors.gray)
------------------ ------------------
-- COOLING LOOP -- -- COOLING LOOP --
------------------ ------------------
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray} local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=TEXT_ALIGN.CENTER,height=1} TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER,height=1}
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1} TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1}
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray} TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray}
@ -111,8 +109,8 @@ local function make(parent, x, y, wide, unit)
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update) hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray} local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=TEXT_ALIGN.CENTER,height=1} TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1}
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=TEXT_ALIGN.CENTER,height=1} TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1}
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
@ -130,8 +128,8 @@ local function make(parent, x, y, wide, unit)
end end
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray} local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=TEXT_ALIGN.CENTER,height=1} TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1}
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=TEXT_ALIGN.CENTER,height=1} TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1}
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
for i = 1, unit.num_turbines do for i = 1, unit.num_turbines do
@ -177,8 +175,8 @@ local function make(parent, x, y, wide, unit)
local function _machine(mx, my, name) local function _machine(mx, my, name)
local l = string.len(name) + 2 local l = string.len(name) + 2
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=TEXT_ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1} TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1}
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=TEXT_ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1} TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1}
end end
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
@ -205,7 +203,7 @@ local function make(parent, x, y, wide, unit)
_machine(_wide(97, 83), 4, "PRC [Po] \x1a"); _machine(_wide(97, 83), 4, "PRC [Po] \x1a");
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b") _machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg} local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn} local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7} local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7}

View File

@ -14,7 +14,7 @@ local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet") local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local pipe = core.pipe local pipe = core.pipe
@ -44,7 +44,7 @@ local function make(parent, x, y, unit)
local root = Div{parent=parent,x=x,y=y,width=80,height=height} local root = Div{parent=parent,x=x,y=y,width=80,height=height}
-- unit header message -- unit header message
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
------------- -------------
-- REACTOR -- -- REACTOR --

View File

@ -25,7 +25,7 @@ local StateIndicator = require("graphics.elements.indicators.state")
local CONTAINER_MODE = types.CONTAINER_MODE local CONTAINER_MODE = types.CONTAINER_MODE
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
@ -46,9 +46,9 @@ local function init(main)
local tank_list = facility.tank_list local tank_list = facility.tank_list
-- window header message -- window header message
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022" -- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
datetime.register(facility.ps, "date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
@ -261,7 +261,7 @@ local function init(main)
if tank_defs[i] > 0 then if tank_defs[i] > 0 then
local vy = 3 + y_ofs(i) local vy = 3 + y_ofs(i)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=cpair(colors.black,colors.lightGray),width=2,height=1} TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2,height=1}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn} local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht} local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
@ -288,8 +288,8 @@ local function init(main)
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14} local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.wh_gray} TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,height=1,fg_bg=style.wh_gray}
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12} local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
@ -338,8 +338,8 @@ local function init(main)
local sps = Div{parent=main,x=140,y=3,height=12} local sps = Div{parent=main,x=140,y=3,height=12}
TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=style.lg_gray}
TextBox{parent=sps,text="SPS",alignment=TEXT_ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray} TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray}
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10} local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
@ -361,8 +361,14 @@ local function init(main)
-- statistics -- -- statistics --
---------------- ----------------
TextBox{parent=main,x=145,y=16,text="PROC. WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg} local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17} local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17} local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17}
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17} local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17}
@ -371,8 +377,8 @@ local function init(main)
po.register(facility.ps, "po_rate", po.update) po.register(facility.ps, "po_rate", po.update)
popl.register(facility.ps, "po_pl_rate", popl.update) popl.register(facility.ps, "po_pl_rate", popl.update)
TextBox{parent=main,x=145,y=23,text="SPENT WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
local sp_waste = Rectangle{parent=main,x=145,y=24,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg} local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17} local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update) sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)

View File

@ -2,39 +2,41 @@
-- Coordinator Front Panel GUI -- Coordinator Front Panel GUI
-- --
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 iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local pgi = require("coordinator.ui.pgi") local pgi = require("coordinator.ui.pgi")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local pkt_entry = require("coordinator.ui.components.pkt_entry") local pkt_entry = require("coordinator.ui.components.pkt_entry")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox") local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane") local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
local TabBar = require("graphics.elements.controls.tabbar") local TabBar = require("graphics.elements.controls.tabbar")
local LED = require("graphics.elements.indicators.led") local LED = require("graphics.elements.indicators.led")
local RGBLED = require("graphics.elements.indicators.ledrgb") local RGBLED = require("graphics.elements.indicators.ledrgb")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local led_grn = style.led_grn
-- create new front panel view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
---@param num_units integer number of units (number of unit monitors) ---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units) local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps local ps = iocontrol.get_db().fp.ps
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.fp.header} TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp.header}
local page_div = Div{parent=panel,x=1,y=3} local page_div = Div{parent=panel,x=1,y=3}
@ -47,13 +49,13 @@ local function init(panel, num_units)
local system = Div{parent=main_page,width=14,height=17,x=2,y=2} local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=led_grn}
status.update(true) status.update(true)
system.line_break() system.line_break()
heartbeat.register(ps, "heartbeat", heartbeat.update) heartbeat.register(ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} local modem = LED{parent=system,label="MODEM",colors=led_grn}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED) network.update(types.PANEL_LINK_STATE.DISCONNECTED)
system.line_break() system.line_break()
@ -61,25 +63,25 @@ local function init(panel, num_units)
modem.register(ps, "has_modem", modem.update) modem.register(ps, "has_modem", modem.update)
network.register(ps, "link_state", network.update) network.register(ps, "link_state", network.update)
local speaker = LED{parent=system,label="SPEAKER",colors=cpair(colors.green,colors.green_off)} local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update) speaker.register(ps, "has_speaker", speaker.update)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp_label}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2} local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=cpair(colors.green,colors.green_off)} local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
main_monitor.register(ps, "main_monitor", main_monitor.update) main_monitor.register(ps, "main_monitor", main_monitor.update)
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=cpair(colors.green,colors.green_off)} local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
flow_monitor.register(ps, "flow_monitor", flow_monitor.update) flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
monitors.line_break() monitors.line_break()
for i = 1, num_units do for i = 1, num_units do
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=cpair(colors.green,colors.green_off)} local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update) unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
end end
@ -87,9 +89,9 @@ local function init(panel, num_units)
-- about footer -- about footer
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)} local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@ -101,7 +103,7 @@ local function init(panel, num_units)
-- API page -- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true} local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp_text,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1,hidden=true} -- padding local _ = Div{parent=api_list,height=1,hidden=true} -- padding
-- assemble page panes -- assemble page panes
@ -111,11 +113,11 @@ local function init(panel, num_units)
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
local tabs = { local tabs = {
{ name = "CRD", color = cpair(colors.black, colors.ivory) }, { name = "CRD", color = style.fp_text },
{ name = "API", color = cpair(colors.black, colors.ivory) }, { name = "API", color = style.fp_text },
} }
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)} TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.bw_fg_bg}
-- link pocket API list management to PGI -- link pocket API list management to PGI
pgi.link_elements(api_list, pkt_entry) pgi.link_elements(api_list, pkt_entry)

View File

@ -2,8 +2,6 @@
-- Main SCADA Coordinator GUI -- Main SCADA Coordinator GUI
-- --
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
@ -18,9 +16,7 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair
-- create new main view -- create new main view
---@param main graphics_element main displaybox ---@param main graphics_element main displaybox
@ -29,10 +25,10 @@ local function init(main)
local units = iocontrol.get_db().units local units = iocontrol.get_db().units
-- window header message -- window header message
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header} local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=style.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022" -- max length example: "01:23:45 AM - Wednesday, September 28 2022"
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
ping.register(facility.ps, "sv_ping", ping.update) ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value) datetime.register(facility.ps, "date_time", datetime.set_value)
@ -77,7 +73,7 @@ local function init(main)
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)") assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)} TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray}
cnc_bottom_align_start = cnc_bottom_align_start + 2 cnc_bottom_align_start = cnc_bottom_align_start + 2

View File

@ -78,11 +78,19 @@ style.lu_colors = cpair(colors.gray, colors.gray)
style.hzd_fg_bg = style.wh_gray style.hzd_fg_bg = style.wh_gray
style.dis_colors = cpair(colors.white, colors.lightGray) style.dis_colors = cpair(colors.white, colors.lightGray)
style.lg_gray = cpair(colors.lightGray, colors.gray)
style.lg_white = cpair(colors.lightGray, colors.white)
style.gray_white = cpair(colors.gray, colors.white)
style.ind_grn = cpair(colors.green, colors.gray) style.ind_grn = cpair(colors.green, colors.gray)
style.ind_yel = cpair(colors.yellow, colors.gray) style.ind_yel = cpair(colors.yellow, colors.gray)
style.ind_red = cpair(colors.red, colors.gray) style.ind_red = cpair(colors.red, colors.gray)
style.ind_wht = style.wh_gray style.ind_wht = style.wh_gray
style.fp_text = cpair(colors.black, colors.ivory)
style.fp_label = cpair(colors.lightGray, colors.ivory)
style.led_grn = cpair(colors.green, colors.green_off)
-- UI COMPONENTS -- -- UI COMPONENTS --
style.reactor = { style.reactor = {

View File

@ -7,19 +7,15 @@ local flasher = require("graphics.flasher")
local core = {} local core = {}
core.version = "1.1.1" core.version = "2.0.0"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events
-- Core Types -- Core Types
---@enum TEXT_ALIGN ---@enum ALIGN
core.TEXT_ALIGN = { core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
LEFT = 1,
CENTER = 2,
RIGHT = 3
}
---@class graphics_border ---@class graphics_border
---@field width integer ---@field width integer
@ -35,11 +31,7 @@ core.TEXT_ALIGN = {
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false ---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
---@return graphics_border ---@return graphics_border
function core.border(width, color, even) function core.border(width, color, even)
return { return { width = width, color = color, even = even or false }
width = width,
color = color,
even = even or false -- convert nil to false
}
end end
---@class graphics_frame ---@class graphics_frame
@ -56,12 +48,7 @@ end
---@param h integer ---@param h integer
---@return graphics_frame ---@return graphics_frame
function core.gframe(x, y, w, h) function core.gframe(x, y, w, h)
return { return { x = x, y = y, w = w, h = h }
x = x,
y = y,
w = w,
h = h
}
end end
---@class cpair ---@class cpair
@ -82,15 +69,9 @@ end
function core.cpair(a, b) function core.cpair(a, b)
return { return {
-- color pairs -- color pairs
color_a = a, color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b),
color_b = b,
blit_a = colors.toBlit(a),
blit_b = colors.toBlit(b),
-- aliases -- aliases
fgd = a, fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b)
bkg = b,
blit_fgd = colors.toBlit(a),
blit_bkg = colors.toBlit(b)
} }
end end
@ -130,4 +111,215 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
} }
end end
-- Assertion Handling
-- extract the custom element assert message, dropping the path to the element file
function core.extract_assert_msg(msg)
return string.sub(msg, (string.find(msg, "@") + 1) or 1)
end
-- Interactive Field Manager
---@param e graphics_base
---@param max_len any
---@param fg_bg any
---@param dis_fg_bg any
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
local self = {
frame_start = 1,
visible_text = e.value,
cursor_pos = string.len(e.value) + 1,
selected_all = false
}
-- update visible text
local function _update_visible()
self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1)
end
-- try shifting frame left
local function _try_lshift()
if self.frame_start > 1 then
self.frame_start = self.frame_start - 1
return true
end
end
-- try shifting frame right
local function _try_rshift()
if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then
self.frame_start = self.frame_start + 1
return true
end
end
---@class ifield
local public = {}
-- censor the display (for private info, for example) with the provided character<br>
-- disable by passing no argument
---@param censor string? character to hide data with
function public.censor(censor)
if type(censor) == "string" and string.len(censor) == 1 then
self.censor = censor
else self.censor = nil end
public.show()
end
-- show the field
function public.show()
_update_visible()
if e.enabled then
e.w_set_bkg(fg_bg.bkg)
e.w_set_fgd(fg_bg.fgd)
else
e.w_set_bkg(dis_fg_bg.bkg)
e.w_set_fgd(dis_fg_bg.fgd)
end
-- clear and print
e.w_set_cur(1, 1)
e.w_write(string.rep(" ", e.frame.w))
e.w_set_cur(1, 1)
local function _write()
if self.censor then
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
else
e.w_write(self.visible_text)
end
end
if e.is_focused() and e.enabled then
-- write text with cursor
if self.selected_all then
e.w_set_bkg(fg_bg.fgd)
e.w_set_fgd(fg_bg.bkg)
_write()
elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then
-- write text with cursor at the end, no need to blit
_write()
e.w_set_fgd(colors.lightGray)
e.w_write("_")
else
local a, b = "", ""
if self.cursor_pos <= string.len(self.visible_text) then
a = fg_bg.blit_bkg
b = fg_bg.blit_fgd
end
local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos)
local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos)
if self.censor then
e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg)
else
e.w_blit(self.visible_text, b_fgd, b_bkg)
end
end
else
self.selected_all = false
-- write text without cursor
_write()
end
end
-- move cursor to x
---@param x integer
function public.move_cursor(x)
self.selected_all = false
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
public.show()
end
-- select all text
function public.select_all()
self.selected_all = true
public.show()
end
-- set field value
---@param val string
function public.set_value(val)
e.value = string.sub(val, 1, math.min(max_len, string.len(val)))
public.nav_end()
end
-- try to insert a character if there is space
---@param char string
function public.try_insert_char(char)
-- limit length
if string.len(e.value) >= max_len then return end
-- replace if selected all, insert otherwise
if self.selected_all then
self.selected_all = false
self.cursor_pos = 2
self.frame_start = 1
e.value = char
public.show()
else
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
_update_visible()
public.nav_right()
end
end
-- remove charcter before cursor if there is anything to remove, or delete all if selected all
function public.backspace()
if self.selected_all then
self.selected_all = false
e.value = ""
self.cursor_pos = 1
self.frame_start = 1
public.show()
else
if self.frame_start + self.cursor_pos > 2 then
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
if self.cursor_pos > 1 then
self.cursor_pos = self.cursor_pos - 1
public.show()
elseif _try_lshift() then public.show() end
end
end
end
-- move cursor left by one
function public.nav_left()
if self.cursor_pos > 1 then
self.cursor_pos = self.cursor_pos - 1
public.show()
elseif _try_lshift() then public.show() end
end
-- move cursor right by one
function public.nav_right()
if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then
self.cursor_pos = self.cursor_pos + 1
public.show()
elseif _try_rshift() then public.show() end
end
-- move cursor to the start
function public.nav_start()
self.cursor_pos = 1
self.frame_start = 1
public.show()
end
-- move cursor to the end
function public.nav_end()
self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2)
_update_visible()
self.cursor_pos = string.len(self.visible_text) + 1
public.show()
end
return public
end
return core return core

View File

@ -2,8 +2,12 @@
-- Generic Graphics Element -- Generic Graphics Element
-- --
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local events = core.events
local element = {} local element = {}
---@class graphics_args_generic ---@class graphics_args_generic
@ -17,6 +21,7 @@ local element = {}
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw
---@field can_focus? boolean true if this element can be focused, false by default
---@alias graphics_args graphics_args_generic ---@alias graphics_args graphics_args_generic
---|waiting_args ---|waiting_args
@ -25,11 +30,14 @@ local element = {}
---|hazard_button_args ---|hazard_button_args
---|multi_button_args ---|multi_button_args
---|push_button_args ---|push_button_args
---|radio_2d_args
---|radio_button_args ---|radio_button_args
---|sidebar_args ---|sidebar_args
---|spinbox_args ---|spinbox_args
---|switch_button_args ---|switch_button_args
---|tabbar_args ---|tabbar_args
---|number_field_args
---|text_field_args
---|alarm_indicator_light ---|alarm_indicator_light
---|core_map_args ---|core_map_args
---|data_indicator_args ---|data_indicator_args
@ -59,6 +67,16 @@ local element = {}
---@field key string data key ---@field key string data key
---@field func function callback ---@field func function callback
-- more detailed assert message for element verification
---@param condition any assert condition
---@param msg string assert message
---@param callstack_offset? integer shift value to change targets of debug.getinfo()
function element.assert(condition, msg, callstack_offset)
callstack_offset = callstack_offset or 0
local caller = debug.getinfo(3 + callstack_offset)
assert(condition, util.c(caller.source, ":", caller.currentline, "{", debug.getinfo(2 + callstack_offset).name, "}: ", msg))
end
-- a base graphics element, should not be created on its own -- a base graphics element, should not be created on its own
---@nodiscard ---@nodiscard
---@param args graphics_args arguments ---@param args graphics_args arguments
@ -67,14 +85,17 @@ local element = {}
function element.new(args, child_offset_x, child_offset_y) function element.new(args, child_offset_x, child_offset_y)
local self = { local self = {
id = nil, ---@type element_id|nil id = nil, ---@type element_id|nil
is_root = args.parent == nil,
elem_type = debug.getinfo(2).name, elem_type = debug.getinfo(2).name,
define_completed = false, define_completed = false,
p_window = nil, ---@type table p_window = nil, ---@type table
position = { x = 1, y = 1 }, ---@type coordinate_2d 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
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 = {},
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
focused = false,
mt = {} mt = {}
} }
@ -86,14 +107,13 @@ function element.new(args, child_offset_x, child_offset_y)
content_window = nil, ---@type table|nil content_window = nil, ---@type table|nil
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 = {},
child_id_map = {}
} }
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
-- element as string -- element as string
function self.mt.__tostring() function self.mt.__tostring()
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self) return util.c("graphics.element{", self.elem_type, "} @ ", self)
end end
---@class graphics_element ---@class graphics_element
@ -101,6 +121,69 @@ function element.new(args, child_offset_x, child_offset_y)
setmetatable(public, self.mt) setmetatable(public, self.mt)
-----------------------
-- PRIVATE FUNCTIONS --
-----------------------
-- use tab to jump to the next focusable field
---@param reverse boolean
local function _tab_focusable(reverse)
local first_f = nil ---@type graphics_element|nil
local prev_f = nil ---@type graphics_element|nil
local cur_f = nil ---@type graphics_element|nil
local done = false
---@param elem graphics_element
local function handle_element(elem)
if elem.is_visible() and elem.is_focusable() and elem.is_enabled() then
if first_f == nil then first_f = elem end
if cur_f == nil then
if elem.is_focused() then
cur_f = elem
if (not done) and (reverse and prev_f ~= nil) then
cur_f.unfocus()
prev_f.focus()
done = true
end
end
else
if elem.is_focused() then
elem.unfocus()
elseif not (reverse or done) then
cur_f.unfocus()
elem.focus()
done = true
end
end
prev_f = elem
end
end
---@param children table
local function traverse(children)
for i = 1, #children do
local child = children[i] ---@type graphics_base
handle_element(child.get())
if child.get().is_visible() then traverse(child.children) end
end
end
traverse(protected.children)
-- if no element was focused, wrap focus
if first_f ~= nil and not done then
if reverse then
if cur_f ~= nil then cur_f.unfocus() end
if prev_f ~= nil then prev_f.focus() end
else
if cur_f ~= nil then cur_f.unfocus() end
first_f.focus()
end
end
end
------------------------- -------------------------
-- PROTECTED FUNCTIONS -- -- PROTECTED FUNCTIONS --
------------------------- -------------------------
@ -134,10 +217,10 @@ function element.new(args, child_offset_x, child_offset_y)
end end
-- check frame -- check frame
assert(f.x >= 1, name_brief .. "frame x not >= 1") element.assert(f.x >= 1, "frame x not >= 1", 2)
assert(f.y >= 1, name_brief .. "frame y not >= 1") element.assert(f.y >= 1, "frame y not >= 1", 2)
assert(f.w >= 1, name_brief .. "frame width not >= 1") element.assert(f.w >= 1, "frame width not >= 1", 2)
assert(f.h >= 1, name_brief .. "frame height not >= 1") element.assert(f.h >= 1, "frame height not >= 1", 2)
-- create window -- create window
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true) protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
@ -166,6 +249,31 @@ function element.new(args, child_offset_x, child_offset_y)
self.bounds.x2 = self.position.x + f.w - 1 self.bounds.x2 = self.position.x + f.w - 1
self.bounds.y1 = self.position.y self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + f.h - 1 self.bounds.y2 = self.position.y + f.h - 1
-- alias functions
-- window set cursor position
---@param x integer
---@param y integer
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
-- set background color
---@param c color
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
-- set foreground (text) color
---@param c color
function protected.w_set_fgd(c) protected.window.setTextColor(c) end
-- write text
---@param str string
function protected.w_write(str) protected.window.write(str) end
-- blit text
---@param str string
---@param fg string
---@param bg string
function protected.w_blit(str, fg, bg) protected.window.blit(str, fg, bg) end
end end
-- check if a coordinate relative to the parent is within the bounds of this element -- check if a coordinate relative to the parent is within the bounds of this element
@ -186,85 +294,6 @@ function element.new(args, child_offset_x, child_offset_y)
return in_x and in_y return in_x and in_y
end end
-- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg
-- handle a child element having been added
---@param id element_id element identifier
---@param child graphics_element child element
function protected.on_added(id, child)
end
-- handle a child element having been removed
---@param id element_id element identifier
function protected.on_removed(id)
end
-- handle a mouse event
---@param event mouse_interaction mouse interaction event
function protected.handle_mouse(event)
end
-- handle data value changes
---@vararg any value(s)
function protected.on_update(...)
end
-- callback on control press responses
---@param result any
function protected.response_callback(result)
end
-- get value
---@nodiscard
function protected.get_value()
return protected.value
end
-- set value
---@param value any value to set
function protected.set_value(value)
end
-- set minimum input value
---@param min integer minimum allowed value
function protected.set_min(min)
end
-- set maximum input value
---@param max integer maximum allowed value
function protected.set_max(max)
end
-- enable the control
function protected.enable()
end
-- disable the control
function protected.disable()
end
-- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s)
function protected.recolor(...)
end
-- custom resize command, varies by element if implemented
---@vararg integer sizing
function protected.resize(...)
end
-- luacheck: pop
---@diagnostic enable: unused-local, unused-vararg
-- start animations
function protected.start_anim()
end
-- stop animations
function protected.stop_anim()
end
-- get public interface -- get public interface
---@nodiscard ---@nodiscard
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
@ -278,6 +307,107 @@ function element.new(args, child_offset_x, child_offset_y)
return public, self.id return public, self.id
end end
-- protected version of public is_focused()
---@nodiscard
---@return boolean is_focused
function protected.is_focused() return self.focused end
-- defocus this element
function protected.defocus() public.unfocus_all() end
-- focus this element and take away focus from all other elements
function protected.take_focus() args.parent.__focus_child(public) end
-- action handlers --
-- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg
-- handle a child element having been added
---@param id element_id element identifier
---@param child graphics_element child element
function protected.on_added(id, child) end
-- handle a child element having been removed
---@param id element_id element identifier
function protected.on_removed(id) end
-- handle enabled
function protected.on_enabled() end
-- handle disabled
function protected.on_disabled() end
-- handle this element having been focused
function protected.on_focused() end
-- handle this element having been unfocused
function protected.on_unfocused() end
-- handle this element having been shown
function protected.on_shown() end
-- handle this element having been hidden
function protected.on_hidden() end
-- handle a mouse event
---@param event mouse_interaction mouse interaction event
function protected.handle_mouse(event) end
-- handle a keyboard event
---@param event key_interaction key interaction event
function protected.handle_key(event) end
-- handle a paste event
---@param text string pasted text
function protected.handle_paste(text) end
-- handle data value changes
---@vararg any value(s)
function protected.on_update(...) end
-- callback on control press responses
---@param result any
function protected.response_callback(result) end
-- accessors and control --
-- get value
---@nodiscard
function protected.get_value() return protected.value end
-- set value
---@param value any value to set
function protected.set_value(value) end
-- set minimum input value
---@param min integer minimum allowed value
function protected.set_min(min) end
-- set maximum input value
---@param max integer maximum allowed value
function protected.set_max(max) end
-- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s)
function protected.recolor(...) end
-- custom resize command, varies by element if implemented
---@vararg integer sizing
function protected.resize(...) end
-- luacheck: pop
---@diagnostic enable: unused-local, unused-vararg
-- re-draw this element
function protected.redraw() end
-- start animations
function protected.start_anim() end
-- stop animations
function protected.stop_anim() end
----------- -----------
-- SETUP -- -- SETUP --
----------- -----------
@ -289,7 +419,7 @@ function element.new(args, child_offset_x, child_offset_y)
end end
-- check window -- check window
assert(self.p_window, name_brief .. "no parent window provided") element.assert(self.p_window, "no parent window provided", 1)
-- prepare the template -- prepare the template
if args.parent == nil then if args.parent == nil then
@ -330,7 +460,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- delete all children -- delete all children
for k, v in pairs(protected.children) do for k, v in pairs(protected.children) do
v.delete() v.get().delete()
protected.children[k] = nil protected.children[k] = nil
end end
@ -352,65 +482,87 @@ function element.new(args, child_offset_x, child_offset_y)
self.next_y = child.frame.y + child.frame.h self.next_y = child.frame.y + child.frame.h
local child_element = child.get()
local id = key ---@type string|integer|nil local id = key ---@type string|integer|nil
if id == nil then if id == nil then
id = self.next_id id = self.next_id
self.next_id = self.next_id + 1 self.next_id = self.next_id + 1
end end
protected.children[id] = child_element table.insert(protected.children, child)
protected.child_id_map[id] = #protected.children
return id return id
end end
-- remove a child element -- remove a child element
---@param key element_id id ---@param id element_id id
function public.__remove_child(key) function public.__remove_child(id)
if protected.children[key] ~= nil then local index = protected.child_id_map[id]
protected.on_removed(key) if protected.children[index] ~= nil then
protected.children[key] = nil protected.on_removed(id)
protected.children[index] = nil
protected.child_id_map[id] = nil
end end
end end
-- actions to take upon a child element becoming ready (initial draw/construction completed) -- actions to take upon a child element becoming ready (initial draw/construction completed)
---@param key element_id id ---@param key element_id id
---@param child graphics_element ---@param child graphics_element
function public.__child_ready(key, child) function public.__child_ready(key, child) protected.on_added(key, child) end
protected.on_added(key, child)
-- focus solely on this child
---@param child graphics_element
function public.__focus_child(child)
if self.is_root then
public.unfocus_all()
child.focus()
else args.parent.__focus_child(child) end
end end
-- get a child element -- get a child element
---@nodiscard ---@nodiscard
---@param id element_id ---@param id element_id
---@return graphics_element ---@return graphics_element
function public.get_child(id) return protected.children[id] end function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
-- remove a child element -- remove a child element
---@param id element_id ---@param id element_id
function public.remove(id) function public.remove(id)
if protected.children[id] ~= nil then local index = protected.child_id_map[id]
protected.children[id].delete() if protected.children[index] ~= nil then
protected.children[index].get().delete()
protected.on_removed(id) protected.on_removed(id)
protected.children[id] = nil protected.children[index] = nil
protected.child_id_map[id] = nil
end end
end end
-- remove all child elements and reset next y
function public.remove_all()
for i = 1, #protected.children do
local child = protected.children[i].get() ---@type graphics_element
child.delete()
protected.on_removed(child.get_id())
end
self.next_y = 1
protected.children = {}
protected.child_id_map = {}
end
-- attempt to get a child element by ID (does not include this element itself) -- attempt to get a child element by ID (does not include this element itself)
---@nodiscard ---@nodiscard
---@param id element_id ---@param id element_id
---@return graphics_element|nil element ---@return graphics_element|nil element
function public.get_element_by_id(id) function public.get_element_by_id(id)
if protected.children[id] == nil then local index = protected.child_id_map[id]
if protected.children[index] == nil then
for _, child in pairs(protected.children) do for _, child in pairs(protected.children) do
local elem = child.get_element_by_id(id) local elem = child.get().get_element_by_id(id)
if elem ~= nil then return elem end if elem ~= nil then return elem end
end end
else else return protected.children[index].get() end
return protected.children[id]
end
return nil
end end
-- AUTO-PLACEMENT -- -- AUTO-PLACEMENT --
@ -422,97 +574,114 @@ function element.new(args, child_offset_x, child_offset_y)
-- PROPERTIES -- -- PROPERTIES --
-- get the foreground/background colors -- get element id
---@nodiscard ---@nodiscard
---@return cpair fg_bg ---@return element_id
function public.get_fg_bg() function public.get_id() return self.id end
return protected.fg_bg
end
-- get element x -- get element x
---@nodiscard ---@nodiscard
---@return integer x ---@return integer x
function public.get_x() function public.get_x() return protected.frame.x end
return protected.frame.x
end
-- get element y -- get element y
---@nodiscard ---@nodiscard
---@return integer y ---@return integer y
function public.get_y() function public.get_y() return protected.frame.y end
return protected.frame.y
end
-- get element width -- get element width
---@nodiscard ---@nodiscard
---@return integer width ---@return integer width
function public.get_width() function public.get_width() return protected.frame.w end
return protected.frame.w
end
-- get element height -- get element height
---@nodiscard ---@nodiscard
---@return integer height ---@return integer height
function public.get_height() function public.get_height() return protected.frame.h end
return protected.frame.h
end -- get the foreground/background colors
---@nodiscard
---@return cpair fg_bg
function public.get_fg_bg() return protected.fg_bg end
-- get the element value -- get the element value
---@nodiscard ---@nodiscard
---@return any value ---@return any value
function public.get_value() function public.get_value() return protected.get_value() end
return protected.get_value()
end
-- set the element value -- set the element value
---@param value any new value ---@param value any new value
function public.set_value(value) function public.set_value(value) protected.set_value(value) end
protected.set_value(value)
end
-- set minimum input value -- set minimum input value
---@param min integer minimum allowed value ---@param min integer minimum allowed value
function public.set_min(min) function public.set_min(min) protected.set_min(min) end
protected.set_min(min)
end
-- set maximum input value -- set maximum input value
---@param max integer maximum allowed value ---@param max integer maximum allowed value
function public.set_max(max) function public.set_max(max) protected.set_max(max) end
protected.set_max(max)
end -- check if this element is enabled
function public.is_enabled() return protected.enabled end
-- enable the element -- enable the element
function public.enable() function public.enable()
protected.enabled = true if not protected.enabled then
protected.enable() protected.enabled = true
protected.on_enabled()
end
end end
-- disable the element -- disable the element
function public.disable() function public.disable()
protected.enabled = false if protected.enabled then
protected.disable() protected.enabled = false
protected.on_disabled()
public.unfocus_all()
end
end
-- can this element be focused
function public.is_focusable() return args.can_focus end
-- is this element focused
function public.is_focused() return self.focused end
-- focus the element
function public.focus()
if args.can_focus and protected.enabled and not self.focused then
self.focused = true
protected.on_focused()
end
end
-- unfocus this element
function public.unfocus()
if args.can_focus and self.focused then
self.focused = false
protected.on_unfocused()
end
end
-- unfocus this element and all its children
function public.unfocus_all()
public.unfocus()
for _, child in pairs(protected.children) do child.get().unfocus() end
end end
-- custom recolor command, varies by element if implemented -- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s) ---@vararg cpair|color color(s)
function public.recolor(...) function public.recolor(...) protected.recolor(...) end
protected.recolor(...)
end
-- resize attributes of the element value if supported -- resize attributes of the element value if supported
---@vararg number dimensions (element specific) ---@vararg number dimensions (element specific)
function public.resize(...) function public.resize(...) protected.resize(...) end
protected.resize(...)
end
-- reposition the element window<br> -- reposition the element window<br>
-- 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) function public.reposition(x, y) protected.window.reposition(x, y) end
protected.window.reposition(x, y)
end
-- FUNCTION CALLBACKS -- -- FUNCTION CALLBACKS --
@ -525,26 +694,62 @@ function element.new(args, child_offset_x, child_offset_y)
local ini_in = protected.in_window_bounds(x_ini, y_ini) local ini_in = protected.in_window_bounds(x_ini, y_ini)
if ini_in then if ini_in then
local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y) if event.type == events.MOUSE_CLICK.UP or event.type == events.MOUSE_CLICK.DRAG then
-- make sure we don't handle mouse events that started before this element was made visible
if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then
return
end
elseif event.type == events.MOUSE_CLICK.DOWN then
self.button_down[event.button] = event.initial
end
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
-- handle the mouse event then pass to children -- 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.handle_mouse(event_T) end for _, child in pairs(protected.children) do child.get().handle_mouse(event_T) end
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
-- clicked out, unfocus this element and children
public.unfocus_all()
end end
else
-- don't track clicks while hidden
self.button_down[event.button] = events.new_coord_2d(-1, -1)
end
end
-- handle a keyboard click if this element is visible and focused
---@param event key_interaction keyboard interaction event
function public.handle_key(event)
if protected.window.isVisible() then
if self.is_root and (event.type == events.KEY_CLICK.DOWN) and (event.key == keys.tab) then
-- try to jump to the next/previous focusable field
_tab_focusable(event.shift)
else
-- handle the key event then pass to children
if self.focused then protected.handle_key(event) end
for _, child in pairs(protected.children) do child.get().handle_key(event) end
end
end
end
-- handle text paste
---@param text string pasted text
function public.handle_paste(text)
if protected.window.isVisible() then
-- handle the paste event then pass to children
if self.focused then protected.handle_paste(text) end
for _, child in pairs(protected.children) do child.get().handle_paste(text) end
end end
end end
-- draw the element given new data -- draw the element given new data
---@vararg any new data ---@vararg any new data
function public.update(...) function public.update(...) protected.on_update(...) end
protected.on_update(...)
end
-- on a control request response -- on a control request response
---@param result any ---@param result any
function public.on_response(result) function public.on_response(result) protected.response_callback(result) end
protected.response_callback(result)
end
-- register a callback with a PSIL, allowing for automatic unregister on delete<br> -- register a callback with a PSIL, allowing for automatic unregister on delete<br>
-- do not use graphics elements directly with PSIL subscribe() -- do not use graphics elements directly with PSIL subscribe()
@ -558,6 +763,9 @@ function element.new(args, child_offset_x, child_offset_y)
-- VISIBILITY & ANIMATIONS -- -- VISIBILITY & ANIMATIONS --
-- check if this element is visible
function public.is_visible() return protected.window.isVisible() end
-- show the element and enables animations by default -- show the element and enables animations by default
---@param animate? boolean true (default) to automatically resume animations ---@param animate? boolean true (default) to automatically resume animations
function public.show(animate) function public.show(animate)
@ -567,47 +775,51 @@ function element.new(args, child_offset_x, child_offset_y)
-- hide the element and disables animations<br> -- hide the element and disables animations<br>
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br> -- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
---@see graphics_element.redraw
---@see graphics_element.content_redraw ---@see graphics_element.content_redraw
function public.hide() ---@param clear? boolean true to visibly hide this element (redraws the parent)
function public.hide(clear)
public.freeze_all() -- stop animations for efficiency/performance public.freeze_all() -- stop animations for efficiency/performance
public.unfocus_all()
protected.window.setVisible(false) protected.window.setVisible(false)
if clear and args.parent then args.parent.redraw() end
end end
-- start/resume animation(s) -- start/resume animation(s)
function public.animate() function public.animate() protected.start_anim() end
protected.start_anim()
end
-- start/resume animation(s) for this element and all its children<br> -- start/resume animation(s) for this element and all its children<br>
-- only animates if a window is visible -- only animates if a window is visible
function public.animate_all() function public.animate_all()
if protected.window.isVisible() then if protected.window.isVisible() then
public.animate() public.animate()
for _, child in pairs(protected.children) do child.animate_all() end for _, child in pairs(protected.children) do child.get().animate_all() end
end end
end end
-- freeze animation(s) -- freeze animation(s)
function public.freeze() function public.freeze() protected.stop_anim() end
protected.stop_anim()
end
-- freeze animation(s) for this element and all its children -- freeze animation(s) for this element and all its children
function public.freeze_all() function public.freeze_all()
public.freeze() public.freeze()
for _, child in pairs(protected.children) do child.freeze_all() end for _, child in pairs(protected.children) do child.get().freeze_all() end
end end
-- re-draw the element -- re-draw this element and all its children
function public.redraw() function public.redraw()
protected.window.redraw() protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear()
protected.redraw()
for _, child in pairs(protected.children) do child.get().redraw() end
end end
-- if a content window is set, clears it then re-draws all children -- if a content window is set, clears it then re-draws all children
function public.content_redraw() function public.content_redraw()
if protected.content_window ~= nil then if protected.content_window ~= nil then
protected.content_window.clear() protected.content_window.clear()
for _, child in pairs(protected.children) do child.redraw() end for _, child in pairs(protected.children) do child.get().redraw() end
end end
end end

View File

@ -36,49 +36,49 @@ local function waiting(args)
if state >= 0 and state < 7 then if state >= 0 and state < 7 then
-- top -- top
e.window.setCursorPos(1 + math.floor(state / 2), 1) e.w_set_cur(1 + math.floor(state / 2), 1)
if state % 2 == 0 then if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
else else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x) e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end end
-- bottom -- bottom
e.window.setCursorPos(4 - math.ceil(state / 2), 3) e.w_set_cur(4 - math.ceil(state / 2), 3)
if state % 2 == 0 then if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
else else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x) e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end end
else else
local st = state - 7 local st = state - 7
-- right -- right
if st % 3 == 0 then if st % 3 == 0 then
e.window.setCursorPos(4, 1 + math.floor(st / 3)) e.w_set_cur(4, 1 + math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg) e.w_blit("\x83", blit_bg, blit_fg)
elseif st % 3 == 1 then elseif st % 3 == 1 then
e.window.setCursorPos(4, 1 + math.floor(st / 3)) e.w_set_cur(4, 1 + math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg) e.w_blit("\x8f", blit_bg, blit_fg)
e.window.setCursorPos(4, 2 + math.floor(st / 3)) e.w_set_cur(4, 2 + math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg) e.w_blit("\x83", blit_fg, blit_bg)
else else
e.window.setCursorPos(4, 2 + math.floor(st / 3)) e.w_set_cur(4, 2 + math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
end end
-- left -- left
if st % 3 == 0 then if st % 3 == 0 then
e.window.setCursorPos(1, 3 - math.floor(st / 3)) e.w_set_cur(1, 3 - math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg) e.w_blit("\x83", blit_fg, blit_bg)
e.window.setCursorPos(1, 2 - math.floor(st / 3)) e.w_set_cur(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg) e.w_blit("\x8f", blit_bg, blit_fg)
elseif st % 3 == 1 then elseif st % 3 == 1 then
e.window.setCursorPos(1, 2 - math.floor(st / 3)) e.w_set_cur(1, 2 - math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg) e.w_blit("\x83", blit_bg, blit_fg)
else else
e.window.setCursorPos(1, 2 - math.floor(st / 3)) e.w_set_cur(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg) e.w_blit("\x8f", blit_fg, blit_bg)
end end
end end

View File

@ -1,7 +1,5 @@
-- Color Map Graphics Element -- Color Map Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element") local element = require("graphics.element")
---@class colormap_args ---@class colormap_args
@ -16,7 +14,7 @@ local element = require("graphics.element")
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function colormap(args) local function colormap(args)
local bkg = "008877FFCCEE114455DD9933BBAA2266" local bkg = "008877FFCCEE114455DD9933BBAA2266"
local spaces = util.spaces(32) local spaces = string.rep(" ", 32)
args.width = 32 args.width = 32
args.height = 1 args.height = 1
@ -25,8 +23,13 @@ local function colormap(args)
local e = element.new(args) local e = element.new(args)
-- draw color map -- draw color map
e.window.setCursorPos(1, 1) function e.redraw()
e.window.blit(spaces, bkg, bkg) e.w_set_cur(1, 1)
e.w_blit(spaces, bkg, bkg)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -5,7 +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 CLICK_TYPE = core.events.CLICK_TYPE local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class app_button_args ---@class app_button_args
---@field text string app icon text ---@field text string app icon text
@ -24,22 +24,17 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@param args app_button_args ---@param args app_button_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function app_button(args) local function app_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.app: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
assert(type(args.title) == "string", "graphics.elements.controls.app: title is a required field") element.assert(type(args.title) == "string", "title is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.app: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.app_fg_bg) == "table", "graphics.elements.controls.app: app_fg_bg is a required field") element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
args.height = 4 args.height = 4
args.width = 5 args.width = 5
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- write app title, centered
e.window.setCursorPos(1, 4)
e.window.setCursorPos(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
e.window.write(args.title)
-- draw the app button -- draw the app button
local function draw() local function draw()
local fgd = args.app_fg_bg.fgd local fgd = args.app_fg_bg.fgd
@ -51,36 +46,36 @@ local function app_button(args)
end end
-- draw icon -- draw icon
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.setTextColor(fgd) e.w_set_fgd(fgd)
e.window.setBackgroundColor(bkg) e.w_set_bkg(bkg)
e.window.write("\x9f\x83\x83\x83") e.w_write("\x9f\x83\x83\x83")
e.window.setTextColor(bkg) e.w_set_fgd(bkg)
e.window.setBackgroundColor(fgd) e.w_set_bkg(fgd)
e.window.write("\x90") e.w_write("\x90")
e.window.setTextColor(fgd) e.w_set_fgd(fgd)
e.window.setBackgroundColor(bkg) e.w_set_bkg(bkg)
e.window.setCursorPos(1, 2) e.w_set_cur(1, 2)
e.window.write("\x95 ") e.w_write("\x95 ")
e.window.setTextColor(bkg) e.w_set_fgd(bkg)
e.window.setBackgroundColor(fgd) e.w_set_bkg(fgd)
e.window.write("\x95") e.w_write("\x95")
e.window.setCursorPos(1, 3) e.w_set_cur(1, 3)
e.window.write("\x82\x8f\x8f\x8f\x81") e.w_write("\x82\x8f\x8f\x8f\x81")
-- write the icon text -- write the icon text
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.setTextColor(fgd) e.w_set_fgd(fgd)
e.window.setBackgroundColor(bkg) e.w_set_bkg(bkg)
e.window.write(args.text) e.w_write(args.text)
end end
-- draw the app button as pressed (if active_fg_bg set) -- draw the app button as pressed (if active_fg_bg set)
local function show_pressed() local function show_pressed()
if e.enabled and args.active_fg_bg ~= nil then if e.enabled and args.active_fg_bg ~= nil then
e.value = true e.value = true
e.window.setTextColor(args.active_fg_bg.fgd) e.w_set_fgd(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg) e.w_set_bkg(args.active_fg_bg.bkg)
draw() draw()
end end
end end
@ -89,8 +84,8 @@ local function app_button(args)
local function show_unpressed() local function show_unpressed()
if e.enabled and args.active_fg_bg ~= nil then if e.enabled and args.active_fg_bg ~= nil then
e.value = false e.value = false
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
draw() draw()
end end
end end
@ -99,14 +94,14 @@ local function app_button(args)
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
function e.handle_mouse(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
if event.type == CLICK_TYPE.TAP then if event.type == MOUSE_CLICK.TAP then
show_pressed() show_pressed()
-- show as unpressed in 0.25 seconds -- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback() args.callback()
elseif event.type == CLICK_TYPE.DOWN then elseif event.type == MOUSE_CLICK.DOWN then
show_pressed() show_pressed()
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
show_unpressed() show_unpressed()
if e.in_frame_bounds(event.current.x, event.current.y) then if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback() args.callback()
@ -118,11 +113,18 @@ local function app_button(args)
-- set the value (true simulates pressing the app button) -- set the value (true simulates pressing the app button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end
-- element redraw
function e.redraw()
e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
e.w_write(args.title)
draw()
end end
-- initial draw -- initial draw
draw() e.redraw()
return e.complete() return e.complete()
end end

View File

@ -6,7 +6,8 @@ local element = require("graphics.element")
---@class checkbox_args ---@class checkbox_args
---@field label string checkbox text ---@field label string checkbox text
---@field box_fg_bg cpair colors for checkbox ---@field box_fg_bg cpair colors for checkbox
---@field callback function function to call on press ---@field default? boolean default value
---@field callback? function function to call on press
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
@ -18,48 +19,75 @@ local element = require("graphics.element")
---@param args checkbox_args ---@param args checkbox_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function checkbox(args) local function checkbox(args)
assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field") element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field")
args.can_focus = true
args.height = 1 args.height = 1
args.width = 3 + string.len(args.label) args.width = 3 + string.len(args.label)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = false e.value = args.default == true
-- show the button state -- show the button state
local function draw() local function draw()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if e.value then if e.value then
-- show as selected -- show as selected
e.window.setTextColor(args.box_fg_bg.bkg) e.w_set_fgd(args.box_fg_bg.bkg)
e.window.setBackgroundColor(args.box_fg_bg.fgd) e.w_set_bkg(args.box_fg_bg.fgd)
e.window.write("\x88") e.w_write("\x88")
e.window.setTextColor(args.box_fg_bg.fgd) e.w_set_fgd(args.box_fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.window.write("\x95") e.w_write("\x95")
else else
-- show as unselected -- show as unselected
e.window.setTextColor(e.fg_bg.bkg) e.w_set_fgd(e.fg_bg.bkg)
e.window.setBackgroundColor(args.box_fg_bg.bkg) e.w_set_bkg(args.box_fg_bg.bkg)
e.window.write("\x88") e.w_write("\x88")
e.window.setTextColor(args.box_fg_bg.bkg) e.w_set_fgd(args.box_fg_bg.bkg)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.window.write("\x95") e.w_write("\x95")
end
end
-- write label text
local function draw_label()
if e.enabled and e.is_focused() then
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
e.w_write(args.label)
else
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write(args.label)
end 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)
if e.enabled and core.events.was_clicked(event.type) then if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = not e.value e.value = not e.value
draw() draw()
args.callback(e.value) if type(args.callback) == "function" then args.callback(e.value) end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == core.events.KEY_CLICK.DOWN then
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
e.value = not e.value
draw()
if type(args.callback) == "function" then args.callback(e.value) end
end
end end
end end
@ -70,14 +98,22 @@ local function checkbox(args)
draw() draw()
end end
-- write label text -- handle focus
e.window.setCursorPos(3, 1) e.on_focused = draw_label
e.window.setTextColor(e.fg_bg.fgd) e.on_unfocused = draw_label
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.write(args.label) -- handle enable
e.on_enabled = draw_label
e.on_disabled = draw_label
-- element redraw
function e.redraw()
draw()
draw_label()
end
-- initial draw -- initial draw
draw() e.redraw()
return e.complete() return e.complete()
end end

View File

@ -1,7 +1,6 @@
-- Hazard-bordered Button Graphics Element -- Hazard-bordered Button Graphics Element
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
@ -22,47 +21,42 @@ local element = require("graphics.element")
---@param args hazard_button_args ---@param args hazard_button_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function hazard_button(args) local function hazard_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.hazard_button: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
assert(type(args.accent) == "number", "graphics.elements.controls.hazard_button: accent is a required field") element.assert(type(args.accent) == "number", "accent is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.hazard_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
-- static dimensions
args.height = 3 args.height = 3
args.width = string.len(args.text) + 4 args.width = string.len(args.text) + 4
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- write the button text
e.window.setCursorPos(3, 2)
e.window.write(args.text)
-- draw border -- draw border
---@param accent color accent color ---@param accent color accent color
local function draw_border(accent) local function draw_border(accent)
-- top -- top
e.window.setTextColor(accent) e.w_set_fgd(accent)
e.window.setBackgroundColor(args.fg_bg.bkg) e.w_set_bkg(args.fg_bg.bkg)
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.write("\x99" .. util.strrep("\x89", args.width - 2) .. "\x99") e.w_write("\x99" .. string.rep("\x89", args.width - 2) .. "\x99")
-- center left -- center left
e.window.setCursorPos(1, 2) e.w_set_cur(1, 2)
e.window.setTextColor(args.fg_bg.bkg) e.w_set_fgd(args.fg_bg.bkg)
e.window.setBackgroundColor(accent) e.w_set_bkg(accent)
e.window.write("\x99") e.w_write("\x99")
-- center right -- center right
e.window.setTextColor(args.fg_bg.bkg) e.w_set_fgd(args.fg_bg.bkg)
e.window.setBackgroundColor(accent) e.w_set_bkg(accent)
e.window.setCursorPos(args.width, 2) e.w_set_cur(args.width, 2)
e.window.write("\x99") e.w_write("\x99")
-- bottom -- bottom
e.window.setTextColor(accent) e.w_set_fgd(accent)
e.window.setBackgroundColor(args.fg_bg.bkg) e.w_set_bkg(args.fg_bg.bkg)
e.window.setCursorPos(1, 3) e.w_set_cur(1, 3)
e.window.write("\x99" .. util.strrep("\x98", args.width - 2) .. "\x99") e.w_write("\x99" .. string.rep("\x98", args.width - 2) .. "\x99")
end end
-- on request timeout: recursively calls itself to double flash button text -- on request timeout: recursively calls itself to double flash button text
@ -73,9 +67,9 @@ local function hazard_button(args)
if n == 0 then if n == 0 then
-- go back off -- go back off
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
if n >= 4 then if n >= 4 then
@ -83,18 +77,18 @@ local function hazard_button(args)
elseif n % 2 == 0 then elseif n % 2 == 0 then
-- toggle text color on after 0.25 seconds -- toggle text color on after 0.25 seconds
tcd.dispatch(0.25, function () tcd.dispatch(0.25, function ()
e.window.setTextColor(args.accent) e.w_set_fgd(args.accent)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_timeout(n + 1) on_timeout(n + 1)
on_timeout(n + 1) on_timeout(n + 1)
end) end)
elseif n % 1 then elseif n % 1 then
-- toggle text color off after 0.25 seconds -- toggle text color off after 0.25 seconds
tcd.dispatch(0.25, function () tcd.dispatch(0.25, function ()
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_timeout(n + 1) on_timeout(n + 1)
end) end)
end end
@ -102,9 +96,9 @@ local function hazard_button(args)
-- blink routine for success indication -- blink routine for success indication
local function on_success() local function on_success()
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
-- blink routine for failure indication -- blink routine for failure indication
@ -115,9 +109,9 @@ local function hazard_button(args)
if n == 0 then if n == 0 then
-- go back off -- go back off
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
if n >= 2 then if n >= 2 then
@ -125,17 +119,17 @@ local function hazard_button(args)
elseif n % 2 == 0 then elseif n % 2 == 0 then
-- toggle text color on after 0.5 seconds -- toggle text color on after 0.5 seconds
tcd.dispatch(0.5, function () tcd.dispatch(0.5, function ()
e.window.setTextColor(args.accent) e.w_set_fgd(args.accent)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_failure(n + 1) on_failure(n + 1)
end) end)
elseif n % 1 then elseif n % 1 then
-- toggle text color off after 0.25 seconds -- toggle text color off after 0.25 seconds
tcd.dispatch(0.25, function () tcd.dispatch(0.25, function ()
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
on_failure(n + 1) on_failure(n + 1)
end) end)
end end
@ -147,9 +141,9 @@ local function hazard_button(args)
if e.enabled then if e.enabled then
if core.events.was_clicked(event.type) then if core.events.was_clicked(event.type) then
-- change text color to indicate clicked -- change text color to indicate clicked
e.window.setTextColor(args.accent) e.w_set_fgd(args.accent)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
-- abort any other callbacks -- abort any other callbacks
tcd.abort(on_timeout) tcd.abort(on_timeout)
@ -159,7 +153,6 @@ local function hazard_button(args)
-- 1.5 second timeout -- 1.5 second timeout
tcd.dispatch(1.5, on_timeout) tcd.dispatch(1.5, on_timeout)
-- call the touch callback
args.callback() args.callback()
end end
end end
@ -175,29 +168,37 @@ local function hazard_button(args)
-- set the value (true simulates pressing the button) -- set the value (true simulates pressing the button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end end
-- show the button as disabled -- show the button as disabled
function e.disable() function e.on_disabled()
if args.dis_colors then if args.dis_colors then
draw_border(args.dis_colors.color_a) draw_border(args.dis_colors.color_a)
e.window.setTextColor(args.dis_colors.color_b) e.w_set_fgd(args.dis_colors.color_b)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
end end
-- show the button as enabled -- show the button as enabled
function e.enable() function e.on_enabled()
draw_border(args.accent) draw_border(args.accent)
e.window.setTextColor(args.fg_bg.fgd) e.w_set_fgd(args.fg_bg.fgd)
e.window.setCursorPos(3, 2) e.w_set_cur(3, 2)
e.window.write(args.text) e.w_write(args.text)
end end
-- initial draw of border -- element redraw
draw_border(args.accent) function e.redraw()
-- write the button text and draw border
e.w_set_cur(3, 2)
e.w_write(args.text)
draw_border(args.accent)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -29,13 +29,11 @@ local element = require("graphics.element")
---@param args multi_button_args ---@param args multi_button_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function multi_button(args) local function multi_button(args)
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field") element.assert(type(args.options) == "table", "options is a required field")
assert(#args.options > 0, "graphics.elements.controls.multi_button: at least one option is required") element.assert(#args.options > 0, "at least one option is required")
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
"graphics.elements.controls.multi_button: default 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")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
"graphics.elements.controls.multi_button: min_width must be nil or a number > 0")
-- single line -- single line
args.height = 1 args.height = 1
@ -71,23 +69,23 @@ local function multi_button(args)
end end
-- show the button state -- show the button state
local function draw() function e.redraw()
for i = 1, #args.options do for i = 1, #args.options do
local opt = args.options[i] ---@type button_option local opt = args.options[i] ---@type button_option
e.window.setCursorPos(opt._start_x, 1) e.w_set_cur(opt._start_x, 1)
if e.value == i then if e.value == i then
-- show as pressed -- show as pressed
e.window.setTextColor(opt.active_fg_bg.fgd) e.w_set_fgd(opt.active_fg_bg.fgd)
e.window.setBackgroundColor(opt.active_fg_bg.bkg) e.w_set_bkg(opt.active_fg_bg.bkg)
else else
-- show as unpressed -- show as unpressed
e.window.setTextColor(opt.fg_bg.fgd) e.w_set_fgd(opt.fg_bg.fgd)
e.window.setBackgroundColor(opt.fg_bg.bkg) e.w_set_bkg(opt.fg_bg.bkg)
end end
e.window.write(util.pad(opt.text, button_width)) e.w_write(util.pad(opt.text, button_width))
end end
end end
@ -115,7 +113,7 @@ local function multi_button(args)
-- tap always has identical coordinates, so this always passes for taps -- tap always has identical coordinates, so this always passes for taps
if button_ini == button_cur and button_cur ~= nil then if button_ini == button_cur and button_cur ~= nil then
e.value = button_cur e.value = button_cur
draw() e.redraw()
args.callback(e.value) args.callback(e.value)
end end
end end
@ -125,11 +123,11 @@ local function multi_button(args)
---@param val integer new value ---@param val integer new value
function e.set_value(val) function e.set_value(val)
e.value = val e.value = val
draw() e.redraw()
end end
-- initial draw -- initial draw
draw() e.redraw()
return e.complete() return e.complete()
end end

View File

@ -5,7 +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 CLICK_TYPE = core.events.CLICK_TYPE local MOUSE_CLICK = core.events.MOUSE_CLICK
local KEY_CLICK = core.events.KEY_CLICK
---@class push_button_args ---@class push_button_args
---@field text string button text ---@field text string button text
@ -25,14 +26,14 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@param args push_button_args ---@param args push_button_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function push_button(args) local function push_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 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")
"graphics.elements.controls.push_button: min_width must be nil or a number > 0")
local text_width = string.len(args.text) local text_width = string.len(args.text)
-- single line height, calculate width -- set automatic settings
args.can_focus = true
args.height = 1 args.height = 1
args.min_width = args.min_width or 0 args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width) args.width = math.max(text_width, args.min_width)
@ -44,21 +45,21 @@ local function push_button(args)
local v_pad = math.floor(e.frame.h / 2) + 1 local v_pad = math.floor(e.frame.h / 2) + 1
-- draw the button -- draw the button
local function draw() function e.redraw()
e.window.clear() e.window.clear()
-- write the button text -- write the button text
e.window.setCursorPos(h_pad, v_pad) e.w_set_cur(h_pad, v_pad)
e.window.write(args.text) e.w_write(args.text)
end end
-- draw the button as pressed (if active_fg_bg set) -- draw the button as pressed (if active_fg_bg set)
local function show_pressed() local function show_pressed()
if e.enabled and args.active_fg_bg ~= nil then if e.enabled and args.active_fg_bg ~= nil then
e.value = true e.value = true
e.window.setTextColor(args.active_fg_bg.fgd) e.w_set_fgd(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg) e.w_set_bkg(args.active_fg_bg.bkg)
draw() e.redraw()
end end
end end
@ -66,9 +67,9 @@ local function push_button(args)
local function show_unpressed() local function show_unpressed()
if e.enabled and args.active_fg_bg ~= nil then if e.enabled and args.active_fg_bg ~= nil then
e.value = false e.value = false
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
draw() e.redraw()
end end
end end
@ -76,14 +77,14 @@ local function push_button(args)
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
function e.handle_mouse(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
if event.type == CLICK_TYPE.TAP then if event.type == MOUSE_CLICK.TAP then
show_pressed() show_pressed()
-- show as unpressed in 0.25 seconds -- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback() args.callback()
elseif event.type == CLICK_TYPE.DOWN then elseif event.type == MOUSE_CLICK.DOWN then
show_pressed() show_pressed()
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
show_unpressed() show_unpressed()
if e.in_frame_bounds(event.current.x, event.current.y) then if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback() args.callback()
@ -92,34 +93,49 @@ local function push_button(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 then
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
args.callback()
e.defocus()
end
end
end
-- set the value (true simulates pressing the button) -- set the value (true simulates pressing the button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end end
-- show butten as enabled -- show butten as enabled
function e.enable() function e.on_enabled()
if args.dis_fg_bg ~= nil then if args.dis_fg_bg ~= nil then
e.value = false e.value = false
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
draw() e.redraw()
end end
end end
-- show button as disabled -- show button as disabled
function e.disable() function e.on_disabled()
if args.dis_fg_bg ~= nil then if args.dis_fg_bg ~= nil then
e.value = false e.value = false
e.window.setTextColor(args.dis_fg_bg.fgd) e.w_set_fgd(args.dis_fg_bg.fgd)
e.window.setBackgroundColor(args.dis_fg_bg.bkg) e.w_set_bkg(args.dis_fg_bg.bkg)
draw() e.redraw()
end end
end end
-- handle focus
e.on_focused = show_pressed
e.on_unfocused = show_unpressed
-- initial draw -- initial draw
draw() e.redraw()
return e.complete() return e.complete()
end end

View File

@ -0,0 +1,203 @@
-- 2D Radio Button Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class radio_2d_args
---@field rows integer
---@field columns integer
---@field options table
---@field radio_colors cpair radio button colors (inner & outer)
---@field select_color? color color for radio button when selected
---@field color_map? table colors for each radio button when selected
---@field disable_color? color color for radio button when disabled
---@field disable_fg_bg? cpair text colors when disabled
---@field default? integer default state, defaults to options[1]
---@field callback? function function to call on touch
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new 2D radio button list (latch selection, exclusively one color at a time)
---@param args radio_2d_args
---@return graphics_element element, element_id id
local function radio_2d_button(args)
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1")
element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers")
element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required")
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
local array = {}
local col_widths = {}
local next_idx = 1
local total_width = 0
local max_rows = 1
local focused_opt = 1
local focus_x, focus_y = 1, 1
-- build table to display
for col = 1, args.columns do
local max_width = 0
array[col] = {}
for row = 1, args.rows do
local len = string.len(args.options[next_idx])
if len > max_width then max_width = len end
if row > max_rows then max_rows = row end
table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len })
next_idx = next_idx + 1
if next_idx > #args.options then break end
end
table.insert(col_widths, max_width + 3)
total_width = total_width + max_width + 3
if next_idx > #args.options then break end
end
args.can_focus = true
args.width = total_width
args.height = max_rows
-- create new graphics element base object
local e = element.new(args)
-- selected option (convert nil to 1 if missing)
e.value = args.default or 1
-- draw the element
function e.redraw()
local col_x = 1
local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b)
for col = 1, #array do
for row = 1, #array[col] do
local opt = array[col][row]
local select_color = args.select_color
if type(args.color_map) == "table" and args.color_map[opt.id] then
select_color = args.color_map[opt.id]
end
local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a)
local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b)
e.w_set_cur(col_x, row)
e.w_set_fgd(inner_color)
e.w_set_bkg(outer_color)
e.w_write("\x88")
e.w_set_fgd(outer_color)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
if opt.id == focused_opt then
focus_x, focus_y = row, col
end
-- write button text
if opt.id == focused_opt and e.is_focused() and e.enabled then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
elseif type(args.disable_fg_bg) == "table" and not e.enabled then
e.w_set_fgd(args.disable_fg_bg.fgd)
e.w_set_bkg(args.disable_fg_bg.bkg)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_write(opt.text)
end
col_x = col_x + col_widths[col]
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
-- determine what was pressed
for _, row in ipairs(array) do
local elem = row[event.current.y]
if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then
e.value = elem.id
focused_opt = elem.id
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
break
end
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then
if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
e.value = focused_opt
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
elseif event.key == keys.down then
if focused_opt < #args.options then
focused_opt = focused_opt + 1
e.redraw()
end
elseif event.key == keys.up then
if focused_opt > 1 then
focused_opt = focused_opt - 1
e.redraw()
end
elseif event.key == keys.right then
if array[focus_y + 1] and array[focus_y + 1][focus_x] then
focused_opt = array[focus_y + 1][focus_x].id
else focused_opt = array[1][focus_x].id end
e.redraw()
elseif event.key == keys.left then
if array[focus_y - 1] and array[focus_y - 1][focus_x] then
focused_opt = array[focus_y - 1][focus_x].id
e.redraw()
elseif array[#array][focus_x] then
focused_opt = array[#array][focus_x].id
e.redraw()
end
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
if type(val) == "number" and val > 0 and val <= #args.options then
e.value = val
e.redraw()
end
end
-- handle focus & enable
e.on_focused = e.redraw
e.on_unfocused = e.redraw
e.on_enabled = e.redraw
e.on_disabled = e.redraw
-- initial draw
e.redraw()
return e.complete()
end
return radio_2d_button

View File

@ -1,15 +1,19 @@
-- Radio Button Graphics Element -- Radio Button Graphics Element
local util = require("scada-common.util")
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
---@class radio_button_args ---@class radio_button_args
---@field options table button options ---@field options table button options
---@field callback function function to call on touch ---@field radio_colors cpair radio button colors (inner & outer)
---@field radio_colors cpair colors for radio button center dot when active (a) or inactive (b) ---@field select_color color color for radio button border when selected
---@field radio_bg color background color of radio button
---@field default? integer default state, defaults to options[1] ---@field default? integer default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted ---@field min_width? integer text length + 2 if omitted
---@field callback? function function to call on touch
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
@ -21,16 +25,12 @@ local element = require("graphics.element")
---@param args radio_button_args ---@param args radio_button_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function radio_button(args) local function radio_button(args)
assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field") element.assert(type(args.options) == "table", "options is a required field")
assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required") element.assert(#args.options > 0, "at least one option is required")
assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field") element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), element.assert(type(args.select_color) == "number", "select_color is a required field")
"graphics.elements.controls.radio_button: default must be nil or a number > 0") element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 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")
"graphics.elements.controls.radio_button: min_width must be nil or a number > 0")
-- one line per option
args.height = #args.options
-- determine widths -- determine widths
local max_width = 1 local max_width = 1
@ -43,41 +43,47 @@ local function radio_button(args)
local button_text_width = math.max(max_width, args.min_width or 0) local button_text_width = math.max(max_width, args.min_width or 0)
-- set automatic args
args.can_focus = true
args.width = button_text_width + 2 args.width = button_text_width + 2
args.height = #args.options -- one line per option
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
local focused_opt = 1
-- button state (convert nil to 1 if missing) -- button state (convert nil to 1 if missing)
e.value = args.default or 1 e.value = args.default or 1
-- show the button state -- show the button state
local function draw() function e.redraw()
for i = 1, #args.options do for i = 1, #args.options do
local opt = args.options[i] ---@type string local opt = args.options[i] ---@type string
e.window.setCursorPos(1, i) local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a)
local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b)
if e.value == i then e.w_set_cur(1, i)
-- show as selected
e.window.setTextColor(args.radio_colors.color_a)
e.window.setBackgroundColor(args.radio_bg)
else
-- show as unselected
e.window.setTextColor(args.radio_colors.color_b)
e.window.setBackgroundColor(args.radio_bg)
end
e.window.write("\x88") e.w_set_fgd(inner_color)
e.w_set_bkg(outer_color)
e.w_write("\x88")
e.window.setTextColor(args.radio_bg) e.w_set_fgd(outer_color)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.window.write("\x95") e.w_write("\x95")
-- write button text -- write button text
e.window.setTextColor(e.fg_bg.fgd) if i == focused_opt and e.is_focused() and e.enabled then
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_fgd(e.fg_bg.bkg)
e.window.write(opt) e.w_set_bkg(e.fg_bg.fgd)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
end
e.w_write(opt)
end end
end end
@ -88,8 +94,31 @@ local function radio_button(args)
-- determine what was pressed -- determine what was pressed
if args.options[event.current.y] ~= nil then if args.options[event.current.y] ~= nil then
e.value = event.current.y e.value = event.current.y
draw() focused_opt = e.value
args.callback(e.value) e.redraw()
if type(args.callback) == "function" then args.callback(e.value) 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.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
e.value = focused_opt
e.redraw()
if type(args.callback) == "function" then args.callback(e.value) end
elseif event.key == keys.down then
if focused_opt < #args.options then
focused_opt = focused_opt + 1
e.redraw()
end
elseif event.key == keys.up then
if focused_opt > 1 then
focused_opt = focused_opt - 1
e.redraw()
end
end end
end end
end end
@ -97,12 +126,20 @@ local function radio_button(args)
-- set the value -- set the value
---@param val integer new value ---@param val integer new value
function e.set_value(val) function e.set_value(val)
e.value = val if type(val) == "number" and val > 0 and val <= #args.options then
draw() e.value = val
e.redraw()
end
end end
-- handle focus & enable
e.on_focused = e.redraw
e.on_unfocused = e.redraw
e.on_enabled = e.redraw
e.on_disabled = e.redraw
-- initial draw -- initial draw
draw() e.redraw()
return e.complete() return e.complete()
end end

View File

@ -1,11 +1,12 @@
-- Sidebar Graphics Element -- Sidebar Graphics Element
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local CLICK_TYPE = core.events.CLICK_TYPE local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class sidebar_tab ---@class sidebar_tab
---@field char string character identifier ---@field char string character identifier
@ -26,25 +27,28 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@param args sidebar_args ---@param args sidebar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function sidebar(args) local function sidebar(args)
assert(type(args.tabs) == "table", "graphics.elements.controls.sidebar: tabs is a required field") element.assert(type(args.tabs) == "table", "tabs is a required field")
assert(#args.tabs > 0, "graphics.elements.controls.sidebar: at least one tab is required") element.assert(#args.tabs > 0, "at least one tab is required")
assert(type(args.callback) == "function", "graphics.elements.controls.sidebar: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
-- always 3 wide
args.width = 3 args.width = 3
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
assert(e.frame.h >= (#args.tabs * 3), "graphics.elements.controls.sidebar: height insufficent to display all tabs") element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
-- default to 1st tab -- default to 1st tab
e.value = 1 e.value = 1
local was_pressed = false
-- show the button state -- show the button state
---@param pressed boolean if the currently selected tab should appear as actively pressed ---@param pressed? boolean if the currently selected tab should appear as actively pressed
---@param pressed_idx? integer optional index to show as held (that is not yet selected) ---@param pressed_idx? integer optional index to show as held (that is not yet selected)
local function draw(pressed, pressed_idx) local function draw(pressed, pressed_idx)
pressed = util.trinary(pressed == nil, was_pressed, pressed)
was_pressed = pressed
pressed_idx = pressed_idx or e.value pressed_idx = pressed_idx or e.value
for i = 1, #args.tabs do for i = 1, #args.tabs do
@ -52,27 +56,23 @@ local function sidebar(args)
local y = ((i - 1) * 3) + 1 local y = ((i - 1) * 3) + 1
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
if pressed and i == pressed_idx then if pressed and i == pressed_idx then
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
else else
e.window.setTextColor(tab.color.fgd) e.w_set_fgd(tab.color.fgd)
e.window.setBackgroundColor(tab.color.bkg) e.w_set_bkg(tab.color.bkg)
end end
e.window.write(" ") e.w_write(" ")
e.window.setCursorPos(1, y + 1) e.w_set_cur(1, y + 1)
if e.value == i then if e.value == i then
-- show as selected e.w_write(" " .. tab.char .. "\x10")
e.window.write(" " .. tab.char .. "\x10") else e.w_write(" " .. tab.char .. " ") end
else e.w_set_cur(1, y + 2)
-- show as unselected e.w_write(" ")
e.window.write(" " .. tab.char .. " ")
end
e.window.setCursorPos(1, y + 2)
e.window.write(" ")
end end
end end
@ -85,22 +85,22 @@ local function sidebar(args)
local ini_idx = math.ceil(event.initial.y / 3) local ini_idx = math.ceil(event.initial.y / 3)
if args.tabs[cur_idx] ~= nil then if args.tabs[cur_idx] ~= nil then
if event.type == CLICK_TYPE.TAP then if event.type == MOUSE_CLICK.TAP then
e.value = cur_idx e.value = cur_idx
draw(true) draw(true)
-- show as unpressed in 0.25 seconds -- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end) tcd.dispatch(0.25, function () draw(false) end)
args.callback(e.value) args.callback(e.value)
elseif event.type == CLICK_TYPE.DOWN then elseif event.type == MOUSE_CLICK.DOWN then
draw(true, cur_idx) draw(true, cur_idx)
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx e.value = cur_idx
draw(false) draw(false)
args.callback(e.value) args.callback(e.value)
else draw(false) end else draw(false) end
end end
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
draw(false) draw(false)
end end
end end
@ -113,8 +113,10 @@ local function sidebar(args)
draw(false) draw(false)
end end
-- initial draw -- element redraw
draw(false) e.redraw = draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -29,8 +29,8 @@ local function spinbox(args)
local wn_prec = args.whole_num_precision local wn_prec = args.whole_num_precision
local fr_prec = args.fractional_precision local fr_prec = args.fractional_precision
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer") element.assert(util.is_int(wn_prec), "whole number precision must be an integer")
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer") element.assert(util.is_int(fr_prec), "fractional precision must be an integer")
local fmt, fmt_init ---@type string, string local fmt, fmt_init ---@type string, string
@ -44,7 +44,7 @@ local function spinbox(args)
local dec_point_x = args.whole_num_precision + 1 local dec_point_x = args.whole_num_precision + 1
assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field") element.assert(type(args.arrow_fg_bg) == "table", "arrow_fg_bg is a required field")
-- determine widths -- determine widths
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0) args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
@ -58,22 +58,20 @@ local function spinbox(args)
-- draw the arrows -- draw the arrows
local function draw_arrows(color) local function draw_arrows(color)
e.window.setBackgroundColor(args.arrow_fg_bg.bkg) e.w_set_bkg(args.arrow_fg_bg.bkg)
e.window.setTextColor(color) e.w_set_fgd(color)
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.write(util.strrep("\x1e", wn_prec)) e.w_write(string.rep("\x1e", wn_prec))
e.window.setCursorPos(1, 3) e.w_set_cur(1, 3)
e.window.write(util.strrep("\x1f", wn_prec)) e.w_write(string.rep("\x1f", wn_prec))
if fr_prec > 0 then if fr_prec > 0 then
e.window.setCursorPos(1 + wn_prec, 1) e.w_set_cur(1 + wn_prec, 1)
e.window.write(" " .. util.strrep("\x1e", fr_prec)) e.w_write(" " .. string.rep("\x1e", fr_prec))
e.window.setCursorPos(1 + wn_prec, 3) e.w_set_cur(1 + wn_prec, 3)
e.window.write(" " .. util.strrep("\x1f", fr_prec)) e.w_write(" " .. string.rep("\x1f", fr_prec))
end end
end end
draw_arrows(args.arrow_fg_bg.fgd)
-- populate digits from current value -- populate digits from current value
local function set_digits() local function set_digits()
local initial_str = util.sprintf(fmt_init, e.value) local initial_str = util.sprintf(fmt_init, e.value)
@ -119,15 +117,12 @@ local function spinbox(args)
end end
-- draw -- draw
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setCursorPos(1, 2) e.w_set_cur(1, 2)
e.window.write(util.sprintf(fmt, e.value)) e.w_write(util.sprintf(fmt, e.value))
end end
-- init with the default value
show_num()
-- 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)
@ -138,10 +133,8 @@ local function spinbox(args)
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x) local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
if digits[idx] ~= nil then if digits[idx] ~= nil then
if event.current.y == 1 then if event.current.y == 1 then
-- increment
digits[idx] = digits[idx] + 1 digits[idx] = digits[idx] + 1
elseif event.current.y == 3 then elseif event.current.y == 3 then
-- decrement
digits[idx] = digits[idx] - 1 digits[idx] = digits[idx] - 1
end end
@ -176,18 +169,19 @@ local function spinbox(args)
end end
-- enable this input -- enable this input
function e.enable() function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end
draw_arrows(args.arrow_fg_bg.fgd)
end
-- disable this input -- disable this input
function e.disable() function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end
draw_arrows(args.arrow_disable or colors.lightGray)
-- element redraw
function e.redraw()
show_num()
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
end end
-- default to zero, init digits table -- initial draw
e.value = 0 e.redraw()
set_digits()
return e.complete() return e.complete()
end end

View File

@ -21,15 +21,13 @@ local element = require("graphics.element")
---@param args switch_button_args ---@param args switch_button_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function switch_button(args) local function switch_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field") element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 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")
"graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
local text_width = string.len(args.text) local text_width = string.len(args.text)
-- single line height, calculate width
args.height = 1 args.height = 1
args.min_width = args.min_width or 0 args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width) args.width = math.max(text_width, args.min_width)
@ -37,44 +35,32 @@ local function switch_button(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- button state (convert nil to false if missing)
e.value = args.default or false e.value = args.default or false
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1 local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
local v_pad = math.floor(e.frame.h / 2) + 1 local v_pad = math.floor(e.frame.h / 2) + 1
-- show the button state -- show the button state
local function draw_state() function e.redraw()
if e.value then if e.value then
-- show as pressed e.w_set_fgd(args.active_fg_bg.fgd)
e.window.setTextColor(args.active_fg_bg.fgd) e.w_set_bkg(args.active_fg_bg.bkg)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
else else
-- show as unpressed e.w_set_fgd(e.fg_bg.fgd)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.bkg)
e.window.setBackgroundColor(e.fg_bg.bkg)
end end
-- clear to redraw background
e.window.clear() e.window.clear()
e.w_set_cur(h_pad, v_pad)
-- write the button text e.w_write(args.text)
e.window.setCursorPos(h_pad, v_pad)
e.window.write(args.text)
end end
-- initial draw
draw_state()
-- 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)
if e.enabled and core.events.was_clicked(event.type) then if e.enabled and core.events.was_clicked(event.type) then
-- toggle state
e.value = not e.value e.value = not e.value
draw_state() e.redraw()
-- call the touch callback with state
args.callback(e.value) args.callback(e.value)
end end
end end
@ -82,11 +68,13 @@ local function switch_button(args)
-- set the value -- set the value
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
-- set state
e.value = val e.value = val
draw_state() e.redraw()
end end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -27,13 +27,11 @@ local element = require("graphics.element")
---@param args tabbar_args ---@param args tabbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tabbar(args) local function tabbar(args)
assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field") element.assert(type(args.tabs) == "table", "tabs is a required field")
assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required") element.assert(#args.tabs > 0, "at least one tab is required")
assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field") element.assert(type(args.callback) == "function", "callback is a required field")
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 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")
"graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
-- always 1 tall
args.height = 1 args.height = 1
-- determine widths -- determine widths
@ -50,7 +48,7 @@ local function tabbar(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs") element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
-- default to 1st tab -- default to 1st tab
e.value = 1 e.value = 1
@ -67,21 +65,21 @@ local function tabbar(args)
end end
-- show the tab state -- show the tab state
local function draw() function e.redraw()
for i = 1, #args.tabs do for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type tabbar_tab local tab = args.tabs[i] ---@type tabbar_tab
e.window.setCursorPos(tab._start_x, 1) e.w_set_cur(tab._start_x, 1)
if e.value == i then if e.value == i then
e.window.setTextColor(tab.color.fgd) e.w_set_fgd(tab.color.fgd)
e.window.setBackgroundColor(tab.color.bkg) e.w_set_bkg(tab.color.bkg)
else else
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
end end
e.window.write(util.pad(tab.name, button_width)) e.w_write(util.pad(tab.name, button_width))
end end
end end
@ -109,7 +107,7 @@ local function tabbar(args)
-- tap always has identical coordinates, so this always passes for taps -- tap always has identical coordinates, so this always passes for taps
if tab_ini == tab_cur and tab_cur ~= nil then if tab_ini == tab_cur and tab_cur ~= nil then
e.value = tab_cur e.value = tab_cur
draw() e.redraw()
args.callback(e.value) args.callback(e.value)
end end
end end
@ -119,11 +117,11 @@ local function tabbar(args)
---@param val integer new value ---@param val integer new value
function e.set_value(val) function e.set_value(val)
e.value = val e.value = val
draw() e.redraw()
end end
-- initial draw -- initial draw
draw() e.redraw()
return e.complete() return e.complete()
end end

View File

@ -0,0 +1,156 @@
-- Numeric Value Entry Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class number_field_args
---@field default? number default value, defaults to 0
---@field min? number minimum, forced on unfocus
---@field max? number maximum, forced on unfocus
---@field max_digits? integer maximum number of digits, defaults to width
---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new numeric entry field
---@param args number_field_args
---@return graphics_element element, element_id id
local function number_field(args)
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new(args)
local has_decimal = false
args.max_digits = args.max_digits or e.frame.w
-- set initial value
e.value = "" .. (args.default or 0)
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled then
if core.events.was_clicked(event.type) then
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then
if tonumber(event.name) then
if e.value == 0 then e.value = "" end
ifield.try_insert_char(event.name)
end
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
ifield.backspace()
has_decimal = string.find(e.value, "%.") ~= nil
elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
has_decimal = true
ifield.try_insert_char(".")
elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
ifield.set_value("-")
elseif event.key == keys.left then
ifield.nav_left()
elseif event.key == keys.right then
ifield.nav_right()
elseif event.key == keys.a and event.ctrl then
ifield.select_all()
elseif event.key == keys.home or event.key == keys.up then
ifield.nav_start()
elseif event.key == keys["end"] or event.key == keys.down then
ifield.nav_end()
end
end
end
-- set the value (must be a number)
---@param val number number to show
function e.set_value(val)
if tonumber(val) then ifield.set_value("" .. tonumber(val)) end
end
-- set minimum input value
---@param min integer minimum allowed value
function e.set_min(min)
args.min = min
e.on_unfocused()
end
-- set maximum input value
---@param max integer maximum allowed value
function e.set_max(max)
args.max = max
e.on_unfocused()
end
-- replace text with pasted text if its a number
---@param text string string pasted
function e.handle_paste(text)
if tonumber(text) then
ifield.set_value("" .. tonumber(text))
else
ifield.set_value("0")
end
end
-- handle unfocused
function e.on_unfocused()
local val = tonumber(e.value)
local max = tonumber(args.max)
local min = tonumber(args.min)
if type(val) == "number" then
if type(args.max) == "number" and val > max then
e.value = "" .. max
ifield.nav_start()
elseif type(args.min) == "number" and val < min then
e.value = "" .. min
ifield.nav_start()
end
else
e.value = ""
end
ifield.show()
end
-- handle focus (not unfocus), enable, and redraw with show()
e.on_focused = ifield.show
e.on_enabled = ifield.show
e.on_disabled = ifield.show
e.redraw = ifield.show
-- initial draw
e.redraw()
return e.complete()
end
return number_field

View File

@ -0,0 +1,105 @@
-- Text Value Entry Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
local KEY_CLICK = core.events.KEY_CLICK
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class text_field_args
---@field value? string initial value
---@field max_len? integer maximum string length
---@field censor? string character to replace text with when printing to screen
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new text entry field
---@param args text_field_args
---@return graphics_element element, element_id id, function censor_ctl
local function text_field(args)
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new(args)
-- set initial value
e.value = args.value or ""
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg)
ifield.censor(args.censor)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled then
if core.events.was_clicked(event.type) then
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()
end
end
end
-- handle keyboard interaction
---@param event key_interaction key event
function e.handle_key(event)
if event.type == KEY_CLICK.CHAR then
ifield.try_insert_char(event.name)
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
if (event.key == keys.backspace or event.key == keys.delete) then
ifield.backspace()
elseif event.key == keys.left then
ifield.nav_left()
elseif event.key == keys.right then
ifield.nav_right()
elseif event.key == keys.a and event.ctrl then
ifield.select_all()
elseif event.key == keys.home or event.key == keys.up then
ifield.nav_start()
elseif event.key == keys["end"] or event.key == keys.down then
ifield.nav_end()
end
end
end
-- set the value
---@param val string string to set
function e.set_value(val)
ifield.set_value(val)
end
-- replace text with pasted text
---@param text string string to set
function e.handle_paste(text)
ifield.set_value(text)
end
-- handle focus, enable, and redraw with show()
e.on_focused = ifield.show
e.on_unfocused = ifield.show
e.on_enabled = ifield.show
e.on_disabled = ifield.show
e.redraw = ifield.show
-- initial draw
e.redraw()
local elem, id = e.complete()
return elem, id, ifield.censor
end
return text_field

View File

@ -25,13 +25,13 @@ local flasher = require("graphics.flasher")
---@param args alarm_indicator_light ---@param args alarm_indicator_light
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function alarm_indicator_light(args) local function alarm_indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.alight: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.alight: c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.alight: c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
assert(type(args.c3) == "number", "graphics.elements.indicators.alight: c3 is a required field") element.assert(type(args.c3) == "number", "c3 is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.alight: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line -- single line
@ -51,19 +51,21 @@ local function alarm_indicator_light(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 1
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
if e.value == 2 then if e.value == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
end end
else else
if e.value == 3 then if e.value == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
@ -76,7 +78,7 @@ local function alarm_indicator_light(args)
local was_off = e.value ~= 2 local was_off = e.value ~= 2
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if args.flash then if args.flash then
if was_off and (new_state == 2) then if was_off and (new_state == 2) then
@ -87,17 +89,17 @@ local function alarm_indicator_light(args)
flasher.stop(flash_callback) flasher.stop(flash_callback)
if new_state == 3 then if new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
elseif new_state == 2 then elseif new_state == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then elseif new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
@ -105,9 +107,14 @@ local function alarm_indicator_light(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(1) function e.redraw()
e.window.write(args.label) e.on_update(e.value)
e.w_write(args.label)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -18,8 +18,8 @@ local element = require("graphics.element")
---@param args core_map_args ---@param args core_map_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function core_map(args) local function core_map(args)
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field") element.assert(util.is_int(args.reactor_l), "reactor_l is a required field")
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field") element.assert(util.is_int(args.reactor_w), "reactor_w is a required field")
-- require max dimensions -- require max dimensions
args.width = 18 args.width = 18
@ -31,6 +31,8 @@ local function core_map(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 0
local alternator = true local alternator = true
local core_l = args.reactor_l - 2 local core_l = args.reactor_l - 2
@ -47,25 +49,25 @@ local function core_map(args)
-- create coordinate grid and frame -- create coordinate grid and frame
local function draw_frame() local function draw_frame()
e.window.setTextColor(colors.white) e.w_set_fgd(colors.white)
for x = 0, (inner_width - 1) do for x = 0, (inner_width - 1) do
e.window.setCursorPos(x + start_x, 1) e.w_set_cur(x + start_x, 1)
e.window.write(util.sprintf("%X", x)) e.w_write(util.sprintf("%X", x))
end end
for y = 0, (inner_height - 1) do for y = 0, (inner_height - 1) do
e.window.setCursorPos(1, y + start_y) e.w_set_cur(1, y + start_y)
e.window.write(util.sprintf("%X", y)) e.w_write(util.sprintf("%X", y))
end end
-- even out bottom edge -- even out bottom edge
e.window.setTextColor(e.fg_bg.bkg) e.w_set_fgd(e.fg_bg.bkg)
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg) e.w_set_bkg(args.parent.get_fg_bg().bkg)
e.window.setCursorPos(1, e.frame.h) e.w_set_cur(1, e.frame.h)
e.window.write(util.strrep("\x8f", e.frame.w)) e.w_write(string.rep("\x8f", e.frame.w))
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
end end
-- draw the core -- draw the core
@ -102,13 +104,13 @@ local function core_map(args)
-- draw pattern -- draw pattern
for y = start_y, inner_height + (start_y - 1) do for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y) e.w_set_cur(start_x, y)
for _ = 1, inner_width do for _ = 1, inner_width do
if alternator then if alternator then
i = i + 1 i = i + 1
e.window.blit("\x07", text_c, back_c) e.w_blit("\x07", text_c, back_c)
else else
e.window.blit("\x07", "7", "8") e.w_blit("\x07", "7", "8")
end end
alternator = not alternator alternator = not alternator
@ -157,11 +159,14 @@ local function core_map(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial (one-time except for resize()) frame draw -- redraw both frame and core
draw_frame() function e.redraw()
draw_frame()
draw_core(e.value)
end
-- initial draw -- initial draw
e.on_update(0) e.redraw()
return e.complete() return e.complete()
end end

View File

@ -24,25 +24,17 @@ local element = require("graphics.element")
---@param args data_indicator_args ---@param args data_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function data(args) local function data(args)
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field") element.assert(type(args.format) == "string", "format is a required field")
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field") element.assert(args.value ~= nil, "value is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field") element.assert(util.is_int(args.width), "width is a required field")
-- single line
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- label color e.value = args.value
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_a)
end
-- write label
e.window.setCursorPos(1, 1)
e.window.write(args.label)
local value_color = e.fg_bg.fgd local value_color = e.fg_bg.fgd
local label_len = string.len(args.label) local label_len = string.len(args.label)
@ -60,25 +52,25 @@ local function data(args)
e.value = value e.value = value
-- clear old data and label -- clear old data and label
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.write(util.spaces(clear_width)) e.w_write(util.spaces(clear_width))
-- write data -- write data
local data_str = util.sprintf(args.format, value) local data_str = util.sprintf(args.format, value)
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.setTextColor(value_color) e.w_set_fgd(value_color)
if args.commas then if args.commas then
e.window.write(util.comma_format(data_str)) e.w_write(util.comma_format(data_str))
else else
e.window.write(data_str) e.w_write(data_str)
end end
-- write label -- write label
if args.unit ~= nil then if args.unit ~= nil then
if args.lu_colors ~= nil then if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b) e.w_set_fgd(args.lu_colors.color_b)
end end
e.window.write(" " .. args.unit) e.w_write(" " .. args.unit)
end end
end end
@ -93,8 +85,17 @@ local function data(args)
e.on_update(e.value) e.on_update(e.value)
end end
-- initial value draw -- element redraw
e.on_update(args.value) function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -22,16 +22,17 @@ local element = require("graphics.element")
---@param args hbar_args ---@param args hbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function hbar(args) local function hbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 0.0
-- bar width is width - 5 characters for " 100%" if showing percent -- bar width is width - 5 characters for " 100%" if showing percent
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w) local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar") element.assert(bar_width > 0, "too small for bar")
local last_num_bars = -1
-- determine bar colors -- determine bar colors
local bar_bkg = e.fg_bg.blit_bkg local bar_bkg = e.fg_bg.blit_bkg
@ -87,16 +88,16 @@ local function hbar(args)
-- draw bar -- draw bar
for y = 1, e.frame.h do for y = 1, e.frame.h do
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite -- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
e.window.blit(spaces, bkg, fgd) e.w_blit(spaces, bkg, fgd)
end end
end end
-- update percentage -- update percentage
if args.show_percent then if args.show_percent then
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2))) e.w_set_cur(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
e.window.write(util.sprintf("%3.0f%%", fraction * 100)) e.w_write(util.sprintf("%3.0f%%", fraction * 100))
end end
end end
@ -105,20 +106,21 @@ local function hbar(args)
function e.recolor(bar_fg_bg) function e.recolor(bar_fg_bg)
bar_bkg = bar_fg_bg.blit_bkg bar_bkg = bar_fg_bg.blit_bkg
bar_fgd = bar_fg_bg.blit_fgd bar_fgd = bar_fg_bg.blit_fgd
e.redraw()
-- re-draw
last_num_bars = 0
if type(e.value) == "number" then
e.on_update(e.value)
end
end end
-- set the percentage value -- set the percentage value
---@param val number 0.0 to 1.0 ---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initialize to 0 -- element redraw
e.on_update(0) function e.redraw()
last_num_bars = -1
e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -1,7 +1,5 @@
-- Icon Indicator Graphics Element -- Icon Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element") local element = require("graphics.element")
---@class icon_sym_color ---@class icon_sym_color
@ -25,18 +23,17 @@ local element = require("graphics.element")
---@param args icon_indicator_args ---@param args icon_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function icon(args) local function icon(args)
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field") element.assert(type(args.states) == "table", "states is a required field")
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = args.value or 1
-- state blit strings -- state blit strings
local state_blit_cmds = {} local state_blit_cmds = {}
for i = 1, #args.states do for i = 1, #args.states do
@ -44,30 +41,34 @@ local function icon(args)
table.insert(state_blit_cmds, { table.insert(state_blit_cmds, {
text = " " .. sym_color.symbol .. " ", text = " " .. sym_color.symbol .. " ",
fgd = util.strrep(sym_color.color.blit_fgd, 3), fgd = string.rep(sym_color.color.blit_fgd, 3),
bkg = util.strrep(sym_color.color.blit_bkg, 3) bkg = string.rep(sym_color.color.blit_bkg, 3)
}) })
end end
-- write label and initial indicator light
e.window.setCursorPos(5, 1)
e.window.write(args.label)
-- on state change -- on state change
---@param new_state integer indicator state ---@param new_state integer indicator state
function e.on_update(new_state) function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state] local blit_cmd = state_blit_cmds[new_state]
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg) e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end end
-- set indicator state -- set indicator state
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initial icon draw -- element redraw
e.on_update(args.value or 1) function e.redraw()
e.w_set_cur(5, 1)
e.w_write(args.label)
e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -23,33 +23,31 @@ local flasher = require("graphics.flasher")
---@param args indicator_led_args ---@param args indicator_led_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_led(args) local function indicator_led(args)
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- flasher state
local flash_on = true local flash_on = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = false
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
else else
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@ -61,8 +59,8 @@ local function indicator_led(args)
flash_on = true flash_on = true
flasher.start(flash_callback, args.period) flasher.start(flash_callback, args.period)
else else
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
end end
end end
@ -73,8 +71,8 @@ local function indicator_led(args)
flasher.stop(flash_callback) flasher.stop(flash_callback)
end end
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end end
-- on state change -- on state change
@ -88,13 +86,18 @@ local function indicator_led(args)
---@param val boolean indicator state ---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(false) function e.redraw()
if string.len(args.label) > 0 then e.on_update(e.value)
e.window.setCursorPos(3, 1) if string.len(args.label) > 0 then
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
end end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -25,25 +25,20 @@ local flasher = require("graphics.flasher")
---@param args indicator_led_pair_args ---@param args indicator_led_pair_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_led_pair(args) local function indicator_led_pair(args)
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field") element.assert(type(args.off) == "number", "off is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- flasher state
local flash_on = true local flash_on = true
-- blit translations
local co = colors.toBlit(args.off) local co = colors.toBlit(args.off)
local c1 = colors.toBlit(args.c1) local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2) local c2 = colors.toBlit(args.c2)
@ -51,21 +46,20 @@ local function indicator_led_pair(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- init value for initial check in on_update
e.value = 1 e.value = 1
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
if e.value == 2 then if e.value == 2 then
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
elseif e.value == 3 then elseif e.value == 3 then
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
end end
else else
e.window.blit("\x8c", co, e.fg_bg.blit_bkg) e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@ -77,7 +71,7 @@ local function indicator_led_pair(args)
local was_off = e.value <= 1 local was_off = e.value <= 1
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if args.flash then if args.flash then
if was_off and (new_state > 1) then if was_off and (new_state > 1) then
@ -86,15 +80,14 @@ local function indicator_led_pair(args)
elseif new_state <= 1 then elseif new_state <= 1 then
flash_on = false flash_on = false
flasher.stop(flash_callback) flasher.stop(flash_callback)
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
end end
elseif new_state == 2 then elseif new_state == 2 then
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
elseif new_state == 3 then elseif new_state == 3 then
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
else else
e.window.blit("\x8c", co, e.fg_bg.blit_bkg) e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
end end
end end
@ -102,13 +95,18 @@ local function indicator_led_pair(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(1) function e.redraw()
if string.len(args.label) > 0 then e.on_update(e.value)
e.window.setCursorPos(3, 1) if string.len(args.label) > 0 then
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
end end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -18,28 +18,24 @@ local element = require("graphics.element")
---@param args indicator_led_rgb_args ---@param args indicator_led_rgb_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_led_rgb(args) local function indicator_led_rgb(args)
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- init value for initial check in on_update
e.value = 1 e.value = 1
-- on state change -- on state change
---@param new_state integer indicator state ---@param new_state integer indicator state
function e.on_update(new_state) function e.on_update(new_state)
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if type(args.colors[new_state]) == "number" then if type(args.colors[new_state]) == "number" then
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg) e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
end end
end end
@ -47,13 +43,18 @@ local function indicator_led_rgb(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(1) function e.redraw()
if string.len(args.label) > 0 then e.on_update(e.value)
e.window.setCursorPos(3, 1) if string.len(args.label) > 0 then
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
end end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -23,33 +23,31 @@ local flasher = require("graphics.flasher")
---@param args indicator_light_args ---@param args indicator_light_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_light(args) local function indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field") element.assert(type(args.colors) == "table", "colors is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.light: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- flasher state
local flash_on = true local flash_on = true
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = false
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@ -61,8 +59,8 @@ local function indicator_light(args)
flash_on = true flash_on = true
flasher.start(flash_callback, args.period) flasher.start(flash_callback, args.period)
else else
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
end end
end end
@ -73,8 +71,8 @@ local function indicator_light(args)
flasher.stop(flash_callback) flasher.stop(flash_callback)
end end
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end end
-- on state change -- on state change
@ -88,10 +86,15 @@ local function indicator_light(args)
---@param val boolean indicator state ---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw label and indicator light
e.on_update(false) function e.redraw()
e.window.setCursorPos(3, 1) e.on_update(false)
e.window.write(args.label) e.w_set_cur(3, 1)
e.w_write(args.label)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -9,7 +9,7 @@ local element = require("graphics.element")
---@field format string power format override (lua string format) ---@field format string power format override (lua string format)
---@field rate boolean? whether to append /t to the end (power per tick) ---@field rate boolean? whether to append /t to the end (power per tick)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b) ---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value any default value ---@field value number default value
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
@ -23,26 +23,17 @@ local element = require("graphics.element")
---@param args power_indicator_args ---@param args power_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function power(args) local function power(args)
assert(args.value ~= nil, "graphics.elements.indicators.power: value is a required field") element.assert(type(args.value) == "number", "value is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.power: width is a required field") element.assert(util.is_int(args.width), "width is a required field")
-- single line
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- label color e.value = args.value
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_a)
end
-- write label local data_start = 0
e.window.setCursorPos(1, 1)
e.window.write(args.label)
local data_start = string.len(args.label) + 2
if string.len(args.label) == 0 then data_start = 1 end
-- on state change -- on state change
---@param value any new value ---@param value any new value
@ -52,13 +43,13 @@ local function power(args)
local data_str, unit = util.power_format(value, false, args.format) local data_str, unit = util.power_format(value, false, args.format)
-- write data -- write data
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.write(util.comma_format(data_str)) e.w_write(util.comma_format(data_str))
-- write unit -- write unit
if args.lu_colors ~= nil then if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b) e.w_set_fgd(args.lu_colors.color_b)
end end
-- append per tick if rate is set -- append per tick if rate is set
@ -70,15 +61,27 @@ local function power(args)
if unit == "FE" then unit = "FE " end if unit == "FE" then unit = "FE " end
end end
e.window.write(" " .. unit) e.w_write(" " .. unit)
end end
-- set the value -- set the value
---@param val any new value ---@param val any new value
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initial value draw -- element redraw
e.on_update(args.value) function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
data_start = string.len(args.label) + 2
if string.len(args.label) == 0 then data_start = 1 end
e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -10,7 +10,7 @@ local element = require("graphics.element")
---@field format string data format (lua string format) ---@field format string data format (lua string format)
---@field commas? boolean whether to use commas if a number is given (default to false) ---@field commas? boolean whether to use commas if a number is given (default to false)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b) ---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value any default value ---@field value? radiation_reading default value
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
@ -24,24 +24,16 @@ local element = require("graphics.element")
---@param args rad_indicator_args ---@param args rad_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function rad(args) local function rad(args)
assert(type(args.label) == "string", "graphics.elements.indicators.rad: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.format) == "string", "graphics.elements.indicators.rad: format is a required field") element.assert(type(args.format) == "string", "format is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.rad: width is a required field") element.assert(util.is_int(args.width), "width is a required field")
-- single line
args.height = 1 args.height = 1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- label color e.value = args.value or types.new_zero_radiation_reading()
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_a)
end
-- write label
e.window.setCursorPos(1, 1)
e.window.write(args.label)
local label_len = string.len(args.label) local label_len = string.len(args.label)
local data_start = 1 local data_start = 1
@ -58,32 +50,41 @@ local function rad(args)
e.value = value.radiation e.value = value.radiation
-- clear old data and label -- clear old data and label
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.write(util.spaces(clear_width)) e.w_write(util.spaces(clear_width))
-- write data -- write data
local data_str = util.sprintf(args.format, e.value) local data_str = util.sprintf(args.format, e.value)
e.window.setCursorPos(data_start, 1) e.w_set_cur(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
if args.commas then if args.commas then
e.window.write(util.comma_format(data_str)) e.w_write(util.comma_format(data_str))
else else
e.window.write(data_str) e.w_write(data_str)
end end
-- write unit -- write unit
if args.lu_colors ~= nil then if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b) e.w_set_fgd(args.lu_colors.color_b)
end end
e.window.write(" " .. value.unit) e.w_write(" " .. value.unit)
end end
-- set the value -- set the value
---@param val any new value ---@param val any new value
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initial value draw -- element redraw
e.on_update(types.new_zero_radiation_reading()) function e.redraw()
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
e.w_set_cur(1, 1)
e.w_write(args.label)
e.on_update(e.value)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -25,16 +25,12 @@ local element = require("graphics.element")
---@param args state_indicator_args ---@param args state_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function state_indicator(args) local function state_indicator(args)
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field") element.assert(type(args.states) == "table", "states is a required field")
-- determine height
if util.is_int(args.height) then if util.is_int(args.height) then
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number") element.assert(args.height % 2 == 1, "height should be an odd number")
else else args.height = 1 end
args.height = 1
end
-- initial guess at width
args.width = args.min_width or 1 args.width = args.min_width or 1
-- state blit strings -- state blit strings
@ -42,7 +38,6 @@ local function state_indicator(args)
for i = 1, #args.states do for i = 1, #args.states do
local state_def = args.states[i] ---@type state_text_color local state_def = args.states[i] ---@type state_text_color
-- re-determine width
if string.len(state_def.text) > args.width then if string.len(state_def.text) > args.width then
args.width = string.len(state_def.text) args.width = string.len(state_def.text)
end end
@ -51,21 +46,28 @@ local function state_indicator(args)
table.insert(state_blit_cmds, { table.insert(state_blit_cmds, {
text = text, text = text,
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)), fgd = string.rep(state_def.color.blit_fgd, string.len(text)),
bkg = util.strrep(state_def.color.blit_bkg, string.len(text)) bkg = string.rep(state_def.color.blit_bkg, string.len(text))
}) })
end end
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = args.value or 1
-- element redraw
function e.redraw()
local blit_cmd = state_blit_cmds[e.value]
e.w_set_cur(1, 1)
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- on state change -- on state change
---@param new_state integer indicator state ---@param new_state integer indicator state
function e.on_update(new_state) function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.redraw()
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end end
-- set indicator state -- set indicator state
@ -73,7 +75,7 @@ local function state_indicator(args)
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- initial draw -- initial draw
e.on_update(args.value or 1) e.redraw()
return e.complete() return e.complete()
end end

View File

@ -25,47 +25,41 @@ local flasher = require("graphics.flasher")
---@param args tristate_indicator_light_args ---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tristate_indicator_light(args) local function tristate_indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field") element.assert(type(args.label) == "string", "label is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field") element.assert(type(args.c1) == "number", "c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field") element.assert(type(args.c2) == "number", "c2 is a required field")
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field") element.assert(type(args.c3) == "number", "c3 is a required field")
if args.flash then if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.trilight: period is a required field if flash is enabled") element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
end end
-- single line
args.height = 1 args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- flasher state
local flash_on = true
-- blit translations
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
local c3 = colors.toBlit(args.c3)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- init value for initial check in on_update
e.value = 1 e.value = 1
local flash_on = true
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
local c3 = colors.toBlit(args.c3)
-- called by flasher when enabled -- called by flasher when enabled
local function flash_callback() local function flash_callback()
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if flash_on then if flash_on then
if e.value == 2 then if e.value == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif e.value == 3 then elseif e.value == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
end end
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
flash_on = not flash_on flash_on = not flash_on
@ -77,7 +71,7 @@ local function tristate_indicator_light(args)
local was_off = e.value <= 1 local was_off = e.value <= 1
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
if args.flash then if args.flash then
if was_off and (new_state > 1) then if was_off and (new_state > 1) then
@ -87,14 +81,14 @@ local function tristate_indicator_light(args)
flash_on = false flash_on = false
flasher.stop(flash_callback) flasher.stop(flash_callback)
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
elseif new_state == 2 then elseif new_state == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then elseif new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end end
end end
@ -102,9 +96,14 @@ local function tristate_indicator_light(args)
---@param val integer indicator state ---@param val integer indicator state
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- draw light and label
e.on_update(1) function e.redraw()
e.window.write(args.label) e.on_update(1)
e.w_write(args.label)
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -20,18 +20,18 @@ local element = require("graphics.element")
---@param args vbar_args ---@param args vbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function vbar(args) local function vbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- blit strings e.value = 0.0
local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w) local last_num_bars = -1
local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w)
local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w)
local spaces = util.spaces(e.frame.w) local spaces = util.spaces(e.frame.w)
local one_third = util.strrep("\x8f", e.frame.w) local one_third = string.rep("\x8f", e.frame.w)
local two_thirds = util.strrep("\x83", e.frame.w) local two_thirds = string.rep("\x83", e.frame.w)
-- handle data changes -- handle data changes
---@param fraction number 0.0 to 1.0 ---@param fraction number 0.0 to 1.0
@ -52,54 +52,55 @@ local function vbar(args)
if num_bars ~= last_num_bars then if num_bars ~= last_num_bars then
last_num_bars = num_bars last_num_bars = num_bars
-- start bottom up
local y = e.frame.h local y = e.frame.h
e.w_set_cur(1, y)
-- start at base of vertical bar
e.window.setCursorPos(1, y)
-- fill percentage -- fill percentage
for _ = 1, num_bars / 3 do for _ = 1, num_bars / 3 do
e.window.blit(spaces, bkg, fgd) e.w_blit(spaces, bkg, fgd)
y = y - 1 y = y - 1
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
end end
-- add fractional bar if needed -- add fractional bar if needed
if num_bars % 3 == 1 then if num_bars % 3 == 1 then
e.window.blit(one_third, bkg, fgd) e.w_blit(one_third, bkg, fgd)
y = y - 1 y = y - 1
elseif num_bars % 3 == 2 then elseif num_bars % 3 == 2 then
e.window.blit(two_thirds, bkg, fgd) e.w_blit(two_thirds, bkg, fgd)
y = y - 1 y = y - 1
end end
-- fill the rest blank -- fill the rest blank
while y > 0 do while y > 0 do
e.window.setCursorPos(1, y) e.w_set_cur(1, y)
e.window.blit(spaces, fgd, bkg) e.w_blit(spaces, fgd, bkg)
y = y - 1 y = y - 1
end end
end end
end end
-- change bar color
---@param fg_bg cpair new bar colors
function e.recolor(fg_bg)
fgd = util.strrep(fg_bg.blit_fgd, e.frame.w)
bkg = util.strrep(fg_bg.blit_bkg, e.frame.w)
-- re-draw
last_num_bars = 0
if type(e.value) == "number" then
e.on_update(e.value)
end
end
-- set the percentage value -- set the percentage value
---@param val number 0.0 to 1.0 ---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end function e.set_value(val) e.on_update(val) end
-- element redraw
function e.redraw()
last_num_bars = -1
e.on_update(e.value)
end
-- change bar color
---@param fg_bg cpair new bar colors
function e.recolor(fg_bg)
fgd = string.rep(fg_bg.blit_fgd, e.frame.w)
bkg = string.rep(fg_bg.blit_bkg, e.frame.w)
e.redraw()
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -5,7 +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 CLICK_TYPE = core.events.CLICK_TYPE local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class listbox_args ---@class listbox_args
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled) ---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
@ -63,34 +63,34 @@ local function listbox(args)
-- draw up/down arrows -- draw up/down arrows
if pressed_arrow == 1 then if pressed_arrow == 1 then
e.window.setTextColor(active_fg_bg.fgd) e.w_set_fgd(active_fg_bg.fgd)
e.window.setBackgroundColor(active_fg_bg.bkg) e.w_set_bkg(active_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1) e.w_set_cur(e.frame.w, 1)
e.window.write("\x1e") e.w_write("\x1e")
e.window.setTextColor(nav_fg_bg.fgd) e.w_set_fgd(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg) e.w_set_bkg(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, e.frame.h) e.w_set_cur(e.frame.w, e.frame.h)
e.window.write("\x1f") e.w_write("\x1f")
elseif pressed_arrow == -1 then elseif pressed_arrow == -1 then
e.window.setTextColor(nav_fg_bg.fgd) e.w_set_fgd(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg) e.w_set_bkg(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1) e.w_set_cur(e.frame.w, 1)
e.window.write("\x1e") e.w_write("\x1e")
e.window.setTextColor(active_fg_bg.fgd) e.w_set_fgd(active_fg_bg.fgd)
e.window.setBackgroundColor(active_fg_bg.bkg) e.w_set_bkg(active_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, e.frame.h) e.w_set_cur(e.frame.w, e.frame.h)
e.window.write("\x1f") e.w_write("\x1f")
else else
e.window.setTextColor(nav_fg_bg.fgd) e.w_set_fgd(nav_fg_bg.fgd)
e.window.setBackgroundColor(nav_fg_bg.bkg) e.w_set_bkg(nav_fg_bg.bkg)
e.window.setCursorPos(e.frame.w, 1) e.w_set_cur(e.frame.w, 1)
e.window.write("\x1e") e.w_write("\x1e")
e.window.setCursorPos(e.frame.w, e.frame.h) e.w_set_cur(e.frame.w, e.frame.h)
e.window.write("\x1f") e.w_write("\x1f")
end end
e.window.setTextColor(e.fg_bg.fgd) e.w_set_fgd(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
end end
-- render the scroll bar and re-cacluate height & bounds -- render the scroll bar and re-cacluate height & bounds
@ -115,23 +115,23 @@ local function listbox(args)
for i = 2, e.frame.h - 1 do for i = 2, e.frame.h - 1 do
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
if args.nav_fg_bg ~= nil then if args.nav_fg_bg ~= nil then
e.window.setBackgroundColor(args.nav_fg_bg.fgd) e.w_set_bkg(args.nav_fg_bg.fgd)
else else
e.window.setBackgroundColor(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.fgd)
end end
else else
if args.nav_fg_bg ~= nil then if args.nav_fg_bg ~= nil then
e.window.setBackgroundColor(args.nav_fg_bg.bkg) e.w_set_bkg(args.nav_fg_bg.bkg)
else else
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
end end
end end
e.window.setCursorPos(e.frame.w, i) e.w_set_cur(e.frame.w, i)
e.window.write(" ") e.w_write(" ")
end end
e.window.setBackgroundColor(e.fg_bg.bkg) e.w_set_bkg(e.fg_bg.bkg)
end end
-- update item y positions and move elements -- update item y positions and move elements
@ -223,7 +223,7 @@ local function listbox(args)
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
function e.handle_mouse(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
if event.type == CLICK_TYPE.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) draw_arrows(1)
@ -235,7 +235,7 @@ local function listbox(args)
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
end end
end end
elseif event.type == CLICK_TYPE.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) draw_arrows(1)
@ -250,10 +250,10 @@ local function listbox(args)
mouse_last_y = event.current.y mouse_last_y = event.current.y
end end
end end
elseif event.type == CLICK_TYPE.UP then elseif event.type == MOUSE_CLICK.UP then
holding_bar = false holding_bar = false
draw_arrows(0) draw_arrows(0)
elseif event.type == CLICK_TYPE.DRAG then elseif event.type == MOUSE_CLICK.DRAG then
if holding_bar then if holding_bar then
-- if mouse is within vertical frame, including the grip point -- if mouse is within vertical frame, including the grip point
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
@ -266,16 +266,22 @@ local function listbox(args)
mouse_last_y = event.current.y mouse_last_y = event.current.y
end end
end end
elseif event.type == CLICK_TYPE.SCROLL_DOWN then elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
scroll_down() scroll_down()
elseif event.type == CLICK_TYPE.SCROLL_UP then elseif event.type == MOUSE_CLICK.SCROLL_UP then
scroll_up() scroll_up()
end end
end end
end end
draw_arrows(0) -- element redraw
draw_bar() function e.redraw()
draw_arrows(0)
draw_bar()
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -19,23 +19,30 @@ local element = require("graphics.element")
---@param args multipane_args ---@param args multipane_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function multipane(args) local function multipane(args)
assert(type(args.panes) == "table", "graphics.elements.multipane: panes is a required field") element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
e.value = 1
-- show the selected pane
function e.redraw()
for i = 1, #args.panes do args.panes[i].hide() end
args.panes[e.value].show()
end
-- select which pane is shown -- select which pane is shown
---@param value integer pane to show ---@param value integer pane to show
function e.set_value(value) function e.set_value(value)
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
e.value = value e.value = value
e.redraw()
for i = 1, #args.panes do args.panes[i].hide() end
args.panes[value].show()
end end
end end
e.set_value(1) -- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -24,12 +24,11 @@ local element = require("graphics.element")
---@param args pipenet_args ---@param args pipenet_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function pipenet(args) local function pipenet(args)
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field") element.assert(type(args.pipes) == "table", "pipes is a required field")
args.width = 0 args.width = 0
args.height = 0 args.height = 0
-- determine width/height
for i = 1, #args.pipes do for i = 1, #args.pipes do
local pipe = args.pipes[i] ---@type pipe local pipe = args.pipes[i] ---@type pipe
@ -57,8 +56,8 @@ local function pipenet(args)
if any_thin then break end if any_thin then break end
end end
if not any_thin then -- draw all pipes by drawing out lines
-- draw all pipes local function vector_draw()
for p = 1, #args.pipes do for p = 1, #args.pipes do
local pipe = args.pipes[p] ---@type pipe local pipe = args.pipes[p] ---@type pipe
@ -73,7 +72,7 @@ local function pipenet(args)
y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step) y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step)
end end
e.window.setCursorPos(x, y) e.w_set_cur(x, y)
local c = core.cpair(pipe.color, e.fg_bg.bkg) local c = core.cpair(pipe.color, e.fg_bg.bkg)
@ -84,24 +83,24 @@ local function pipenet(args)
if i == pipe.w then if i == pipe.w then
-- corner -- corner
if y_step > 0 then if y_step > 0 then
e.window.blit("\x93", c.blit_bkg, c.blit_fgd) e.w_blit("\x93", c.blit_bkg, c.blit_fgd)
else else
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg) e.w_blit("\x8e", c.blit_fgd, c.blit_bkg)
end end
else else
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
end end
else else
if i == pipe.w and y_step > 0 then if i == pipe.w and y_step > 0 then
-- corner -- corner
e.window.blit(" ", c.blit_bkg, c.blit_fgd) e.w_blit(" ", c.blit_bkg, c.blit_fgd)
else else
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg) e.w_blit("\x8f", c.blit_fgd, c.blit_bkg)
end end
end end
x = x + x_step x = x + x_step
e.window.setCursorPos(x, y) e.w_set_cur(x, y)
end end
-- back up one -- back up one
@ -109,12 +108,12 @@ local function pipenet(args)
for _ = 1, pipe.h - 1 do for _ = 1, pipe.h - 1 do
y = y + y_step y = y + y_step
e.window.setCursorPos(x, y) e.w_set_cur(x, y)
if pipe.thin then if pipe.thin then
e.window.blit("\x95", c.blit_bkg, c.blit_fgd) e.w_blit("\x95", c.blit_bkg, c.blit_fgd)
else else
e.window.blit(" ", c.blit_bkg, c.blit_fgd) e.w_blit(" ", c.blit_bkg, c.blit_fgd)
end end
end end
else else
@ -124,26 +123,26 @@ local function pipenet(args)
if i == pipe.h then if i == pipe.h then
-- corner -- corner
if y_step < 0 then if y_step < 0 then
e.window.blit("\x97", c.blit_bkg, c.blit_fgd) e.w_blit("\x97", c.blit_bkg, c.blit_fgd)
elseif y_step > 0 then elseif y_step > 0 then
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg) e.w_blit("\x8d", c.blit_fgd, c.blit_bkg)
else else
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
end end
else else
e.window.blit("\x95", c.blit_fgd, c.blit_bkg) e.w_blit("\x95", c.blit_fgd, c.blit_bkg)
end end
else else
if i == pipe.h and y_step < 0 then if i == pipe.h and y_step < 0 then
-- corner -- corner
e.window.blit("\x83", c.blit_bkg, c.blit_fgd) e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
else else
e.window.blit(" ", c.blit_bkg, c.blit_fgd) e.w_blit(" ", c.blit_bkg, c.blit_fgd)
end end
end end
y = y + y_step y = y + y_step
e.window.setCursorPos(x, y) e.w_set_cur(x, y)
end end
-- back up one -- back up one
@ -151,21 +150,119 @@ local function pipenet(args)
for _ = 1, pipe.w - 1 do for _ = 1, pipe.w - 1 do
x = x + x_step x = x + x_step
e.window.setCursorPos(x, y) e.w_set_cur(x, y)
if pipe.thin then if pipe.thin then
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
else else
e.window.blit("\x83", c.blit_bkg, c.blit_fgd) e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
end end
end end
end end
end end
else end
-- build map if using thin pipes, easist way to check adjacent blocks (cannot 'cheat' like with standard width)
-- draw a particular map cell
---@param map table 2D cell map
---@param x integer x coord
---@param y integer y coord
local function draw_map_cell(map, x, y)
local entry = map[x][y] ---@type _pipe_map_entry already confirmed not false
local char
local invert = false
local function check(cx, cy)
return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg)
end
if entry.thin then
if check(x - 1, y) then -- if left
if check(x, y - 1) then -- if above
if check(x + 1, y) then -- if right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x8d")
end
else -- not right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x95")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x85")
end
end
elseif check(x, y + 1) then-- not above, if below
if check(x + 1, y) then -- if right
char = util.trinary(entry.atr, "\x93", "\x9c")
invert = entry.atr
else -- not right
char = util.trinary(entry.atr, "\x93", "\x94")
invert = entry.atr
end
else -- not above, not below
char = "\x8c"
end
elseif check(x + 1, y) then -- not left, if right
if check(x, y - 1) then -- if above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x95", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8a", "\x8d")
end
else -- not above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x97", "\x9c")
invert = entry.atr
else -- not below
char = "\x8c"
end
end
else -- not left, not right
char = "\x95"
invert = entry.atr
end
else
if check(x, y - 1) then -- above
-- not below and (if left or right)
if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
char = util.trinary(entry.atr, "\x8f", " ")
invert = not entry.atr
else -- not below w/ sides only
char = " "
invert = true
end
elseif check(x, y + 1) then -- not above, if below
-- if left or right
if (check(x - 1, y) or check(x + 1, y)) then
char = "\x83"
invert = true
else -- not left or right
char = " "
invert = true
end
else -- not above, not below
char = util.trinary(entry.atr, "\x8f", "\x83")
invert = not entry.atr
end
end
e.w_set_cur(x, y)
if invert then
e.w_blit(char, entry.bg, entry.fg)
else
e.w_blit(char, entry.fg, entry.bg)
end
end
-- draw all pipes by assembling and marking up a 2D map<br>
-- this is an easy way to check adjacent blocks, which is required to properly draw thin pipes
local function map_draw()
local map = {} local map = {}
-- allocate map
for x = 1, args.width do for x = 1, args.width do
table.insert(map, {}) table.insert(map, {})
for _ = 1, args.height do table.insert(map[x], false) end for _ = 1, args.height do table.insert(map[x], false) end
@ -215,101 +312,19 @@ local function pipenet(args)
-- render -- render
for x = 1, args.width do for x = 1, args.width do
for y = 1, args.height do for y = 1, args.height do
local entry = map[x][y] ---@type _pipe_map_entry|false if map[x][y] ~= false then draw_map_cell(map, x, y) end
local char
local invert = false
if entry ~= false then
local function check(cx, cy)
return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg)
end
if entry.thin then
if check(x - 1, y) then -- if left
if check(x, y - 1) then -- if above
if check(x + 1, y) then -- if right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x8d")
end
else -- not right
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x91", "\x95")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8e", "\x85")
end
end
elseif check(x, y + 1) then-- not above, if below
if check(x + 1, y) then -- if right
char = util.trinary(entry.atr, "\x93", "\x9c")
invert = entry.atr
else -- not right
char = util.trinary(entry.atr, "\x93", "\x94")
invert = entry.atr
end
else -- not above, not below
char = "\x8c"
end
elseif check(x + 1, y) then -- not left, if right
if check(x, y - 1) then -- if above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x95", "\x9d")
invert = entry.atr
else -- not below
char = util.trinary(entry.atr, "\x8a", "\x8d")
end
else -- not above
if check(x, y + 1) then -- if below
char = util.trinary(entry.atr, "\x97", "\x9c")
invert = entry.atr
else -- not below
char = "\x8c"
end
end
else -- not left, not right
char = "\x95"
invert = entry.atr
end
else
if check(x, y - 1) then -- above
-- not below and (if left or right)
if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
char = util.trinary(entry.atr, "\x8f", " ")
invert = not entry.atr
else -- not below w/ sides only
char = " "
invert = true
end
elseif check(x, y + 1) then -- not above, if below
-- if left or right
if (check(x - 1, y) or check(x + 1, y)) then
char = "\x83"
invert = true
else -- not left or right
char = " "
invert = true
end
else -- not above, not below
char = util.trinary(entry.atr, "\x8f", "\x83")
invert = not entry.atr
end
end
e.window.setCursorPos(x, y)
if invert then
e.window.blit(char, entry.bg, entry.fg)
else
e.window.blit(char, entry.fg, entry.bg)
end
end
end end
end end
end end
-- element redraw
function e.redraw()
if any_thin then map_draw() else vector_draw() end
end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -22,7 +22,7 @@ local element = require("graphics.element")
---@param args rectangle_args ---@param args rectangle_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function rectangle(args) local function rectangle(args)
assert(args.border ~= nil or args.thin ~= true, "graphics.elements.rectangle: thin requires border to be provided") element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided")
-- if thin, then width will always need to be 1 -- if thin, then width will always need to be 1
if args.thin == true then if args.thin == true then
@ -56,7 +56,7 @@ local function rectangle(args)
-- draw bordered box if requested -- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless -- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then if args.border ~= nil then
e.window.setCursorPos(1, 1) e.w_set_cur(1, 1)
local border_width = offset_x local border_width = offset_x
local border_height = offset_y local border_height = offset_y
@ -65,50 +65,50 @@ local function rectangle(args)
local inner_width = e.frame.w - width_x2 local inner_width = e.frame.w - width_x2
-- check dimensions -- check dimensions
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width") element.assert(width_x2 <= e.frame.w, "border too thick for width")
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height") element.assert(width_x2 <= e.frame.h, "border too thick for height")
-- form the basic line strings and top/bottom blit strings -- form the basic line strings and top/bottom blit strings
local spaces = util.spaces(e.frame.w) local spaces = util.spaces(e.frame.w)
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w) local blit_fg = string.rep(e.fg_bg.blit_fgd, e.frame.w)
local blit_fg_sides = blit_fg local blit_fg_sides = blit_fg
local blit_bg_sides = "" local blit_bg_sides = ""
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w) local blit_bg_top_bot = string.rep(border_blit, e.frame.w)
-- partial bars -- partial bars
local p_a, p_b, p_s local p_a, p_b, p_s
if args.thin == true then if args.thin == true then
if args.even_inner == true then if args.even_inner == true then
p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93" p_a = "\x9c" .. string.rep("\x8c", inner_width) .. "\x93"
p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e" p_b = "\x8d" .. string.rep("\x8c", inner_width) .. "\x8e"
else else
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94" p_a = "\x97" .. string.rep("\x83", inner_width) .. "\x94"
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85" p_b = "\x8a" .. string.rep("\x8f", inner_width) .. "\x85"
end end
p_s = "\x95" .. util.spaces(inner_width) .. "\x95" p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
else else
if args.even_inner == true then if args.even_inner == true then
p_a = util.strrep("\x83", inner_width + width_x2) p_a = string.rep("\x83", inner_width + width_x2)
p_b = util.strrep("\x8f", inner_width + width_x2) p_b = string.rep("\x8f", inner_width + width_x2)
else else
p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width) p_a = util.spaces(border_width) .. string.rep("\x8f", inner_width) .. util.spaces(border_width)
p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width) p_b = util.spaces(border_width) .. string.rep("\x83", inner_width) .. util.spaces(border_width)
end end
p_s = spaces p_s = spaces
end end
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. local p_inv_fg = string.rep(border_blit, border_width) .. string.rep(e.fg_bg.blit_bkg, inner_width) ..
util.strrep(border_blit, border_width) string.rep(border_blit, border_width)
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) .. local p_inv_bg = string.rep(e.fg_bg.blit_bkg, border_width) .. string.rep(border_blit, inner_width) ..
util.strrep(e.fg_bg.blit_bkg, border_width) string.rep(e.fg_bg.blit_bkg, border_width)
if args.thin == true then if args.thin == true then
p_inv_fg = e.fg_bg.blit_bkg .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. util.strrep(border_blit, border_width) p_inv_fg = e.fg_bg.blit_bkg .. string.rep(e.fg_bg.blit_bkg, inner_width) .. string.rep(border_blit, border_width)
p_inv_bg = border_blit .. util.strrep(border_blit, inner_width) .. util.strrep(e.fg_bg.blit_bkg, border_width) p_inv_bg = border_blit .. string.rep(border_blit, inner_width) .. string.rep(e.fg_bg.blit_bkg, border_width)
blit_fg_sides = border_blit .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg blit_fg_sides = border_blit .. string.rep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
end end
-- form the body blit strings (sides are border, inside is normal) -- form the body blit strings (sides are border, inside is normal)
@ -126,64 +126,69 @@ local function rectangle(args)
end end
-- draw rectangle with borders -- draw rectangle with borders
for y = 1, e.frame.h do function e.redraw()
e.window.setCursorPos(1, y) for y = 1, e.frame.h do
-- top border e.w_set_cur(1, y)
if y <= border_height then -- top border
-- partial pixel fill if y <= border_height then
if args.border.even and y == border_height then -- partial pixel fill
if args.thin == true then if args.border.even and y == border_height then
e.window.blit(p_a, p_inv_bg, p_inv_fg) if args.thin == true then
else e.w_blit(p_a, p_inv_bg, p_inv_fg)
local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg) else
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) local _fg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
if width_x2 % 3 == 1 then if width_x2 % 3 == 1 then
e.window.blit(p_b, _fg, _bg) e.w_blit(p_b, _fg, _bg)
elseif width_x2 % 3 == 2 then elseif width_x2 % 3 == 2 then
e.window.blit(p_a, _fg, _bg) e.w_blit(p_a, _fg, _bg)
else else
-- skip line -- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides) e.w_blit(spaces, blit_fg, blit_bg_sides)
end end
end
else
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end
-- bottom border
elseif y > (e.frame.h - border_width) then
-- partial pixel fill
if args.border.even and y == ((e.frame.h - border_width) + 1) then
if args.thin == true then
if args.even_inner == true then
e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w))
else
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
end end
else else
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) e.w_blit(spaces, blit_fg, blit_bg_top_bot)
local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) end
-- bottom border
if width_x2 % 3 == 1 then elseif y > (e.frame.h - border_width) then
e.window.blit(p_a, _fg, _bg) -- partial pixel fill
elseif width_x2 % 3 == 2 then if args.border.even and y == ((e.frame.h - border_width) + 1) then
e.window.blit(p_b, _fg, _bg) if args.thin == true then
if args.even_inner == true then
e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w))
else
e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
end
else else
-- skip line local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
e.window.blit(spaces, blit_fg, blit_bg_sides) local _bg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
if width_x2 % 3 == 1 then
e.w_blit(p_a, _fg, _bg)
elseif width_x2 % 3 == 2 then
e.w_blit(p_b, _fg, _bg)
else
-- skip line
e.w_blit(spaces, blit_fg, blit_bg_sides)
end
end end
else
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
end end
else else
e.window.blit(spaces, blit_fg, blit_bg_top_bot) if args.thin == true then
end e.w_blit(p_s, blit_fg_sides, blit_bg_sides)
else else
if args.thin == true then e.w_blit(p_s, blit_fg, blit_bg_sides)
e.window.blit(p_s, blit_fg_sides, blit_bg_sides) end
else
e.window.blit(p_s, blit_fg, blit_bg_sides)
end end
end end
end end
-- initial draw of border
e.redraw()
end end
return e.complete() return e.complete()

View File

@ -5,11 +5,11 @@ local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local element = require("graphics.element") local element = require("graphics.element")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
---@class textbox_args ---@class textbox_args
---@field text string text to show ---@field text string text to show
---@field alignment? TEXT_ALIGN text alignment, left by default ---@field alignment? ALIGN text alignment, left by default
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
@ -24,19 +24,20 @@ local TEXT_ALIGN = core.TEXT_ALIGN
---@param args textbox_args ---@param args textbox_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function textbox(args) local function textbox(args)
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field") element.assert(type(args.text) == "string", "text is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
local alignment = args.alignment or TEXT_ALIGN.LEFT e.value = args.text
local alignment = args.alignment or ALIGN.LEFT
-- draw textbox -- draw textbox
function e.redraw()
e.window.clear()
local function display_text(text) local lines = util.strwrap(e.value, e.frame.w)
e.value = text
local lines = util.strwrap(text, e.frame.w)
for i = 1, #lines do for i = 1, #lines do
if i > e.frame.h then break end if i > e.frame.h then break end
@ -44,27 +45,28 @@ local function textbox(args)
local len = string.len(lines[i]) local len = string.len(lines[i])
-- use cursor position to align this line -- use cursor position to align this line
if alignment == TEXT_ALIGN.CENTER then if alignment == ALIGN.CENTER then
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i) e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i)
elseif alignment == TEXT_ALIGN.RIGHT then elseif alignment == ALIGN.RIGHT then
e.window.setCursorPos((e.frame.w - len) + 1, i) e.w_set_cur((e.frame.w - len) + 1, i)
else else
e.window.setCursorPos(1, i) e.w_set_cur(1, i)
end end
e.window.write(lines[i]) e.w_write(lines[i])
end end
end end
display_text(args.text)
-- set the string value and re-draw the text -- set the string value and re-draw the text
---@param val string value ---@param val string value
function e.set_value(val) function e.set_value(val)
e.window.clear() e.value = val
display_text(val) e.redraw()
end end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -22,13 +22,11 @@ local element = require("graphics.element")
---@param args tiling_args ---@param args tiling_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tiling(args) local function tiling(args)
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field") element.assert(type(args.fill_c) == "table", "fill_c is a required field")
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
-- draw tiling box
local fill_a = args.fill_c.blit_a local fill_a = args.fill_c.blit_a
local fill_b = args.fill_c.blit_b local fill_b = args.fill_c.blit_b
@ -38,13 +36,9 @@ local function tiling(args)
local start_y = 1 local start_y = 1
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1)) local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
local inner_height = e.frame.h local inner_height = e.frame.h
local alternator = true
-- border -- border
if args.border_c ~= nil then if args.border_c ~= nil then
e.window.setBackgroundColor(args.border_c)
e.window.clear()
start_x = 1 + util.trinary(even, 2, 1) start_x = 1 + util.trinary(even, 2, 1)
start_y = 2 start_y = 2
@ -53,35 +47,48 @@ local function tiling(args)
end end
-- check dimensions -- check dimensions
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0") element.assert(inner_width > 0, "inner_width <= 0")
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0") element.assert(inner_height > 0, "inner_height <= 0")
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width") element.assert(start_x <= inner_width, "start_x > inner_width")
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height") element.assert(start_y <= inner_height, "start_y > inner_height")
-- create pattern -- draw tiling box
for y = start_y, inner_height + (start_y - 1) do function e.redraw()
e.window.setCursorPos(start_x, y) local alternator = true
for _ = 1, inner_width do
if alternator then
if even then
e.window.blit(" ", "00", fill_a .. fill_a)
else
e.window.blit(" ", "0", fill_a)
end
else
if even then
e.window.blit(" ", "00", fill_b .. fill_b)
else
e.window.blit(" ", "0", fill_b)
end
end
alternator = not alternator if args.border_c ~= nil then
e.w_set_bkg(args.border_c)
e.window.clear()
end end
if inner_width % 2 == 0 then alternator = not alternator end -- draw pattern
for y = start_y, inner_height + (start_y - 1) do
e.w_set_cur(start_x, y)
for _ = 1, inner_width do
if alternator then
if even then
e.w_blit(" ", "00", fill_a .. fill_a)
else
e.w_blit(" ", "0", fill_a)
end
else
if even then
e.w_blit(" ", "00", fill_b .. fill_b)
else
e.w_blit(" ", "0", fill_b)
end
end
alternator = not alternator
end
if inner_width % 2 == 0 then alternator = not alternator end
end
end end
-- initial draw
e.redraw()
return e.complete() return e.complete()
end end

View File

@ -4,46 +4,77 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local DOUBLE_CLICK_MS = 500
local events = {} local events = {}
---@enum CLICK_BUTTON ---@enum CLICK_BUTTON
events.CLICK_BUTTON = { local CLICK_BUTTON = {
GENERIC = 0, GENERIC = 0,
LEFT_BUTTON = 1, LEFT_BUTTON = 1,
RIGHT_BUTTON = 2, RIGHT_BUTTON = 2,
MID_BUTTON = 3 MID_BUTTON = 3
} }
---@enum CLICK_TYPE events.CLICK_BUTTON = CLICK_BUTTON
events.CLICK_TYPE = {
---@enum MOUSE_CLICK
local MOUSE_CLICK = {
TAP = 1, -- screen tap (complete click) TAP = 1, -- screen tap (complete click)
DOWN = 2, -- button down DOWN = 2, -- button down
UP = 3, -- button up (completed a click) UP = 3, -- button up (completed a click)
DRAG = 4, -- mouse dragged DRAG = 4, -- mouse dragged
SCROLL_DOWN = 5, -- scroll down SCROLL_DOWN = 5, -- scroll down
SCROLL_UP = 6 -- scroll up SCROLL_UP = 6, -- scroll up
DOUBLE_CLICK = 7 -- double left click
} }
events.MOUSE_CLICK = MOUSE_CLICK
---@enum KEY_CLICK
local KEY_CLICK = {
DOWN = 1,
HELD = 2,
UP = 3,
CHAR = 4
}
events.KEY_CLICK = KEY_CLICK
-- create a new 2D coordinate -- create a new 2D coordinate
---@param x integer ---@param x integer
---@param y integer ---@param y integer
---@return coordinate_2d ---@return coordinate_2d
local function _coord2d(x, y) return { x = x, y = y } end local function _coord2d(x, y) return { x = x, y = y } end
events.new_coord_2d = _coord2d
---@class mouse_interaction ---@class mouse_interaction
---@field monitor string ---@field monitor string
---@field button CLICK_BUTTON ---@field button CLICK_BUTTON
---@field type CLICK_TYPE ---@field type MOUSE_CLICK
---@field initial coordinate_2d ---@field initial coordinate_2d
---@field current coordinate_2d ---@field current coordinate_2d
---@class key_interaction
---@field type KEY_CLICK
---@field key number key code
---@field name string key character name
---@field shift boolean shift held
---@field ctrl boolean ctrl held
---@field alt boolean alt held
local handler = { local handler = {
-- left, right, middle button down tracking -- left, right, middle button down tracking
button_down = { button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) },
_coord2d(0, 0), -- keyboard modifiers
_coord2d(0, 0), shift = false,
_coord2d(0, 0) alt = false,
} ctrl = false,
-- double click tracking
dc_start = 0,
dc_step = 1,
dc_coord = _coord2d(0, 0)
} }
-- create a new monitor touch mouse interaction event -- create a new monitor touch mouse interaction event
@ -55,8 +86,8 @@ local handler = {
local function _monitor_touch(monitor, x, y) local function _monitor_touch(monitor, x, y)
return { return {
monitor = monitor, monitor = monitor,
button = events.CLICK_BUTTON.GENERIC, button = CLICK_BUTTON.GENERIC,
type = events.CLICK_TYPE.TAP, type = MOUSE_CLICK.TAP,
initial = _coord2d(x, y), initial = _coord2d(x, y),
current = _coord2d(x, y) current = _coord2d(x, y)
} }
@ -65,7 +96,7 @@ end
-- create a new mouse button mouse interaction event -- create a new mouse button mouse interaction event
---@nodiscard ---@nodiscard
---@param button CLICK_BUTTON mouse button ---@param button CLICK_BUTTON mouse button
---@param type CLICK_TYPE click type ---@param type MOUSE_CLICK click type
---@param x1 integer initial x ---@param x1 integer initial x
---@param y1 integer initial y ---@param y1 integer initial y
---@param x2 integer current x ---@param x2 integer current x
@ -83,14 +114,14 @@ end
-- create a new generic mouse interaction event -- create a new generic mouse interaction event
---@nodiscard ---@nodiscard
---@param type CLICK_TYPE ---@param type MOUSE_CLICK
---@param x integer ---@param x integer
---@param y integer ---@param y integer
---@return mouse_interaction ---@return mouse_interaction
function events.mouse_generic(type, x, y) function events.mouse_generic(type, x, y)
return { return {
monitor = "", monitor = "",
button = events.CLICK_BUTTON.GENERIC, button = CLICK_BUTTON.GENERIC,
type = type, type = type,
initial = _coord2d(x, y), initial = _coord2d(x, y),
current = _coord2d(x, y) current = _coord2d(x, y)
@ -115,8 +146,8 @@ end
-- check if an event qualifies as a click (tap or up) -- check if an event qualifies as a click (tap or up)
---@nodiscard ---@nodiscard
---@param t CLICK_TYPE ---@param t MOUSE_CLICK
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end
-- create a new mouse event to pass onto graphics renderer<br> -- create a new mouse event to pass onto graphics renderer<br>
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch -- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
@ -126,35 +157,97 @@ function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.
---@param y integer y coordinate ---@param y integer y coordinate
---@return mouse_interaction|nil ---@return mouse_interaction|nil
function events.new_mouse_event(event_type, opt, x, y) function events.new_mouse_event(event_type, opt, x, y)
local h = handler
if event_type == "mouse_click" then if event_type == "mouse_click" then
---@cast opt 1|2|3 ---@cast opt 1|2|3
handler.button_down[opt] = _coord2d(x, y)
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y) local init = true
if opt == 1 and (h.dc_step % 2) == 1 then
if h.dc_step ~= 1 and h.dc_coord.x == x and h.dc_coord.y == y and (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
init = false
h.dc_step = h.dc_step + 1
end
end
if init then
h.dc_start = util.time_ms()
h.dc_coord = _coord2d(x, y)
h.dc_step = 2
end
h.button_down[opt] = _coord2d(x, y)
return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y)
elseif event_type == "mouse_up" then elseif event_type == "mouse_up" then
---@cast opt 1|2|3 ---@cast opt 1|2|3
local initial = handler.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y) if opt == 1 and (h.dc_step % 2) == 0 and h.dc_coord.x == x and h.dc_coord.y == y and
(util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
if h.dc_step == 4 then
util.push_event("double_click", 1, x, y)
h.dc_step = 1
else h.dc_step = h.dc_step + 1 end
else h.dc_step = 1 end
local initial = h.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y)
elseif event_type == "monitor_touch" then elseif event_type == "monitor_touch" then
---@cast opt string ---@cast opt string
return _monitor_touch(opt, x, y) return _monitor_touch(opt, x, y)
elseif event_type == "mouse_drag" then elseif event_type == "mouse_drag" then
---@cast opt 1|2|3 ---@cast opt 1|2|3
local initial = handler.button_down[opt] ---@type coordinate_2d local initial = h.button_down[opt] ---@type coordinate_2d
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y) return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y)
elseif event_type == "mouse_scroll" then elseif event_type == "mouse_scroll" then
---@cast opt 1|-1 ---@cast opt 1|-1
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP) local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP)
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y) return _mouse_event(CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
elseif event_type == "double_click" then
return _mouse_event(CLICK_BUTTON.LEFT_BUTTON, MOUSE_CLICK.DOUBLE_CLICK, x, y, x, y)
end end
end end
-- create a new key event to pass onto graphics renderer<br> -- create a new keyboard interaction event
---@nodiscard
---@param click_type KEY_CLICK key click type
---@param key integer|string keyboard key code or character for 'char' event
---@return key_interaction
local function _key_event(click_type, key)
local name = key
if type(key) == "number" then name = keys.getName(key) end
return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt }
end
-- create a new keyboard event to pass onto graphics renderer<br>
-- supports: char, key, and key_up -- supports: char, key, and key_up
---@param event_type os_event ---@param event_type os_event OS event to handle
function events.new_key_event(event_type) ---@param key integer keyboard key code
---@param held boolean? if the key is being held (for 'key' event)
---@return key_interaction|nil
function events.new_key_event(event_type, key, held)
if event_type == "char" then if event_type == "char" then
return _key_event(KEY_CLICK.CHAR, key)
elseif event_type == "key" then elseif event_type == "key" then
if key == keys.leftShift or key == keys.rightShift then
handler.shift = true
elseif key == keys.leftCtrl or key == keys.rightCtrl then
handler.ctrl = true
elseif key == keys.leftAlt or key == keys.rightAlt then
handler.alt = true
else
return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key)
end
elseif event_type == "key_up" then elseif event_type == "key_up" then
if key == keys.leftShift or key == keys.rightShift then
handler.shift = false
elseif key == keys.leftCtrl or key == keys.rightCtrl then
handler.ctrl = false
elseif key == keys.leftAlt or key == keys.rightAlt then
handler.alt = false
else
return _key_event(KEY_CLICK.UP, key)
end
end end
end end

View File

@ -60,7 +60,7 @@ def make_manifest(size):
}, },
"files" : { "files" : {
# common files # common files
"system" : [ "initenv.lua", "startup.lua" ], "system" : [ "initenv.lua", "startup.lua", "configure.lua" ],
"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"),
@ -82,7 +82,7 @@ def make_manifest(size):
# manifest file estimate # manifest file estimate
"manifest" : size, "manifest" : size,
# common files # common files
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua"), "system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua") + os.path.getsize("configure.lua"),
"common" : dir_size("./scada-common"), "common" : dir_size("./scada-common"),
"graphics" : dir_size("./graphics"), "graphics" : dir_size("./graphics"),
"lockbox" : dir_size("./lockbox"), "lockbox" : dir_size("./lockbox"),

View File

@ -1,18 +1,10 @@
--
-- Initialize the Post-Boot Module Environment
--
return { return {
-- initialize booted environment -- initialize booted environment
init_env = function () init_env = function ()
local _require = require("cc.require") local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
local _env = setmetatable({}, { __index = _ENV }) -- overwrite require/package globals
require, package = _require.make(_env, "/")
-- overwrite require/package globals -- reset terminal
require, package = _require.make(_env, "/") term.clear(); term.setCursorPos(1, 1)
end
-- reset terminal
term.clear()
term.setCursorPos(1, 1)
end
} }

View File

@ -1,5 +1,3 @@
require("lockbox").insecure();
local Bit = require("lockbox.util.bit"); local Bit = require("lockbox.util.bit");
local String = require("string"); local String = require("string");
local Math = require("math"); local Math = require("math");

View File

@ -1,5 +1,3 @@
require("lockbox").insecure();
local Bit = require("lockbox.util.bit"); local Bit = require("lockbox.util.bit");
local String = require("string"); local String = require("string");
local Math = require("math"); local Math = require("math");

View File

@ -1,25 +1,6 @@
local Lockbox = {}; local Lockbox = {}
-- cc-mek-scada lockbox version -- cc-mek-scada lockbox version
Lockbox.version = "1.0" Lockbox.version = "1.1"
--[[ return Lockbox
package.path = "./?.lua;"
.. "./cipher/?.lua;"
.. "./digest/?.lua;"
.. "./kdf/?.lua;"
.. "./mac/?.lua;"
.. "./padding/?.lua;"
.. "./test/?.lua;"
.. "./util/?.lua;"
.. package.path;
--]]
Lockbox.ALLOW_INSECURE = true;
Lockbox.insecure = function()
assert(Lockbox.ALLOW_INSECURE,
"This module is insecure! It should not be used in production." ..
"If you really want to use it, set Lockbox.ALLOW_INSECURE to true before importing it");
end
return Lockbox;

View File

@ -8,7 +8,7 @@ local HMAC = function()
local public = {}; local public = {};
local blockSize = 64; local blockSize = 64;
local Digest = nil; local Digest;
local outerPadding = {}; local outerPadding = {};
local innerPadding = {} local innerPadding = {}
local digest; local digest;

View File

@ -1,4 +1,3 @@
local String = require("string"); local String = require("string");
local Bit = require("lockbox.util.bit"); local Bit = require("lockbox.util.bit");
local Queue = require("lockbox.util.queue"); local Queue = require("lockbox.util.queue");

View File

@ -7,7 +7,7 @@ local iocontrol = require("pocket.iocontrol")
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local LINK_STATE = iocontrol.LINK_STATE local LINK_STATE = iocontrol.LINK_STATE
@ -51,7 +51,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
nic.open(pkt_channel) nic.open(pkt_channel)
-- send a management packet to the supervisor -- send a management packet to the supervisor
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send_sv(msg_type, msg) local function _send_sv(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
@ -65,7 +65,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
end end
-- send a management packet to the coordinator -- send a management packet to the coordinator
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send_crd(msg_type, msg) local function _send_crd(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
@ -80,24 +80,24 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
-- attempt supervisor connection establishment -- attempt supervisor connection establishment
local function _send_sv_establish() local function _send_sv_establish()
_send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) _send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
end end
-- attempt coordinator API connection establishment -- attempt coordinator API connection establishment
local function _send_api_establish() local function _send_api_establish()
_send_crd(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) _send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
end end
-- keep alive ack to supervisor -- keep alive ack to supervisor
---@param srv_time integer ---@param srv_time integer
local function _send_sv_keep_alive_ack(srv_time) local function _send_sv_keep_alive_ack(srv_time)
_send_sv(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send_sv(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- keep alive ack to coordinator -- keep alive ack to coordinator
---@param srv_time integer ---@param srv_time integer
local function _send_api_keep_alive_ack(srv_time) local function _send_api_keep_alive_ack(srv_time)
_send_crd(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send_crd(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@ -111,7 +111,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST self.sv.addr = comms.BROADCAST
_send_sv(SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(MGMT_TYPE.CLOSE, {})
end end
-- close connection to coordinator API server -- close connection to coordinator API server
@ -120,7 +120,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
self.api.linked = false self.api.linked = false
self.api.r_seq_num = nil self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST self.api.addr = comms.BROADCAST
_send_crd(SCADA_MGMT_TYPE.CLOSE, {}) _send_crd(MGMT_TYPE.CLOSE, {})
end end
-- close the connections to the servers -- close the connections to the servers
@ -157,21 +157,21 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
-- supervisor get active alarm tones -- supervisor get active alarm tones
function public.diag__get_alarm_tones() function public.diag__get_alarm_tones()
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_GET, {}) end if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_TONE_GET, {}) end
end end
-- supervisor test alarm tones by tone -- supervisor test alarm tones by tone
---@param id TONE|0 tone ID, or 0 to stop all ---@param id TONE|0 tone ID, or 0 to stop all
---@param state boolean tone state ---@param state boolean tone state
function public.diag__set_alarm_tone(id, state) function public.diag__set_alarm_tone(id, state)
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_SET, { id, state }) end if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_TONE_SET, { id, state }) end
end end
-- supervisor test alarm tones by alarm -- supervisor test alarm tones by alarm
---@param id ALARM|0 alarm ID, 0 to stop all ---@param id ALARM|0 alarm ID, 0 to stop all
---@param state boolean alarm state ---@param state boolean alarm state
function public.diag__set_alarm(id, state) function public.diag__set_alarm(id, state)
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end end
-- parse a packet -- parse a packet
@ -180,7 +180,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
---@param reply_to integer ---@param reply_to integer
---@param message any ---@param message any
---@param distance integer ---@param distance integer
---@return mgmt_frame|capi_frame|nil packet ---@return mgmt_frame|crdn_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance) local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
@ -192,11 +192,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get() pkt = mgmt_pkt.get()
end end
-- get as coordinator API packet -- get as coordinator packet
elseif s_pkt.protocol() == PROTOCOL.COORD_API then elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local capi_pkt = comms.capi_packet() local crdn_pkt = comms.crdn_packet()
if capi_pkt.decode(s_pkt) then if crdn_pkt.decode(s_pkt) then
pkt = capi_pkt.get() pkt = crdn_pkt.get()
end end
else else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
@ -207,7 +207,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
end end
-- handle a packet -- handle a packet
---@param packet mgmt_frame|capi_frame|nil ---@param packet mgmt_frame|crdn_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
local diag = iocontrol.get_db().diag local diag = iocontrol.get_db().diag
@ -240,7 +240,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
if protocol == PROTOCOL.SCADA_MGMT then if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if self.api.linked then if self.api.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 then if packet.length == 1 then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -256,7 +256,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else else
log.debug("coordinator SCADA keep alive packet length mismatch") log.debug("coordinator SCADA keep alive packet length mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
api_watchdog.cancel() api_watchdog.cancel()
self.api.linked = false self.api.linked = false
@ -266,7 +266,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
end end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with coordinator established -- connection with coordinator established
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] local est_ack = packet.data[1]
@ -330,7 +330,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
if protocol == PROTOCOL.SCADA_MGMT then if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if self.sv.linked then if self.sv.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 then if packet.length == 1 then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -346,14 +346,14 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else else
log.debug("supervisor SCADA keep alive packet length mismatch") log.debug("supervisor SCADA keep alive packet length mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv.linked = false self.sv.linked = false
self.sv.r_seq_num = nil self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST self.sv.addr = comms.BROADCAST
log.info("supervisor server connection closed by remote host") log.info("supervisor server connection closed by remote host")
elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then
if packet.length == 8 then if packet.length == 8 then
for i = 1, #packet.data do for i = 1, #packet.data do
diag.tone_test.tone_indicators[i].update(packet.data[i] == true) diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
@ -361,7 +361,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else else
log.debug("supervisor SCADA diag alarm states packet length mismatch") log.debug("supervisor SCADA diag alarm states packet length mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied") diag.tone_test.ready_warn.set_value("testing denied")
log.debug("supervisor SCADA diag tone set failed") log.debug("supervisor SCADA diag tone set failed")
@ -380,7 +380,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else else
log.debug("supervisor SCADA diag tone set packet length/type mismatch") log.debug("supervisor SCADA diag tone set packet length/type mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then
if packet.length == 1 and packet.data[1] == false then if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied") diag.tone_test.ready_warn.set_value("testing denied")
log.debug("supervisor SCADA diag alarm set failed") log.debug("supervisor SCADA diag alarm set failed")
@ -401,7 +401,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor") log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
end end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established -- connection with supervisor established
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] local est_ack = packet.data[1]

View File

@ -5,18 +5,23 @@
local main_view = require("pocket.ui.main") local main_view = require("pocket.ui.main")
local style = require("pocket.ui.style") local style = require("pocket.ui.style")
local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
---@class pocket_renderer
local renderer = {} local renderer = {}
local ui = { local ui = {
display = nil display = nil
} }
-- start the pocket GUI -- try to start the pocket GUI
function renderer.start_ui() ---@return boolean success, any error_msg
function renderer.try_start_ui()
local status, msg = true, nil
if ui.display == nil then if ui.display == nil then
-- reset screen -- reset screen
term.setTextColor(colors.white) term.setTextColor(colors.white)
@ -30,12 +35,22 @@ function renderer.start_ui()
end end
-- init front panel view -- init front panel view
ui.display = DisplayBox{window=term.current(),fg_bg=style.root} status, msg = pcall(function ()
main_view(ui.display) ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
main_view(ui.display)
end)
-- start flasher callback task if status then
flasher.run() -- start flasher callback task
flasher.run()
else
-- report fail and close ui
msg = core.extract_assert_msg(msg)
renderer.close_ui()
end
end end
return status, msg
end end
-- close out the UI -- close out the UI

View File

@ -18,7 +18,7 @@ local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local POCKET_VERSION = "v0.6.0-alpha" local POCKET_VERSION = "v0.6.3-alpha"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -111,9 +111,8 @@ local function main()
-- start the UI -- start the UI
---------------------------------------- ----------------------------------------
local ui_ok, message = pcall(renderer.start_ui) local ui_ok, message = renderer.try_start_ui()
if not ui_ok then if not ui_ok then
renderer.close_ui()
println(util.c("UI error: ", message)) println(util.c("UI error: ", message))
log.error(util.c("startup> GUI render failed with error ", message)) log.error(util.c("startup> GUI render failed with error ", message))
else else
@ -171,7 +170,8 @@ local function main()
-- got a packet -- got a packet
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5) local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
pocket_comms.handle_packet(packet) pocket_comms.handle_packet(packet)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then
-- handle a monitor touch event -- handle a monitor touch event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end end

View File

@ -11,7 +11,7 @@ local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting") local WaitingAnim = require("graphics.elements.animations.waiting")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
@ -29,10 +29,10 @@ local function init(parent, y, is_api)
if is_api then if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
TextBox{parent=box,text="Connecting to API",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
else else
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)} WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
TextBox{parent=box,text="Connecting to Supervisor",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)}
end end
return root return root

View File

@ -26,7 +26,7 @@ local Sidebar = require("graphics.elements.controls.sidebar")
local LINK_STATE = iocontrol.LINK_STATE local LINK_STATE = iocontrol.LINK_STATE
local NAV_PAGE = iocontrol.NAV_PAGE local NAV_PAGE = iocontrol.NAV_PAGE
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
@ -37,7 +37,7 @@ local function init(main)
local ps = iocontrol.get_db().ps local ps = iocontrol.get_db().ps
-- window header message -- window header message
TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header} TextBox{parent=main,y=1,text="",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
-- --
-- root panel panes (connection screens + main screen) -- root panel panes (connection screens + main screen)

View File

@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
-- new boiler page view -- new boiler page view
---@param root graphics_element parent ---@param root graphics_element parent
local function new_view(root) local function new_view(root)
local main = Div{parent=root,x=1,y=1} local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main return main
end end

View File

@ -17,7 +17,7 @@ local cpair = core.cpair
local NAV_PAGE = iocontrol.NAV_PAGE local NAV_PAGE = iocontrol.NAV_PAGE
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
-- new diagnostics page view -- new diagnostics page view
---@param root graphics_element parent ---@param root graphics_element parent
@ -28,7 +28,7 @@ local function new_view(root)
local diag_home = Div{parent=main,x=1,y=1} local diag_home = Div{parent=main,x=1,y=1}
TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=TEXT_ALIGN.CENTER} TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
local alarm_test = Div{parent=main,x=1,y=1} local alarm_test = Div{parent=main,x=1,y=1}
@ -63,15 +63,15 @@ local function new_view(root)
local audio = Div{parent=alarm_test,x=1,y=1} local audio = Div{parent=alarm_test,x=1,y=1}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=TEXT_ALIGN.CENTER} TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=ALIGN.CENTER}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)} ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag} PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag}
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)} local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()} TextBox{parent=tones,text="Tones",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local test_btns = {} local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1} test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
@ -94,7 +94,7 @@ local function new_view(root)
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)} local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()} TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local alarm_btns = {} local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach} alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
@ -121,7 +121,7 @@ local function new_view(root)
local states = Div{parent=audio,x=2,y=14,height=5,width=8} local states = Div{parent=audio,x=2,y=14,height=5,width=8}
TextBox{parent=states,text="States",height=1,alignment=TEXT_ALIGN.CENTER} TextBox{parent=states,text="States",height=1,alignment=ALIGN.CENTER}
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray} local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray} local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray} local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}

View File

@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
-- new reactor page view -- new reactor page view
---@param root graphics_element parent ---@param root graphics_element parent
local function new_view(root) local function new_view(root)
local main = Div{parent=root,x=1,y=1} local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main return main
end end

View File

@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
-- new turbine page view -- new turbine page view
---@param root graphics_element parent ---@param root graphics_element parent
local function new_view(root) local function new_view(root)
local main = Div{parent=root,x=1,y=1} local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main return main
end end

View File

@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox")
-- local cpair = core.cpair -- local cpair = core.cpair
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
-- new unit page view -- new unit page view
---@param root graphics_element parent ---@param root graphics_element parent
local function new_view(root) local function new_view(root)
local main = Div{parent=root,x=1,y=1} local main = Div{parent=root,x=1,y=1}
TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
return main return main
end end

View File

@ -1,34 +0,0 @@
local config = {}
-- set to false to run in offline mode (safety regulation only)
config.NETWORKED = true
-- unique reactor ID
config.REACTOR_ID = 1
-- for offline mode, this redstone interface will turn off (open a valve)
-- when emergency coolant is needed due to low coolant
-- config.EMERGENCY_COOL = { side = "right", color = nil }
-- supervisor comms channel
config.SVR_CHANNEL = 16240
-- PLC comms channel
config.PLC_CHANNEL = 16241
-- 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"
-- 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
return config

687
reactor-plc/configure.lua Normal file
View File

@ -0,0 +1,687 @@
--
-- Configuration GUI
--
local log = require("scada-common.log")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local CheckBox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local Radio2D = require("graphics.elements.controls.radio_2d")
local RadioButton = require("graphics.elements.controls.radio_button")
local NumberField = require("graphics.elements.form.number_field")
local TextField = require("graphics.elements.form.text_field")
local println = util.println
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
---@class plc_configurator
local configurator = {}
local style = {}
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.lime, hex = 0x80ff80 },
{ c = colors.green, hex = 0x4aee8a },
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xf26ba2 },
{ c = colors.magenta, hex = 0xf9488a },
{ c = colors.lightGray, hex = 0xcacaca },
{ c = colors.gray, hex = 0x575757 }
}
local tool_ctl = {
ask_config = false,
has_config = false,
viewing_config = false,
importing_legacy = false,
view_cfg = nil, ---@type graphics_element
settings_apply = nil, ---@type graphics_element
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
gen_summary = nil, ---@type function
show_current_cfg = nil, ---@type function
load_legacy = nil, ---@type function
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type graphics_element
auth_key_textbox = nil, ---@type graphics_element
auth_key_value = ""
}
---@class plc_config
local tmp_cfg = {
Networked = false,
UnitID = 0,
EmerCoolEnable = false,
EmerCoolSide = nil,
EmerCoolColor = nil,
SVR_Channel = nil,
PLC_Channel = nil,
ConnTimeout = nil,
TrustedRange = nil,
AuthKey = nil,
LogMode = 0,
LogPath = "",
LogDebug = false,
}
---@class plc_config
local ini_cfg = {}
local fields = {
{ "Networked", "Networked" },
{ "UnitID", "Unit ID" },
{ "EmerCoolEnable", "Emergency Coolant" },
{ "EmerCoolSide", "Emergency Coolant Side" },
{ "EmerCoolColor", "Emergency Coolant Color" },
{ "SVR_Channel", "SVR Channel" },
{ "PLC_Channel", "PLC Channel" },
{ "ConnTimeout", "Connection Timeout" },
{ "TrustedRange", "Trusted Range" },
{ "AuthKey", "Facility Auth Key" },
{ "LogMode", "Log Mode" },
{ "LogPath", "Log Path" },
{ "LogDebug","Log Debug Messages" }
}
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
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_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
---@param side string
local function side_to_idx(side)
for k, v in ipairs(side_options_map) do
if v == side then return k end
end
end
-- convert color to index
---@param color color
local function color_to_idx(color)
for k, v in ipairs(color_options_map) do
if v == color then return k end
end
end
-- load data from the settings file
---@param target plc_config
local function load_settings(target)
target.Networked = settings.get("Networked", false)
target.UnitID = settings.get("UnitID", 1)
target.EmerCoolEnable = settings.get("EmerCoolEnable", false)
target.EmerCoolSide = settings.get("EmerCoolSide", nil)
target.EmerCoolColor = settings.get("EmerCoolColor", nil)
target.SVR_Channel = settings.get("SVR_Channel", 16240)
target.PLC_Channel = settings.get("PLC_Channel", 16241)
target.ConnTimeout = settings.get("ConnTimeout", 5)
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
-- create the config view
---@param display graphics_element
local function config_view(display)
local nav_fg_bg = cpair(colors.black,colors.white)
local btn_act_fg_bg = cpair(colors.white,colors.gray)
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,height=1,fg_bg=style.header}
local root_pane_div = Div{parent=display,x=1,y=2}
local main_page = Div{parent=root_pane_div,x=1,y=1}
local plc_cfg = Div{parent=root_pane_div,x=1,y=1}
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
local summary = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary}}
-- MAIN PAGE
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."}
if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=2,text_align=CENTER,text="Notice: This device has no valid config. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)}
y_start = y_start + 3
end
local function view_config()
tool_ctl.viewing_config = true
tool_ctl.gen_summary(ini_cfg)
tool_ctl.settings_apply.hide(true)
main_pane.set_value(5)
end
if fs.exists("/reactor-plc/config.lua") then
PushButton{parent=main_page,x=2,y=y_start,min_width=28,text="Import Legacy 'config.lua'",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
y_start = y_start + 2
end
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
-- PLC CONFIG
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_3 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_4 = Div{parent=plc_cfg,x=2,y=4,width=49}
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_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=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=cpair(colors.gray,colors.lightGray)}
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 function submit_networked()
tool_ctl.set_networked(networked.get_value())
plc_pane.set_value(2)
end
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}
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=3,height=3,text_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=cpair(colors.gray,colors.lightGray)}
TextBox{parent=plc_c_2,x=1,y=6,height=1,text_align=CENTER,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=cpair(colors.black,colors.white)}
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 function submit_id()
local unit_id = tonumber(u_id.get_value())
if unit_id ~= nil then
u_id_err.hide(true)
tmp_cfg.UnitID = unit_id
plc_pane.set_value(3)
else u_id_err.show() end
end
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}
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=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=cpair(colors.gray,colors.lightGray)}
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 function next_from_plc()
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end
end
local function submit_en_emcool()
tmp_cfg.EmerCoolEnable = en_em_cool.get_value()
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
end
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}
TextBox{parent=plc_c_4,x=1,y=1,height=1,text_align=CENTER,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}
TextBox{parent=plc_c_4,x=1,y=5,height=1,text_align=CENTER,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 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=cpair(colors.gray,colors.lightGray)}
if ini_cfg.EmerCoolColor == nil then color.disable() end
local function submit_emcool()
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
tmp_cfg.EmerCoolColor = color_options_map[color.get_value()]
next_from_plc()
end
PushButton{parent=plc_c_4,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_4,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- NET CONFIG
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
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_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=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=cpair(colors.gray,colors.lightGray)}
TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,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=cpair(colors.black,colors.white)}
TextBox{parent=net_c_1,x=9,y=9,height=4,text_align=CENTER,text="[SVR_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)}
TextBox{parent=net_c_1,x=1,y=11,height=1,text_align=CENTER,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=cpair(colors.black,colors.white)}
TextBox{parent=net_c_1,x=9,y=12,height=4,text_align=CENTER,text="[PLC_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)}
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 function submit_channels()
local svr_c = tonumber(svr_chan.get_value())
local plc_c = tonumber(plc_chan.get_value())
if svr_c ~= nil and plc_c ~= nil then
tmp_cfg.SVR_Channel = svr_c
tmp_cfg.PLC_Channel = plc_c
net_pane.set_value(2)
chan_err.hide(true)
elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.")
chan_err.show()
else
chan_err.set_value("Please set the PLC channel.")
chan_err.show()
end
end
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}
TextBox{parent=net_c_2,x=1,y=1,height=1,text_align=CENTER,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=cpair(colors.black,colors.white)}
TextBox{parent=net_c_2,x=9,y=2,height=2,text_align=CENTER,text="seconds (default 5)",fg_bg=cpair(colors.gray,colors.lightGray)}
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=cpair(colors.gray,colors.lightGray)}
TextBox{parent=net_c_2,x=1,y=8,height=1,text_align=CENTER,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=cpair(colors.black,colors.white)}
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=cpair(colors.gray,colors.lightGray)}
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 function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value())
local range_val = tonumber(range.get_value())
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = range_val
net_pane.set_value(3)
p2_err.hide(true)
elseif timeout_val == nil then
p2_err.set_value("Please set the connection timeout.")
p2_err.show()
else
p2_err.set_value("Please set the trusted range.")
p2_err.show()
end
end
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}
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=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=cpair(colors.gray,colors.lightGray)}
TextBox{parent=net_c_3,x=1,y=11,height=1,text_align=CENTER,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=cpair(colors.black,colors.white)}
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local function submit_auth()
tmp_cfg.AuthKey = key.get_value()
main_pane.set_value(4)
end
PushButton{parent=net_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- LOG CONFIG
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_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,height=1,text_align=CENTER,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}
TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,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=cpair(colors.black,colors.white)}
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=cpair(colors.gray,colors.lightGray)}
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 function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(5)
else path_err.show() end
end
local function back_from_log()
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
end
PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
-- SUMMARY OF CHANGES
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
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)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=cpair(colors.black,colors.white),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local function back_from_settings()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
tool_ctl.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(4)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
if settings.save("reactor-plc.settings") then
load_settings(ini_cfg)
try_set(networked, ini_cfg.Networked)
try_set(u_id, ini_cfg.UnitID)
try_set(en_em_cool, ini_cfg.EmerCoolEnable)
try_set(side, side_to_idx(ini_cfg.EmerCoolSide))
try_set(bundled, ini_cfg.EmerCoolColor ~= nil)
if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
if tool_ctl.importing_legacy then
tool_ctl.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
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}
TextBox{parent=sum_c_2,x=1,y=1,height=1,text_align=CENTER,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
plc_pane.set_value(1)
net_pane.set_value(1)
sum_pane.set_value(1)
end
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)}
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."}
local function delete_legacy()
fs.delete("/reactor-plc/config.lua")
exit()
end
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)}
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."}
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)}
-- set tool functions now that we have the elements
function tool_ctl.set_networked(enable)
tmp_cfg.Networked = enable
if enable then u_id.set_max(4) else u_id.set_max(999) end
end
function tool_ctl.bundled_emcool(en) if en then color.enable() else color.disable() end end
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("reactor-plc.config")
tmp_cfg.Networked = config.NETWORKED
tmp_cfg.UnitID = config.REACTOR_ID
tmp_cfg.EmerCoolEnable = type(config.EMERGENCY_COOL) == "table"
if tmp_cfg.EmerCoolEnable then
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
else
tmp_cfg.EmerCoolSide = nil
tmp_cfg.EmerCoolColor = nil
end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(5)
tool_ctl.importing_legacy = true
end
-- expose the auth key on the summary page
function tool_ctl.show_auth_key()
tool_ctl.show_key_btn.disable()
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
end
-- generate the summary list
---@param cfg plc_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
tool_ctl.show_key_btn.enable()
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) + 1
local raw = cfg[f[1]]
local val = util.strval(raw)
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] == "EmerCoolColor" and raw ~= nil then val = color_name_map[raw] end
if val == "nil" then val = "n/a" end
local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
end
end
end
-- reset terminal screen
local function reset_term()
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
-- run the reactor PLC configurator
---@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)
tool_ctl.ask_config = ask_config == true
tool_ctl.has_config = settings.load("/reactor-plc.settings")
load_settings(ini_cfg)
reset_term()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
local status, error = pcall(function ()
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
while true do
local event, param1, param2, param3 = util.pull_event()
-- handle event
if event == "timer" then
-- notify timer callback dispatcher
tcd.handle(param1)
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)
if m_e then display.handle_mouse(m_e) end
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)
if k_e then display.handle_key(k_e) end
elseif event == "paste" then
-- handle a paste event
display.handle_paste(param1)
end
if event == "terminate" then return end
end
end)
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
reset_term()
if not status then
println("configurator error: " .. error)
end
return status, error
end
return configurator

View File

@ -5,8 +5,8 @@
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("reactor-plc.config")
local databus = require("reactor-plc.databus") local databus = require("reactor-plc.databus")
local plc = require("reactor-plc.plc")
local style = require("reactor-plc.panel.style") local style = require("reactor-plc.panel.style")
@ -23,15 +23,18 @@ local LED = require("graphics.elements.indicators.led")
local LEDPair = require("graphics.elements.indicators.ledpair") local LEDPair = require("graphics.elements.indicators.ledpair")
local RGBLED = require("graphics.elements.indicators.ledrgb") local RGBLED = require("graphics.elements.indicators.ledrgb")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
local ind_grn = style.ind_grn
local ind_red = style.ind_red
-- create new front panel view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
local function init(panel) local function init(panel)
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end) header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
-- --
@ -41,14 +44,14 @@ local function init(panel)
local system = Div{parent=panel,width=14,height=18,x=2,y=3} local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
system.line_break() system.line_break()
init_ok.register(databus.ps, "init_ok", init_ok.update) init_ok.register(databus.ps, "init_ok", init_ok.update)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} local modem = LED{parent=system,label="MODEM",colors=ind_grn}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED) network.update(types.PANEL_LINK_STATE.DISCONNECTED)
system.line_break() system.line_break()
@ -57,11 +60,11 @@ local function init(panel)
modem.register(databus.ps, "has_modem", modem.update) modem.register(databus.ps, "has_modem", modem.update)
network.register(databus.ps, "link_state", network.update) network.register(databus.ps, "link_state", network.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)} local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)} local rt_rps = LED{parent=system,label="RT RPS",colors=ind_grn}
local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.green_off)} local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=ind_grn}
local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=cpair(colors.green,colors.green_off)} local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=ind_grn}
local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)} local rt_sctl = LED{parent=system,label="RT SPCTL",colors=ind_grn}
system.line_break() system.line_break()
rt_main.register(databus.ps, "routine__main", rt_main.update) rt_main.register(databus.ps, "routine__main", rt_main.update)
@ -80,17 +83,17 @@ local function init(panel)
local status = Div{parent=panel,width=19,height=18,x=17,y=3} local status = Div{parent=panel,width=19,height=18,x=17,y=3}
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)} local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=ind_grn}
-- only show emergency coolant LED if emergency coolant is configured for this device -- only show emergency coolant LED if emergency coolant is configured for this device
if type(config.EMERGENCY_COOL) == "table" then if plc.config.EmerCoolEnable then
local emer_cool = LED{parent=status,x=2,width=14,label="EMER COOLANT",colors=cpair(colors.yellow,colors.yellow_off)} local emer_cool = LED{parent=status,x=2,width=14,label="EMER COOLANT",colors=cpair(colors.yellow,colors.yellow_off)}
emer_cool.register(databus.ps, "emer_cool", emer_cool.update) emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
end end
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)} local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)} local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS} local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,flash=true,period=flasher.PERIOD.BLINK_250_MS}
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)} local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)} local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)}
@ -105,8 +108,8 @@ local function init(panel)
-- --
local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)} local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)}
local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@ -116,20 +119,20 @@ local function init(panel)
-- --
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)} local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)} local rps_man = LED{parent=rps,label="MANUAL",colors=ind_red}
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)} local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=ind_red}
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)} local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=ind_red}
local rps_flt = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)} local rps_flt = LED{parent=rps,label="PLC FAULT",colors=ind_red}
local rps_fail = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)} local rps_fail = LED{parent=rps,label="RCT FAULT",colors=ind_red}
rps.line_break() rps.line_break()
local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)} local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=ind_red}
local rps_tmp = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)} local rps_tmp = LED{parent=rps,label="HI TEMP",colors=ind_red}
rps.line_break() rps.line_break()
local rps_nof = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)} local rps_nof = LED{parent=rps,label="LO FUEL",colors=ind_red}
local rps_wst = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)} local rps_wst = LED{parent=rps,label="HI WASTE",colors=ind_red}
rps.line_break() rps.line_break()
local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)} local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=ind_red}
local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)} local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=ind_red}
rps_man.register(databus.ps, "rps_manual", rps_man.update) rps_man.register(databus.ps, "rps_manual", rps_man.update)
rps_auto.register(databus.ps, "rps_automatic", rps_auto.update) rps_auto.register(databus.ps, "rps_automatic", rps_auto.update)

View File

@ -39,4 +39,9 @@ style.colors = {
{ c = colors.brown, hex = 0x672223 } -- RED OFF { c = colors.brown, hex = 0x672223 } -- RED OFF
} }
-- COMMON COLOR PAIRS --
style.ind_grn = cpair(colors.green, colors.green_off)
style.ind_red = cpair(colors.red, colors.red_off)
return style return style

View File

@ -16,7 +16,7 @@ local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local RPLC_TYPE = comms.RPLC_TYPE local RPLC_TYPE = comms.RPLC_TYPE
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local AUTO_ACK = comms.PLC_AUTO_ACK local AUTO_ACK = comms.PLC_AUTO_ACK
local RPS_LIMITS = const.RPS_LIMITS local RPS_LIMITS = const.RPS_LIMITS
@ -26,14 +26,61 @@ local RPS_LIMITS = const.RPS_LIMITS
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active." local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active." local PCALL_START_MSG = "pcall: Reactor is already active."
---@type plc_config
local config = {}
plc.config = config
-- load the PLC configuration
function plc.load_config()
if not settings.load("/reactor-plc.settings") then return false end
config.Networked = settings.get("Networked")
config.UnitID = settings.get("UnitID")
config.EmerCoolEnable = settings.get("EmerCoolEnable")
config.EmerCoolSide = settings.get("EmerCoolSide")
config.EmerCoolColor = settings.get("EmerCoolColor")
config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_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_bool(config.Networked)
cfv.assert_type_int(config.UnitID)
cfv.assert_type_bool(config.EmerCoolEnable)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.PLC_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)
cfv.assert_type_int(config.LogMode)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
-- check emergency coolant configuration if enabled
if config.EmerCoolEnable then
cfv.assert_eq(rsio.is_valid_side(config.EmerCoolSide), true)
cfv.assert_eq(config.EmerCoolColor == nil or rsio.is_color(config.EmerCoolColor), true)
end
return cfv.valid()
end
-- RPS: Reactor Protection System<br> -- RPS: Reactor Protection System<br>
-- identifies dangerous states and SCRAMs reactor if warranted<br> -- identifies dangerous states and SCRAMs reactor if warranted<br>
-- autonomous from main SCADA supervisor/coordinator control -- autonomous from main SCADA supervisor/coordinator control
---@nodiscard ---@nodiscard
---@param reactor table ---@param reactor table
---@param is_formed boolean ---@param is_formed boolean
---@param emer_cool nil|table emergency coolant configuration function plc.rps_init(reactor, is_formed)
function plc.rps_init(reactor, is_formed, emer_cool)
local state_keys = { local state_keys = {
high_dmg = 1, high_dmg = 1,
high_temp = 2, high_temp = 2,
@ -73,22 +120,22 @@ function plc.rps_init(reactor, is_formed, emer_cool)
---@param state boolean true to enable emergency coolant, false to disable ---@param state boolean true to enable emergency coolant, false to disable
local function _set_emer_cool(state) local function _set_emer_cool(state)
-- check if this was configured: if it's a table, fields have already been validated. -- check if this was configured: if it's a table, fields have already been validated.
if type(emer_cool) == "table" then if config.EmerCoolEnable then
local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, state) local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, state)
if level ~= false then if level ~= false then
if rsio.is_color(emer_cool.color) then if rsio.is_color(config.EmerCoolColor) then
local output = rs.getBundledOutput(emer_cool.side) local output = rs.getBundledOutput(config.EmerCoolSide)
if rsio.digital_write(level) then if rsio.digital_write(level) then
output = colors.combine(output, emer_cool.color) output = colors.combine(output, config.EmerCoolColor)
else else
output = colors.subtract(output, emer_cool.color) output = colors.subtract(output, config.EmerCoolColor)
end end
rs.setBundledOutput(emer_cool.side, output) rs.setBundledOutput(config.EmerCoolSide, output)
else else
rs.setOutput(emer_cool.side, rsio.digital_write(level)) rs.setOutput(config.EmerCoolSide, rsio.digital_write(level))
end end
if state ~= self.emer_cool_active then if state ~= self.emer_cool_active then
@ -238,8 +285,9 @@ function plc.rps_init(reactor, is_formed, emer_cool)
self.state[state_keys.sys_fail] = true self.state[state_keys.sys_fail] = true
end end
-- SCRAM the reactor now (blocks waiting for server tick) -- SCRAM the reactor now<br>
---@return boolean success ---@return boolean success
--- EVENT_CONSUMER: this function consumes events
function public.scram() function public.scram()
log.info("RPS: reactor SCRAM") log.info("RPS: reactor SCRAM")
@ -254,8 +302,9 @@ function plc.rps_init(reactor, is_formed, emer_cool)
end end
end end
-- start the reactor now (blocks waiting for server tick) -- start the reactor now<br>
---@return boolean success ---@return boolean success
--- EVENT_CONSUMER: this function consumes events
function public.activate() function public.activate()
if not self.tripped then if not self.tripped then
log.info("RPS: reactor start") log.info("RPS: reactor start")
@ -443,16 +492,12 @@ end
-- Reactor PLC Communications -- Reactor PLC Communications
---@nodiscard ---@nodiscard
---@param id integer reactor ID
---@param version string PLC version ---@param version string PLC version
---@param nic nic network interface device ---@param nic nic network interface device
---@param plc_channel integer PLC comms channel
---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range
---@param reactor table reactor device ---@param reactor table reactor device
---@param rps rps RPS reference ---@param rps rps RPS reference
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, rps, conn_watchdog) function plc.comms(version, nic, reactor, rps, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
@ -466,13 +511,13 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
max_burn_rate = nil max_burn_rate = nil
} }
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure network channels -- configure network channels
nic.closeAll() nic.closeAll()
nic.open(plc_channel) nic.open(config.PLC_Channel)
-- send an RPLC packet -- send an RPLC packet
---@param msg_type RPLC_TYPE ---@param msg_type RPLC_TYPE
@ -481,15 +526,15 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet() local r_pkt = comms.rplc_packet()
r_pkt.make(id, msg_type, msg) r_pkt.make(config.UnitID, msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
nic.transmit(svr_channel, plc_channel, s_pkt) nic.transmit(config.SVR_Channel, config.PLC_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send_mgmt(msg_type, msg) local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
@ -498,7 +543,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
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, plc_channel, s_pkt) nic.transmit(config.SVR_Channel, config.PLC_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@ -600,7 +645,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
-- keep alive ack -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) local function _send_keep_alive_ack(srv_time)
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- general ack -- general ack
@ -612,10 +657,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
-- send structure properties (these should not change, server will cache these) -- send structure properties (these should not change, server will cache these)
local function _send_struct() local function _send_struct()
local min_pos = { x = 0, y = 0, z = 0 } local mek_data = { false, 0, 0, 0, types.new_zero_coordinate(), types.new_zero_coordinate(), 0, 0, 0, 0, 0, 0, 0, 0 }
local max_pos = { x = 0, y = 0, z = 0 }
local mek_data = { false, 0, 0, 0, min_pos, max_pos, 0, 0, 0, 0, 0, 0, 0, 0 }
local tasks = { local tasks = {
function () mek_data[1] = reactor.getLength() end, function () mek_data[1] = reactor.getLength() end,
@ -668,12 +710,12 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
function public.close() function public.close()
conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink() public.unlink()
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
end end
-- attempt to establish link with supervisor -- attempt to establish link with supervisor
function public.send_link_req() function public.send_link_req()
_send_mgmt(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, id }) _send_mgmt(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, config.UnitID })
end end
-- send live status information -- send live status information
@ -685,21 +727,18 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
local heating_rate = 0.0 ---@type number local heating_rate = 0.0 ---@type number
if (not no_reactor) and rps.is_formed() then if (not no_reactor) and rps.is_formed() then
if _update_status_cache() then if _update_status_cache() then mek_data = self.status_cache end
mek_data = self.status_cache
end
heating_rate = reactor.getHeatingRate() heating_rate = reactor.getHeatingRate()
end end
local sys_status = { local sys_status = {
util.time(), -- timestamp util.time(), -- timestamp
(not self.scrammed), -- requested control state (not self.scrammed), -- requested control state
no_reactor, -- no reactor peripheral connected no_reactor, -- no reactor peripheral connected
formed, -- reactor formed formed, -- reactor formed
self.auto_ack_token, -- token to indicate auto command has been received before this status update self.auto_ack_token, -- indicate auto command received prior to this status update
heating_rate, -- heating rate heating_rate, -- heating rate
mek_data -- mekanism status data mek_data -- mekanism status data
} }
_send(RPLC_TYPE.STATUS, sys_status) _send(RPLC_TYPE.STATUS, sys_status)
@ -769,7 +808,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
-- handle packets now that we have prints setup -- handle packets now that we have prints setup
if l_chan == plc_channel then if l_chan == config.PLC_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()
@ -837,6 +876,10 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
-- enable the reactor -- enable the reactor
self.scrammed = false self.scrammed = false
_send_ack(packet.type, rps.activate()) _send_ack(packet.type, rps.activate())
elseif packet.type == RPLC_TYPE.RPS_DISABLE then
-- disable the reactor, but do not trip
self.scrammed = true
_send_ack(packet.type, rps.scram())
elseif packet.type == RPLC_TYPE.RPS_SCRAM then elseif packet.type == RPLC_TYPE.RPS_SCRAM then
-- disable the reactor per manual request -- disable the reactor per manual request
self.scrammed = true self.scrammed = true
@ -929,7 +972,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- if linked, only accept packets from configured supervisor -- if linked, only accept packets from configured supervisor
if self.linked then if self.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -945,7 +988,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
else else
log.debug("SCADA_MGMT keep alive packet length/type mismatch") log.debug("SCADA_MGMT keep alive packet length/type mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink() public.unlink()
@ -954,7 +997,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
else else
log.debug("received unsupported SCADA_MGMT packet type " .. packet.type) log.debug("received unsupported SCADA_MGMT packet type " .. packet.type)
end end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- link request confirmation -- link request confirmation
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] local est_ack = packet.data[1]

View File

@ -5,18 +5,23 @@
local panel_view = require("reactor-plc.panel.front_panel") local panel_view = require("reactor-plc.panel.front_panel")
local style = require("reactor-plc.panel.style") local style = require("reactor-plc.panel.style")
local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
---@class reactor_plc_renderer
local renderer = {} local renderer = {}
local ui = { local ui = {
display = nil display = nil
} }
-- start the UI -- try to start the UI
function renderer.start_ui() ---@return boolean success, any error_msg
function renderer.try_start_ui()
local status, msg = true, nil
if ui.display == nil then if ui.display == nil then
-- reset terminal -- reset terminal
term.setTextColor(colors.white) term.setTextColor(colors.white)
@ -30,12 +35,22 @@ function renderer.start_ui()
end end
-- init front panel view -- init front panel view
ui.display = DisplayBox{window=term.current(),fg_bg=style.root} status, msg = pcall(function ()
panel_view(ui.display) ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
panel_view(ui.display)
end)
-- start flasher callback task if status then
flasher.run() -- start flasher callback task
flasher.run()
else
-- report fail and close ui
msg = core.extract_assert_msg(msg)
renderer.close_ui()
end
end end
return status, msg
end end
-- close out the UI -- close out the UI

View File

@ -4,58 +4,46 @@
require("/initenv").init_env() require("/initenv").init_env()
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network") local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio") local util = require("scada-common.util")
local util = require("scada-common.util")
local config = require("reactor-plc.config") local configure = require("reactor-plc.configure")
local databus = require("reactor-plc.databus") local databus = require("reactor-plc.databus")
local plc = require("reactor-plc.plc") 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.5.7" local R_PLC_VERSION = "v1.6.0"
local println = util.println 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 plc.load_config() then
-- try to reconfigure (user action)
cfv.assert_type_bool(config.NETWORKED) local success, error = configure.configure(true)
cfv.assert_type_int(config.REACTOR_ID) if success then
cfv.assert_channel(config.SVR_CHANNEL) assert(plc.load_config(), "failed to load valid reactor PLC configuration")
cfv.assert_channel(config.PLC_CHANNEL) else
cfv.assert_type_int(config.TRUSTED_RANGE) assert(success, "reactor PLC configuration error: " .. error)
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)
assert(cfv.valid(), "bad config file: missing/invalid fields")
-- check emergency coolant configuration
if type(config.EMERGENCY_COOL) == "table" then
if not rsio.is_valid_side(config.EMERGENCY_COOL.side) then
assert(false, "bad config file: emergency coolant side unrecognized")
elseif config.EMERGENCY_COOL.color ~= nil and not rsio.is_color(config.EMERGENCY_COOL.color) then
assert(false, "bad config file: emergency coolant invalid redstone channel color provided")
end end
end end
local config = plc.config
---------------------------------------- ----------------------------------------
-- 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 reactor-plc.startup " .. R_PLC_VERSION) log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
@ -75,32 +63,32 @@ local function main()
-- record firmware versions and ID -- record firmware versions and ID
databus.tx_versions(R_PLC_VERSION, comms.version) databus.tx_versions(R_PLC_VERSION, comms.version)
databus.tx_id(config.REACTOR_ID) databus.tx_id(config.UnitID)
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if string.len(config.AuthKey) > 0 then
network.init_mac(config.AUTH_KEY) network.init_mac(config.AuthKey)
end end
-- shared memory across threads -- shared memory across threads
---@class plc_shared_memory ---@class plc_shared_memory
local __shared_memory = { local __shared_memory = {
-- networked setting -- networked setting
networked = config.NETWORKED, ---@type boolean networked = config.Networked,
-- PLC system state flags -- PLC system state flags
---@class plc_state ---@class plc_state
plc_state = { plc_state = {
init_ok = true, init_ok = true,
fp_ok = false, fp_ok = false,
shutdown = false, shutdown = false,
degraded = true, degraded = true,
reactor_formed = true, reactor_formed = true,
no_reactor = true, no_reactor = true,
no_modem = true no_modem = true
}, },
-- control setpoints -- control setpoints
@ -118,10 +106,10 @@ local function main()
-- system objects -- system objects
plc_sys = { plc_sys = {
rps = nil, ---@type rps rps = nil, ---@type rps
nic = nil, ---@type nic nic = nil, ---@type nic
plc_comms = nil, ---@type plc_comms plc_comms = nil, ---@type plc_comms
conn_watchdog = nil ---@type watchdog conn_watchdog = nil ---@type watchdog
}, },
-- message queues -- message queues
@ -184,10 +172,9 @@ local function main()
-- front panel time! -- front panel time!
if not renderer.ui_ready() then if not renderer.ui_ready() then
local message local message
plc_state.fp_ok, message = pcall(renderer.start_ui) plc_state.fp_ok, message = renderer.try_start_ui()
if not plc_state.fp_ok then if not plc_state.fp_ok then
renderer.close_ui()
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("init> running without front panel") println("init> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message)) log.error(util.c("front panel GUI render failed with error ", message))
@ -197,18 +184,17 @@ local function main()
if plc_state.init_ok then if plc_state.init_ok then
-- init reactor protection system -- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed, config.EMERGENCY_COOL) smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed)
log.debug("init> rps init") log.debug("init> rps init")
if __shared_memory.networked then if __shared_memory.networked then
-- comms watchdog -- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("init> conn watchdog started") log.debug("init> conn watchdog started")
-- create network interface then setup comms -- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem) smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_sys.nic, config.PLC_CHANNEL, config.SVR_CHANNEL, smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init") log.debug("init> comms init")
else else
_println_no_fp("init> starting in offline mode") _println_no_fp("init> starting in offline mode")
@ -216,7 +202,7 @@ local function main()
end end
-- notify user of emergency coolant configuration status -- notify user of emergency coolant configuration status
if config.EMERGENCY_COOL ~= nil then if config.EmerCoolEnable then
println("init> emergency coolant control ready") println("init> emergency coolant control ready")
log.info("init> running with emergency coolant control available") log.info("init> running with emergency coolant control available")
end end

View File

@ -265,7 +265,8 @@ function threads.thread__main(smem, init)
-- update indicators -- update indicators
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" 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 -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "clock_start" then elseif event == "clock_start" then

View File

@ -215,7 +215,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param value any ---@param value any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _5_write_single_coil(c_addr, value) local function _5_write_single_coil(c_addr, value)
local response = nil local response = MODBUS_EXCODE.OK
local _, coils, _, _ = rtu_dev.io_count() local _, coils, _, _ = rtu_dev.io_count()
local return_ok = c_addr <= coils local return_ok = c_addr <= coils
@ -239,7 +239,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param value any ---@param value any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _6_write_single_holding_register(hr_addr, value) local function _6_write_single_holding_register(hr_addr, value)
local response = nil local response = MODBUS_EXCODE.OK
local _, _, _, hold_regs = rtu_dev.io_count() local _, _, _, hold_regs = rtu_dev.io_count()
local return_ok = hr_addr <= hold_regs local return_ok = hr_addr <= hold_regs
@ -263,7 +263,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param values any ---@param values any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _15_write_multiple_coils(c_addr_start, values) local function _15_write_multiple_coils(c_addr_start, values)
local response = nil local response = MODBUS_EXCODE.OK
local _, coils, _, _ = rtu_dev.io_count() local _, coils, _, _ = rtu_dev.io_count()
local count = #values local count = #values
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0) local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
@ -292,7 +292,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param values any ---@param values any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _16_write_multiple_holding_registers(hr_addr_start, values) local function _16_write_multiple_holding_registers(hr_addr_start, values)
local response = nil local response = MODBUS_EXCODE.OK
local _, _, _, hold_regs = rtu_dev.io_count() local _, _, _, hold_regs = rtu_dev.io_count()
local count = #values local count = #values
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0) local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
@ -403,7 +403,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
if type(response) == "table" then if type(response) == "table" then
elseif type(response) == "nil" then elseif response == MODBUS_EXCODE.OK then
response = {} response = {}
else else
response = { response } response = { response }

View File

@ -18,18 +18,21 @@ local DataIndicator = require("graphics.elements.indicators.data")
local LED = require("graphics.elements.indicators.led") local LED = require("graphics.elements.indicators.led")
local RGBLED = require("graphics.elements.indicators.ledrgb") local RGBLED = require("graphics.elements.indicators.ledrgb")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" } local fp_label = style.fp_label
local ind_grn = style.ind_grn
local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" }
-- create new front panel view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
---@param units table unit list ---@param units table unit list
local function init(panel, units) local function init(panel, units)
TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
-- --
-- system indicators -- system indicators
@ -38,13 +41,13 @@ local function init(panel, units)
local system = Div{parent=panel,width=14,height=18,x=2,y=3} local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
on.update(true) on.update(true)
system.line_break() system.line_break()
heartbeat.register(databus.ps, "heartbeat", heartbeat.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} local modem = LED{parent=system,label="MODEM",colors=ind_grn}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED) network.update(types.PANEL_LINK_STATE.DISCONNECTED)
system.line_break() system.line_break()
@ -52,8 +55,8 @@ local function init(panel, units)
modem.register(databus.ps, "has_modem", modem.update) modem.register(databus.ps, "has_modem", modem.update)
network.register(databus.ps, "link_state", network.update) network.register(databus.ps, "link_state", network.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)} local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
local rt_comm = LED{parent=system,label="RT COMMS",colors=cpair(colors.green,colors.green_off)} local rt_comm = LED{parent=system,label="RT COMMS",colors=ind_grn}
system.line_break() system.line_break()
rt_main.register(databus.ps, "routine__main", rt_main.update) rt_main.register(databus.ps, "routine__main", rt_main.update)
@ -61,7 +64,7 @@ local function init(panel, units)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=fp_label}
TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.label} TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.label}
local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=cpair(colors.gray,colors.white)} local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=cpair(colors.gray,colors.white)}
@ -71,9 +74,9 @@ local function init(panel, units)
-- about label -- about label
-- --
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=cpair(colors.lightGray,colors.ivory)} local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=fp_label}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@ -90,7 +93,7 @@ local function init(panel, units)
-- show routine statuses -- show routine statuses
for i = 1, list_length do for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1} TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=cpair(colors.green,colors.green_off)} local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn}
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update) rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end end
@ -115,7 +118,7 @@ local function init(panel, units)
-- 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)
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=cpair(colors.lightGray,colors.ivory)} TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=fp_label}
end end
end end

View File

@ -39,4 +39,10 @@ style.colors = {
{ c = colors.brown, hex = 0x672223 } -- RED OFF { c = colors.brown, hex = 0x672223 } -- RED OFF
} }
-- COMMON COLOR PAIRS --
style.fp_label = cpair(colors.lightGray, colors.ivory)
style.ind_grn = cpair(colors.green, colors.green_off)
return style return style

View File

@ -5,19 +5,24 @@
local panel_view = require("rtu.panel.front_panel") local panel_view = require("rtu.panel.front_panel")
local style = require("rtu.panel.style") local style = require("rtu.panel.style")
local core = require("graphics.core")
local flasher = require("graphics.flasher") local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
---@class rtu_renderer
local renderer = {} local renderer = {}
local ui = { local ui = {
display = nil display = nil
} }
-- start the UI -- try to start the UI
---@param units table RTU units ---@param units table RTU units
function renderer.start_ui(units) ---@return boolean success, any error_msg
function renderer.try_start_ui(units)
local status, msg = true, nil
if ui.display == nil then if ui.display == nil then
-- reset terminal -- reset terminal
term.setTextColor(colors.white) term.setTextColor(colors.white)
@ -30,13 +35,23 @@ function renderer.start_ui(units)
term.setPaletteColor(style.colors[i].c, style.colors[i].hex) term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end end
-- start flasher callback task
flasher.run()
-- init front panel view -- init front panel view
ui.display = DisplayBox{window=term.current(),fg_bg=style.root} status, msg = pcall(function ()
panel_view(ui.display, units) ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
panel_view(ui.display, units)
end)
if status then
-- start flasher callback task
flasher.run()
else
-- report fail and close ui
msg = core.extract_assert_msg(msg)
renderer.close_ui()
end
end end
return status, msg
end end
-- close out the UI -- close out the UI

View File

@ -14,7 +14,7 @@ local rtu = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
-- create a new RTU unit -- create a new RTU unit
@ -227,7 +227,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
nic.open(rtu_channel) nic.open(rtu_channel)
-- send a scada management packet -- send a scada management packet
---@param msg_type SCADA_MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send(msg_type, msg) local function _send(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
@ -243,7 +243,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
-- keep alive ack -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) local function _send_keep_alive_ack(srv_time)
_send(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) _send(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end end
-- generate device advertisement table -- generate device advertisement table
@ -298,25 +298,25 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
function public.close(rtu_state) function public.close(rtu_state)
conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink(rtu_state) public.unlink(rtu_state)
_send(SCADA_MGMT_TYPE.CLOSE, {}) _send(MGMT_TYPE.CLOSE, {})
end end
-- send establish request (includes advertisement) -- send establish request (includes advertisement)
---@param units table ---@param units table
function public.send_establish(units) function public.send_establish(units)
_send(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) }) _send(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) })
end end
-- send capability advertisement -- send capability advertisement
---@param units table ---@param units table
function public.send_advertisement(units) function public.send_advertisement(units)
_send(SCADA_MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units)) _send(MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units))
end end
-- notify that a peripheral was remounted -- notify that a peripheral was remounted
---@param unit_index integer RTU unit ID ---@param unit_index integer RTU unit ID
function public.send_remounted(unit_index) function public.send_remounted(unit_index)
_send(SCADA_MGMT_TYPE.RTU_DEV_REMOUNT, { unit_index }) _send(MGMT_TYPE.RTU_DEV_REMOUNT, { unit_index })
end end
-- parse a MODBUS/SCADA packet -- parse a MODBUS/SCADA packet
@ -433,7 +433,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- SCADA management packet -- SCADA management packet
if rtu_state.linked then if rtu_state.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if packet.type == MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -449,16 +449,16 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
else else
log.debug("SCADA_MGMT keep alive packet length/type mismatch") log.debug("SCADA_MGMT keep alive packet length/type mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then elseif packet.type == MGMT_TYPE.CLOSE then
-- close connection -- close connection
conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink(rtu_state) public.unlink(rtu_state)
println_ts("server connection closed by remote host") println_ts("server connection closed by remote host")
log.warning("server connection closed by remote host") log.warning("server connection closed by remote host")
elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then elseif packet.type == MGMT_TYPE.RTU_ADVERT then
-- request for capabilities again -- request for capabilities again
public.send_advertisement(units) public.send_advertisement(units)
elseif packet.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM then elseif packet.type == MGMT_TYPE.RTU_TONE_ALARM then
-- alarm tone update from supervisor -- alarm tone update from supervisor
if (packet.length == 1) and type(packet.data[1] == "table") and (#packet.data[1] == 8) then if (packet.length == 1) and type(packet.data[1] == "table") and (#packet.data[1] == 8) then
local states = packet.data[1] local states = packet.data[1]
@ -474,7 +474,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
-- not supported -- not supported
log.debug("received unsupported SCADA_MGMT message type " .. packet.type) log.debug("received unsupported SCADA_MGMT message type " .. packet.type)
end end
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] local est_ack = packet.data[1]

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.6.2" local RTU_VERSION = "v1.6.6"
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
@ -480,10 +480,9 @@ local function main()
if configure() then if configure() then
-- start UI -- start UI
local message local message
rtu_state.fp_ok, message = pcall(renderer.start_ui, units) rtu_state.fp_ok, message = renderer.try_start_ui(units)
if not rtu_state.fp_ok then if not rtu_state.fp_ok then
renderer.close_ui()
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("startup> running without front panel") println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message)) log.error(util.c("front panel GUI render failed with error ", message))

View File

@ -279,7 +279,8 @@ function threads.thread__main(smem)
end end
end end
end end
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" 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 -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then elseif event == "speaker_audio_empty" then

View File

@ -6,23 +6,25 @@ local log = require("scada-common.log")
local insert = table.insert local insert = table.insert
---@type integer computer ID
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local COMPUTER_ID = os.getComputerID() ---@type integer computer ID local COMPUTER_ID = os.getComputerID()
local max_distance = nil ---@type number|nil maximum acceptable transmission distance ---@type number|nil maximum acceptable transmission distance
local max_distance = nil
---@class comms ---@class comms
local comms = {} local comms = {}
comms.version = "2.2.1" -- protocol version (non-protocol changes tracked by util.lua version)
comms.version = "2.4.0"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol MODBUS_TCP = 0, -- the "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers SCADA_CRDN = 3 -- data/control packets for coordinators to/from supervisory controllers
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
} }
---@enum RPLC_TYPE ---@enum RPLC_TYPE
@ -31,17 +33,18 @@ local RPLC_TYPE = {
MEK_STRUCT = 1, -- mekanism build structure MEK_STRUCT = 1, -- mekanism build structure
MEK_BURN_RATE = 2, -- set burn rate MEK_BURN_RATE = 2, -- set burn rate
RPS_ENABLE = 3, -- enable reactor RPS_ENABLE = 3, -- enable reactor
RPS_SCRAM = 4, -- SCRAM reactor (manual request) RPS_DISABLE = 4, -- disable the reactor
RPS_ASCRAM = 5, -- SCRAM reactor (automatic request) RPS_SCRAM = 5, -- SCRAM reactor (manual request)
RPS_STATUS = 6, -- RPS status RPS_ASCRAM = 6, -- SCRAM reactor (automatic request)
RPS_ALARM = 7, -- RPS alarm broadcast RPS_STATUS = 7, -- RPS status
RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately) RPS_ALARM = 8, -- RPS alarm broadcast
RPS_AUTO_RESET = 9, -- clear RPS trip if it is just a timeout or auto scram RPS_RESET = 9, -- clear RPS trip (if in bad state, will trip immediately)
AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited RPS_AUTO_RESET = 10, -- clear RPS trip if it is just a timeout or auto scram
AUTO_BURN_RATE = 11 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited
} }
---@enum SCADA_MGMT_TYPE ---@enum MGMT_TYPE
local SCADA_MGMT_TYPE = { local MGMT_TYPE = {
ESTABLISH = 0, -- establish new connection ESTABLISH = 0, -- establish new connection
KEEP_ALIVE = 1, -- keep alive packet w/ RTT KEEP_ALIVE = 1, -- keep alive packet w/ RTT
CLOSE = 2, -- close a connection CLOSE = 2, -- close a connection
@ -53,8 +56,8 @@ local SCADA_MGMT_TYPE = {
DIAG_ALARM_SET = 8 -- diagnostic: set alarm to simulate audio for DIAG_ALARM_SET = 8 -- diagnostic: set alarm to simulate audio for
} }
---@enum SCADA_CRDN_TYPE ---@enum CRDN_TYPE
local SCADA_CRDN_TYPE = { local CRDN_TYPE = {
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
FAC_BUILDS = 1, -- facility RTU builds FAC_BUILDS = 1, -- facility RTU builds
FAC_STATUS = 2, -- state of facility and facility devices FAC_STATUS = 2, -- state of facility and facility devices
@ -64,10 +67,6 @@ local SCADA_CRDN_TYPE = {
UNIT_CMD = 6 -- command a reactor unit UNIT_CMD = 6 -- command a reactor unit
} }
---@enum CAPI_TYPE
local CAPI_TYPE = {
}
---@enum ESTABLISH_ACK ---@enum ESTABLISH_ACK
local ESTABLISH_ACK = { local ESTABLISH_ACK = {
ALLOW = 0, -- link approved ALLOW = 0, -- link approved
@ -76,14 +75,8 @@ local ESTABLISH_ACK = {
BAD_VERSION = 3 -- link denied due to comms version mismatch BAD_VERSION = 3 -- link denied due to comms version mismatch
} }
---@enum DEVICE_TYPE ---@enum DEVICE_TYPE device types for establish messages
local DEVICE_TYPE = { local DEVICE_TYPE = { PLC = 0, RTU = 1, SVR = 2, CRD = 3, PKT = 4 }
PLC = 0, -- PLC device type for establish
RTU = 1, -- RTU device type for establish
SV = 2, -- supervisor device type for establish
CRDN = 3, -- coordinator device type for establish
PKT = 4 -- pocket device type for establish
}
---@enum PLC_AUTO_ACK ---@enum PLC_AUTO_ACK
local PLC_AUTO_ACK = { local PLC_AUTO_ACK = {
@ -119,9 +112,8 @@ local UNIT_COMMAND = {
comms.PROTOCOL = PROTOCOL comms.PROTOCOL = PROTOCOL
comms.RPLC_TYPE = RPLC_TYPE comms.RPLC_TYPE = RPLC_TYPE
comms.SCADA_MGMT_TYPE = SCADA_MGMT_TYPE comms.MGMT_TYPE = MGMT_TYPE
comms.SCADA_CRDN_TYPE = SCADA_CRDN_TYPE comms.CRDN_TYPE = CRDN_TYPE
comms.CAPI_TYPE = CAPI_TYPE
comms.ESTABLISH_ACK = ESTABLISH_ACK comms.ESTABLISH_ACK = ESTABLISH_ACK
comms.DEVICE_TYPE = DEVICE_TYPE comms.DEVICE_TYPE = DEVICE_TYPE
@ -134,8 +126,8 @@ comms.FAC_COMMAND = FAC_COMMAND
-- destination broadcast address (to all devices) -- destination broadcast address (to all devices)
comms.BROADCAST = -1 comms.BROADCAST = -1
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet ---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet
---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame ---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame
-- configure the maximum allowable message receive distance<br> -- configure the maximum allowable message receive distance<br>
-- packets received with distances greater than this will be silently discarded -- packets received with distances greater than this will be silently discarded
@ -144,7 +136,7 @@ function comms.set_trusted_range(distance)
if distance == 0 then max_distance = nil else max_distance = distance end if distance == 0 then max_distance = nil else max_distance = distance end
end end
-- generic SCADA packet object -- generic SCADA packet
---@nodiscard ---@nodiscard
function comms.scada_packet() function comms.scada_packet()
local self = { local self = {
@ -199,9 +191,9 @@ function comms.scada_packet()
self.valid = false self.valid = false
self.raw = self.modem_msg_in.msg self.raw = self.modem_msg_in.msg
if (type(max_distance) == "number") and (distance > max_distance) then if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance -- outside of maximum allowable transmission distance
-- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range") -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 5 then if #self.raw == 5 then
@ -227,12 +219,8 @@ function comms.scada_packet()
-- check if this packet is destined for this device -- check if this packet is destined for this device
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID) local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
self.valid = is_destination and self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and
type(self.src_addr) == "number" and type(self.seq_num) == "number" and type(self.protocol) == "number" and type(self.payload) == "table"
type(self.dest_addr) == "number" and
type(self.seq_num) == "number" and
type(self.protocol) == "number" and
type(self.payload) == "table"
end end
end end
@ -275,7 +263,7 @@ function comms.scada_packet()
return public return public
end end
-- authenticated SCADA packet object -- authenticated SCADA packet
---@nodiscard ---@nodiscard
function comms.authd_packet() function comms.authd_packet()
local self = { local self = {
@ -325,7 +313,7 @@ function comms.authd_packet()
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance -- outside of maximum allowable transmission distance
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range") -- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 4 then if #self.raw == 4 then
@ -343,11 +331,8 @@ function comms.authd_packet()
-- check if this packet is destined for this device -- check if this packet is destined for this device
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID) local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
self.valid = is_destination and self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and
type(self.src_addr) == "number" and type(self.mac) == "string" and type(self.payload) == "string"
type(self.dest_addr) == "number" and
type(self.mac) == "string" and
type(self.payload) == "string"
end end
end end
@ -381,8 +366,7 @@ function comms.authd_packet()
return public return public
end end
-- MODBUS packet<br> -- MODBUS packet, modeled after MODBUS TCP
-- modeled after MODBUS TCP packet
---@nodiscard ---@nodiscard
function comms.modbus_packet() function comms.modbus_packet()
local self = { local self = {
@ -413,9 +397,7 @@ function comms.modbus_packet()
-- populate raw array -- populate raw array
self.raw = { self.txn_id, self.unit_id, self.func_code } self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do for i = 1, self.length do insert(self.raw, data[i]) end
insert(self.raw, data[i])
end
else else
log.error("comms.modbus_packet.make(): data not table") log.error("comms.modbus_packet.make(): data not table")
end end
@ -436,9 +418,7 @@ function comms.modbus_packet()
public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) })
end end
local valid = type(self.txn_id) == "number" and local valid = type(self.txn_id) == "number" and type(self.unit_id) == "number" and type(self.func_code) == "number"
type(self.unit_id) == "number" and
type(self.func_code) == "number"
return size_ok and valid return size_ok and valid
else else
@ -489,21 +469,6 @@ function comms.rplc_packet()
---@class rplc_packet ---@class rplc_packet
local public = {} local public = {}
-- check that type is known
local function _rplc_type_valid()
return self.type == RPLC_TYPE.STATUS or
self.type == RPLC_TYPE.MEK_STRUCT or
self.type == RPLC_TYPE.MEK_BURN_RATE or
self.type == RPLC_TYPE.RPS_ENABLE or
self.type == RPLC_TYPE.RPS_SCRAM or
self.type == RPLC_TYPE.RPS_ASCRAM or
self.type == RPLC_TYPE.RPS_STATUS or
self.type == RPLC_TYPE.RPS_ALARM or
self.type == RPLC_TYPE.RPS_RESET or
self.type == RPLC_TYPE.RPS_AUTO_RESET or
self.type == RPLC_TYPE.AUTO_BURN_RATE
end
-- make an RPLC packet -- make an RPLC packet
---@param id integer ---@param id integer
---@param packet_type RPLC_TYPE ---@param packet_type RPLC_TYPE
@ -518,9 +483,7 @@ function comms.rplc_packet()
-- populate raw array -- populate raw array
self.raw = { self.id, self.type } self.raw = { self.id, self.type }
for i = 1, #data do for i = 1, #data do insert(self.raw, data[i]) end
insert(self.raw, data[i])
end
else else
log.error("comms.rplc_packet.make(): data not table") log.error("comms.rplc_packet.make(): data not table")
end end
@ -539,7 +502,6 @@ function comms.rplc_packet()
if ok then if ok then
local data = frame.data() local data = frame.data()
public.make(data[1], data[2], { table.unpack(data, 3, #data) }) public.make(data[1], data[2], { table.unpack(data, 3, #data) })
ok = _rplc_type_valid()
end end
ok = ok and type(self.id) == "number" ok = ok and type(self.id) == "number"
@ -583,7 +545,7 @@ function comms.mgmt_packet()
local self = { local self = {
frame = nil, frame = nil,
raw = {}, raw = {},
type = 0, ---@type SCADA_MGMT_TYPE type = 0, ---@type MGMT_TYPE
length = 0, length = 0,
data = {} data = {}
} }
@ -591,22 +553,8 @@ function comms.mgmt_packet()
---@class mgmt_packet ---@class mgmt_packet
local public = {} local public = {}
-- check that type is known
local function _scada_type_valid()
return self.type == SCADA_MGMT_TYPE.ESTABLISH or
self.type == SCADA_MGMT_TYPE.KEEP_ALIVE or
self.type == SCADA_MGMT_TYPE.CLOSE or
self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or
self.type == SCADA_MGMT_TYPE.RTU_ADVERT or
self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT or
self.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM or
self.type == SCADA_MGMT_TYPE.DIAG_TONE_GET or
self.type == SCADA_MGMT_TYPE.DIAG_TONE_SET or
self.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET
end
-- make a SCADA management packet -- make a SCADA management packet
---@param packet_type SCADA_MGMT_TYPE ---@param packet_type MGMT_TYPE
---@param data table ---@param data table
function public.make(packet_type, data) function public.make(packet_type, data)
if type(data) == "table" then if type(data) == "table" then
@ -617,9 +565,7 @@ function comms.mgmt_packet()
-- populate raw array -- populate raw array
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do for i = 1, #data do insert(self.raw, data[i]) end
insert(self.raw, data[i])
end
else else
log.error("comms.mgmt_packet.make(): data not table") log.error("comms.mgmt_packet.make(): data not table")
end end
@ -638,7 +584,6 @@ function comms.mgmt_packet()
if ok then if ok then
local data = frame.data() local data = frame.data()
public.make(data[1], { table.unpack(data, 2, #data) }) public.make(data[1], { table.unpack(data, 2, #data) })
ok = _scada_type_valid()
end end
return ok return ok
@ -679,7 +624,7 @@ function comms.crdn_packet()
local self = { local self = {
frame = nil, frame = nil,
raw = {}, raw = {},
type = 0, ---@type SCADA_CRDN_TYPE type = 0, ---@type CRDN_TYPE
length = 0, length = 0,
data = {} data = {}
} }
@ -687,20 +632,8 @@ function comms.crdn_packet()
---@class crdn_packet ---@class crdn_packet
local public = {} local public = {}
-- check that type is known
---@nodiscard
local function _crdn_type_valid()
return self.type == SCADA_CRDN_TYPE.INITIAL_BUILDS or
self.type == SCADA_CRDN_TYPE.FAC_BUILDS or
self.type == SCADA_CRDN_TYPE.FAC_STATUS or
self.type == SCADA_CRDN_TYPE.FAC_CMD or
self.type == SCADA_CRDN_TYPE.UNIT_BUILDS or
self.type == SCADA_CRDN_TYPE.UNIT_STATUSES or
self.type == SCADA_CRDN_TYPE.UNIT_CMD
end
-- make a coordinator packet -- make a coordinator packet
---@param packet_type SCADA_CRDN_TYPE ---@param packet_type CRDN_TYPE
---@param data table ---@param data table
function public.make(packet_type, data) function public.make(packet_type, data)
if type(data) == "table" then if type(data) == "table" then
@ -711,9 +644,7 @@ function comms.crdn_packet()
-- populate raw array -- populate raw array
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do for i = 1, #data do insert(self.raw, data[i]) end
insert(self.raw, data[i])
end
else else
log.error("comms.crdn_packet.make(): data not table") log.error("comms.crdn_packet.make(): data not table")
end end
@ -732,7 +663,6 @@ function comms.crdn_packet()
if ok then if ok then
local data = frame.data() local data = frame.data()
public.make(data[1], { table.unpack(data, 2, #data) }) public.make(data[1], { table.unpack(data, 2, #data) })
ok = _crdn_type_valid()
end end
return ok return ok
@ -767,92 +697,4 @@ function comms.crdn_packet()
return public return public
end end
-- coordinator API (CAPI) packet
---@todo implement for pocket access, set enum type for self.type
---@nodiscard
function comms.capi_packet()
local self = {
frame = nil,
raw = {},
type = 0,
length = 0,
data = {}
}
---@class capi_packet
local public = {}
local function _capi_type_valid()
---@todo
return false
end
-- make a coordinator API packet
---@param packet_type CAPI_TYPE
---@param data table
function public.make(packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.capi_packet.make(): data not table")
end
end
-- decode a coordinator API packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
function public.decode(frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOL.COORD_API then
local ok = frame.length() >= 1
if ok then
local data = frame.data()
public.make(data[1], { table.unpack(data, 2, #data) })
ok = _capi_type_valid()
end
return ok
else
log.debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log.debug("nil frame encountered", true)
return false
end
end
-- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get()
---@class capi_frame
local frame = {
scada_frame = self.frame,
type = self.type,
length = self.length,
data = self.data
}
return frame
end
return public
end
return comms return comms

View File

@ -16,7 +16,7 @@ local logger = {
path = "/log.txt", path = "/log.txt",
mode = MODE.APPEND, mode = MODE.APPEND,
debug = false, debug = false,
file = nil, file = nil, ---@type table|nil
dmesg_out = nil, dmesg_out = nil,
dmesg_restore_coord = { 1, 1 }, dmesg_restore_coord = { 1, 1 },
dmesg_scroll_count = 0 dmesg_scroll_count = 0
@ -54,7 +54,7 @@ local function _log(msg)
end end
end end
if out_of_space or (free_space(logger.path) < 100) then if out_of_space or (free_space(logger.path) < 512) then
-- delete the old log file before opening a new one -- delete the old log file before opening a new one
logger.file.close() logger.file.close()
fs.delete(logger.path) fs.delete(logger.path)

View File

@ -270,6 +270,7 @@ types.ALARM_STATE_NAMES = {
---| "mouse_drag" ---| "mouse_drag"
---| "mouse_scroll" ---| "mouse_scroll"
---| "mouse_up" ---| "mouse_up"
---| "double_click" (custom)
---| "paste" ---| "paste"
---| "peripheral" ---| "peripheral"
---| "peripheral_detach" ---| "peripheral_detach"
@ -285,7 +286,7 @@ types.ALARM_STATE_NAMES = {
---| "websocket_failure" ---| "websocket_failure"
---| "websocket_message" ---| "websocket_message"
---| "websocket_success" ---| "websocket_success"
---| "clock_start" custom, added for reactor PLC ---| "clock_start" (custom)
---@alias fluid ---@alias fluid
---| "mekanism:empty_gas" ---| "mekanism:empty_gas"
@ -375,6 +376,7 @@ types.MODBUS_FCODE = {
-- MODBUS exception codes -- MODBUS exception codes
---@enum MODBUS_EXCODE ---@enum MODBUS_EXCODE
types.MODBUS_EXCODE = { types.MODBUS_EXCODE = {
OK = 0x00,
ILLEGAL_FUNCTION = 0x01, ILLEGAL_FUNCTION = 0x01,
ILLEGAL_DATA_ADDR = 0x02, ILLEGAL_DATA_ADDR = 0x02,
ILLEGAL_DATA_VALUE = 0x03, ILLEGAL_DATA_VALUE = 0x03,

View File

@ -8,7 +8,7 @@ local cc_strings = require("cc.strings")
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.0.2" util.version = "1.1.2"
-- ENVIRONMENT CONSTANTS -- -- ENVIRONMENT CONSTANTS --
@ -76,25 +76,12 @@ function util.strval(val)
end end
end end
-- repeat a string n times
---@nodiscard
---@param str string
---@param n integer
---@return string
function util.strrep(str, n)
local repeated = ""
for _ = 1, n do repeated = repeated .. str end
return repeated
end
-- repeat a space n times -- repeat a space n times
---@nodiscard ---@nodiscard
---@param n integer ---@param n integer
---@return string ---@return string
function util.spaces(n) function util.spaces(n)
return util.strrep(" ", n) return string.rep(" ", n)
end end
-- pad text to a minimum width -- pad text to a minimum width

View File

@ -1,6 +1,6 @@
local util = require("scada-common.util") local util = require("scada-common.util")
local BOOTLOADER_VERSION = "0.2" local BOOTLOADER_VERSION = "0.3"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -12,39 +12,26 @@ local exit_code ---@type boolean
println_ts("BOOT> SCANNING FOR APPLICATIONS...") println_ts("BOOT> SCANNING FOR APPLICATIONS...")
if fs.exists("reactor-plc/startup.lua") then if fs.exists("reactor-plc/startup.lua") then
-- found reactor-plc application println("BOOT> FOUND REACTOR PLC CODE: EXEC STARTUP")
println("BOOT> FOUND REACTOR PLC APPLICATION")
println("BOOT> EXEC 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
-- found rtu application println("BOOT> FOUND RTU CODE: EXEC STARTUP")
println("BOOT> FOUND RTU APPLICATION")
println("BOOT> EXEC 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
-- found supervisor application println("BOOT> FOUND SUPERVISOR CODE: EXEC STARTUP")
println("BOOT> FOUND SUPERVISOR APPLICATION")
println("BOOT> EXEC 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
-- found coordinator application println("BOOT> FOUND COORDINATOR CODE: EXEC STARTUP")
println("BOOT> FOUND COORDINATOR APPLICATION")
println("BOOT> EXEC 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
-- found pocket application println("BOOT> FOUND POCKET CODE: EXEC STARTUP")
println("BOOT> FOUND POCKET APPLICATION")
println("BOOT> EXEC STARTUP")
exit_code = shell.execute("pocket/startup") exit_code = shell.execute("pocket/startup")
else else
-- no known applications found println("BOOT> NO SCADA STARTUP FOUND")
println("BOOT> NO SCADA STARTUP APPLICATION FOUND")
println("BOOT> EXIT") println("BOOT> EXIT")
return false return false
end end
if not exit_code then if not exit_code then println_ts("BOOT> APPLICATION CRASHED") end
println_ts("BOOT> APPLICATION CRASHED")
end
return exit_code return exit_code

View File

@ -399,10 +399,9 @@ function facility.new(num_reactors, cooling_conf)
end end
elseif self.mode == PROCESS.INACTIVE then elseif self.mode == PROCESS.INACTIVE then
for i = 1, #self.prio_defs do for i = 1, #self.prio_defs do
-- SCRAM reactors and disengage auto control -- disable reactors and disengage auto control
-- use manual SCRAM since inactive was requested, and automatic SCRAM trips an alarm
for _, u in pairs(self.prio_defs[i]) do for _, u in pairs(self.prio_defs[i]) do
u.scram() u.disable()
u.auto_disengage() u.auto_disengage()
end end
end end

View File

@ -4,6 +4,8 @@
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local style = require("supervisor.panel.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
@ -11,32 +13,35 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local black_lg = style.black_lg
local lg_white = style.lg_white
-- create a pocket diagnostics list entry -- create a pocket diagnostics list entry
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer PDG session ID ---@param id integer PDG session ID
local function init(parent, id) local function init(parent, id)
-- root div -- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg}
local ps_prefix = "pdg_" .. id .. "_" local ps_prefix = "pdg_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=black_lg}
local pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} local pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=black_lg,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=black_lg}
pdg_addr.register(databus.ps, ps_prefix .. "addr", pdg_addr.set_value) pdg_addr.register(databus.ps, ps_prefix .. "addr", pdg_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)} local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=lg_white}
pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value) pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1} TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_white}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_white}
pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update) pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor) pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)

View File

@ -4,6 +4,8 @@
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local style = require("supervisor.panel.style")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
@ -11,36 +13,39 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local black_lg = style.black_lg
local lg_white = style.lg_white
-- create an RTU list entry -- create an RTU list entry
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer RTU session ID ---@param id integer RTU session ID
local function init(parent, id) local function init(parent, id)
-- root div -- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg}
local ps_prefix = "rtu_" .. id .. "_" local ps_prefix = "rtu_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=black_lg}
local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=black_lg,nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=black_lg}
rtu_addr.register(databus.ps, ps_prefix .. "addr", rtu_addr.set_value) rtu_addr.register(databus.ps, ps_prefix .. "addr", rtu_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1} TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1}
local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=cpair(colors.gray,colors.white)} local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=style.gray_white}
unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value) unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
TextBox{parent=entry,x=21,y=2,text="FW:",width=3,height=1} TextBox{parent=entry,x=21,y=2,text="FW:",width=3,height=1}
local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=lg_white}
rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value) rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
TextBox{parent=entry,x=36,y=2,text="RTT:",width=4,height=1} TextBox{parent=entry,x=36,y=2,text="RTT:",width=4,height=1}
local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_white}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_white}
rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update) rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor) rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)

View File

@ -25,14 +25,22 @@ local TabBar = require("graphics.elements.controls.tabbar")
local LED = require("graphics.elements.indicators.led") local LED = require("graphics.elements.indicators.led")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local bw_fg_bg = style.bw_fg_bg
local black_lg = style.black_lg
local lg_white = style.lg_white
local gry_wht = style.gray_white
local ind_grn = style.ind_grn
-- create new front panel view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
local function init(panel) local function init(panel)
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
local page_div = Div{parent=panel,x=1,y=3} local page_div = Div{parent=panel,x=1,y=3}
@ -45,28 +53,28 @@ local function init(panel)
local system = Div{parent=main_page,width=14,height=17,x=2,y=2} local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
on.update(true) on.update(true)
system.line_break() system.line_break()
heartbeat.register(databus.ps, "heartbeat", heartbeat.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} local modem = LED{parent=system,label="MODEM",colors=ind_grn}
system.line_break() system.line_break()
modem.register(databus.ps, "has_modem", modem.update) modem.register(databus.ps, "has_modem", modem.update)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp_label}
-- --
-- about footer -- about footer
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)} local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@ -82,25 +90,25 @@ local function init(panel)
for i = 1, config.NUM_REACTORS do for i = 1, config.NUM_REACTORS do
local ps_prefix = "plc_" .. i .. "_" local ps_prefix = "plc_" .. i .. "_"
local plc_entry = Div{parent=plc_list,height=3,fg_bg=cpair(colors.black,colors.white)} local plc_entry = Div{parent=plc_list,height=3,fg_bg=bw_fg_bg}
TextBox{parent=plc_entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=plc_entry,x=1,y=1,text="",width=8,height=1,fg_bg=black_lg}
TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=ALIGN.CENTER,width=8,height=1,fg_bg=black_lg}
TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=black_lg}
local conn = LED{parent=plc_entry,x=10,y=2,label="LINK",colors=cpair(colors.green,colors.green_off)} local conn = LED{parent=plc_entry,x=10,y=2,label="LINK",colors=ind_grn}
conn.register(databus.ps, ps_prefix .. "conn", conn.update) conn.register(databus.ps, ps_prefix .. "conn", conn.update)
local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=gry_wht}
plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value) plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value)
TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1} TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1}
local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=lg_white}
plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value) plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4,height=1} TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4,height=1}
local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=cpair(colors.lightGray,colors.white)} local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=lg_white}
TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=lg_white}
plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update) plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor) plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
@ -116,22 +124,22 @@ local function init(panel)
-- coordinator page -- coordinator page
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true} local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=cpair(colors.black,colors.white)} local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=bw_fg_bg}
local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green,colors.green_off)} local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=ind_grn}
crd_conn.register(databus.ps, "crd_conn", crd_conn.update) crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,height=1,fg_bg=gry_wht}
local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=gry_wht}
crd_addr.register(databus.ps, "crd_addr", crd_addr.set_value) crd_addr.register(databus.ps, "crd_addr", crd_addr.set_value)
TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1} TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1}
local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=lg_white}
crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value) crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4,height=1} TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4,height=1}
local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_white}
TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=lg_white}
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update) crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor) crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
@ -155,7 +163,7 @@ local function init(panel)
{ name = "PKT", color = cpair(colors.black, colors.ivory) }, { name = "PKT", color = cpair(colors.black, colors.ivory) },
} }
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)} TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=bw_fg_bg}
-- link RTU/PDG list management to PGI -- link RTU/PDG list management to PGI
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry) pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)

Some files were not shown because too many files have changed in this diff Show More