From 31a663e86b446b4e08f983b8d03ca7ec1870d751 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 7 Apr 2024 19:37:06 -0400 Subject: [PATCH 1/9] #460 threaded coordinator and moved UI start to render thread --- coordinator/iocontrol.lua | 7 + coordinator/startup.lua | 285 +++++---------------- coordinator/threads.lua | 346 ++++++++++++++++++++++++++ coordinator/ui/layout/front_panel.lua | 8 + 4 files changed, 428 insertions(+), 218 deletions(-) create mode 100644 coordinator/threads.lua diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 0cb50e4..9be448a 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -376,6 +376,13 @@ function iocontrol.fp_monitor_state(id, connected) end end +-- report thread (routine) statuses +---@param thread string thread name +---@param ok boolean thread state +function iocontrol.fp_rt_status(thread, ok) + io.fp.ps.publish(util.c("routine__", thread), ok) +end + -- report PKT firmware version and PKT session connection state ---@param session_id integer PKT session ---@param fw string firmware version diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 152d37b..750dde0 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -7,22 +7,19 @@ require("/initenv").init_env() local comms = require("scada-common.comms") local crash = require("scada-common.crash") local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") local network = require("scada-common.network") local ppm = require("scada-common.ppm") -local tcd = require("scada-common.tcd") local util = require("scada-common.util") -local core = require("graphics.core") - local configure = require("coordinator.configure") local coordinator = require("coordinator.coordinator") local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") +local threads = require("coordinator.threads") -local apisessions = require("coordinator.session.apisessions") - -local COORDINATOR_VERSION = "v1.3.5" +local COORDINATOR_VERSION = "v1.4.0" local CHUNK_LOAD_DELAY_S = 30.0 @@ -133,12 +130,54 @@ local function main() log_sys("system start on " .. os.date("%c")) log_boot("starting " .. COORDINATOR_VERSION) + ---------------------------------------- + -- memory allocation + ---------------------------------------- + + -- shared memory across threads + ---@class crd_shared_memory + local __shared_memory = { + -- time and date format for display + date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y"), + + -- coordinator system state flags + ---@class crd_state + crd_state = { + fp_ok = false, + ui_ok = false, + link_fail = false, + shutdown = false + }, + + -- core coordinator devices + crd_dev = { + speaker = ppm.get_device("speaker"), + modem = ppm.get_wireless_modem() + }, + + -- system objects + crd_sys = { + nic = nil, ---@type nic + coord_comms = nil, ---@type coord_comms + conn_watchdog = nil ---@type watchdog + }, + + -- message queues + q = { + mq_render = mqueue.new() + } + } + + local smem_dev = __shared_memory.crd_dev + local smem_sys = __shared_memory.crd_sys + + local crd_state = __shared_memory.crd_state + ---------------------------------------- -- setup alarm sounder subsystem ---------------------------------------- - local speaker = ppm.get_device("speaker") - if speaker == nil then + if smem_dev.speaker == nil then log_boot("annunciator alarm speaker not found") println("startup> speaker not found") log.fatal("no annunciator alarm speaker found") @@ -146,7 +185,7 @@ local function main() else local sounder_start = util.time_ms() log_boot("annunciator alarm speaker connected") - sounder.init(speaker, config.SpeakerVolume) + sounder.init(smem_dev.speaker, config.SpeakerVolume) log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_sys("annunciator alarm configured") iocontrol.fp_has_speaker(true) @@ -163,8 +202,7 @@ local function main() end -- get the communications modem - local modem = ppm.get_wireless_modem() - if modem == nil then + if smem_dev.modem == nil then log_comms("wireless modem not found") println("startup> wireless modem not found") log.fatal("no wireless modem on startup") @@ -175,243 +213,54 @@ local function main() end -- create connection watchdog - local conn_watchdog = util.new_watchdog(config.SVR_Timeout) - conn_watchdog.cancel() + smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout) + smem_sys.conn_watchdog.cancel() log.debug("startup> conn watchdog created") -- create network interface then setup comms - local nic = network.nic(modem) - local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog) + smem_sys.nic = network.nic(smem_dev.modem) + smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog) log.debug("startup> comms init") log_comms("comms initialized") - -- base loop clock (2Hz, 10 ticks) - local MAIN_CLOCK = 0.5 - local loop_clock = util.new_clock(MAIN_CLOCK) - ---------------------------------------- - -- start front panel & UI start function + -- start front panel ---------------------------------------- log_graphics("starting front panel UI...") - local fp_ok, fp_message = renderer.try_start_fp() - if not fp_ok then + local fp_message + crd_state.fp_ok, fp_message = renderer.try_start_fp() + if not crd_state.fp_ok then log_graphics(util.c("front panel UI error: ", fp_message)) println_ts("front panel UI creation failed") log.fatal(util.c("front panel GUI render failed with error ", fp_message)) return else log_graphics("front panel ready") end - -- start up the main UI - ---@return boolean ui_ok started ok - local function start_main_ui() - log_graphics("starting main UI...") - - local draw_start = util.time_ms() - - local ui_ok, ui_message = renderer.try_start_ui() - if not ui_ok then - log_graphics(util.c("main UI error: ", ui_message)) - log.fatal(util.c("main GUI render failed with error ", ui_message)) - else - log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms") - end - - return ui_ok - end - ---------------------------------------- - -- main event loop + -- start system ---------------------------------------- - local link_failed = false - local ui_ok = true - local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") + -- init threads + local main_thread = threads.thread__main(__shared_memory) + local render_thread = threads.thread__render(__shared_memory) - -- start clock - loop_clock.start() + log.info("startup> completed") - log_sys("system started successfully") - - -- main event loop - while true do - local event, param1, param2, param3, param4, param5 = util.pull_event() - - -- handle event - if event == "peripheral_detach" then - local type, device = ppm.handle_unmount(param1) - - if type ~= nil and device ~= nil then - if type == "modem" then - -- we only really care if this is our wireless modem - -- if it is another modem, handle other peripheral losses separately - if nic.is_modem(device) then - nic.disconnect() - log_sys("comms modem disconnected") - - local other_modem = ppm.get_wireless_modem() - if other_modem then - log_sys("found another wireless modem, using it for comms") - nic.connect(other_modem) - else - -- close out main UI - renderer.close_ui() - - -- alert user to status - log_sys("awaiting comms modem reconnect...") - - iocontrol.fp_has_modem(false) - end - else - log_sys("non-comms modem disconnected") - end - elseif type == "monitor" then - if renderer.handle_disconnect(device) then - log_sys("lost a configured monitor") - else - log_sys("lost an unused monitor") - end - elseif type == "speaker" then - log_sys("lost alarm sounder speaker") - iocontrol.fp_has_speaker(false) - end - end - elseif event == "peripheral" then - local type, device = ppm.mount(param1) - - if type ~= nil and device ~= nil then - if type == "modem" then - if device.isWireless() and not nic.is_connected() then - -- reconnected modem - log_sys("comms modem reconnected") - nic.connect(device) - iocontrol.fp_has_modem(true) - elseif device.isWireless() then - log.info("unused wireless modem reconnected") - else - log_sys("wired modem reconnected") - end - elseif type == "monitor" then - if renderer.handle_reconnect(param1, device) then - log_sys(util.c("configured monitor ", param1, " reconnected")) - else - log_sys(util.c("unused monitor ", param1, " connected")) - end - elseif type == "speaker" then - log_sys("alarm sounder speaker reconnected") - sounder.reconnect(device) - iocontrol.fp_has_speaker(true) - end - end - elseif event == "monitor_resize" then - local is_used, is_ok = renderer.handle_resize(param1) - if is_used then - log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits"))) - end - elseif event == "timer" then - if loop_clock.is_clock(param1) then - -- main loop tick - - -- toggle heartbeat - iocontrol.heartbeat() - - -- maintain connection - if nic.is_connected() then - local ok, start_ui = coord_comms.try_connect() - if not ok then - link_failed = true - log_sys("supervisor connection failed, shutting down...") - log.fatal("failed to connect to supervisor") - break - elseif start_ui then - log_sys("supervisor connected, proceeding to main UI start") - ui_ok = start_main_ui() - if not ui_ok then break end - end - end - - -- iterate sessions - apisessions.iterate_all() - - -- free any closed sessions - apisessions.free_all_closed() - - -- update date and time string for main display - if coord_comms.is_linked() then - iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format)) - end - - loop_clock.start() - elseif conn_watchdog.is_timer(param1) then - -- supervisor watchdog timeout - log_comms("supervisor server timeout") - - -- close connection, main UI, and stop sounder - coord_comms.close() - renderer.close_ui() - sounder.stop() - else - -- a non-clock/main watchdog timer event - - -- check API watchdogs - apisessions.check_all_watchdogs(param1) - - -- notify timer callback dispatcher - tcd.handle(param1) - end - elseif event == "modem_message" then - -- got a packet - local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5) - - -- handle then check if it was a disconnect - if coord_comms.handle_packet(packet) then - log_comms("supervisor closed connection") - - -- close connection, main UI, and stop sounder - coord_comms.close() - renderer.close_ui() - sounder.stop() - end - elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or - event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then - -- handle a mouse event - renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) - elseif event == "speaker_audio_empty" then - -- handle speaker buffer emptied - sounder.continue() - end - - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - -- handle supervisor connection - coord_comms.try_connect(true) - - if coord_comms.is_linked() then - log_comms("terminate requested, closing supervisor connection...") - else link_failed = true end - - coord_comms.close() - log_comms("supervisor connection closed") - - -- handle API sessions - log_comms("closing api sessions...") - apisessions.close_all() - log_comms("api sessions closed") - break - end - end + -- run threads + parallel.waitForAll(main_thread.p_exec, render_thread.p_exec) renderer.close_ui() renderer.close_fp() sounder.stop() log_sys("system shutdown") - if link_failed then println_ts("failed to connect to supervisor") end - if not ui_ok then println_ts("main UI creation failed") end + if crd_state.link_fail then println_ts("failed to connect to supervisor") end + if not crd_state.ui_ok then println_ts("main UI creation failed") end -- close on error exit (such as UI error) - if coord_comms.is_linked() then coord_comms.close() end + if smem_sys.coord_comms.is_linked() then smem_sys.coord_comms.close() end println_ts("exited") log.info("exited") diff --git a/coordinator/threads.lua b/coordinator/threads.lua new file mode 100644 index 0000000..6ec7d18 --- /dev/null +++ b/coordinator/threads.lua @@ -0,0 +1,346 @@ +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") + +local coordinator = require("coordinator.coordinator") +local iocontrol = require("coordinator.iocontrol") +local renderer = require("coordinator.renderer") +local sounder = require("coordinator.sounder") + +local apisessions = require("coordinator.session.apisessions") + +local core = require("graphics.core") + +local log_graphics = coordinator.log_graphics +local log_sys = coordinator.log_sys +local log_comms = coordinator.log_comms + +local threads = {} + +local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) +local RENDER_SLEEP = 100 -- (100ms, 2 ticks) + +local MQ__RENDER_CMD = { + START_MAIN_UI = 1, + MON_DISCONNECT = 2, + MON_CONNECT = 3, + MON_RESIZE = 4, + UPDATE = 5 +} + +-- main thread +---@nodiscard +---@param smem crd_shared_memory +function threads.thread__main(smem) + ---@class parallel_thread + local public = {} + + -- execute thread + function public.exec() + iocontrol.fp_rt_status("main", true) + log.debug("main thread start") + + local loop_clock = util.new_clock(MAIN_CLOCK) + + -- start clock + loop_clock.start() + + log_sys("system started successfully") + + -- load in from shared memory + local crd_state = smem.crd_state + local nic = smem.crd_sys.nic + local coord_comms = smem.crd_sys.coord_comms + local conn_watchdog = smem.crd_sys.conn_watchdog + + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = util.pull_event() + + -- handle event + if event == "peripheral_detach" then + local type, device = ppm.handle_unmount(param1) + + if type ~= nil and device ~= nil then + if type == "modem" then + -- we only really care if this is our wireless modem + -- if it is another modem, handle other peripheral losses separately + if nic.is_modem(device) then + nic.disconnect() + log_sys("comms modem disconnected") + + local other_modem = ppm.get_wireless_modem() + if other_modem then + log_sys("found another wireless modem, using it for comms") + nic.connect(other_modem) + else + -- close out main UI + renderer.close_ui() + + -- alert user to status + log_sys("awaiting comms modem reconnect...") + + iocontrol.fp_has_modem(false) + end + else + log_sys("non-comms modem disconnected") + end + elseif type == "monitor" then + if renderer.handle_disconnect(device) then + log_sys("lost a configured monitor") + else + log_sys("lost an unused monitor") + end + elseif type == "speaker" then + log_sys("lost alarm sounder speaker") + iocontrol.fp_has_speaker(false) + end + end + elseif event == "peripheral" then + local type, device = ppm.mount(param1) + + if type ~= nil and device ~= nil then + if type == "modem" then + if device.isWireless() and not nic.is_connected() then + -- reconnected modem + log_sys("comms modem reconnected") + nic.connect(device) + iocontrol.fp_has_modem(true) + elseif device.isWireless() then + log.info("unused wireless modem reconnected") + else + log_sys("wired modem reconnected") + end + elseif type == "monitor" then + if renderer.handle_reconnect(param1, device) then + log_sys(util.c("configured monitor ", param1, " reconnected")) + else + log_sys(util.c("unused monitor ", param1, " connected")) + end + elseif type == "speaker" then + log_sys("alarm sounder speaker reconnected") + sounder.reconnect(device) + iocontrol.fp_has_speaker(true) + end + end + elseif event == "monitor_resize" then + local is_used, is_ok = renderer.handle_resize(param1) + if is_used then + log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits"))) + end + elseif event == "timer" then + if loop_clock.is_clock(param1) then + -- main loop tick + + -- toggle heartbeat + iocontrol.heartbeat() + + -- maintain connection + if nic.is_connected() then + local ok, start_ui = coord_comms.try_connect() + if not ok then + crd_state.link_fail = true + log_sys("supervisor connection failed, shutting down...") + log.fatal("failed to connect to supervisor") + break + elseif start_ui then + log_sys("supervisor connected, dispatching main UI start") + smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI) + --@todo if not ui_ok then break end + end + end + + -- iterate sessions + apisessions.iterate_all() + + -- free any closed sessions + apisessions.free_all_closed() + + -- update date and time string for main display + if coord_comms.is_linked() then + iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format)) + end + + loop_clock.start() + elseif conn_watchdog.is_timer(param1) then + -- supervisor watchdog timeout + log_comms("supervisor server timeout") + + -- close connection, main UI, and stop sounder + coord_comms.close() + renderer.close_ui() + sounder.stop() + else + -- a non-clock/main watchdog timer event + + -- check API watchdogs + apisessions.check_all_watchdogs(param1) + + -- notify timer callback dispatcher + tcd.handle(param1) + end + elseif event == "modem_message" then + -- got a packet + local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5) + + -- handle then check if it was a disconnect + if coord_comms.handle_packet(packet) then + log_comms("supervisor closed connection") + + -- close connection, main UI, and stop sounder + coord_comms.close() + renderer.close_ui() + sounder.stop() + end + elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or + event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then + -- handle a mouse event + renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) + elseif event == "speaker_audio_empty" then + -- handle speaker buffer emptied + sounder.continue() + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + crd_state.shutdown = true + log.info("terminate requested, main thread exiting") + + -- handle closing supervisor connection + coord_comms.try_connect(true) + + if coord_comms.is_linked() then + log_comms("terminate requested, closing supervisor connection...") + else crd_state.link_fail = true end + + coord_comms.close() + log_comms("supervisor connection closed") + + -- handle API sessions + log_comms("closing api sessions...") + apisessions.close_all() + log_comms("api sessions closed") + break + end + end + end + + -- execute the thread in a protected mode, retrying it on return if not shutting down + function public.p_exec() + local crd_state = smem.crd_state + + while not crd_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(util.strval(result)) + end + + iocontrol.fp_rt_status("main", false) + + -- if status is true, then we are probably exiting, so this won't matter + -- if not, we need to restart the clock + -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) + if not crd_state.shutdown then + log.info("main thread restarting now...") + end + end + end + + return public +end + +-- coordinator renderer thread +---@nodiscard +---@param smem crd_shared_memory +function threads.thread__render(smem) + ---@class parallel_thread + local public = {} + + -- execute thread + function public.exec() + iocontrol.fp_rt_status("render", true) + log.debug("render thread start") + + -- load in from shared memory + local crd_state = smem.crd_state + local render_queue = smem.q.mq_render + + local last_update = util.time() + + -- thread loop + while true do + -- check for messages in the message queue + while render_queue.ready() and not crd_state.shutdown do + local msg = render_queue.pop() + + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if msg.message == MQ__RENDER_CMD.START_MAIN_UI then + -- start up the main UI + log_graphics("starting main UI...") + + local draw_start = util.time_ms() + + local ui_message + crd_state.ui_ok, ui_message = renderer.try_start_ui() + if not crd_state.ui_ok then + log_graphics(util.c("main UI error: ", ui_message)) + log.fatal(util.c("main GUI render failed with error ", ui_message)) + else + log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms") + end + elseif msg.message == MQ__RENDER_CMD.MON_DISCONNECT then + -- monitor lost + elseif msg.message == MQ__RENDER_CMD.MON_CONNECT then + -- monitor connected + elseif msg.message == MQ__RENDER_CMD.UPDATE then + -- new data + end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + end + end + + -- quick yield + util.nop() + end + + -- check for termination request + if crd_state.shutdown then + log.info("render thread exiting") + break + end + + -- delay before next check + last_update = util.adaptive_delay(RENDER_SLEEP, last_update) + end + end + + -- execute the thread in a protected mode, retrying it on return if not shutting down + function public.p_exec() + local crd_state = smem.crd_state + + while not crd_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(util.strval(result)) + end + + iocontrol.fp_rt_status("render", false) + + if not crd_state.shutdown then + log.info("render thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public +end + +return threads diff --git a/coordinator/ui/layout/front_panel.lua b/coordinator/ui/layout/front_panel.lua index 7124083..7f2738a 100644 --- a/coordinator/ui/layout/front_panel.lua +++ b/coordinator/ui/layout/front_panel.lua @@ -100,6 +100,14 @@ local function init(panel, num_units) local speaker = LED{parent=system,label="SPEAKER",colors=led_grn} speaker.register(ps, "has_speaker", speaker.update) + system.line_break() + + local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn} + local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn} + + rt_main.register(ps, "routine__main", rt_main.update) + rt_render.register(ps, "routine__render", rt_render.update) + ---@diagnostic disable-next-line: undefined-field local comp_id = util.sprintf("(%d)", os.getComputerID()) TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.disabled_fg} From 92a4277f730b556b990af27f191f21bf5af32a97 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 7 Apr 2024 19:54:22 -0400 Subject: [PATCH 2/9] #460 moved connect/disconnect/resize to render thread --- coordinator/threads.lua | 52 ++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/coordinator/threads.lua b/coordinator/threads.lua index 6ec7d18..255241e 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -24,8 +24,8 @@ local RENDER_SLEEP = 100 -- (100ms, 2 ticks) local MQ__RENDER_CMD = { START_MAIN_UI = 1, - MON_DISCONNECT = 2, - MON_CONNECT = 3, + MON_CONNECT = 2, + MON_DISCONNECT = 3, MON_RESIZE = 4, UPDATE = 5 } @@ -51,8 +51,8 @@ function threads.thread__main(smem) -- load in from shared memory local crd_state = smem.crd_state - local nic = smem.crd_sys.nic - local coord_comms = smem.crd_sys.coord_comms + local nic = smem.crd_sys.nic + local coord_comms = smem.crd_sys.coord_comms local conn_watchdog = smem.crd_sys.conn_watchdog -- event loop @@ -88,11 +88,7 @@ function threads.thread__main(smem) log_sys("non-comms modem disconnected") end elseif type == "monitor" then - if renderer.handle_disconnect(device) then - log_sys("lost a configured monitor") - else - log_sys("lost an unused monitor") - end + smem.q.mq_render.push_data(MQ__RENDER_CMD.MON_DISCONNECT, device) elseif type == "speaker" then log_sys("lost alarm sounder speaker") iocontrol.fp_has_speaker(false) @@ -114,11 +110,7 @@ function threads.thread__main(smem) log_sys("wired modem reconnected") end elseif type == "monitor" then - if renderer.handle_reconnect(param1, device) then - log_sys(util.c("configured monitor ", param1, " reconnected")) - else - log_sys(util.c("unused monitor ", param1, " connected")) - end + smem.q.mq_render.push_data(MQ__RENDER_CMD.MON_CONNECT, { name = param1, device = device }) elseif type == "speaker" then log_sys("alarm sounder speaker reconnected") sounder.reconnect(device) @@ -126,10 +118,7 @@ function threads.thread__main(smem) end end elseif event == "monitor_resize" then - local is_used, is_ok = renderer.handle_resize(param1) - if is_used then - log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits"))) - end + smem.q.mq_render.push_data(MQ__RENDER_CMD.MON_RESIZE, param1) elseif event == "timer" then if loop_clock.is_clock(param1) then -- main loop tick @@ -292,15 +281,34 @@ function threads.thread__render(smem) else log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms") end - elseif msg.message == MQ__RENDER_CMD.MON_DISCONNECT then - -- monitor lost - elseif msg.message == MQ__RENDER_CMD.MON_CONNECT then - -- monitor connected elseif msg.message == MQ__RENDER_CMD.UPDATE then -- new data end elseif msg.qtype == mqueue.TYPE.DATA then -- received data + local cmd = msg.message ---@type queue_data + + if cmd.key == MQ__RENDER_CMD.MON_CONNECT then + -- monitor connected + if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then + log_sys(util.c("configured monitor ", cmd.val.name, " reconnected")) + else + log_sys(util.c("unused monitor ", cmd.val.name, " connected")) + end + elseif cmd.key == MQ__RENDER_CMD.MON_DISCONNECT then + -- monitor disconnected + if renderer.handle_disconnect(cmd.val) then + log_sys("lost a configured monitor") + else + log_sys("lost an unused monitor") + end + elseif cmd.key == MQ__RENDER_CMD.MON_RESIZE then + -- monitor resized + local is_used, is_ok = renderer.handle_resize(cmd.val) + if is_used then + log_sys(util.c("configured monitor ", cmd.val, " resized, ", util.trinary(is_ok, "display fits", "display does not fit"))) + end + end elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet end From 1504247b33270992fceefd8439e182b2dab9b4c0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 7 Apr 2024 20:21:57 -0400 Subject: [PATCH 3/9] #460 added yields throughout main UI rendering --- coordinator/renderer.lua | 4 ++++ coordinator/ui/components/unit_detail.lua | 2 ++ coordinator/ui/layout/flow_view.lua | 3 +++ coordinator/ui/layout/main_view.lua | 7 +++++++ 4 files changed, 16 insertions(+) diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index d9e4b58..0e34ee1 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -3,6 +3,7 @@ -- local log = require("scada-common.log") +local util = require("scada-common.util") local iocontrol = require("coordinator.iocontrol") @@ -195,18 +196,21 @@ function renderer.try_start_ui() 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) diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 2264b00..fe36a85 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -352,6 +352,8 @@ local function init(parent, id) t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update) end + util.nop() + ---------------------- -- reactor controls -- ---------------------- diff --git a/coordinator/ui/layout/flow_view.lua b/coordinator/ui/layout/flow_view.lua index 98d16e9..06edf27 100644 --- a/coordinator/ui/layout/flow_view.lua +++ b/coordinator/ui/layout/flow_view.lua @@ -250,6 +250,7 @@ local function init(main) local y_offset = y_ofs(i) unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i]) table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true)) + util.nop() end PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=style.theme.bg} @@ -335,6 +336,8 @@ local function init(main) end end + util.nop() + --------- -- SPS -- --------- diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index f31c200..cba1747 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -3,6 +3,7 @@ -- local iocontrol = require("coordinator.iocontrol") +local util = require("scada-common.util") local style = require("coordinator.ui.style") @@ -53,6 +54,8 @@ local function init(main) cnc_y_start = cnc_y_start + row_1_height + 1 + util.nop() + if facility.num_units >= 3 then -- base offset 3, spacing 1, max height of units 1 and 2 local row_2_offset = cnc_y_start @@ -64,6 +67,8 @@ local function init(main) uo_4 = unit_overview(main, 84, row_2_offset, units[4]) cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1) end + + util.nop() end -- command & control @@ -79,6 +84,8 @@ local function init(main) process_ctl(main, 2, cnc_bottom_align_start) + util.nop() + imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1]) end From eab1ffbe039bd3960b90174d6bf80f4ec4cbcd25 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 7 Apr 2024 20:24:27 -0400 Subject: [PATCH 4/9] #460 cleanup and moved date and time update to be behind UI ready rather than linked --- coordinator/threads.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/coordinator/threads.lua b/coordinator/threads.lua index 255241e..7c849b5 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -141,14 +141,12 @@ function threads.thread__main(smem) end end - -- iterate sessions + -- iterate sessions and free any closed sessions apisessions.iterate_all() - - -- free any closed sessions apisessions.free_all_closed() - -- update date and time string for main display - if coord_comms.is_linked() then + if renderer.ui_ready() then + -- update clock used on main and flow monitors iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format)) end @@ -253,7 +251,7 @@ function threads.thread__render(smem) log.debug("render thread start") -- load in from shared memory - local crd_state = smem.crd_state + local crd_state = smem.crd_state local render_queue = smem.q.mq_render local last_update = util.time() From 45573be8c76aa6de63ec0bf7c922ed3dd478657d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 7 Apr 2024 20:47:31 -0400 Subject: [PATCH 5/9] #460 renderer logging --- coordinator/coordinator.lua | 4 ++-- coordinator/renderer.lua | 46 ++++++++++++++++++++++--------------- coordinator/startup.lua | 10 ++++---- coordinator/threads.lua | 8 +++---- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index a8d6e5e..40d71d8 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -192,7 +192,7 @@ end ---@return function? update, function? done local function log_dmesg(message, dmesg_tag, working) local colors = { - GRAPHICS = colors.green, + RENDER = colors.green, SYSTEM = colors.cyan, BOOT = colors.blue, COMMS = colors.purple, @@ -206,7 +206,7 @@ local function log_dmesg(message, dmesg_tag, working) end end -function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end +function coordinator.log_render(message) log_dmesg(message, "RENDER") end function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end function coordinator.log_boot(message) log_dmesg(message, "BOOT") end function coordinator.log_comms(message) log_dmesg(message, "COMMS") end diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index 0e34ee1..6920845 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -2,23 +2,26 @@ -- Graphics Rendering Control -- -local log = require("scada-common.log") -local util = require("scada-common.util") +local log = require("scada-common.log") +local util = require("scada-common.util") -local iocontrol = require("coordinator.iocontrol") +local coordinator = require("coordinator.coordinator") +local iocontrol = require("coordinator.iocontrol") -local style = require("coordinator.ui.style") -local pgi = require("coordinator.ui.pgi") +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 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 core = require("graphics.core") +local flasher = require("graphics.flasher") -local DisplayBox = require("graphics.elements.displaybox") +local DisplayBox = require("graphics.elements.displaybox") + +local log_render = coordinator.log_render ---@class coord_renderer local renderer = {} @@ -387,12 +390,15 @@ function renderer.handle_resize(name) 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 not ok then + 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 @@ -420,14 +426,15 @@ function renderer.handle_resize(name) iocontrol.fp_monitor_state("flow", true) if engine.ui_ready then - engine.dmesg_window.setVisible(false) - + 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 not ok then + 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 @@ -457,14 +464,15 @@ function renderer.handle_resize(name) iocontrol.fp_monitor_state(idx, true) if engine.ui_ready then - engine.dmesg_window.setVisible(false) - + 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 not ok then + 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 diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 750dde0..2ed9fdb 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -26,7 +26,7 @@ local CHUNK_LOAD_DELAY_S = 30.0 local println = util.println local println_ts = util.println_ts -local log_graphics = coordinator.log_graphics +local log_render = coordinator.log_render local log_sys = coordinator.log_sys local log_boot = coordinator.log_boot local log_comms = coordinator.log_comms @@ -126,7 +126,7 @@ local function main() -- lets get started! log.info("monitors ready, dmesg output incoming...") - log_graphics("displays connected and reset") + log_render("displays connected and reset") log_sys("system start on " .. os.date("%c")) log_boot("starting " .. COORDINATOR_VERSION) @@ -227,16 +227,16 @@ local function main() -- start front panel ---------------------------------------- - log_graphics("starting front panel UI...") + log_render("starting front panel UI...") local fp_message crd_state.fp_ok, fp_message = renderer.try_start_fp() if not crd_state.fp_ok then - log_graphics(util.c("front panel UI error: ", fp_message)) + log_render(util.c("front panel UI error: ", fp_message)) println_ts("front panel UI creation failed") log.fatal(util.c("front panel GUI render failed with error ", fp_message)) return - else log_graphics("front panel ready") end + else log_render("front panel ready") end ---------------------------------------- -- start system diff --git a/coordinator/threads.lua b/coordinator/threads.lua index 7c849b5..7a433f5 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -13,7 +13,7 @@ local apisessions = require("coordinator.session.apisessions") local core = require("graphics.core") -local log_graphics = coordinator.log_graphics +local log_render = coordinator.log_render local log_sys = coordinator.log_sys local log_comms = coordinator.log_comms @@ -267,17 +267,17 @@ function threads.thread__render(smem) -- received a command if msg.message == MQ__RENDER_CMD.START_MAIN_UI then -- start up the main UI - log_graphics("starting main UI...") + log_render("starting main UI...") local draw_start = util.time_ms() local ui_message crd_state.ui_ok, ui_message = renderer.try_start_ui() if not crd_state.ui_ok then - log_graphics(util.c("main UI error: ", ui_message)) + log_render(util.c("main UI error: ", ui_message)) log.fatal(util.c("main GUI render failed with error ", ui_message)) else - log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms") + log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms") end elseif msg.message == MQ__RENDER_CMD.UPDATE then -- new data From f734c4307b11145c35ec34d1304284e26a992015 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 7 Apr 2024 20:55:07 -0400 Subject: [PATCH 6/9] #460 exit on UI crash --- coordinator/startup.lua | 2 +- coordinator/threads.lua | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 2ed9fdb..8b42414 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -144,7 +144,7 @@ local function main() ---@class crd_state crd_state = { fp_ok = false, - ui_ok = false, + ui_ok = true, -- default true, used to abort on fail link_fail = false, shutdown = false }, diff --git a/coordinator/threads.lua b/coordinator/threads.lua index 7a433f5..8fac12b 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -190,16 +190,21 @@ function threads.thread__main(smem) sounder.continue() end - -- check for termination request + -- check for termination request or UI crash if event == "terminate" or ppm.should_terminate() then crd_state.shutdown = true log.info("terminate requested, main thread exiting") + elseif not crd_state.ui_ok then + crd_state.shutdown = true + log.info("terminating due to fatal UI error") + end + if crd_state.shutdown then -- handle closing supervisor connection coord_comms.try_connect(true) if coord_comms.is_linked() then - log_comms("terminate requested, closing supervisor connection...") + log_comms("closing supervisor connection...") else crd_state.link_fail = true end coord_comms.close() From cc50e4a0201c56a584fb011dafb6b1db11598e53 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 7 Apr 2024 22:33:48 -0400 Subject: [PATCH 7/9] #460 removed coordinator connecting grace period --- supervisor/session/coordinator.lua | 13 +------------ supervisor/startup.lua | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 98edaca..c0e6482 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -17,9 +17,6 @@ local FAC_COMMAND = comms.FAC_COMMAND local SV_Q_DATA = svqtypes.SV_Q_DATA --- grace period in seconds for coordinator to finish UI draw to prevent timeout -local WATCHDOG_GRACE = 20.0 - -- retry time constants in ms -- local INITIAL_WAIT = 1500 local RETRY_PERIOD = 1000 @@ -360,15 +357,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil -- check if a timer matches this session's watchdog ---@nodiscard function public.check_wd(timer) - local is_wd = self.conn_watchdog.is_timer(timer) and self.connected - - -- if we are waiting for initial coordinator UI draw, don't close yet - if is_wd and (util.time_s() - self.establish_time) <= WATCHDOG_GRACE then - self.conn_watchdog.feed() - is_wd = false - end - - return is_wd + return self.conn_watchdog.is_timer(timer) and self.connected end -- close the connection diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 806f7b0..b63b399 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.4" +local SUPERVISOR_VERSION = "v1.3.5" local println = util.println local println_ts = util.println_ts From 98c37caecd70847048b934de8e4da88c83c20d31 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 8 Apr 2024 23:59:16 -0400 Subject: [PATCH 8/9] #460 cleanup --- coordinator/startup.lua | 8 ++++---- coordinator/threads.lua | 31 ++++++++++++++--------------- coordinator/ui/layout/main_view.lua | 3 ++- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 8b42414..236fa79 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -23,13 +23,13 @@ local COORDINATOR_VERSION = "v1.4.0" local CHUNK_LOAD_DELAY_S = 30.0 -local println = util.println +local println = util.println local println_ts = util.println_ts local log_render = coordinator.log_render -local log_sys = coordinator.log_sys -local log_boot = coordinator.log_boot -local log_comms = coordinator.log_comms +local log_sys = coordinator.log_sys +local log_boot = coordinator.log_boot +local log_comms = coordinator.log_comms local log_crypto = coordinator.log_crypto ---------------------------------------- diff --git a/coordinator/threads.lua b/coordinator/threads.lua index 8fac12b..d684fc0 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -14,8 +14,8 @@ local apisessions = require("coordinator.session.apisessions") local core = require("graphics.core") local log_render = coordinator.log_render -local log_sys = coordinator.log_sys -local log_comms = coordinator.log_comms +local log_sys = coordinator.log_sys +local log_comms = coordinator.log_comms local threads = {} @@ -23,11 +23,13 @@ local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local RENDER_SLEEP = 100 -- (100ms, 2 ticks) local MQ__RENDER_CMD = { - START_MAIN_UI = 1, - MON_CONNECT = 2, - MON_DISCONNECT = 3, - MON_RESIZE = 4, - UPDATE = 5 + START_MAIN_UI = 1 +} + +local MQ__RENDER_DATA = { + MON_CONNECT = 1, + MON_DISCONNECT = 2, + MON_RESIZE = 3 } -- main thread @@ -88,7 +90,7 @@ function threads.thread__main(smem) log_sys("non-comms modem disconnected") end elseif type == "monitor" then - smem.q.mq_render.push_data(MQ__RENDER_CMD.MON_DISCONNECT, device) + smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device) elseif type == "speaker" then log_sys("lost alarm sounder speaker") iocontrol.fp_has_speaker(false) @@ -110,7 +112,7 @@ function threads.thread__main(smem) log_sys("wired modem reconnected") end elseif type == "monitor" then - smem.q.mq_render.push_data(MQ__RENDER_CMD.MON_CONNECT, { name = param1, device = device }) + smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device }) elseif type == "speaker" then log_sys("alarm sounder speaker reconnected") sounder.reconnect(device) @@ -118,7 +120,7 @@ function threads.thread__main(smem) end end elseif event == "monitor_resize" then - smem.q.mq_render.push_data(MQ__RENDER_CMD.MON_RESIZE, param1) + smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1) elseif event == "timer" then if loop_clock.is_clock(param1) then -- main loop tick @@ -137,7 +139,6 @@ function threads.thread__main(smem) elseif start_ui then log_sys("supervisor connected, dispatching main UI start") smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI) - --@todo if not ui_ok then break end end end @@ -284,28 +285,26 @@ function threads.thread__render(smem) else log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms") end - elseif msg.message == MQ__RENDER_CMD.UPDATE then - -- new data end elseif msg.qtype == mqueue.TYPE.DATA then -- received data local cmd = msg.message ---@type queue_data - if cmd.key == MQ__RENDER_CMD.MON_CONNECT then + if cmd.key == MQ__RENDER_DATA.MON_CONNECT then -- monitor connected if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then log_sys(util.c("configured monitor ", cmd.val.name, " reconnected")) else log_sys(util.c("unused monitor ", cmd.val.name, " connected")) end - elseif cmd.key == MQ__RENDER_CMD.MON_DISCONNECT then + elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then -- monitor disconnected if renderer.handle_disconnect(cmd.val) then log_sys("lost a configured monitor") else log_sys("lost an unused monitor") end - elseif cmd.key == MQ__RENDER_CMD.MON_RESIZE then + elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then -- monitor resized local is_used, is_ok = renderer.handle_resize(cmd.val) if is_used then diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index cba1747..5343139 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -2,9 +2,10 @@ -- Main SCADA Coordinator GUI -- -local iocontrol = require("coordinator.iocontrol") local util = require("scada-common.util") +local iocontrol = require("coordinator.iocontrol") + local style = require("coordinator.ui.style") local imatrix = require("coordinator.ui.components.imatrix") From cc3174ee769e196553a2996b3c140996990773fc Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 9 Apr 2024 00:08:47 -0400 Subject: [PATCH 9/9] comments and whitespace --- coordinator/threads.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/coordinator/threads.lua b/coordinator/threads.lua index d684fc0..3b04b5b 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -142,7 +142,7 @@ function threads.thread__main(smem) end end - -- iterate sessions and free any closed sessions + -- iterate sessions and free any closed ones apisessions.iterate_all() apisessions.free_all_closed() @@ -183,7 +183,7 @@ function threads.thread__main(smem) sounder.stop() end elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or - event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then + event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) elseif event == "speaker_audio_empty" then @@ -233,8 +233,7 @@ function threads.thread__main(smem) iocontrol.fp_rt_status("main", false) -- if status is true, then we are probably exiting, so this won't matter - -- if not, we need to restart the clock - -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) + -- this thread cannot be slept because it will miss events (namely "terminate") if not crd_state.shutdown then log.info("main thread restarting now...") end @@ -244,7 +243,7 @@ function threads.thread__main(smem) return public end --- coordinator renderer thread +-- coordinator renderer thread, tasked with long duration re-draws ---@nodiscard ---@param smem crd_shared_memory function threads.thread__render(smem)