From e851a5275f5e5a6a12e9197fc8dc53f31a23b756 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 13 Jun 2024 16:45:44 +0000 Subject: [PATCH] #496 pocket threading --- coordinator/threads.lua | 2 +- pocket/startup.lua | 154 ++++++++++++---------------- pocket/threads.lua | 216 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 281 insertions(+), 91 deletions(-) create mode 100644 pocket/threads.lua diff --git a/coordinator/threads.lua b/coordinator/threads.lua index e7c2d8c..20ba7ae 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -244,7 +244,7 @@ function threads.thread__main(smem) return public end --- coordinator renderer thread, tasked with long duration re-draws +-- coordinator renderer thread, tasked with long duration draws ---@nodiscard ---@param smem crd_shared_memory function threads.thread__render(smem) diff --git a/pocket/startup.lua b/pocket/startup.lua index 335bdc1..0c08061 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -6,19 +6,18 @@ require("/initenv").init_env() 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("pocket.configure") local iocontrol = require("pocket.iocontrol") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") +local threads = require("pocket.threads") -local POCKET_VERSION = "v0.9.2-alpha" +local POCKET_VERSION = "v0.10.0-alpha" local println = util.println local println_ts = util.println_ts @@ -71,6 +70,44 @@ local function main() -- record version for GUI iocontrol.get_db().version = POCKET_VERSION + ---------------------------------------- + -- memory allocation + ---------------------------------------- + + -- shared memory across threads + ---@class pkt_shared_memory + local __shared_memory = { + -- pocket system state flags + ---@class pkt_state + pkt_state = { + ui_ok = false, + shutdown = false + }, + + -- core pocket devices + pkt_dev = { + modem = ppm.get_wireless_modem() + }, + + -- system objects + pkt_sys = { + nic = nil, ---@type nic + pocket_comms = nil, ---@type pocket_comms + sv_wd = nil, ---@type watchdog + api_wd = nil ---@type watchdog + }, + + -- message queues + q = { + mq_render = mqueue.new() + } + } + + local smem_dev = __shared_memory.pkt_dev + local smem_sys = __shared_memory.pkt_sys + + local pkt_state = __shared_memory.pkt_state + ---------------------------------------- -- setup communications & clocks ---------------------------------------- @@ -83,118 +120,55 @@ local function main() iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) -- get the communications modem - local modem = ppm.get_wireless_modem() - if modem == nil then + if smem_dev.modem == nil then println("startup> wireless modem not found: please craft the pocket computer with a wireless modem") log.fatal("startup> no wireless modem on startup") return end -- create connection watchdogs - local conn_wd = { - sv = util.new_watchdog(config.ConnTimeout), - api = util.new_watchdog(config.ConnTimeout) - } - - conn_wd.sv.cancel() - conn_wd.api.cancel() - + smem_sys.sv_wd = util.new_watchdog(config.ConnTimeout) + smem_sys.sv_wd.cancel() + smem_sys.api_wd = util.new_watchdog(config.ConnTimeout) + smem_sys.api_wd.cancel() log.debug("startup> conn watchdogs created") -- create network interface then setup comms - local nic = network.nic(modem) - local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api) + smem_sys.nic = network.nic(smem_dev.modem) + smem_sys.pocket_comms = pocket.comms(POCKET_VERSION, smem_sys.nic, smem_sys.sv_wd, smem_sys.api_wd) log.debug("startup> comms init") - -- base loop clock (2Hz, 10 ticks) - local MAIN_CLOCK = 0.5 - local loop_clock = util.new_clock(MAIN_CLOCK) - -- init I/O control - iocontrol.init_core(pocket_comms) + iocontrol.init_core(smem_sys.pocket_comms) ---------------------------------------- -- start the UI ---------------------------------------- - local ui_ok, message = renderer.try_start_ui() - if not ui_ok then - println(util.c("UI error: ", message)) - log.error(util.c("startup> GUI render failed with error ", message)) - else - -- start clock - loop_clock.start() + local ui_message + pkt_state.ui_ok, ui_message = renderer.try_start_ui() + if not pkt_state.ui_ok then + println(util.c("UI error: ", ui_message)) + log.error(util.c("startup> GUI render failed with error ", ui_message)) end ---------------------------------------- - -- main event loop + -- start system ---------------------------------------- - if ui_ok then - -- start connection watchdogs - conn_wd.sv.feed() - conn_wd.api.feed() - log.debug("startup> conn watchdogs started") + if pkt_state.ui_ok then + -- init threads + local main_thread = threads.thread__main(__shared_memory) + local render_thread = threads.thread__render(__shared_memory) - local io_db = iocontrol.get_db() - local nav = io_db.nav + log.info("startup> completed") - -- main event loop - while true do - local event, param1, param2, param3, param4, param5 = util.pull_event() - - -- handle event - if event == "timer" then - if loop_clock.is_clock(param1) then - -- main loop tick - - -- relink if necessary - pocket_comms.link_update() - - -- update any tasks for the active page - local page_tasks = nav.get_current_page().tasks - for i = 1, #page_tasks do page_tasks[i]() end - - loop_clock.start() - elseif conn_wd.sv.is_timer(param1) then - -- supervisor watchdog timeout - log.info("supervisor server timeout") - pocket_comms.close_sv() - elseif conn_wd.api.is_timer(param1) then - -- coordinator watchdog timeout - log.info("coordinator api server timeout") - pocket_comms.close_api() - else - -- a non-clock/main watchdog timer event - -- notify timer callback dispatcher - tcd.handle(param1) - end - elseif event == "modem_message" then - -- got a packet - local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5) - pocket_comms.handle_packet(packet) - 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 - renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) - elseif event == "char" or event == "key" or event == "key_up" then - -- handle a keyboard event - renderer.handle_key(core.events.new_key_event(event, param1, param2)) - elseif event == "paste" then - -- handle a paste event - renderer.handle_paste(param1) - end - - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - log.info("terminate requested, closing server connections...") - pocket_comms.close() - log.info("connections closed") - break - end - end + -- run threads + parallel.waitForAll(main_thread.p_exec, render_thread.p_exec) renderer.close_ui() + else + println_ts("UI creation failed") end println_ts("exited") diff --git a/pocket/threads.lua b/pocket/threads.lua new file mode 100644 index 0000000..22a4576 --- /dev/null +++ b/pocket/threads.lua @@ -0,0 +1,216 @@ +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 iocontrol = require("pocket.iocontrol") +local renderer = require("pocket.renderer") + +local core = require("graphics.core") + +local threads = {} + +local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) +local RENDER_SLEEP = 100 -- (100ms, 2 ticks) + +local MQ__RENDER_CMD = { + UNLOAD_SV_APPS = 1, + UNLOAD_API_APPS = 2 +} + +local MQ__RENDER_DATA = { + LOAD_APP = 1 +} + +-- main thread +---@nodiscard +---@param smem pkt_shared_memory +function threads.thread__main(smem) + ---@class parallel_thread + local public = {} + + -- execute thread + function public.exec() + log.debug("main thread start") + + local loop_clock = util.new_clock(MAIN_CLOCK) + + -- start clock + loop_clock.start() + + -- load in from shared memory + local pkt_state = smem.pkt_state + local pocket_comms = smem.pkt_sys.pocket_comms + local sv_wd = smem.pkt_sys.sv_wd + local api_wd = smem.pkt_sys.api_wd + + -- start connection watchdogs + sv_wd.feed() + api_wd.feed() + log.debug("startup> conn watchdogs started") + + local io_db = iocontrol.get_db() + local nav = io_db.nav + + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = util.pull_event() + + -- handle event + if event == "timer" then + if loop_clock.is_clock(param1) then + -- main loop tick + + -- relink if necessary + pocket_comms.link_update() + + -- update any tasks for the active page + local page_tasks = nav.get_current_page().tasks + for i = 1, #page_tasks do page_tasks[i]() end + + loop_clock.start() + elseif sv_wd.is_timer(param1) then + -- supervisor watchdog timeout + log.info("supervisor server timeout") + pocket_comms.close_sv() + elseif api_wd.is_timer(param1) then + -- coordinator watchdog timeout + log.info("coordinator api server timeout") + pocket_comms.close_api() + else + -- a non-clock/main watchdog timer event + -- notify timer callback dispatcher + tcd.handle(param1) + end + elseif event == "modem_message" then + -- got a packet + local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5) + pocket_comms.handle_packet(packet) + 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 + renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) + elseif event == "char" or event == "key" or event == "key_up" then + -- handle a keyboard event + renderer.handle_key(core.events.new_key_event(event, param1, param2)) + elseif event == "paste" then + -- handle a paste event + renderer.handle_paste(param1) + end + + -- check for termination request or UI crash + if event == "terminate" or ppm.should_terminate() then + log.info("terminate requested, main thread exiting") + pkt_state.shutdown = true + elseif not pkt_state.ui_ok then + pkt_state.shutdown = true + log.info("terminating due to fatal UI error") + end + + if pkt_state.shutdown then + log.info("closing server connections...") + pocket_comms.close() + log.info("connections 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 pkt_state = smem.pkt_state + + while not pkt_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(util.strval(result)) + end + + -- if status is true, then we are probably exiting, so this won't matter + -- this thread cannot be slept because it will miss events (namely "terminate") + if not pkt_state.shutdown then + log.info("main thread restarting now...") + end + end + end + + return public +end + +-- pocket renderer thread, tasked with long duration draws +---@nodiscard +---@param smem pkt_shared_memory +function threads.thread__render(smem) + ---@class parallel_thread + local public = {} + + -- execute thread + function public.exec() + log.debug("render thread start") + + -- load in from shared memory + local pkt_state = smem.pkt_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 pkt_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.UNLOAD_SV_APPS then + elseif msg.message == MQ__RENDER_CMD.UNLOAD_API_APPS then + end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + local cmd = msg.message ---@type queue_data + + if cmd.key == MQ__RENDER_DATA.LOAD_APP then + end + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + end + end + + -- quick yield + util.nop() + end + + -- check for termination request + if pkt_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 pkt_state = smem.pkt_state + + while not pkt_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(util.strval(result)) + end + + if not pkt_state.shutdown then + log.info("render thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public +end + +return threads