diff --git a/.vscode/settings.json b/.vscode/settings.json index d2812b6..80f7c42 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,8 @@ "parallel", "colors", "textutils", - "shell" + "shell", + "settings", + "window" ] } diff --git a/coordinator/config.lua b/coordinator/config.lua new file mode 100644 index 0000000..57cabf2 --- /dev/null +++ b/coordinator/config.lua @@ -0,0 +1,22 @@ +local config = {} + +-- port of the SCADA supervisor +config.SCADA_SV_PORT = 16100 +-- port to listen to incoming packets from supervisor +config.SCADA_SV_LISTEN = 16101 +-- listen port for SCADA coordinator API access +config.SCADA_API_LISTEN = 16200 +-- expected number of reactor units +config.NUM_UNITS = 4 +-- 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 +-- crypto config +config.SECURE = true +-- must be common between all devices +config.PASSWORD = "testpassword!" + +return config diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index a6bf236..76c9824 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -1,9 +1,141 @@ + local comms = require("scada-common.comms") +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") + +local dialog = require("coordinator.util.dialog") + +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts local coordinator = {} +local function ask_monitor(names) + println("available monitors:") + for i = 1, #names do + print(" " .. names[i]) + end + println("") + println("select a monitor or type c to cancel") + + local iface = dialog.ask_options(names, "c") + + if iface ~= false and iface ~= nil then + util.filter_table(names, function (x) return x ~= iface end) + end + + return iface +end + +function coordinator.configure_monitors(num_units) + ---@class monitors_struct + local monitors = { + primary = nil, + unit_displays = {} + } + + local monitors_avail = ppm.get_monitor_list() + local names = {} + + -- get all interface names + for iface, _ in pairs(monitors_avail) do + table.insert(names, iface) + end + + -- we need a certain number of monitors (1 per unit + 1 primary display) + if #names ~= num_units + 1 then + println("not enough monitors connected (need " .. num_units + 1 .. ")") + log.warning("insufficient monitors present (need " .. num_units + 1 .. ")") + return false + end + + -- attempt to load settings + settings.load("/coord.settings") + + --------------------- + -- PRIMARY DISPLAY -- + --------------------- + + local iface_primary_display = settings.get("PRIMARY_DISPLAY") + + if not util.table_contains(names, iface_primary_display) then + println("primary display is not connected") + local response = dialog.ask_y_n("would you like to change it", true) + if response == false then return false end + iface_primary_display = nil + end + + while iface_primary_display == nil and #names > 0 do + -- lets get a monitor + iface_primary_display = ask_monitor(names) + end + + if iface_primary_display == false then return false end + + settings.set("PRIMARY_DISPLAY", iface_primary_display) + util.filter_table(names, function (x) return x ~= iface_primary_display end) + + monitors.primary = ppm.get_periph(iface_primary_display) + + ------------------- + -- UNIT DISPLAYS -- + ------------------- + + local unit_displays = settings.get("UNIT_DISPLAYS") + + if unit_displays == nil then + unit_displays = {} + for i = 1, num_units do + local display = nil + + while display == nil and #names > 0 do + -- lets get a monitor + println("please select monitor for unit " .. i) + display = ask_monitor(names) + end + + if display == false then return false end + + unit_displays[i] = display + end + else + -- make sure all displays are connected + for i = 1, num_units do +---@diagnostic disable-next-line: need-check-nil + local display = unit_displays[i] + + if not util.table_contains(names, display) then + local response = dialog.ask_y_n("unit display " .. i .. " is not connected, would you like to change it?", true) + if response == false then return false end + display = nil + end + + while display == nil and #names > 0 do + -- lets get a monitor + display = ask_monitor(names) + end + + if display == false then return false end + + unit_displays[i] = display + end + end + + settings.set("UNIT_DISPLAYS", unit_displays) + settings.save("/coord.settings") + + for i = 1, #unit_displays do + monitors.unit_displays[i] = ppm.get_periph(unit_displays[i]) + end + + return true, monitors +end + -- coordinator communications -coordinator.coord_comms = function () +function coordinator.coord_comms() local self = { reactor_struct_cache = nil } diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua new file mode 100644 index 0000000..57553ca --- /dev/null +++ b/coordinator/renderer.lua @@ -0,0 +1,41 @@ +local log = require("scada-common.log") +local util = require("scada-common.util") + +local renderer = {} + +local engine = { + monitors = nil, + dmesg_window = nil +} + +---@param monitors monitors_struct +function renderer.set_displays(monitors) + engine.monitors = monitors +end + +function renderer.reset() + -- reset primary monitor + engine.monitors.primary.setTextScale(0.5) + engine.monitors.primary.setTextColor(colors.white) + engine.monitors.primary.setBackgroundColor(colors.black) + engine.monitors.primary.clear() + engine.monitors.primary.setCursorPos(1, 1) + + -- reset unit displays + for _, monitor in pairs(engine.monitors.unit_displays) do + monitor.setTextScale(0.5) + monitor.setTextColor(colors.white) + monitor.setBackgroundColor(colors.black) + monitor.clear() + monitor.setCursorPos(1, 1) + end +end + +function renderer.init_dmesg() + local disp_x, disp_y = engine.monitors.primary.getSize() + engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y) + + log.direct_dmesg(engine.dmesg_window) +end + +return renderer diff --git a/coordinator/startup.lua b/coordinator/startup.lua index aa65be4..25a6c5e 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -10,6 +10,7 @@ local util = require("scada-common.util") local config = require("coordinator.config") local coordinator = require("coordinator.coordinator") +local renderer = require("coordinator.renderer") local COORDINATOR_VERSION = "alpha-v0.1.2" @@ -18,7 +19,7 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -log.init("/log.txt", log.MODE.APPEND) +log.init(config.LOG_PATH, config.LOG_MODE) log.info("========================================") log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) @@ -28,10 +29,31 @@ println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<") -- mount connected devices ppm.mount_all() -local modem = ppm.get_wireless_modem() - --- we need a modem -if modem == nil then - println("please connect a wireless modem") +-- setup monitors +local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) +if not configured then + println("boot> monitor setup failed") + log.fatal("monitor configuration failed") return end + +log.info("monitors ready, dmesg input incoming...") + +-- init renderer +renderer.set_displays(monitors) +renderer.reset() +renderer.init_dmesg() + +log.dmesg("displays connected and reset", "GRAPHICS", colors.green) +log.dmesg("system start on " .. os.date("%c"), "SYSTEM", colors.cyan) +log.dmesg("starting " .. COORDINATOR_VERSION, "BOOT", colors.blue) + +-- get the communications modem +local modem = ppm.get_wireless_modem() +if modem == nil then + println("boot> wireless modem not found") + log.fatal("no wireless modem on startup") + return +end + +log.dmesg("wireless modem connected", "COMMS", colors.purple) diff --git a/coordinator/util/dialog.lua b/coordinator/util/dialog.lua new file mode 100644 index 0000000..ca9a8fe --- /dev/null +++ b/coordinator/util/dialog.lua @@ -0,0 +1,45 @@ +local completion = require("cc.completion") + +local util = require("scada-common.util") + +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + +local dialog = {} + +function dialog.ask_y_n(question, default) + print(question) + + if default == true then + print(" (Y/n)? ") + else + print(" (y/N)? ") + end + + local response = read(nil, nil) + + if response == "" then + return default + elseif response == "Y" or response == "y" then + return true + elseif response == "N" or response == "n" then + return false + else + return nil + end +end + +function dialog.ask_options(options, cancel) + print("> ") + local response = read(nil, nil, function(text) return completion.choice(text, options) end) + + if response == cancel then return false end + + if util.table_contains(options, response) then + return response + else return nil end +end + +return dialog diff --git a/scada-common/log.lua b/scada-common/log.lua index 2fa0b32..791b11b 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -49,6 +49,12 @@ log.init = function (path, write_mode, dmesg_redirect) end end +-- direct dmesg output to a monitor/window +---@param window table window or terminal reference +log.direct_dmesg = function (window) + _log_sys.dmesg_out = window +end + -- private log write function ---@param msg string local _log = function (msg) @@ -84,9 +90,16 @@ local _log = function (msg) end end --- write a message to the dmesg output ----@param msg string message to write -local _write = function (msg) +-- dmesg style logging for boot because I like linux-y things +---@param msg string message +---@param tag? string log tag +---@param tag_color? integer log tag color +log.dmesg = function (msg, tag, tag_color) + msg = util.strval(msg) + tag = tag or "" + tag = util.strval(tag) + + local t_stamp = string.format("%12.2f", os.clock()) local out = _log_sys.dmesg_out local out_w, out_h = out.getSize() @@ -116,11 +129,43 @@ local _write = function (msg) end end + -- start output with tag and time, assuming we have enough width for this to be on one line + local cur_x, cur_y = out.getCursorPos() + + if cur_x > 1 then + if cur_y == out_h then + out.scroll(1) + out.setCursorPos(1, cur_y) + else + out.setCursorPos(1, cur_y + 1) + end + end + + -- colored time + local initial_color = out.getTextColor() + out.setTextColor(colors.white) + out.write("[") + out.setTextColor(colors.lightGray) + out.write(t_stamp) + out.setTextColor(colors.white) + out.write("] ") + + -- colored tag + if tag ~= "" then + out.write("[") + out.setTextColor(tag_color) + out.write(tag) + out.setTextColor(colors.white) + out.write("] ") + end + + out.setTextColor(initial_color) + -- output message for i = 1, #lines do - local cur_x, cur_y = out.getCursorPos() + cur_x, cur_y = out.getCursorPos() - if cur_x > 1 then + if i > 1 and cur_x > 1 then if cur_y == out_h then out.scroll(1) out.setCursorPos(1, cur_y) @@ -131,15 +176,8 @@ local _write = function (msg) out.write(lines[i]) end -end --- dmesg style logging for boot because I like linux-y things ----@param msg string message ----@param show_term? boolean whether or not to show on terminal output -log.dmesg = function (msg, show_term) - local message = string.format("[%10.3f] ", os.clock()) .. util.strval(msg) - if show_term then _write(message) end - _log(message) + _log("[" .. t_stamp .. "] " .. tag .. " " .. msg) end -- log debug messages diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index b42731b..5b65f47 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -317,8 +317,16 @@ end -- list all connected monitors ---@return table monitors -ppm.list_monitors = function () - return ppm.get_all_devices("monitor") +ppm.get_monitor_list = function () + local list = {} + + for iface, device in pairs(_ppm_sys.mounts) do + if device.type == "monitor" then + list[iface] = device + end + end + + return list end return ppm