mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
525 lines
16 KiB
Lua
525 lines
16 KiB
Lua
--
|
|
-- Graphics Rendering Control
|
|
--
|
|
|
|
local log = require("scada-common.log")
|
|
local util = require("scada-common.util")
|
|
|
|
local coordinator = require("coordinator.coordinator")
|
|
local iocontrol = require("coordinator.iocontrol")
|
|
|
|
local style = require("coordinator.ui.style")
|
|
local pgi = require("coordinator.ui.pgi")
|
|
|
|
local flow_view = require("coordinator.ui.layout.flow_view")
|
|
local panel_view = require("coordinator.ui.layout.front_panel")
|
|
local main_view = require("coordinator.ui.layout.main_view")
|
|
local unit_view = require("coordinator.ui.layout.unit_view")
|
|
|
|
local core = require("graphics.core")
|
|
local flasher = require("graphics.flasher")
|
|
|
|
local DisplayBox = require("graphics.elements.displaybox")
|
|
|
|
local log_render = coordinator.log_render
|
|
|
|
---@class coord_renderer
|
|
local renderer = {}
|
|
|
|
-- render engine
|
|
local engine = {
|
|
color_mode = 1, ---@type COLOR_MODE
|
|
monitors = nil, ---@type monitors_struct|nil
|
|
dmesg_window = nil, ---@type table|nil
|
|
ui_ready = false,
|
|
fp_ready = false,
|
|
ui = {
|
|
front_panel = nil, ---@type graphics_element|nil
|
|
main_display = nil, ---@type graphics_element|nil
|
|
flow_display = nil, ---@type graphics_element|nil
|
|
unit_displays = {}
|
|
},
|
|
disable_flow_view = false
|
|
}
|
|
|
|
-- init a display to the "default", but set text scale to 0.5
|
|
---@param monitor table monitor
|
|
local function _init_display(monitor)
|
|
monitor.setTextScale(0.5)
|
|
monitor.setTextColor(colors.white)
|
|
monitor.setBackgroundColor(colors.black)
|
|
monitor.clear()
|
|
monitor.setCursorPos(1, 1)
|
|
|
|
-- set overridden colors
|
|
for i = 1, #style.theme.colors do
|
|
monitor.setPaletteColor(style.theme.colors[i].c, style.theme.colors[i].hex)
|
|
end
|
|
|
|
-- apply color mode
|
|
local c_mode_overrides = style.theme.color_modes[engine.color_mode]
|
|
for i = 1, #c_mode_overrides do
|
|
monitor.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
|
end
|
|
end
|
|
|
|
-- print out that the monitor is too small
|
|
---@param monitor table monitor
|
|
local function _print_too_small(monitor)
|
|
monitor.setCursorPos(1, 1)
|
|
monitor.setBackgroundColor(colors.black)
|
|
monitor.setTextColor(colors.red)
|
|
monitor.clear()
|
|
monitor.write("monitor too small")
|
|
end
|
|
|
|
-- apply renderer configurations
|
|
---@param config crd_config
|
|
function renderer.configure(config)
|
|
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
|
|
|
|
engine.color_mode = config.ColorMode
|
|
engine.disable_flow_view = config.DisableFlowView
|
|
end
|
|
|
|
-- link to the monitor peripherals
|
|
---@param monitors monitors_struct
|
|
function renderer.set_displays(monitors)
|
|
engine.monitors = monitors
|
|
|
|
-- report to front panel as connected
|
|
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
|
|
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
|
|
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
|
|
end
|
|
|
|
-- init all displays in use by the renderer
|
|
function renderer.init_displays()
|
|
-- init main and flow monitors
|
|
_init_display(engine.monitors.main)
|
|
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
|
|
|
|
-- init unit displays
|
|
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
|
_init_display(monitor)
|
|
end
|
|
|
|
-- init terminal
|
|
term.setTextColor(colors.white)
|
|
term.setBackgroundColor(colors.black)
|
|
term.clear()
|
|
term.setCursorPos(1, 1)
|
|
|
|
-- set overridden colors
|
|
for i = 1, #style.fp_theme.colors do
|
|
term.setPaletteColor(style.fp_theme.colors[i].c, style.fp_theme.colors[i].hex)
|
|
end
|
|
|
|
-- apply color mode
|
|
local c_mode_overrides = style.fp_theme.color_modes[engine.color_mode]
|
|
for i = 1, #c_mode_overrides do
|
|
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
|
end
|
|
end
|
|
|
|
-- initialize the dmesg output window
|
|
function renderer.init_dmesg()
|
|
local disp_w, disp_h = engine.monitors.main.getSize()
|
|
engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h)
|
|
log.direct_dmesg(engine.dmesg_window)
|
|
end
|
|
|
|
-- try to start the front panel
|
|
---@return boolean success, any error_msg
|
|
function renderer.try_start_fp()
|
|
local status, msg = true, nil
|
|
|
|
if not engine.fp_ready then
|
|
-- show front panel view on terminal
|
|
status, msg = pcall(function ()
|
|
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
|
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
|
end)
|
|
|
|
if status then
|
|
-- start flasher callback task and report ready
|
|
flasher.run()
|
|
engine.fp_ready = true
|
|
else
|
|
-- report fail and close front panel
|
|
msg = core.extract_assert_msg(msg)
|
|
renderer.close_fp()
|
|
end
|
|
end
|
|
|
|
return status, msg
|
|
end
|
|
|
|
-- close out the front panel
|
|
function renderer.close_fp()
|
|
if engine.fp_ready then
|
|
if not engine.ui_ready then
|
|
-- stop blinking indicators
|
|
flasher.clear()
|
|
end
|
|
|
|
-- disable PGI
|
|
pgi.unlink()
|
|
|
|
-- hide to stop animation callbacks and clear root UI elements
|
|
engine.ui.front_panel.hide()
|
|
engine.ui.front_panel = nil
|
|
engine.fp_ready = false
|
|
|
|
-- restore colors
|
|
for i = 1, #style.fp_theme.colors do
|
|
local r, g, b = term.nativePaletteColor(style.fp_theme.colors[i].c)
|
|
term.setPaletteColor(style.fp_theme.colors[i].c, r, g, b)
|
|
end
|
|
|
|
-- reset terminal
|
|
term.setTextColor(colors.white)
|
|
term.setBackgroundColor(colors.black)
|
|
term.clear()
|
|
term.setCursorPos(1, 1)
|
|
end
|
|
end
|
|
|
|
-- try to start the main GUI
|
|
---@return boolean success, any error_msg
|
|
function renderer.try_start_ui()
|
|
local status, msg = true, nil
|
|
|
|
if not engine.ui_ready then
|
|
-- hide dmesg
|
|
engine.dmesg_window.setVisible(false)
|
|
|
|
status, msg = pcall(function ()
|
|
-- show main view on main monitor
|
|
if engine.monitors.main ~= nil then
|
|
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
|
|
main_view(engine.ui.main_display)
|
|
util.nop()
|
|
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)
|
|
util.nop()
|
|
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)
|
|
util.nop()
|
|
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
|
|
|
|
return status, msg
|
|
end
|
|
|
|
-- close out the UI
|
|
function renderer.close_ui()
|
|
if not engine.fp_ready then
|
|
-- stop blinking indicators
|
|
flasher.clear()
|
|
end
|
|
|
|
-- delete element trees
|
|
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
|
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
|
|
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
|
|
|
|
-- report ui as not ready
|
|
engine.ui_ready = false
|
|
|
|
-- clear root UI elements
|
|
engine.ui.main_display = nil
|
|
engine.ui.flow_display = nil
|
|
engine.ui.unit_displays = {}
|
|
|
|
-- clear unit monitors
|
|
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
|
|
|
|
if not engine.disable_flow_view then
|
|
-- clear flow monitor
|
|
engine.monitors.flow.clear()
|
|
end
|
|
|
|
-- re-draw dmesg
|
|
engine.dmesg_window.setVisible(true)
|
|
engine.dmesg_window.redraw()
|
|
end
|
|
|
|
-- is the front panel ready?
|
|
---@nodiscard
|
|
---@return boolean ready
|
|
function renderer.fp_ready() return engine.fp_ready end
|
|
|
|
-- is the UI ready?
|
|
---@nodiscard
|
|
---@return boolean ready
|
|
function renderer.ui_ready() return engine.ui_ready end
|
|
|
|
-- handle a monitor peripheral being disconnected
|
|
---@param device table monitor
|
|
---@return boolean is_used if the monitor is one of the configured monitors
|
|
function renderer.handle_disconnect(device)
|
|
local is_used = false
|
|
|
|
if not engine.monitors then return false end
|
|
|
|
if engine.monitors.main == device then
|
|
if engine.ui.main_display ~= nil then
|
|
-- delete element tree and clear root UI elements
|
|
engine.ui.main_display.delete()
|
|
end
|
|
|
|
is_used = true
|
|
engine.monitors.main = nil
|
|
engine.ui.main_display = nil
|
|
|
|
iocontrol.fp_monitor_state("main", false)
|
|
elseif engine.monitors.flow == device then
|
|
if engine.ui.flow_display ~= nil then
|
|
-- delete element tree and clear root UI elements
|
|
engine.ui.flow_display.delete()
|
|
end
|
|
|
|
is_used = true
|
|
engine.monitors.flow = nil
|
|
engine.ui.flow_display = nil
|
|
|
|
iocontrol.fp_monitor_state("flow", false)
|
|
else
|
|
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
|
if monitor == device then
|
|
if engine.ui.unit_displays[idx] ~= nil then
|
|
engine.ui.unit_displays[idx].delete()
|
|
end
|
|
|
|
is_used = true
|
|
engine.monitors.unit_displays[idx] = nil
|
|
engine.ui.unit_displays[idx] = nil
|
|
|
|
iocontrol.fp_monitor_state(idx, false)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return is_used
|
|
end
|
|
|
|
-- handle a monitor peripheral being reconnected
|
|
---@param name string monitor name
|
|
---@param device table monitor
|
|
---@return boolean is_used if the monitor is one of the configured monitors
|
|
function renderer.handle_reconnect(name, device)
|
|
local is_used = false
|
|
|
|
if not engine.monitors then return false end
|
|
|
|
-- note: handle_resize is a more adaptive way of re-initializing a connected monitor
|
|
-- since it can handle a monitor being reconnected that isn't the right size
|
|
|
|
if engine.monitors.main_name == name then
|
|
is_used = true
|
|
engine.monitors.main = device
|
|
|
|
renderer.handle_resize(name)
|
|
elseif engine.monitors.flow_name == name then
|
|
is_used = true
|
|
engine.monitors.flow = device
|
|
|
|
renderer.handle_resize(name)
|
|
else
|
|
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
|
if monitor == name then
|
|
is_used = true
|
|
engine.monitors.unit_displays[idx] = device
|
|
|
|
renderer.handle_resize(name)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return is_used
|
|
end
|
|
|
|
-- handle a monitor being resized<br>
|
|
-- returns if this monitor is assigned + if the assigned screen still fits
|
|
---@param name string monitor name
|
|
---@return boolean is_used, boolean is_ok
|
|
function renderer.handle_resize(name)
|
|
local is_used = false
|
|
local is_ok = true
|
|
local ui = engine.ui
|
|
|
|
if not engine.monitors then return false, false end
|
|
|
|
if engine.monitors.main_name == name and engine.monitors.main then
|
|
local device = engine.monitors.main ---@type table
|
|
|
|
-- this is necessary if the bottom left block was broken and on reconnect
|
|
_init_display(device)
|
|
|
|
is_used = true
|
|
|
|
-- resize dmesg window if needed, but don't make it thinner
|
|
local disp_w, disp_h = engine.monitors.main.getSize()
|
|
local dmsg_w, _ = engine.dmesg_window.getSize()
|
|
engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main)
|
|
|
|
if ui.main_display then
|
|
ui.main_display.delete()
|
|
ui.main_display = nil
|
|
end
|
|
|
|
iocontrol.fp_monitor_state("main", true)
|
|
|
|
engine.dmesg_window.setVisible(not engine.ui_ready)
|
|
|
|
if engine.ui_ready then
|
|
local draw_start = util.time_ms()
|
|
local ok = pcall(function ()
|
|
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
|
main_view(ui.main_display)
|
|
end)
|
|
|
|
if ok then
|
|
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
|
else
|
|
if ui.main_display then
|
|
ui.main_display.delete()
|
|
ui.main_display = nil
|
|
end
|
|
|
|
_print_too_small(device)
|
|
|
|
iocontrol.fp_monitor_state("main", false)
|
|
is_ok = false
|
|
end
|
|
else engine.dmesg_window.redraw() end
|
|
elseif engine.monitors.flow_name == name and engine.monitors.flow then
|
|
local device = engine.monitors.flow ---@type table
|
|
|
|
-- this is necessary if the bottom left block was broken and on reconnect
|
|
_init_display(device)
|
|
|
|
is_used = true
|
|
|
|
if ui.flow_display then
|
|
ui.flow_display.delete()
|
|
ui.flow_display = nil
|
|
end
|
|
|
|
iocontrol.fp_monitor_state("flow", true)
|
|
|
|
if engine.ui_ready then
|
|
local draw_start = util.time_ms()
|
|
local ok = pcall(function ()
|
|
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
|
|
flow_view(ui.flow_display)
|
|
end)
|
|
|
|
if ok then
|
|
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
|
else
|
|
if ui.flow_display then
|
|
ui.flow_display.delete()
|
|
ui.flow_display = nil
|
|
end
|
|
|
|
_print_too_small(device)
|
|
|
|
iocontrol.fp_monitor_state("flow", false)
|
|
is_ok = false
|
|
end
|
|
end
|
|
else
|
|
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
|
local device = engine.monitors.unit_displays[idx]
|
|
|
|
if monitor == name and device then
|
|
-- this is necessary if the bottom left block was broken and on reconnect
|
|
_init_display(device)
|
|
|
|
is_used = true
|
|
|
|
if ui.unit_displays[idx] then
|
|
ui.unit_displays[idx].delete()
|
|
ui.unit_displays[idx] = nil
|
|
end
|
|
|
|
iocontrol.fp_monitor_state(idx, true)
|
|
|
|
if engine.ui_ready then
|
|
local draw_start = util.time_ms()
|
|
local ok = pcall(function ()
|
|
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
|
unit_view(ui.unit_displays[idx], idx)
|
|
end)
|
|
|
|
if ok then
|
|
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
|
else
|
|
if ui.unit_displays[idx] then
|
|
ui.unit_displays[idx].delete()
|
|
ui.unit_displays[idx] = nil
|
|
end
|
|
|
|
_print_too_small(device)
|
|
|
|
iocontrol.fp_monitor_state(idx, false)
|
|
is_ok = false
|
|
end
|
|
end
|
|
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return is_used, is_ok
|
|
end
|
|
|
|
-- handle a touch event
|
|
---@param event mouse_interaction|nil
|
|
function renderer.handle_mouse(event)
|
|
if event ~= nil then
|
|
if engine.fp_ready and event.monitor == "terminal" then
|
|
engine.ui.front_panel.handle_mouse(event)
|
|
elseif engine.ui_ready then
|
|
if event.monitor == engine.monitors.main_name then
|
|
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
|
|
elseif event.monitor == engine.monitors.flow_name then
|
|
if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
|
|
else
|
|
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
|
local display = engine.ui.unit_displays[id]
|
|
if event.monitor == monitor and display then
|
|
if display then display.handle_mouse(event) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return renderer
|