From f6feab83ea0d101e1c28b097eb8d0d6e917e7c95 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 7 Jun 2024 17:25:08 -0300 Subject: [PATCH 01/49] feat: rework plugins to new format Signed-off-by: Matheus T. dos Santos --- plugins/new_plugin.lua | 30 +++++++++++ plugins/scope.lua | 114 +++++++++++++++++++++++------------------ plugins/west.lua | 52 ++++++++++--------- 3 files changed, 122 insertions(+), 74 deletions(-) create mode 100644 plugins/new_plugin.lua diff --git a/plugins/new_plugin.lua b/plugins/new_plugin.lua new file mode 100644 index 0000000..26241aa --- /dev/null +++ b/plugins/new_plugin.lua @@ -0,0 +1,30 @@ +local scope = require("scope") + +local M = scope.plugin.new({"event.serial", "cmd"}) + +M.event.serial.rx = function (msg) + +end + +M.event.serial.tx = function (msg) + +end + +M.event.serial.connect = function (port, baudrate) + +end + +M.event.serial.disconnect = function () + +end + +M.cmd.hello = function (arg1, arg2) + +end + +M.cmd.world = function () + +end + +return M + diff --git a/plugins/scope.lua b/plugins/scope.lua index 10b9eff..25a9a6e 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -1,61 +1,73 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by matheuswhite. ---- DateTime: 23/01/24 18:42 ---- +local Shell = require("shell.Shell") -function bytes2str(bytes) - msg_str = '' +local scope = {} - for _, byte in pairs(bytes) do - msg_str = msg_str .. string.char(byte) - end +scope.bytes.to_str = function (bytes) + local str = "" - return msg_str -end + for _, byte in pairs(bytes) do + str = str .. string.char(byte) + end -function str2bytes(str) - bytes = {} - for _, c in utf8.codes(str) do - table.insert(bytes, c) - end - return bytes + return str end -function osname() - if os.getenv('OS') == 'Windows_NT' then - return "win" - else - return "unix" - end + +scope.string.to_bytes = function (str) + local bytes = {} + + for _, c in utf8.codes(str) do + table.insert(bytes, c) + end + + return bytes end -scope = { - println = function(msg) - coroutine.yield({ ":println", msg }) - end, - eprintln = function(msg) - coroutine.yield({ ":eprintln", msg }) - end, - connect = function(port, baud_rate) - coroutine.yield({ ":connect", port, baud_rate }) - end, - disconnect = function() - coroutine.yield({ ":disconnect" }) - end, - serial_tx = function(msg) - coroutine.yield({ ":serial_tx", msg }) - end, - sleep = function(time) - coroutine.yield({ ":sleep", time }) - end, - exec = function(cmd, quiet) - quiet = quiet or false - local _, stdout, stderr = coroutine.yield({ ":exec", cmd, quiet }) - return stdout, stderr - end, - info = function() - local _, output = coroutine.yield({ ":info" }) - return output +scope.log = { + dbg = function (msg) + coroutine.yield({":log.dbg", msg}) + end, + inf = function (msg) + coroutine.yield({":log.inf", msg}) + end, + wrn = function (msg) + coroutine.yield({":log.wrn", msg}) + end, + err = function (msg) + coroutine.yield({":log.err", msg}) + end, +} + +scope.serial = { + info = function () + local _, port, baud_rate = coroutine.yield({":serial.info"}) + return port, baud_rate + end, + send = function (msg) + coroutine.yield({":serial.send", msg}) + end, + recv = function (timeout_ms) + local _, err, msg = coroutine.yield({":serial.recv", timeout_ms}) + return err, msg + end, +} + +scope.sys = { + os = function () + if os.getenv("OS") == "Windows_NT" then + return "windows" + else + return "unix" end + end, + sleep = function (time_ms) + coroutine.yield({":sys.sleep", time_ms}) + end, + shell = function () + local _, id = coroutine.yield({":sys.shell"}) + return Shell:new(id) + end, } + +return scope + diff --git a/plugins/west.lua b/plugins/west.lua index 3e37505..9b06454 100644 --- a/plugins/west.lua +++ b/plugins/west.lua @@ -1,30 +1,36 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by matheuswhite. ---- DateTime: 26/02/24 21:00 ---- +local scope = require("scope") -require "scope" +local check_zephyr_base = function (sh) + local stdout, _ = sh:run("echo $ZEPHYR_BASE") + return stdout and stdout ~= "" and stdout ~= "\n" +end + +local west = {} + +west.load = function () + west.shell = scope.sys.shell() + + local res = check_zephyr_base(west.shell) + if not res then + scope.log.err("$ZEPHYR_BASE is empty") + end -function check_zephyr_base() - stdout, stderr = scope.exec('echo $ZEPHYR_BASE') - return stdout ~= nil and stdout ~= '' and stdout ~= '\n' + west.shell:run("source $ZEPHYR_BASE/../.venv/bin/activate") + + return res end -function serial_rx(msg) +west.action.build = function () + west.shell:run("west build") end -function user_command(arg_list) - if not check_zephyr_base() then - scope.eprintln('$ZEPHYR_BASE is empty') - return - end - - local cmd = 'west ' .. table.concat(arg_list, ' ') - - if osname() == 'unix' then - cmd = 'source $ZEPHYR_BASE/../.venv/bin/activate && ' .. cmd - end - - scope.exec(cmd) +west.action.flash = function () + local info = scope.serial.info() + + scope.serial.disconnect() + west.shell:run("west flash") + scope.serial.connect(info.port, info.baudrate) end + +return west + From 7c97a7797797efa86c1bf8e741226efb98494af1 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Mon, 10 Jun 2024 09:38:07 -0300 Subject: [PATCH 02/49] docs: stabilize plugin APIs and document it Signed-off-by: Matheus T. dos Santos --- plugins/api.md | 233 +++++++++++++++++++++++++++++++++++++++++ plugins/auto_test.lua | 32 ++++++ plugins/echo.lua | 51 +++++---- plugins/idf.lua | 58 +++++----- plugins/new_plugin.lua | 30 ------ plugins/peripheral.lua | 66 ++++++++++++ plugins/scope.lua | 104 ++++++++++-------- plugins/serial.lua | 21 ---- plugins/shell.lua | 19 ++++ plugins/test.lua | 33 ------ plugins/west.lua | 33 +++--- 11 files changed, 482 insertions(+), 198 deletions(-) create mode 100644 plugins/api.md create mode 100644 plugins/auto_test.lua delete mode 100644 plugins/new_plugin.lua create mode 100644 plugins/peripheral.lua delete mode 100644 plugins/serial.lua create mode 100644 plugins/shell.lua delete mode 100644 plugins/test.lua diff --git a/plugins/api.md b/plugins/api.md new file mode 100644 index 0000000..15d5d6d --- /dev/null +++ b/plugins/api.md @@ -0,0 +1,233 @@ +# Plugin API + +### `on_load` + +```lua +function M.on_load() + -- code ... +end +``` + +## Serial + +### `on_send` + +```lua +--- Event function called when a message is sent via serial. +--- +--- @param msg string The message sent via serial. +function M.serial.on_send(msg) + -- code ... +end +``` + +The message transmission occurs before the call of the `on_send` event function. +It'll call this function again, only if the current call finish. +So, be careful when using blocking functions, such as `sys.sleep` and `serial.recv`, +inside a event function. + +```lua +--- @type { pattern: string, evt_fn: function}[] +M.serial.on_send = { + { + -- Sample pattern + pattern = "AT\r", + evt_fn = function (msg) + end + } + { + -- Any message + pattern = ".*", + evt_fn = function (msg) + end + }, +} +``` + +The on_send could also be a list of event functions. Each event function must has a pattern. +So, a event function is called, when the sent message matches the pattern. The patterns is tested +in order it appers on the array. The messages sent to serial ends with `\n`. So the pattern, must +not contains `\n` in the end. + +### `on_recv` + +```lua +--- Event function called when a message is received from serial +--- +--- @param msg string The message received from serial +function on_recv(msg) + -- code ... +end +``` + +```lua +--- @type { pattern: string, evt_fn: function}[] +M.serial.on_recv = { + { + -- Sample pattern + pattern = "OK\r", + evt_fn = function (msg) + end + } + { + -- Any message + pattern = ".*", + evt_fn = function (msg) + end + }, +} +``` + + +### `on_connect` + +```lua +--- Event function called when Scope connects to a serial port +--- +--- @param port string Name of the serial port +--- @param baudrate integer Baudrate of the serial port +function on_connect(port, baudrate) + -- code ... +end +``` + +```lua +--- @type { pattern: string, evt_fn: function}[] +M.serial.on_connect = { + { + -- Connected on Unix + port = "/dev/tty.*", + baudrate = ".*", + evt_fn = function (port, baudrate) + end + } + { + -- Connected on Windows + port = "COM[1-9][0-9]*", + baudrate = ".*", + evt_fn = function (msg) + end + }, +} +``` + + +### `on_disconnect` + +```lua +--- Event function called when Scope disconnect from a serial port +--- +function on_disconnect() + -- code ... +end +``` + +## BLE + +```lua +local M = { + ble = { + on_connect = function (uuid) end + on_disconnect = function (uuid) end + on_read = function (serv, char, val) end + on_write = function (serv, char, val) end + on_write_without_rsp = function (serv, char, val) end + on_notify = function(serv, char, val) end + on_indicate = function(serv, char, val) end + }, +} +``` + + +## User Commands + +```lua +local M = { + cmd = { + = function (arg_1, arg_2, ...) + -- code here + end + -- Examples: +-- !plugin bye + -- bye = function() + -- log.inf("Good bye!") + -- end + -- + -- !plugin world "Matheus" 28 + -- greetings = function(name, age) + -- log.inf("Hello, " .. name) + -- log.inf("Are you " .. tostring(age) .. " years old?") + -- end + }, +} +``` + + +# Scope API + +## Plugin + +```lua +--- @return +function scope.plugin.new() +end +``` + +## Bytes + +```lua +--- @param bytes integer[] +--- @return string +function scope.bytes.to_str(bytes) +end +``` + +## String + +```lua +--- @param str string +--- @return integer[] +function scope.str.to_bytes(str) +end +``` + +## Log + +```lua +--- @param msg string +function scope.log.dbg(msg) +end +``` + +## Serial + +```lua +--- @return {port: string, baudrate: integer} +function scope.serial.info() +end + +--- @param msg string +function scope.serial.send(msg) +end + +--- @param opts {timeout_ms: integer} +--- @return {err: integer, msg: string} +function scope.serial.recv(opts) +end +``` + +## System + +```lua +--- @return "windows" | "unix" +function scope.sys.os_name() +end + +--- @param time integer +function scope.sys.sleep_ms(time) +end +``` + +# Shell API + + diff --git a/plugins/auto_test.lua b/plugins/auto_test.lua new file mode 100644 index 0000000..860b14f --- /dev/null +++ b/plugins/auto_test.lua @@ -0,0 +1,32 @@ +local plugin = require("scope").plugin +local log = require("scope").log +local serial = require("scope").serial + +local M = plugin.new() + +M.serial.on_recv = function (msg) + log.dbg('Rx evt: ' .. msg) +end + +M.serial.on_send = function (msg) + log.wrn('Tx evt: ' .. msg) +end + +M.cmd.test1 = function (apn) + serial.send("AT+CREG=1," .. apn .. ",0\r\n") + local err, rsp = serial.recv({timeout = 200}) + + if err ~= 0 then + log.err("[ERR] Test1 Timeout") + return + end + + if rsp == "OK\r\n" then + log.inf("[ OK] Test1 Success") + else + log.err("[ERR] Test1 Fail") + end +end + +return M + diff --git a/plugins/echo.lua b/plugins/echo.lua index 58b2aea..e90ea7b 100644 --- a/plugins/echo.lua +++ b/plugins/echo.lua @@ -1,30 +1,35 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by matheuswhite. ---- DateTime: 24/01/24 09:45 ---- +local log = require("scope").log +local serial = require("scope").serial +local plugin = require("scope").plugin -require "scope" +local M = plugin.new() -function serial_rx(msg) - msg_str = bytes2str(msg) - - if msg_str ~= "AT\r\n" then - return +M.serial.on_recv = { + { + "AT\r", + function (_) + log.inf("Sending msg \"OK\" via serial tx...") + serial.send("\r\nOK\r\n") + log.inf("Message sent!") end + }, + { + "AT+COPS?\r", + function (_) + serial.send("+COPS: 0\r\nOK\r\n") + end + }, + { + ".*", + function (_) + serial.send("ERROR\r\n") + end + } +} - scope.println("Sending msg \"OK\" via serial tx...") - scope.serial_tx(str2bytes("OK\r\n")) - scope.println("Message sent!") - - -- ILLEGAL USAGE - scope.exec('echo "' .. bytes2str(msg) .. '"') +M.cmd.hello = function () + log.inf("Hello, World!\r\n") end -function user_command(arg_list) - if arg_list[1] ~= "hello" then - return - end +return M - scope.println("Hello, World!\r\n") -end diff --git a/plugins/idf.lua b/plugins/idf.lua index 90e1da3..abdabe4 100644 --- a/plugins/idf.lua +++ b/plugins/idf.lua @@ -1,39 +1,37 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by MilhoNerfado. ---- DateTime: 07/05/24 17:00 ---- - -require("scope") - --- Validates if esp-idf is available -local function check_esp_idf() - stdout, stderr = scope.exec("idf.py", true) - return #stderr == 0 -end +local plugin = require("scope").plugin +local log = require("scope").log +local serial = require("scope").serial +local shell = require("shell") + +local M = plugin.new() -function serial_rx(msg) end +M.on_load = function () + M.shell = shell.new() -function user_command(arg_list) - if arg_list[1] == "monitor" then - scope.eprintln("Cannot run idf.py monitor inside Scope") + if not M.shell:exist("idf.py") then + log.err("There isn't a command called idf.py. Export it before enter in scope") return end +end - if not check_esp_idf() then - scope.eprintln("idf.py not found, try exporting idf.py environment variables first") - return - end - - local cmd = "idf.py " .. table.concat(arg_list, " ") +M.cmd.build = function () + M.shell:run("idf.py build") +end - if arg_list[1] == "flash" then - scope.disconnect() +--- comment +--- @param port string? +M.cmd.flash = function (port) + local cmd = "west flash" + if port then + cmd = cmd .. " -p " .. port end - - scope.exec(cmd) - if arg_list[1] == "flash" then - scope.connect() - end + local info = serial.info() + + serial.disconnect() + M.shell:run(cmd) + serial.connect(info.port, info.baudrate) end + +return M + diff --git a/plugins/new_plugin.lua b/plugins/new_plugin.lua deleted file mode 100644 index 26241aa..0000000 --- a/plugins/new_plugin.lua +++ /dev/null @@ -1,30 +0,0 @@ -local scope = require("scope") - -local M = scope.plugin.new({"event.serial", "cmd"}) - -M.event.serial.rx = function (msg) - -end - -M.event.serial.tx = function (msg) - -end - -M.event.serial.connect = function (port, baudrate) - -end - -M.event.serial.disconnect = function () - -end - -M.cmd.hello = function (arg1, arg2) - -end - -M.cmd.world = function () - -end - -return M - diff --git a/plugins/peripheral.lua b/plugins/peripheral.lua new file mode 100644 index 0000000..f4b03e2 --- /dev/null +++ b/plugins/peripheral.lua @@ -0,0 +1,66 @@ +local plugin = require("scope").plugin +local log = require("scope").plugin +local serial = require("scope").plugin +local sys = require("scope").sys + +local M = plugin.new() +M.tries = 1 +M.success = "OK\r" + +M.serial.on_recv = { + { + "AT\r", + function (_) + serial.send("OK\r\n") + end, + }, + { + "AT+COPS?\r", + function (_) + sys.sleep(1000) + serial.send("+COPS: 0\r\n") + serial.send("OK\r\n") + end, + }, + { + ".*", + function (msg) + serial.send("+CMERR: Invalid " .. msg .. "\r\n") + serial.send("ERROR\r\n") + end + }, +} + +local function ord_ends(n) + if n == 1 then + return "st" + elseif n == 2 then + return "nd" + elseif n == 3 then + return "rd" + else + return "th" + end +end + +M.serial.on_send = function (msg) + local err, rsp = serial.recv({timeout = 200}) + if not err and rsp == M.success then + return + end + + local tries = M.tries + for i=1,tries do + log.wrn(tostring(i) .. ord_ends(i) .. " try fail") + log.wrn("Trying to send \"" .. msg .. "\" again...") + serial.send(msg) + + err, rsp = serial.recv({timeout = 200}) + if not err and rsp == "OK\r" then + break + end + end +end + +return M + diff --git a/plugins/scope.lua b/plugins/scope.lua index 25a9a6e..b078ef1 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -1,8 +1,24 @@ -local Shell = require("shell.Shell") +local M = { + plugin = {}, + bytes = {}, + str = {}, + log = {}, + serial = {}, + ble = {}, + sys = {}, +} -local scope = {} +M.plugin.new = function () + return { + evt = { + serial = {}, + ble = {}, + }, + cmd = {}, + } +end -scope.bytes.to_str = function (bytes) +M.bytes.to_str = function (bytes) local str = "" for _, byte in pairs(bytes) do @@ -13,7 +29,7 @@ scope.bytes.to_str = function (bytes) end -scope.string.to_bytes = function (str) +M.str.to_bytes = function (str) local bytes = {} for _, c in utf8.codes(str) do @@ -23,51 +39,47 @@ scope.string.to_bytes = function (str) return bytes end -scope.log = { - dbg = function (msg) - coroutine.yield({":log.dbg", msg}) - end, - inf = function (msg) - coroutine.yield({":log.inf", msg}) - end, - wrn = function (msg) - coroutine.yield({":log.wrn", msg}) - end, - err = function (msg) - coroutine.yield({":log.err", msg}) - end, -} +M.log.dbg = function (msg) + coroutine.yield({":log.dbg", msg}) +end + +M.log.inf = function (msg) + coroutine.yield({":log.inf", msg}) +end -scope.serial = { - info = function () +M.log.wrn = function (msg) + coroutine.yield({":log.wrn", msg}) +end + +M.log.err = function (msg) + coroutine.yield({":log.err", msg}) +end + +M.serial.info = function () local _, port, baud_rate = coroutine.yield({":serial.info"}) return port, baud_rate - end, - send = function (msg) - coroutine.yield({":serial.send", msg}) - end, - recv = function (timeout_ms) - local _, err, msg = coroutine.yield({":serial.recv", timeout_ms}) - return err, msg - end, -} + end -scope.sys = { - os = function () - if os.getenv("OS") == "Windows_NT" then - return "windows" - else - return "unix" - end - end, - sleep = function (time_ms) - coroutine.yield({":sys.sleep", time_ms}) - end, - shell = function () - local _, id = coroutine.yield({":sys.shell"}) - return Shell:new(id) - end, -} +M.serial.send = function (msg) + coroutine.yield({":serial.send", msg}) +end + +M.serial.recv = function (timeout_ms) + local _, err, msg = coroutine.yield({":serial.recv", timeout_ms}) + return err, msg +end + +M.sys.os = function () + if os.getenv("OS") == "Windows_NT" then + return "windows" + else + return "unix" + end +end + +M.sys.sleep = function (time_ms) + coroutine.yield({":sys.sleep", time_ms}) +end -return scope +return M diff --git a/plugins/serial.lua b/plugins/serial.lua deleted file mode 100644 index 982f7ce..0000000 --- a/plugins/serial.lua +++ /dev/null @@ -1,21 +0,0 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by matheuswhite. ---- DateTime: 06/04/24 20:54 ---- -require "scope" - -function serial_rx(msg) -end - -function user_command(arg_list) - if arg_list[1] == 'connect' or arg_list[1] == 'reconnect' then - scope.connect(arg_list[2], arg_list[3]) - elseif arg_list[1] == 'disconnect' then - scope.disconnect() - elseif #arg_list == 0 or arg_list == nil then - scope.eprintln('Invalid command. Enter a "connect" or "disconnect" command') - else - scope.eprintln('Invalid command "' .. arg_list[1] .. '".') - end -end diff --git a/plugins/shell.lua b/plugins/shell.lua new file mode 100644 index 0000000..af2cbff --- /dev/null +++ b/plugins/shell.lua @@ -0,0 +1,19 @@ +local Shell = { + pid = nil, +} + +function Shell.new() + local _, pid = coroutine.yield({":Shell.new"}) + local self = setmetatable({}, Shell) + self.pid = pid + + return self +end + +function Shell:run(cmd, opts) + local _, stdout, stderr = coroutine.yield({":Shell:run", cmd, opts}) + return stdout, stderr +end + +return Shell + diff --git a/plugins/test.lua b/plugins/test.lua deleted file mode 100644 index bb1c8f4..0000000 --- a/plugins/test.lua +++ /dev/null @@ -1,33 +0,0 @@ ---- ---- Generated by EmmyLua(https://github.com/EmmyLua) ---- Created by matheuswhite. ---- DateTime: 23/01/24 18:49 ---- - -require "scope" - -function serial_rx(msg) - scope.disconnect() - scope.sleep(500) - scope.connect('COM1', 115200) - scope.sleep(500) - scope.println('Sent ' .. bytes2str(msg)) - scope.eprintln('Timeout') -end - -function user_command(arg_list) - scope.disconnect() - scope.sleep(500) - scope.connect('COM1', 115200) - scope.sleep(500) - scope.println('Sending ' .. arg_list[1] .. ' ...') - scope.serial_tx(str2bytes(arg_list[1])) - scope.println('Sent ' .. arg_list[1]) - scope.eprintln('Timeout') - scope.exec('echo hello', true) - scope.exec('echo hello') - info = scope.info() - -- info: {'serial': {port: 'COM1', baudrate: '115200', is_connected: true}} - text = '[' .. tostring(info.serial.is_connected) .. '] serial: ' .. info.serial.port .. '@' .. tostring(info.serial.baudrate) .. 'bps' - scope.println(text) -end diff --git a/plugins/west.lua b/plugins/west.lua index 9b06454..6f25164 100644 --- a/plugins/west.lua +++ b/plugins/west.lua @@ -1,36 +1,39 @@ -local scope = require("scope") +local plugin = require("scope").plugin +local log = require("scope").log +local serial = require("serial").serial +local shell = require("shell") local check_zephyr_base = function (sh) local stdout, _ = sh:run("echo $ZEPHYR_BASE") return stdout and stdout ~= "" and stdout ~= "\n" end -local west = {} +local M = plugin.new() -west.load = function () - west.shell = scope.sys.shell() +M.on_load = function () + M.shell = shell.new() - local res = check_zephyr_base(west.shell) + local res = check_zephyr_base(M.shell) if not res then - scope.log.err("$ZEPHYR_BASE is empty") + log.err("$ZEPHYR_BASE is empty") end - west.shell:run("source $ZEPHYR_BASE/../.venv/bin/activate") + M.shell:run("source $ZEPHYR_BASE/../.venv/bin/activate") return res end -west.action.build = function () - west.shell:run("west build") +M.cmd.build = function () + M.shell:run("west build") end -west.action.flash = function () - local info = scope.serial.info() +M.cmd.flash = function () + local info = serial.info() - scope.serial.disconnect() - west.shell:run("west flash") - scope.serial.connect(info.port, info.baudrate) + serial.disconnect() + M.shell:run("west flash") + serial.connect(info.port, info.baudrate) end -return west +return M From 6d6942011983ebc2dc4ab22fa15bd53789272e78 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Mon, 10 Jun 2024 10:24:18 -0300 Subject: [PATCH 03/49] docs: add on_mtu_change to ble plugin API Signed-off-by: Matheus T. dos Santos --- plugins/api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/api.md b/plugins/api.md index 15d5d6d..37427d7 100644 --- a/plugins/api.md +++ b/plugins/api.md @@ -134,6 +134,7 @@ local M = { on_write_without_rsp = function (serv, char, val) end on_notify = function(serv, char, val) end on_indicate = function(serv, char, val) end + on_mtu_change = function (uuid, val) end }, } ``` From d9cd6ef34517354276ffcd8c8e27b1b76e508dba Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Mon, 10 Jun 2024 10:25:38 -0300 Subject: [PATCH 04/49] feat: add delay between send tries Signed-off-by: Matheus T. dos Santos --- plugins/peripheral.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/peripheral.lua b/plugins/peripheral.lua index f4b03e2..8e44827 100644 --- a/plugins/peripheral.lua +++ b/plugins/peripheral.lua @@ -51,6 +51,7 @@ M.serial.on_send = function (msg) local tries = M.tries for i=1,tries do + sys.sleep_ms(50) log.wrn(tostring(i) .. ord_ends(i) .. " try fail") log.wrn("Trying to send \"" .. msg .. "\" again...") serial.send(msg) From e7539bc4ee837a61d951a3e72e5f199ea0b23e92 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 6 Jul 2024 12:36:34 -0300 Subject: [PATCH 05/49] docs: tidy up docs and update interfaces and APIs --- plugins/api.md | 272 ++++++++++++++++++++++++------------------------- 1 file changed, 131 insertions(+), 141 deletions(-) diff --git a/plugins/api.md b/plugins/api.md index 37427d7..f6da74e 100644 --- a/plugins/api.md +++ b/plugins/api.md @@ -1,234 +1,224 @@ -# Plugin API +# Plugins Developer Guide + +## Plugin Interfaces + +You have a idea to extend some Scope's feature. The first question that you do to yourself is: _How can I implement this new +feature?_. Actually, you would say this: _What are the functions I need to write to implement the feature a thought?_. In this +section, I'll show to you all the possible functions you can write, on your plugin, to implement you new feature. But, before +we start the function's descriptions, I need to say that you don't need to implement all functions. You only need to implement +the functions you want. + ### `on_load` +This function is called when the user runs `load` or `reload` command. On `reload` command, the function call occurs after the +`on_unload` call. + ```lua function M.on_load() - -- code ... + -- ... end ``` -## Serial +You can use this function to initialize any data or behaviour of your plugin. -### `on_send` +### `on_unload` + +This function is called when the user runs `reload` or `unload` command. On `reload` command, the function call occurs before the +`on_load` call. ```lua ---- Event function called when a message is sent via serial. ---- ---- @param msg string The message sent via serial. -function M.serial.on_send(msg) - -- code ... +function M.on_unload() + -- ... end ``` -The message transmission occurs before the call of the `on_send` event function. -It'll call this function again, only if the current call finish. -So, be careful when using blocking functions, such as `sys.sleep` and `serial.recv`, -inside a event function. +You can use this function to clean-up some resources or to unlock some resources. + +### `on_serial_send` + +This function is called when the user, or another plugin, sends a message to serial interface. This function has one parameter: +`msg` the message sent to serial interface. This argument is a copy os message, so it's not possible to change the original message +sent to serial. This function isn't parallel, so the next call only occurs after the current call. Thus, be careful when using +blocking function from Scope's API. ```lua ---- @type { pattern: string, evt_fn: function}[] -M.serial.on_send = { - { - -- Sample pattern - pattern = "AT\r", - evt_fn = function (msg) - end - } - { - -- Any message - pattern = ".*", - evt_fn = function (msg) - end - }, -} +function M.on_serial_send(msg) + -- ... +end ``` -The on_send could also be a list of event functions. Each event function must has a pattern. -So, a event function is called, when the sent message matches the pattern. The patterns is tested -in order it appers on the array. The messages sent to serial ends with `\n`. So the pattern, must -not contains `\n` in the end. +You can use this function to show aditional infos about the sent message, to send messages automaticaly based on sent messages +or to store some metadatas about sent messages. -### `on_recv` +### `on_serial_recv` ```lua ---- Event function called when a message is received from serial ---- ---- @param msg string The message received from serial -function on_recv(msg) - -- code ... +function M.on_serial_recv(msg) + -- ... end ``` +### `on_serial_connect` + ```lua ---- @type { pattern: string, evt_fn: function}[] -M.serial.on_recv = { - { - -- Sample pattern - pattern = "OK\r", - evt_fn = function (msg) - end - } - { - -- Any message - pattern = ".*", - evt_fn = function (msg) - end - }, -} +function M.on_serial_connect(port, baudrate) + -- ... +end ``` +### `on_serial_disconnect` -### `on_connect` +```lua +function M.on_serial_disconnect() + -- ... +end +``` + +### `on_ble_connect` ```lua ---- Event function called when Scope connects to a serial port ---- ---- @param port string Name of the serial port ---- @param baudrate integer Baudrate of the serial port -function on_connect(port, baudrate) - -- code ... +function M.on_ble_connect(uuid) + -- ... end ``` +### `on_ble_disconnect` + ```lua ---- @type { pattern: string, evt_fn: function}[] -M.serial.on_connect = { - { - -- Connected on Unix - port = "/dev/tty.*", - baudrate = ".*", - evt_fn = function (port, baudrate) - end - } - { - -- Connected on Windows - port = "COM[1-9][0-9]*", - baudrate = ".*", - evt_fn = function (msg) - end - }, -} +function M.on_ble_disconnect(uuid) + -- ... +end ``` +### `on_ble_write` -### `on_disconnect` +```lua +function M.on_ble_write(serv, char, val) + -- ... +end +``` + +### `on_ble_write_nowait` ```lua ---- Event function called when Scope disconnect from a serial port ---- -function on_disconnect() - -- code ... +function M.on_ble_write_nowait(serv, char, val) + -- ... end ``` -## BLE +### `on_ble_read` ```lua -local M = { - ble = { - on_connect = function (uuid) end - on_disconnect = function (uuid) end - on_read = function (serv, char, val) end - on_write = function (serv, char, val) end - on_write_without_rsp = function (serv, char, val) end - on_notify = function(serv, char, val) end - on_indicate = function(serv, char, val) end - on_mtu_change = function (uuid, val) end - }, -} +function M.on_ble_read(serv, char, val) + -- ... +end ``` +### `on_ble_notify` + +```lua +function M.on_ble_notify(serv, char, val) + -- ... +end +``` -## User Commands +### `on_ble_indicate` ```lua -local M = { - cmd = { - = function (arg_1, arg_2, ...) - -- code here - end - -- Examples: --- !plugin bye - -- bye = function() - -- log.inf("Good bye!") - -- end - -- - -- !plugin world "Matheus" 28 - -- greetings = function(name, age) - -- log.inf("Hello, " .. name) - -- log.inf("Are you " .. tostring(age) .. " years old?") - -- end - }, -} +function M.on_ble_indicate(serv, char, val) + -- ... +end ``` +### `on_mtu_change` -# Scope API +```lua +function M.on_mtu_change(uuid, val) + -- ... +end +``` -## Plugin +### User Commands ```lua ---- @return -function scope.plugin.new() +--- A command to greeting the user using random words +--- @param name string The user name +--- @param age number The user age +function M.greetings(name, age) + -- ... end ``` -## Bytes + +## Plugin API ```lua ---- @param bytes integer[] ---- @return string -function scope.bytes.to_str(bytes) +function scope.fmt.to_str(bytes) end ``` -## String +```lua +function scope.fmt.to_bytes(str) +end +``` ```lua ---- @param str string ---- @return integer[] -function scope.str.to_bytes(str) +function scope.log.debug(msg) end ``` -## Log +```lua +function scope.log.info(msg) +end +``` ```lua ---- @param msg string -function scope.log.dbg(msg) +function scope.log.success(msg) end ``` -## Serial +```lua +function scope.log.warning(msg) +end +``` + +```lua +function scope.log.error(msg) +end +``` ```lua ---- @return {port: string, baudrate: integer} function scope.serial.info() end +``` ---- @param msg string +```lua function scope.serial.send(msg) end +``` ---- @param opts {timeout_ms: integer} ---- @return {err: integer, msg: string} +```lua function scope.serial.recv(opts) end ``` -## System - ```lua ---- @return "windows" | "unix" function scope.sys.os_name() end - ---- @param time integer +``` + +```lua function scope.sys.sleep_ms(time) end ``` -# Shell API - +```lua +function shell.new() +end +``` +```lua +function Shell:run(cmd) +end +``` From 6b8e071b0e3fbbff69524f54998bc77e2cb28279 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 6 Jul 2024 13:16:36 -0300 Subject: [PATCH 06/49] feat: normalize interfaces and APIs, and update examples --- plugins/api.md | 13 +++++--- plugins/auto_test.lua | 33 ++++++++++---------- plugins/echo.lua | 52 +++++++++++++++---------------- plugins/idf.lua | 21 +++++++------ plugins/peripheral.lua | 51 ++++++++---------------------- plugins/scope.lua | 70 ++++++++++++++---------------------------- plugins/shell.lua | 8 +++-- plugins/west.lua | 29 +++++++---------- 8 files changed, 113 insertions(+), 164 deletions(-) diff --git a/plugins/api.md b/plugins/api.md index f6da74e..3538735 100644 --- a/plugins/api.md +++ b/plugins/api.md @@ -154,12 +154,12 @@ end ## Plugin API ```lua -function scope.fmt.to_str(bytes) +function scope.fmt.to_str(val) end ``` ```lua -function scope.fmt.to_bytes(str) +function scope.fmt.to_bytes(val) end ``` @@ -214,11 +214,16 @@ end ``` ```lua -function shell.new() +function Shell.new() end ``` ```lua -function Shell:run(cmd) +function Shell:run(cmd, opts) +end +``` + +```lua +function Shell:exist(program) end ``` diff --git a/plugins/auto_test.lua b/plugins/auto_test.lua index 860b14f..8a4918a 100644 --- a/plugins/auto_test.lua +++ b/plugins/auto_test.lua @@ -1,32 +1,31 @@ -local plugin = require("scope").plugin local log = require("scope").log local serial = require("scope").serial -local M = plugin.new() +local M = {} -M.serial.on_recv = function (msg) - log.dbg('Rx evt: ' .. msg) -end - -M.serial.on_send = function (msg) - log.wrn('Tx evt: ' .. msg) -end - -M.cmd.test1 = function (apn) +--- Test CREG comamnd +--- @param apn string? The APN to use on CREG command +function M.test_creg(apn) + apn = apn or "virtueyes.com.br" + serial.send("AT+CREG=1," .. apn .. ",0\r\n") - local err, rsp = serial.recv({timeout = 200}) + local err, rsp = serial.recv({timeout_ms = 200}) - if err ~= 0 then - log.err("[ERR] Test1 Timeout") + if err then + log.err("[ERR] Test CREG Timeout") return end if rsp == "OK\r\n" then - log.inf("[ OK] Test1 Success") + log.inf("[ OK] Test CREG Success") else - log.err("[ERR] Test1 Fail") + log.err("[ERR] Test GREG Fail") end end -return M +--- Run all tests with default parameters +function M.run_all() + M.test_creg() +end +return M diff --git a/plugins/echo.lua b/plugins/echo.lua index e90ea7b..b4bb7a2 100644 --- a/plugins/echo.lua +++ b/plugins/echo.lua @@ -1,35 +1,33 @@ local log = require("scope").log -local serial = require("scope").serial -local plugin = require("scope").plugin -local M = plugin.new() - -M.serial.on_recv = { - { - "AT\r", - function (_) - log.inf("Sending msg \"OK\" via serial tx...") - serial.send("\r\nOK\r\n") - log.inf("Message sent!") - end - }, - { - "AT+COPS?\r", - function (_) - serial.send("+COPS: 0\r\nOK\r\n") - end - }, - { - ".*", - function (_) - serial.send("ERROR\r\n") - end +local M = { + data = { + level = 'info' } } -M.cmd.hello = function () - log.inf("Hello, World!\r\n") +function M.on_serial_recv(msg) + if M.data.level == "debug" then + log.debug(msg) + elseif M.data.level == "info" then + log.info(msg) + elseif M.data.level == "success" then + log.success(msg) + elseif M.data.level == "warning" then + log.warning(msg) + elseif M.data.level == "error" then + log.error(msg) + end end -return M +--- Set up the level of echo message +--- @param lvl string The level of echo message +function M.level(lvl) + if not (lvl == "debug" or lvl == "info" or lvl == "success" or lvl == "warning" or lvl == "error") then + return + end + + M.data.level = level +end +return M diff --git a/plugins/idf.lua b/plugins/idf.lua index abdabe4..831bd35 100644 --- a/plugins/idf.lua +++ b/plugins/idf.lua @@ -1,26 +1,28 @@ -local plugin = require("scope").plugin local log = require("scope").log local serial = require("scope").serial local shell = require("shell") -local M = plugin.new() +local M = {} -M.on_load = function () +function M.on_load() M.shell = shell.new() if not M.shell:exist("idf.py") then - log.err("There isn't a command called idf.py. Export it before enter in scope") - return + log.err("There isn't a command called idf.py. Export it before enter in Scope") + return false end + + return true end -M.cmd.build = function () +--- Build the firmware +function M.build() M.shell:run("idf.py build") end ---- comment ---- @param port string? -M.cmd.flash = function (port) +--- Flash the firmware +--- @param port string? The board port +function M.flash(port) local cmd = "west flash" if port then cmd = cmd .. " -p " .. port @@ -34,4 +36,3 @@ M.cmd.flash = function (port) end return M - diff --git a/plugins/peripheral.lua b/plugins/peripheral.lua index 8e44827..7e999fb 100644 --- a/plugins/peripheral.lua +++ b/plugins/peripheral.lua @@ -1,35 +1,29 @@ -local plugin = require("scope").plugin local log = require("scope").plugin local serial = require("scope").plugin local sys = require("scope").sys +local re = require("scope").re -local M = plugin.new() -M.tries = 1 -M.success = "OK\r" +local M = { + tries = 1, + success = "OK\r" +} -M.serial.on_recv = { - { - "AT\r", - function (_) +function M.serial_on_recv(msg) + re.matches(msg, { + ["AT\r"] = function (_) serial.send("OK\r\n") end, - }, - { - "AT+COPS?\r", - function (_) + [re.literal("AT+COPS?")] = function (_) sys.sleep(1000) serial.send("+COPS: 0\r\n") serial.send("OK\r\n") end, - }, - { - ".*", - function (msg) + [".*"] = function (msg) serial.send("+CMERR: Invalid " .. msg .. "\r\n") serial.send("ERROR\r\n") end - }, -} + }) +end local function ord_ends(n) if n == 1 then @@ -43,25 +37,4 @@ local function ord_ends(n) end end -M.serial.on_send = function (msg) - local err, rsp = serial.recv({timeout = 200}) - if not err and rsp == M.success then - return - end - - local tries = M.tries - for i=1,tries do - sys.sleep_ms(50) - log.wrn(tostring(i) .. ord_ends(i) .. " try fail") - log.wrn("Trying to send \"" .. msg .. "\" again...") - serial.send(msg) - - err, rsp = serial.recv({timeout = 200}) - if not err and rsp == "OK\r" then - break - end - end -end - return M - diff --git a/plugins/scope.lua b/plugins/scope.lua index b078ef1..e73f90d 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -1,75 +1,52 @@ local M = { - plugin = {}, - bytes = {}, - str = {}, + fmt = {}, log = {}, serial = {}, ble = {}, sys = {}, } -M.plugin.new = function () - return { - evt = { - serial = {}, - ble = {}, - }, - cmd = {}, - } +function M.fmt.to_str(val) end -M.bytes.to_str = function (bytes) - local str = "" - - for _, byte in pairs(bytes) do - str = str .. string.char(byte) - end - - return str +function M.fmt.to_bytes(val) end - -M.str.to_bytes = function (str) - local bytes = {} - - for _, c in utf8.codes(str) do - table.insert(bytes, c) - end - - return bytes +function M.log.debug(msg) + coroutine.yield({":log.debug", msg}) end -M.log.dbg = function (msg) - coroutine.yield({":log.dbg", msg}) +function M.log.info(msg) + coroutine.yield({":log.info", msg}) end -M.log.inf = function (msg) - coroutine.yield({":log.inf", msg}) +function M.log.success(msg) + coroutine.yield({":log.success", msg}) end -M.log.wrn = function (msg) - coroutine.yield({":log.wrn", msg}) +function M.log.warning(msg) + coroutine.yield({":log.warning", msg}) end -M.log.err = function (msg) - coroutine.yield({":log.err", msg}) +function M.log.error(msg) + coroutine.yield({":log.error", msg}) end -M.serial.info = function () - local _, port, baud_rate = coroutine.yield({":serial.info"}) - return port, baud_rate - end +function M.serial.info() + local _, port, baud_rate = coroutine.yield({":serial.info"}) + return port, baud_rate +end -M.serial.send = function (msg) +function M.serial.send(msg) coroutine.yield({":serial.send", msg}) end -M.serial.recv = function (timeout_ms) - local _, err, msg = coroutine.yield({":serial.recv", timeout_ms}) +function M.serial.recv(opts) + local _, err, msg = coroutine.yield({":serial.recv", opts}) return err, msg end -M.sys.os = function () +function M.sys.os_name() if os.getenv("OS") == "Windows_NT" then return "windows" else @@ -77,9 +54,8 @@ M.sys.os = function () end end -M.sys.sleep = function (time_ms) - coroutine.yield({":sys.sleep", time_ms}) +function M.sys.sleep_ms(time) + coroutine.yield({":sys.sleep", time}) end return M - diff --git a/plugins/shell.lua b/plugins/shell.lua index af2cbff..c198fe5 100644 --- a/plugins/shell.lua +++ b/plugins/shell.lua @@ -11,9 +11,13 @@ function Shell.new() end function Shell:run(cmd, opts) - local _, stdout, stderr = coroutine.yield({":Shell:run", cmd, opts}) + local _, stdout, stderr = coroutine.yield({":Shell:run", self, cmd, opts}) return stdout, stderr end -return Shell +function Shell:exist(program) + local _, res = coroutine.yield({"Shell:run", self, program}) + return res +end +return Shell diff --git a/plugins/west.lua b/plugins/west.lua index 6f25164..af0ea84 100644 --- a/plugins/west.lua +++ b/plugins/west.lua @@ -1,33 +1,27 @@ -local plugin = require("scope").plugin local log = require("scope").log -local serial = require("serial").serial +local serial = require("scope").serial local shell = require("shell") -local check_zephyr_base = function (sh) - local stdout, _ = sh:run("echo $ZEPHYR_BASE") - return stdout and stdout ~= "" and stdout ~= "\n" -end - -local M = plugin.new() +local M = {} -M.on_load = function () +function M.on_load() M.shell = shell.new() - local res = check_zephyr_base(M.shell) - if not res then - log.err("$ZEPHYR_BASE is empty") + if not M.shell:exist("west") then + log.err("west not found. Export it before enter in Scope") + return false end - M.shell:run("source $ZEPHYR_BASE/../.venv/bin/activate") - - return res + return true end -M.cmd.build = function () +--- Build the firmware +function M.build() M.shell:run("west build") end -M.cmd.flash = function () +--- Flash the firmware +function M.flash() local info = serial.info() serial.disconnect() @@ -36,4 +30,3 @@ M.cmd.flash = function () end return M - From 3553edc81487cf2dc247bb18c2a8162361a9ec9f Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 6 Jul 2024 13:19:28 -0300 Subject: [PATCH 07/49] feat: add initial regex APIs --- plugins/scope.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/scope.lua b/plugins/scope.lua index e73f90d..4368f52 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -4,6 +4,7 @@ local M = { serial = {}, ble = {}, sys = {}, + re = {}, } function M.fmt.to_str(val) @@ -58,4 +59,13 @@ function M.sys.sleep_ms(time) coroutine.yield({":sys.sleep", time}) end +function M.re.literal(str) +end + +function M.re.matches(str, pattern_table) +end + +function M.re.match(str, pattern, code) +end + return M From 0a364194780a8c17618894427cf0c498a465dff7 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 6 Jul 2024 13:27:20 -0300 Subject: [PATCH 08/49] docs: add docs regex API --- plugins/api.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/api.md b/plugins/api.md index 3538735..619f409 100644 --- a/plugins/api.md +++ b/plugins/api.md @@ -213,6 +213,21 @@ function scope.sys.sleep_ms(time) end ``` +```lua +function M.re.literal(str) +end +``` + +```lua +function M.re.matches(str, pattern_table) +end +``` + +```lua +function M.re.match(str, pattern, code) +end +``` + ```lua function Shell.new() end From 34ba74947ac39544690d7796bb3948136be2c529 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 6 Jul 2024 16:29:27 -0300 Subject: [PATCH 09/49] refactor: remove all plugin code --- src/command_bar.rs | 75 +--- src/main.rs | 10 +- src/messages.rs | 55 --- src/plugin.rs | 762 ---------------------------------------- src/plugin_installer.rs | 131 ------- src/plugin_manager.rs | 324 ----------------- src/process.rs | 105 ------ src/serial.rs | 60 ---- src/task_bridge.rs | 8 - 9 files changed, 2 insertions(+), 1528 deletions(-) delete mode 100644 src/plugin.rs delete mode 100644 src/plugin_installer.rs delete mode 100644 src/plugin_manager.rs delete mode 100644 src/process.rs diff --git a/src/command_bar.rs b/src/command_bar.rs index 7eeb0cc..e6ca71c 100644 --- a/src/command_bar.rs +++ b/src/command_bar.rs @@ -2,7 +2,6 @@ use crate::blink_color::BlinkColor; use crate::command_bar::InputEvent::{HorizontalScroll, Key, VerticalScroll}; use crate::error_pop_up::ErrorPopUp; use crate::messages::{SerialRxData, UserTxData}; -use crate::plugin_manager::PluginManager; use crate::serial::SerialIF; use crate::text::TextView; use chrono::Local; @@ -39,7 +38,6 @@ pub struct CommandBar { key_receiver: UnboundedReceiver, current_hint: Option<&'static str>, hints: Vec<&'static str>, - plugin_manager: PluginManager, blink_color: BlinkColor, } @@ -62,8 +60,6 @@ impl CommandBar { let interface = Arc::new(Mutex::new(interface)); let text_view = Arc::new(Mutex::new(TextView::new(view_capacity, save_filename))); - let plugin_manager = PluginManager::new(interface.clone(), text_view.clone()); - Self { interface, text_view, @@ -78,7 +74,6 @@ impl CommandBar { command_list: CommandList::new(), hints: hints.clone(), current_hint: Some(hints.choose(&mut rand::thread_rng()).unwrap()), - plugin_manager, blink_color: BlinkColor::new(Color::Black, Duration::from_millis(200), 2), } } @@ -147,9 +142,7 @@ impl CommandBar { chunks[1].x + self.command_line_idx as u16 + 2, chunks[1].y + 1, ); - let bar_color = if self.plugin_manager.has_process_running() { - Color::DarkGray - } else if is_connected { + let bar_color = if is_connected { Color::LightGreen } else { Color::LightRed @@ -256,9 +249,6 @@ impl CommandBar { KeyCode::Char('q') if key.modifiers == KeyModifiers::CONTROL => { self.error_pop_up.take(); } - KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { - self.plugin_manager.stop_process().await; - } KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => { let is_recording = { let mut text_view = self.text_view.lock().await; @@ -472,13 +462,6 @@ impl CommandBar { return Err(()); } KeyCode::Enter if !self.command_line.is_empty() => { - if self.plugin_manager.has_process_running() { - self.set_error_pop_up( - "Cannot send data or command while a command is running".to_string(), - ); - return Ok(()); - } - let command_line = self.command_line.clone(); self.show_hint(); self.history.push(self.command_line.clone()); @@ -514,60 +497,6 @@ impl CommandBar { content: data_to_send, }); } - '!' => { - let command_line_split = command_line - .strip_prefix('!') - .unwrap() - .split_whitespace() - .map(|arg| arg.to_string()) - .collect::>(); - if command_line_split.is_empty() { - let interface = self.interface.lock().await; - interface.send(UserTxData::Data { - content: command_line, - }); - return Ok(()); - } - - let name = command_line_split[0].to_lowercase(); - - match name.as_str() { - "plugin" => { - match self - .plugin_manager - .handle_plugin_command(command_line_split[1..].to_vec()) - { - Ok(plugin_name) => { - let mut text_view = self.text_view.lock().await; - text_view - .add_data_out(SerialRxData::Plugin { - timestamp: Local::now(), - plugin_name: plugin_name.clone(), - content: format!( - "Plugin \"{}\" loaded!", - plugin_name - ), - is_successful: true, - }) - .await; - } - Err(err_msg) => { - self.set_error_pop_up(err_msg); - return Ok(()); - } - } - } - _ => { - if let Err(err_msg) = self.plugin_manager.call_plugin_user_command( - &name, - command_line_split[1..].to_vec(), - ) { - self.set_error_pop_up(err_msg); - return Ok(()); - } - } - } - } '$' => { let command_line = command_line .strip_prefix('$') @@ -619,8 +548,6 @@ impl CommandBar { if let Ok(data_out) = interface.try_recv() { let mut text_view = self.text_view.lock().await; text_view.add_data_out(data_out.clone()).await; - - self.plugin_manager.call_plugins_serial_rx(data_out); } } diff --git a/src/main.rs b/src/main.rs index 5f46823..db82588 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ extern crate core; use crate::command_bar::CommandBar; -use crate::plugin_installer::PluginInstaller; use crate::serial::SerialIF; use chrono::Local; use clap::{Parser, Subcommand}; @@ -22,10 +21,7 @@ mod blink_color; mod command_bar; mod error_pop_up; mod messages; -mod plugin; -mod plugin_installer; -mod plugin_manager; -mod process; +mod mpmc; mod recorder; mod rich_string; mod serial; @@ -66,10 +62,6 @@ async fn app() -> Result<(), String> { ctrlc::set_handler(|| { /* Do nothing on user ctrl+c */ }) .expect("Error setting Ctrl-C handler"); - let plugin_installer = PluginInstaller; - - plugin_installer.post()?; - let cli = Cli::parse(); let view_length = cli.view_length.unwrap_or(CAPACITY); diff --git a/src/messages.rs b/src/messages.rs index d060895..f7f2603 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -15,10 +15,6 @@ pub enum UserTxData { HexString { content: Vec, }, - PluginSerialTx { - plugin_name: String, - content: Vec, - }, } #[derive(Clone)] @@ -49,19 +45,9 @@ pub enum SerialRxData { content: String, is_successful: bool, }, - PluginSerialTx { - timestamp: DateTime, - plugin_name: String, - content: Vec, - is_successful: bool, - }, } impl SerialRxData { - pub fn is_plugin_serial_tx(&self) -> bool { - matches!(self, SerialRxData::PluginSerialTx { .. }) - } - fn from_utf8_print_invalid(v: &[u8]) -> Cow<'_, str> { let mut iter = v.utf8_chunks(); @@ -152,20 +138,6 @@ impl SerialRxData { content ) } - SerialRxData::PluginSerialTx { - timestamp, - content, - is_successful, - plugin_name, - } => { - format!( - "[{}|=>|{}|!{}]{}", - timestamp.format("%H:%M:%S.%3f"), - if *is_successful { success } else { fail }, - plugin_name, - Self::from_utf8_print_invalid(content) - ) - } } } } @@ -266,33 +238,6 @@ impl Into for SerialRxData { .into_view_data(timestamp) } } - SerialRxData::PluginSerialTx { - timestamp, - plugin_name, - content, - is_successful, - } => { - if is_successful { - RichText::from_string( - format!(" [{plugin_name}] => {} ", String::from_utf8_lossy(&content)), - Color::Black, - Color::White, - ) - .highlight_invisible() - .into_view_data(timestamp) - } else { - RichText::from_string( - format!( - " [{plugin_name}] => Fail to send {} ", - String::from_utf8_lossy(&content) - ), - Color::White, - Color::Red, - ) - .highlight_invisible() - .into_view_data(timestamp) - } - } } } } diff --git a/src/plugin.rs b/src/plugin.rs deleted file mode 100644 index 62bb115..0000000 --- a/src/plugin.rs +++ /dev/null @@ -1,762 +0,0 @@ -use crate::messages::SerialRxData; -use crate::text::TextView; -use anyhow::Result; -use chrono::Local; -use homedir::get_my_home; -use mlua::{Function, Lua, RegistryKey, Table, Thread}; -use std::ffi::OsStr; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::Mutex; - -#[derive(Clone)] -pub struct Plugin { - name: String, - code: String, -} - -#[derive(Debug, PartialEq)] -pub enum PluginRequest { - Println { - msg: String, - }, - Eprintln { - msg: String, - }, - Connect { - port: Option, - baud_rate: Option, - }, - Disconnect, - SerialTx { - msg: Vec, - }, - Sleep { - time: Duration, - }, - Exec { - cmd: String, - quiet: bool, - }, - Info, -} - -pub enum PluginRequestResult { - Exec { - stdout: Vec, - stderr: Vec, - }, - Info { - serial: SerialInfoResult, - }, -} - -pub struct SerialInfoResult { - port: String, - baudrate: u32, - is_connected: bool, -} - -impl SerialInfoResult { - pub fn new(port: String, baudrate: u32, is_connected: bool) -> Self { - Self { - port, - baudrate, - is_connected, - } - } -} - -pub struct SerialRxCall { - lua: Lua, - thread: RegistryKey, - msg: Vec, - req_result: Option, -} - -pub struct UserCommandCall { - lua: Lua, - thread: RegistryKey, - arg_list: Vec, - req_result: Option, -} - -pub trait PluginRequestResultHolder { - fn attach_request_result(&mut self, request_result: PluginRequestResult); - fn take_request_result(&mut self) -> Option; -} - -impl PluginRequestResultHolder for UserCommandCall { - fn attach_request_result(&mut self, request_result: PluginRequestResult) { - self.req_result = Some(request_result); - } - - fn take_request_result(&mut self) -> Option { - self.req_result.take() - } -} - -impl PluginRequestResultHolder for SerialRxCall { - fn attach_request_result(&mut self, request_result: PluginRequestResult) { - self.req_result = Some(request_result); - } - - fn take_request_result(&mut self) -> Option { - self.req_result.take() - } -} - -impl<'a> TryFrom> for PluginRequest { - type Error = String; - - fn try_from(value: Table) -> std::result::Result { - let id: String = value.get(1).map_err(|err| err.to_string())?; - - match id.as_str() { - ":println" => Ok(PluginRequest::Println { - msg: value.get(2).map_err(|err| err.to_string())?, - }), - ":eprintln" => Ok(PluginRequest::Eprintln { - msg: value.get(2).map_err(|err| err.to_string())?, - }), - ":connect" => { - let Some(first_arg): Option = value.get(2).ok() else { - return Ok(PluginRequest::Connect { - port: None, - baud_rate: None, - }); - }; - - let (port, baud_rate) = if first_arg.chars().all(|x| x.is_ascii_digit()) { - ( - None, - Some( - first_arg - .parse::() - .map_err(|_| "Cannot parse baud rate".to_string())?, - ), - ) - } else { - (Some(first_arg), None) - }; - - let Some(second_arg): Option = value.get(3).ok() else { - return Ok(PluginRequest::Connect { port, baud_rate }); - }; - - let (port, baud_rate) = if port.is_some() { - ( - port, - Some( - second_arg - .parse::() - .map_err(|_| "Cannot parse baud rate".to_string())?, - ), - ) - } else { - (Some(second_arg), baud_rate) - }; - - Ok(PluginRequest::Connect { port, baud_rate }) - } - ":disconnect" => Ok(PluginRequest::Disconnect), - ":serial_tx" => Ok(PluginRequest::SerialTx { - msg: value.get(2).map_err(|err| err.to_string())?, - }), - ":sleep" => { - let time: i32 = value.get(2).map_err(|err| err.to_string())?; - Ok(PluginRequest::Sleep { - time: Duration::from_millis(time as u64), - }) - } - ":exec" => Ok(PluginRequest::Exec { - cmd: value.get(2).map_err(|err| err.to_string())?, - quiet: value.get(3).map_err(|err| err.to_string())?, - }), - ":info" => Ok(PluginRequest::Info), - _ => Err("Unknown function".to_string()), - } - } -} - -impl Plugin { - pub fn new(filepath: PathBuf) -> Result { - let name = filepath - .with_extension("") - .file_name() - .ok_or("Cannot get filename of plugin".to_string())? - .to_str() - .ok_or("Cannot convert plugin name to string".to_string())? - .to_string(); - let extension = filepath.extension().unwrap_or(OsStr::new("lua")); - let filepath = filepath.with_extension(extension); - - let code = std::fs::read_to_string(filepath).map_err(|_| "Cannot read plugin file")?; - - Self::from_string(name, code) - } - - pub fn from_string(name: String, code: String) -> Result { - let lua = Lua::new_with(mlua::StdLib::ALL_SAFE, mlua::LuaOptions::default()) - .map_err(|_| "Cannot create Lua obj".to_string())?; - - Plugin::check_integrity(&lua, &code)?; - - Ok(Plugin { name, code }) - } - - pub async fn println(text_view: Arc>, plugin_name: String, content: String) { - let mut text_view = text_view.lock().await; - text_view - .add_data_out(SerialRxData::Plugin { - timestamp: Local::now(), - plugin_name, - content, - is_successful: true, - }) - .await; - } - - pub async fn eprintln(text_view: Arc>, plugin_name: String, content: String) { - let mut text_view = text_view.lock().await; - text_view - .add_data_out(SerialRxData::Plugin { - timestamp: Local::now(), - plugin_name, - content, - is_successful: false, - }) - .await; - } - - pub fn name(&self) -> &str { - self.name.as_str() - } - - fn create_lua_thread( - lua: &Lua, - code: &str, - coroutine_name: &str, - ) -> Result { - Plugin::append_plugins_dir(lua)?; - - lua.load(code) - .exec() - .map_err(|_| "Fail to load Lua code".to_string())?; - - let serial_rx: Thread = lua - .load(format!("coroutine.create({})", coroutine_name)) - .eval() - .map_err(|_| format!("Fail to create coroutine for {}", coroutine_name))?; - let reg = lua - .create_registry_value(serial_rx) - .map_err(|_| format!("Fail to create register for {} coroutines", coroutine_name))?; - Ok(reg) - } - - pub fn serial_rx_call(&self, msg: Vec) -> SerialRxCall { - let lua = Lua::new_with(mlua::StdLib::ALL_SAFE, mlua::LuaOptions::default()) - .expect("Cannot create Lua obj"); - - let serial_rx_reg = Self::create_lua_thread(&lua, self.code.as_str(), "serial_rx"); - - SerialRxCall { - lua, - thread: serial_rx_reg.expect("Cannot get serial_rx register"), - msg, - req_result: None, - } - } - - pub fn user_command_call(&self, arg_list: Vec) -> UserCommandCall { - let lua = Lua::new_with(mlua::StdLib::ALL_SAFE, mlua::LuaOptions::default()) - .expect("Cannot create Lua obj"); - - let user_command_reg = Self::create_lua_thread(&lua, self.code.as_str(), "user_command"); - - UserCommandCall { - lua, - thread: user_command_reg.expect("Cannot get user_command register"), - arg_list, - req_result: None, - } - } - - fn append_plugins_dir(lua: &Lua) -> Result<(), String> { - let home_dir = get_my_home() - .expect("Cannot get home directory") - .expect("Cannot get home directory") - .to_str() - .expect("Cannot get home directory") - .to_string(); - - if lua - .load( - format!( - "package.path = package.path .. ';{}/.config/scope/plugins/?.lua'", - home_dir.replace('\\', "/") - ) - .as_str(), - ) - .exec() - .is_err() - { - return Err("Cannot get default plugin path".to_string()); - } - - Ok(()) - } - - fn check_integrity(lua: &Lua, code: &str) -> Result<(), String> { - let globals = lua.globals(); - - Plugin::append_plugins_dir(lua)?; - - lua.load(code) - .exec() - .map_err(|_| "Fail to load Lua code".to_string())?; - - globals - .get::<_, Function>("serial_rx") - .map_err(|_| "serial_rx function not found in Lua code")?; - globals - .get::<_, Function>("user_command") - .map_err(|_| "user_command function not found in Lua code")?; - - Ok(()) - } -} - -fn resume_lua_thread(thread: &Thread, data: T) -> Option -where - T: for<'a> mlua::IntoLuaMulti<'a>, -{ - match thread.resume::<_, Table>(data) { - Ok(req) => { - let req: PluginRequest = match req.try_into() { - Ok(req) => req, - Err(msg) => return Some(PluginRequest::Eprintln { msg }), - }; - Some(req) - } - Err(_) => None, - } -} - -impl Iterator for SerialRxCall { - type Item = PluginRequest; - - fn next(&mut self) -> Option { - let req_result = self.take_request_result(); - let thread = &self.thread; - let msg = self.msg.clone(); - - let serial_rx: Thread = self - .lua - .registry_value(thread) - .expect("Cannot get serial_rx register"); - - let Some(req_result) = req_result else { - return resume_lua_thread(&serial_rx, msg); - }; - - match req_result { - PluginRequestResult::Exec { stdout, stderr } => { - match serial_rx.resume::<_, Table>((msg, stdout, stderr)) { - Ok(req) => { - let req: PluginRequest = match req.try_into() { - Ok(req) => req, - Err(msg) => return Some(PluginRequest::Eprintln { msg }), - }; - Some(req) - } - Err(_) => None, - } - } - PluginRequestResult::Info { - serial: - SerialInfoResult { - port, - baudrate, - is_connected, - }, - } => { - let serial = self - .lua - .create_table() - .expect("Cannot create serial lua table"); - serial.set("port", port).expect("Cannot add port"); - serial - .set("baudrate", baudrate) - .expect("Cannot add baudrate"); - serial - .set("is_connected", is_connected) - .expect("Cannot add baudrate"); - - let table = self.lua.create_table().expect("Cannot create a lua table"); - table.set("serial", serial).expect("Cannot add serial"); - - match serial_rx.resume::<_, Table>((msg, table)) { - Ok(req) => { - let req: PluginRequest = match req.try_into() { - Ok(req) => req, - Err(msg) => return Some(PluginRequest::Eprintln { msg }), - }; - Some(req) - } - Err(_) => None, - } - } - } - } -} - -impl Iterator for UserCommandCall { - type Item = PluginRequest; - - fn next(&mut self) -> Option { - let req_result = self.take_request_result(); - let thread = &self.thread; - let arg_list = self.arg_list.clone(); - - let user_command: Thread = self - .lua - .registry_value(thread) - .expect("Cannot get user_command register"); - - let Some(req_result) = req_result else { - return resume_lua_thread(&user_command, arg_list); - }; - - match req_result { - PluginRequestResult::Exec { stdout, stderr } => { - match user_command.resume::<_, Table>((arg_list, stdout, stderr)) { - Ok(req) => { - let req: PluginRequest = match req.try_into() { - Ok(req) => req, - Err(msg) => return Some(PluginRequest::Eprintln { msg }), - }; - Some(req) - } - Err(_) => None, - } - } - PluginRequestResult::Info { - serial: - SerialInfoResult { - port, - baudrate, - is_connected, - }, - } => { - let serial = self - .lua - .create_table() - .expect("Cannot create serial lua table"); - serial.set("port", port).expect("Cannot add port"); - serial - .set("baudrate", baudrate) - .expect("Cannot add baudrate"); - serial - .set("is_connected", is_connected) - .expect("Cannot add baudrate"); - - let table = self.lua.create_table().expect("Cannot create a lua table"); - table.set("serial", serial).expect("Cannot add serial"); - - match user_command.resume::<_, Table>((arg_list, table)) { - Ok(req) => { - let req: PluginRequest = match req.try_into() { - Ok(req) => req, - Err(msg) => return Some(PluginRequest::Eprintln { msg }), - }; - Some(req) - } - Err(_) => None, - } - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::plugin::{Plugin, PluginRequest}; - use crate::plugin_installer::PluginInstaller; - use std::path::PathBuf; - use std::time::Duration; - - #[test] - fn test_echo() -> Result<(), String> { - PluginInstaller.post()?; - Plugin::new(PathBuf::from("plugins/echo.lua"))?; - - Ok(()) - } - - #[test] - fn test_get_name() -> Result<(), String> { - PluginInstaller.post()?; - let plugin = Plugin::new(PathBuf::from("plugins/test.lua"))?; - let expected = "test"; - - assert_eq!(plugin.name(), expected); - - Ok(()) - } - - #[test] - fn test_serial_rx_iter() -> Result<(), String> { - PluginInstaller.post()?; - let msg = "Hello, World!"; - let plugin = Plugin::new(PathBuf::from("plugins/test.lua"))?; - let serial_rx_call = plugin.serial_rx_call(msg.as_bytes().to_vec()); - let expected = [ - PluginRequest::Disconnect, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Connect { - port: Some("COM1".to_string()), - baud_rate: Some(115200), - }, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Println { - msg: format!("Sent {}", msg), - }, - PluginRequest::Eprintln { - msg: "Timeout".to_string(), - }, - PluginRequest::Exec { - cmd: "echo hello".to_string(), - quiet: false, - }, - PluginRequest::Info, - PluginRequest::Println { - msg: "".to_string(), - }, - ]; - - for (i, req) in serial_rx_call.enumerate() { - assert_eq!(req, expected[i]); - } - - Ok(()) - } - - #[test] - fn test_2_serial_rx_iter() -> Result<(), String> { - PluginInstaller.post()?; - let msg = ["Hello, World!", "Other Message"]; - let plugin = [ - Plugin::new(PathBuf::from("plugins/test.lua"))?, - Plugin::new(PathBuf::from("plugins/test.lua"))?, - ]; - let mut serial_rx_call = [ - plugin[0].serial_rx_call(msg[0].as_bytes().to_vec()), - plugin[1].serial_rx_call(msg[1].as_bytes().to_vec()), - ]; - let expected = vec![ - (PluginRequest::Disconnect, PluginRequest::Disconnect), - ( - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - ), - ( - PluginRequest::Connect { - port: Some("COM1".to_string()), - baud_rate: Some(115200), - }, - PluginRequest::Connect { - port: Some("COM1".to_string()), - baud_rate: Some(115200), - }, - ), - ( - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - ), - ( - PluginRequest::Println { - msg: format!("Sent {}", msg[0]), - }, - PluginRequest::Println { - msg: format!("Sent {}", msg[1]), - }, - ), - ( - PluginRequest::Eprintln { - msg: "Timeout".to_string(), - }, - PluginRequest::Eprintln { - msg: "Timeout".to_string(), - }, - ), - ]; - - for (exp1, exp2) in expected { - let req1 = serial_rx_call[0].next(); - let req2 = serial_rx_call[1].next(); - - assert_eq!(req1, Some(exp1)); - assert_eq!(req2, Some(exp2)); - } - - Ok(()) - } - - #[test] - fn test_user_command_iter() -> Result<(), String> { - PluginInstaller.post()?; - let arg_list = vec!["Hello"] - .into_iter() - .map(|arg| arg.to_string()) - .collect(); - let plugin = Plugin::new(PathBuf::from("plugins/test.lua"))?; - let user_command_call = plugin.user_command_call(arg_list); - let expected = [ - PluginRequest::Disconnect, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Connect { - port: Some("COM1".to_string()), - baud_rate: Some(115200), - }, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Println { - msg: "Sending Hello ...".to_string(), - }, - PluginRequest::SerialTx { - msg: "Hello".as_bytes().to_vec(), - }, - PluginRequest::Println { - msg: "Sent Hello".to_string(), - }, - PluginRequest::Eprintln { - msg: "Timeout".to_string(), - }, - PluginRequest::Exec { - cmd: "echo hello".to_string(), - quiet: true, - }, - PluginRequest::Exec { - cmd: "echo hello".to_string(), - quiet: false, - }, - PluginRequest::Info, - ]; - - for (i, req) in user_command_call.enumerate() { - assert_eq!(req, expected[i]); - } - - Ok(()) - } - - #[test] - fn test_2_user_command_iter() -> Result<(), String> { - PluginInstaller.post()?; - let arg_list = [vec!["Hello"], vec!["Other"]] - .into_iter() - .map(|arg_list| { - arg_list - .into_iter() - .map(|arg| arg.to_string()) - .collect::>() - }) - .collect::>(); - let plugin = [ - Plugin::new(PathBuf::from("plugins/test.lua"))?, - Plugin::new(PathBuf::from("plugins/test.lua"))?, - ]; - let mut user_command_call = [ - plugin[0].user_command_call(arg_list[0].clone()), - plugin[1].user_command_call(arg_list[1].clone()), - ]; - let expected = vec![ - (PluginRequest::Disconnect, PluginRequest::Disconnect), - ( - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - ), - ( - PluginRequest::Connect { - port: Some("COM1".to_string()), - baud_rate: Some(115200), - }, - PluginRequest::Connect { - port: Some("COM1".to_string()), - baud_rate: Some(115200), - }, - ), - ( - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - PluginRequest::Sleep { - time: Duration::from_millis(500), - }, - ), - ( - PluginRequest::Println { - msg: format!("Sending {} ...", arg_list[0][0]), - }, - PluginRequest::Println { - msg: format!("Sending {} ...", arg_list[1][0]), - }, - ), - ( - PluginRequest::SerialTx { - msg: arg_list[0][0].as_bytes().to_vec(), - }, - PluginRequest::SerialTx { - msg: arg_list[1][0].as_bytes().to_vec(), - }, - ), - ( - PluginRequest::Println { - msg: format!("Sent {}", arg_list[0][0]), - }, - PluginRequest::Println { - msg: format!("Sent {}", arg_list[1][0]), - }, - ), - ( - PluginRequest::Eprintln { - msg: "Timeout".to_string(), - }, - PluginRequest::Eprintln { - msg: "Timeout".to_string(), - }, - ), - ]; - - for (exp1, exp2) in expected { - let req1 = user_command_call[0].next(); - let req2 = user_command_call[1].next(); - - assert_eq!(req1, Some(exp1)); - assert_eq!(req2, Some(exp2)); - } - - Ok(()) - } -} diff --git a/src/plugin_installer.rs b/src/plugin_installer.rs deleted file mode 100644 index 3486ecf..0000000 --- a/src/plugin_installer.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use std::path::Path; - -pub struct PluginInstaller; - -macro_rules! wrn { - ($content:expr) => { - println!("[\x1b[33mWRN\x1b[0m] {}", $content); - }; -} - -macro_rules! ok { - ($content:expr) => { - println!("[\x1b[32mOK\x1b[0m] {}", $content); - }; -} - -impl PluginInstaller { - const SCOPE: &'static str = include_str!("../plugins/scope.lua"); - - fn get_hash(&self, content: &str) -> u64 { - let mut hasher = DefaultHasher::default(); - content.hash(&mut hasher); - hasher.finish() - } - - fn create_dir(&self, path: &Path) -> Result<(), String> { - if !path.exists() { - wrn!(format!("Creating {:?}...", path)); - std::fs::create_dir(path).map_err(|_| format!("Cannot create {:?}", path))?; - ok!(format!("{:?} created with success", path)); - } else { - ok!(format!("{:?} is green", path)); - } - - Ok(()) - } - - fn create_file(&self, path: &Path, content: &str) -> Result<(), String> { - if !path.exists() { - wrn!(format!("Creating {:?}...", path)); - std::fs::write(path, content) - .map_err(|_| format!("Cannot create {:?} into plugins directory", path))?; - ok!(format!("{:?} created with success", path)); - } else { - ok!(format!("{:?} is green", path)); - } - - Ok(()) - } - - pub fn post(&self) -> Result<(), String> { - let mut dir = homedir::get_my_home() - .map_err(|_| "Cannot get home dir".to_string())? - .ok_or("Home dir path is empty".to_string())?; - - dir.push(".config/"); - self.create_dir(&dir)?; - - dir.push("scope/"); - self.create_dir(&dir)?; - - dir.push("plugins/"); - self.create_dir(&dir)?; - - dir.push("scope.lua"); - self.create_file(&dir, PluginInstaller::SCOPE)?; - - wrn!(format!("Checking {:?} integrity", dir)); - let right_hash = self.get_hash(PluginInstaller::SCOPE); - let read_hash = self.get_hash( - std::fs::read_to_string(&dir) - .map_err(|_| "Cannot read scope.lua content".to_string())? - .as_str(), - ); - - if right_hash != read_hash { - wrn!(format!( - "{:?} is corrupted. Replacing by original content...", - &dir - )); - std::fs::write(&dir, PluginInstaller::SCOPE).map_err(|_| { - "Cannot replace original content of scope.lua into file".to_string() - })?; - } - - ok!(format!("{:?} is green", &dir)); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use crate::plugin::Plugin; - use crate::plugin_installer::PluginInstaller; - - #[test] - fn test_post_only_config() { - let mut config_dir = homedir::get_my_home().unwrap().unwrap(); - config_dir.push(".config/scope"); - - std::fs::remove_dir_all(&config_dir).unwrap(); - - PluginInstaller.post().unwrap(); - Plugin::new("plugins/test.lua".into()).unwrap(); - } - - #[test] - fn test_post_only_scope() { - let mut config_dir = homedir::get_my_home().unwrap().unwrap(); - config_dir.push(".config/scope/plugins"); - - std::fs::remove_dir_all(&config_dir).unwrap(); - - PluginInstaller.post().unwrap(); - Plugin::new("plugins/test.lua".into()).unwrap(); - } - - #[test] - fn test_post_only_plugins_folder() { - let mut config_dir = homedir::get_my_home().unwrap().unwrap(); - config_dir.push(".config/scope/plugins/scope.lua"); - - std::fs::remove_file(&config_dir).unwrap(); - - PluginInstaller.post().unwrap(); - Plugin::new("plugins/test.lua".into()).unwrap(); - } -} diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs deleted file mode 100644 index 860389a..0000000 --- a/src/plugin_manager.rs +++ /dev/null @@ -1,324 +0,0 @@ -use crate::messages::{SerialRxData, UserTxData}; -use crate::plugin::{ - Plugin, PluginRequest, PluginRequestResult, PluginRequestResultHolder, SerialInfoResult, - SerialRxCall, UserCommandCall, -}; -use crate::process::ProcessRunner; -use crate::serial::SerialIF; -use crate::text::TextView; -use std::collections::HashMap; -use std::env; -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::mpsc::UnboundedSender; -use tokio::sync::Mutex; - -pub struct PluginManager { - text_view: Arc>, - plugins: HashMap, - serial_rx_tx: UnboundedSender<(String, SerialRxCall)>, - user_command_tx: UnboundedSender<(String, UserCommandCall)>, - flags: PluginManagerFlags, -} - -impl PluginManager { - pub fn new(interface: Arc>, text_view: Arc>) -> Self { - let (serial_rx_tx, mut serial_rx_rx) = - tokio::sync::mpsc::unbounded_channel::<(String, SerialRxCall)>(); - let (user_command_tx, mut user_command_rx) = - tokio::sync::mpsc::unbounded_channel::<(String, UserCommandCall)>(); - let process_runner = ProcessRunner::new(text_view.clone()); - - let (text_view2, interface2, process_runner2) = - (text_view.clone(), interface.clone(), process_runner.clone()); - let text_view3 = text_view.clone(); - - let flags = PluginManagerFlags::default(); - let flags2 = flags.clone(); - let flags3 = flags.clone(); - - tokio::spawn(async move { - 'user_command: loop { - let Some((plugin_name, mut user_command_call)) = user_command_rx.recv().await - else { - break 'user_command; - }; - - while let Some(req) = user_command_call.next() { - let Some(req_result) = PluginManager::exec_plugin_request( - text_view.clone(), - interface.clone(), - plugin_name.clone(), - &process_runner, - flags2.clone(), - ExecutionOrigin::UserCommand, - req, - ) - .await - else { - continue; - }; - - user_command_call.attach_request_result(req_result); - } - } - }); - - tokio::spawn(async move { - 'serial_rx: loop { - let Some((plugin_name, mut serial_rx_call)) = serial_rx_rx.recv().await else { - break 'serial_rx; - }; - - while let Some(req) = serial_rx_call.next() { - let Some(req_result) = PluginManager::exec_plugin_request( - text_view2.clone(), - interface2.clone(), - plugin_name.clone(), - &process_runner2, - flags3.clone(), - ExecutionOrigin::SerialRx, - req, - ) - .await - else { - continue; - }; - - serial_rx_call.attach_request_result(req_result); - } - } - }); - - let serial_plugin = Plugin::from_string( - "serial".to_string(), - include_str!("../plugins/serial.lua").to_string(), - ) - .unwrap(); - - let mut plugins = HashMap::new(); - plugins.insert(serial_plugin.name().to_string(), serial_plugin); - - Self { - text_view: text_view3, - plugins, - serial_rx_tx, - user_command_tx, - flags, - } - } - - pub fn has_process_running(&self) -> bool { - self.flags.has_process_running.load(Ordering::SeqCst) - } - - pub async fn stop_process(&mut self) { - if self.has_process_running() { - self.flags.stop_process.store(true, Ordering::SeqCst); - Plugin::eprintln( - self.text_view.clone(), - "system".to_string(), - "Execution stopped".to_string(), - ) - .await; - } - } - - pub fn handle_plugin_command(&mut self, arg_list: Vec) -> Result { - if arg_list.is_empty() { - return Err("Please, use !plugin followed by a command".to_string()); - } - - let command = arg_list[0].as_str(); - let mut plugin_path = env::current_dir().expect("Cannot get the current directory"); - - let plugin_name = match command { - "load" | "reload" => { - if arg_list.len() < 2 { - return Err("Please, inform the plugin path to be loaded".to_string()); - } - - plugin_path.push(PathBuf::from(arg_list[1].as_str())); - - let plugin = match Plugin::new(plugin_path.clone()) { - Ok(plugin) => plugin, - Err(err) => return Err(err), - }; - - let plugin_name = plugin.name().to_string(); - - if self.plugins.contains_key(plugin_name.as_str()) { - self.plugins.remove(plugin_name.as_str()); - } - - self.plugins.insert(plugin_name.clone(), plugin.clone()); - - plugin_name - } - _ => return Err(format!("Unknown command {} for !plugin", command)), - }; - - Ok(plugin_name) - } - - pub fn call_plugin_user_command( - &self, - name: &str, - arg_list: Vec, - ) -> Result<(), String> { - if !self.plugins.contains_key(name) { - return Err(format!("Command not found", &name)); - } - - let plugin = self.plugins.get(name).unwrap(); - let user_command_call = plugin.user_command_call(arg_list); - - let plugin_name = plugin.name().to_string(); - self.user_command_tx - .send((plugin_name, user_command_call)) - .unwrap(); - - Ok(()) - } - - pub fn call_plugins_serial_rx(&self, data_out: SerialRxData) { - for plugin in self.plugins.values().cloned().collect::>() { - let SerialRxData::RxData { - timestamp: _timestamp, - content: line, - } = &data_out - else { - continue; - }; - - let serial_rx_call = plugin.serial_rx_call(line.clone()); - let plugin_name = plugin.name().to_string(); - self.serial_rx_tx - .send((plugin_name, serial_rx_call)) - .map_err(|e| e.to_string()) - .unwrap(); - } - } - - async fn exec_plugin_request( - text_view: Arc>, - interface: Arc>, - plugin_name: String, - process_runner: &ProcessRunner, - flags: PluginManagerFlags, - origin: ExecutionOrigin, - req: PluginRequest, - ) -> Option { - match req { - PluginRequest::Println { msg } => { - Plugin::println(text_view, plugin_name, msg).await; - } - PluginRequest::Eprintln { msg } => { - Plugin::eprintln(text_view, plugin_name, msg).await; - } - PluginRequest::Connect { port, baud_rate } => { - let mut interface = interface.lock().await; - if interface.is_connected() && port.is_none() && baud_rate.is_none() { - Plugin::eprintln( - text_view, - plugin_name, - "Serial port already connected".to_string(), - ) - .await; - return None; - } - - interface.setup(port, baud_rate).await; - } - PluginRequest::Disconnect => { - let mut interface = interface.lock().await; - if !interface.is_connected() { - Plugin::eprintln( - text_view, - plugin_name, - "Serial port already disconnected".to_string(), - ) - .await; - return None; - } - - interface.disconnect().await; - } - PluginRequest::SerialTx { msg } => { - let mut text_view = text_view.lock().await; - let mut interface = interface.lock().await; - interface.send(UserTxData::PluginSerialTx { - plugin_name: plugin_name.clone(), - content: msg.clone(), - }); - - /* time to serial task respond us, with SerialRxData::PluginSerialTx */ - tokio::time::sleep(Duration::from_millis(200)).await; - - 'plugin_serial_tx: loop { - match interface.recv().await { - Some(x) if x.is_plugin_serial_tx() => { - text_view.add_data_out(x).await; - break 'plugin_serial_tx; - } - Some(x) => text_view.add_data_out(x).await, - None => { - break 'plugin_serial_tx; - } - } - } - } - PluginRequest::Sleep { time } => tokio::time::sleep(time).await, - PluginRequest::Exec { cmd, quiet } => match origin { - ExecutionOrigin::SerialRx => { - Plugin::eprintln( - text_view, - plugin_name, - "Cannot call \"scope.exec\" from \"serial_rx\"".to_string(), - ) - .await - } - ExecutionOrigin::UserCommand => { - flags.has_process_running.store(true, Ordering::SeqCst); - let res = Some( - process_runner - .run(plugin_name, cmd, quiet, flags.stop_process.clone()) - .await - .unwrap(), - ); - flags.has_process_running.store(false, Ordering::SeqCst); - flags.stop_process.store(false, Ordering::SeqCst); - return res; - } - }, - PluginRequest::Info => { - let serial_if = interface.lock().await; - let serial_info = serial_if.get_info(); - let serial_is_connected = serial_if.is_connected(); - - return Some(PluginRequestResult::Info { - serial: SerialInfoResult::new( - serial_info.port().to_owned(), - serial_info.baudrate(), - serial_is_connected, - ), - }); - } - } - - None - } -} - -#[derive(Default, Clone)] -struct PluginManagerFlags { - has_process_running: Arc, - stop_process: Arc, -} - -enum ExecutionOrigin { - SerialRx, - UserCommand, -} diff --git a/src/process.rs b/src/process.rs deleted file mode 100644 index 4b59f42..0000000 --- a/src/process.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::plugin::{Plugin, PluginRequestResult}; -use crate::text::TextView; -use std::process::Stdio; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; -use tokio::sync::Mutex; - -pub struct ProcessRunner { - text_view: Arc>, -} - -impl Clone for ProcessRunner { - fn clone(&self) -> Self { - Self { - text_view: self.text_view.clone(), - } - } -} - -impl ProcessRunner { - pub fn new(text_view: Arc>) -> Self { - Self { text_view } - } - - pub async fn run( - &self, - plugin_name: String, - cmd: String, - quiet: bool, - stop_process_flag: Arc, - ) -> Result { - if !quiet { - Plugin::println(self.text_view.clone(), plugin_name.clone(), cmd.clone()).await; - } - - let mut child = if cfg!(target_os = "windows") { - Command::new("cmd") - .arg("/C") - .arg(cmd) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() - .map_err(|err| err.to_string())? - } else { - Command::new("sh") - .arg("-c") - .arg(cmd) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|err| err.to_string())? - }; - - let text_view2 = self.text_view.clone(); - let text_view3 = self.text_view.clone(); - let plugin_name2 = plugin_name.clone(); - let stop_process_flag2 = stop_process_flag.clone(); - - let stdout = child.stdout.take().ok_or("Cannot get stdout".to_string())?; - let mut stdout = BufReader::new(stdout).lines(); - let stdout_pipe = tokio::spawn(async move { - let mut buffer = vec![]; - - while let Ok(Some(line)) = stdout.next_line().await { - if stop_process_flag.load(Ordering::SeqCst) { - return buffer; - } - - buffer.push(line.clone()); - if !quiet { - Plugin::println(text_view2.clone(), plugin_name.clone(), line.clone()).await; - } - } - - buffer - }); - - let stderr = child.stderr.take().ok_or("Cannot get stderr".to_string())?; - let mut stderr = BufReader::new(stderr).lines(); - let stderr_pipe = tokio::spawn(async move { - let mut buffer = vec![]; - - while let Ok(Some(line)) = stderr.next_line().await { - if stop_process_flag2.load(Ordering::SeqCst) { - return buffer; - } - - buffer.push(line.clone()); - if !quiet { - Plugin::eprintln(text_view3.clone(), plugin_name2.clone(), line).await; - } - } - - buffer - }); - - Ok(PluginRequestResult::Exec { - stdout: stdout_pipe.await.unwrap(), - stderr: stderr_pipe.await.unwrap(), - }) - } -} diff --git a/src/serial.rs b/src/serial.rs index d40613c..cd4c5b3 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -50,10 +50,6 @@ impl SerialIF { ) } - pub fn get_info(&self) -> SerialInfo { - self.synchronized().info.clone() - } - pub fn is_connected(&self) -> bool { self.synchronized().is_connected.load(Ordering::SeqCst) } @@ -67,31 +63,6 @@ impl SerialIF { pub async fn exit(&self) { self.shared().await.is_exit = true; } - - pub async fn setup(&mut self, port: Option, baudrate: Option) { - self.synchronized_mut().info = { - let mut shared = self.shared().await; - - if let Some(port) = port { - shared.info.port = port; - } - - if let Some(baudrate) = baudrate { - shared.info.baudrate = baudrate; - } - - shared.reconnect = true; - - shared.info.clone() - }; - } - - pub async fn disconnect(&mut self) { - let mut shared = self.shared().await; - shared.reconnect = false; - let info = shared.info.clone(); - let _ = shared.disconnect.insert(info); - } } struct SerialTask; @@ -250,27 +221,6 @@ impl Task for SerialTask { }) .expect("Cannot send hex string fail"), }, - UserTxData::PluginSerialTx { - plugin_name, - content, - } => match serial.write(&content) { - Ok(_) => to_bridge - .send(SerialRxData::PluginSerialTx { - timestamp: Local::now(), - plugin_name, - content, - is_successful: true, - }) - .expect("Cannot send plugin serial tx comfirm"), - Err(_) => to_bridge - .send(SerialRxData::PluginSerialTx { - timestamp: Local::now(), - plugin_name, - content, - is_successful: false, - }) - .expect("Cannot send plugin serial tx fail"), - }, } } @@ -324,13 +274,3 @@ pub struct SerialInfo { port: String, baudrate: u32, } - -impl SerialInfo { - pub fn port(&self) -> &str { - &self.port - } - - pub fn baudrate(&self) -> u32 { - self.baudrate - } -} diff --git a/src/task_bridge.rs b/src/task_bridge.rs index 7c182b9..39323b7 100644 --- a/src/task_bridge.rs +++ b/src/task_bridge.rs @@ -48,10 +48,6 @@ where &self.synchronized } - pub fn synchronized_mut(&mut self) -> &mut Synchronized { - &mut self.synchronized - } - pub async fn shared(&self) -> MutexGuard { self.shared.lock().await } @@ -63,10 +59,6 @@ where pub fn try_recv(&mut self) -> Result { self.from_task.try_recv() } - - pub async fn recv(&mut self) -> Option { - self.from_task.recv().await - } } pub trait Task { From 25dcc8c0c42805506f7ac8d2a0b735540911fcbb Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 13 Jul 2024 15:00:57 -0300 Subject: [PATCH 10/49] wip: writting the code in the new archtecture --- Cargo.lock | 61 ++-- Cargo.toml | 2 +- src/error_pop_up.rs | 44 --- src/graphics/graphics_task.rs | 469 ++++++++++++++++++++++++++++++ src/graphics/mod.rs | 2 + src/{ => graphics}/rich_string.rs | 12 +- src/infra/blink.rs | 53 ++++ src/infra/logger.rs | 77 +++++ src/infra/messages.rs | 7 + src/infra/mod.rs | 21 ++ src/infra/mpmc.rs | 64 ++++ src/{ => infra}/recorder.rs | 3 +- src/infra/task.rs | 78 +++++ src/infra/timer.rs | 45 +++ src/{ => infra}/typewriter.rs | 3 +- src/inputs/inputs_task.rs | 73 +++++ src/inputs/mod.rs | 1 + src/main.rs | 214 ++++++-------- src/{ => old}/blink_color.rs | 0 src/{ => old}/command_bar.rs | 0 src/{ => old}/messages.rs | 0 src/{ => old}/serial.rs | 0 src/{ => old}/task_bridge.rs | 0 src/{ => old}/text.rs | 0 src/plugin/mod.rs | 1 + src/plugin/plugin_engine.rs | 58 ++++ src/serial/mod.rs | 1 + src/serial/serial_if.rs | 284 ++++++++++++++++++ 28 files changed, 1375 insertions(+), 198 deletions(-) delete mode 100644 src/error_pop_up.rs create mode 100644 src/graphics/graphics_task.rs create mode 100644 src/graphics/mod.rs rename src/{ => graphics}/rich_string.rs (98%) create mode 100644 src/infra/blink.rs create mode 100644 src/infra/logger.rs create mode 100644 src/infra/messages.rs create mode 100644 src/infra/mod.rs create mode 100644 src/infra/mpmc.rs rename src/{ => infra}/recorder.rs (98%) create mode 100644 src/infra/task.rs create mode 100644 src/infra/timer.rs rename src/{ => infra}/typewriter.rs (97%) create mode 100644 src/inputs/inputs_task.rs create mode 100644 src/inputs/mod.rs rename src/{ => old}/blink_color.rs (100%) rename src/{ => old}/command_bar.rs (100%) rename src/{ => old}/messages.rs (100%) rename src/{ => old}/serial.rs (100%) rename src/{ => old}/task_bridge.rs (100%) rename src/{ => old}/text.rs (100%) create mode 100644 src/plugin/mod.rs create mode 100644 src/plugin/plugin_engine.rs create mode 100644 src/serial/mod.rs create mode 100644 src/serial/serial_if.rs diff --git a/Cargo.lock b/Cargo.lock index df0f1cf..9ced417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,9 +531,9 @@ checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "io-kit-sys" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" dependencies = [ "core-foundation-sys", "mach2", @@ -569,6 +569,26 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -664,19 +684,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mio-serial" -version = "5.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a4c60ca5c9c0e114b3bd66ff4aa5f9b2b175442be51ca6c4365d687a97a2ac" -dependencies = [ - "log", - "mio", - "nix 0.26.4", - "serialport", - "winapi", -] - [[package]] name = "mlua" version = "0.9.6" @@ -976,8 +983,8 @@ dependencies = [ "regex", "serde", "serde_yaml", + "serialport", "tokio", - "tokio-serial", ] [[package]] @@ -1021,14 +1028,15 @@ dependencies = [ [[package]] name = "serialport" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5a15d0be940df84846264b09b51b10b931fb2f275becb80934e3568a016828" +checksum = "de7c4f0cce25b9b3518eea99618112f9ee4549f974480c8f43d3c06f03c131a0" dependencies = [ "bitflags 2.4.2", "cfg-if", "core-foundation-sys", "io-kit-sys", + "libudev", "mach2", "nix 0.26.4", "regex", @@ -1208,24 +1216,11 @@ dependencies = [ "syn 2.0.50", ] -[[package]] -name = "tokio-serial" -version = "5.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6e2e4cf0520a99c5f87d5abb24172b5bd220de57c3181baaaa5440540c64aa" -dependencies = [ - "cfg-if", - "futures", - "log", - "mio-serial", - "tokio", -] - [[package]] name = "unescaper" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0adf6ad32eb5b3cadff915f7b770faaac8f7ff0476633aa29eb0d9584d889d34" +checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" dependencies = [ "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 4e3547d..46da825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ name = "scope" path = "src/main.rs" [dependencies] -tokio-serial = "5.4.1" +serialport = "4.4.0" ratatui = "0.26.1" crossterm = { version = "0.27.0", features = ["default", "event-stream"] } futures = "0.3.30" diff --git a/src/error_pop_up.rs b/src/error_pop_up.rs deleted file mode 100644 index 74e9bbb..0000000 --- a/src/error_pop_up.rs +++ /dev/null @@ -1,44 +0,0 @@ -use ratatui::layout::{Alignment, Rect}; -use ratatui::style::{Color, Style}; -use ratatui::text::Span; -use ratatui::widgets::{Block, Borders, Clear, Paragraph}; -use ratatui::Frame; -use std::time::{Duration, Instant}; - -pub struct ErrorPopUp { - message: String, - spawn_time: Instant, -} - -impl ErrorPopUp { - const TIMEOUT: Duration = Duration::from_millis(5000); - - pub fn new(message: String) -> Self { - Self { - message, - spawn_time: Instant::now(), - } - } - - pub fn draw(&self, f: &mut Frame, command_bar_y: u16) { - let area_size = (self.message.chars().count() as u16 + 4, 3); - let area = Rect::new( - (f.size().width - area_size.0) / 2, - command_bar_y - area_size.1 + 1, - area_size.0, - area_size.1, - ); - let block = Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(Color::LightRed)); - let paragraph = Paragraph::new(Span::from(self.message.clone())) - .block(block) - .alignment(Alignment::Center); - f.render_widget(Clear, area); - f.render_widget(paragraph, area); - } - - pub fn is_timeout(&self) -> bool { - self.spawn_time.elapsed() >= ErrorPopUp::TIMEOUT - } -} diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs new file mode 100644 index 0000000..c0dc807 --- /dev/null +++ b/src/graphics/graphics_task.rs @@ -0,0 +1,469 @@ +use crate::{ + infra::{ + logger::{LogLevel, LogMessage, Logger}, + messages::TimedBytes, + mpmc::Consumer, + recorder::Recorder, + task::{Shared, Task}, + timer::Timer, + typewriter::TypeWriter, + }, + inputs::inputs_task::InputsShared, + serial::serial_if::{SerialMode, SerialShared}, + warning, +}; +use chrono::{DateTime, Local}; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::{block::Title, Block, BorderType, Borders, Clear, Paragraph}, + Frame, Terminal, +}; +use std::{ + cmp::{max, min}, + time::Duration, +}; +use std::{ + fs::File, + io, + sync::{ + mpsc::{Receiver, Sender}, + Arc, RwLock, + }, +}; + +use super::rich_string::RichText; + +pub type GraphicsTask = Task<(), GraphicsCommand>; + +pub struct GraphicsConnections { + logger: Logger, + logger_receiver: Receiver, + tx: Consumer>, + rx: Consumer>, + inputs_shared: Shared, + serial_shared: Shared, + history: Vec, + typewriter: TypeWriter, + recorder: Recorder, + capacity: usize, + auto_scroll: bool, + scroll: (u16, u16), +} + +pub enum GraphicsCommand { + SaveData, + RecordData, + Exit, +} + +enum GraphicalMessage { + Log(LogMessage), + Tx { + timestamp: DateTime, + message: Vec<(String, Color)>, + }, + Rx { + timestamp: DateTime, + message: Vec<(String, Color)>, + }, +} + +impl GraphicsTask { + const COMMAND_BAR_HEIGHT: u16 = 3; + + pub fn spawn_graphics_task( + connections: GraphicsConnections, + cmd_sender: Sender, + cmd_receiver: Receiver, + ) -> Self { + Self::new((), connections, Self::task, cmd_sender, cmd_receiver) + } + + fn draw_history( + private: &GraphicsConnections, + frame: &mut Frame, + rect: Rect, + blink_color: Option, + ) { + let scroll = if private.auto_scroll { + ( + Self::max_main_axis(frame.size().height, &private.history), + private.scroll.1, + ) + } else { + private.scroll + }; + + let (coll, coll_size) = ( + &private.history[(scroll.0 as usize)..], + private.history.len(), + ); + let border_type = if private.auto_scroll { + BorderType::Thick + } else { + BorderType::Double + }; + + let block = if private.recorder.is_recording() { + Block::default() + .title(format!( + "[{:03}][ASCII] ◉ {}", + coll_size, + private.recorder.get_filename() + )) + .title( + Title::from(format!("[{}]", private.recorder.get_size())) + .alignment(Alignment::Right), + ) + .borders(Borders::ALL) + .border_type(border_type) + .border_style(Style::default().fg(Color::Yellow)) + } else { + Block::default() + .title(format!( + "[{:03}][ASCII] {}", + coll_size, + private.typewriter.get_filename() + )) + .title( + Title::from(format!("[{}]", private.typewriter.get_size())) + .alignment(Alignment::Right), + ) + .borders(Borders::ALL) + .border_type(border_type) + .border_style(Style::default().fg(blink_color.unwrap_or(Color::Reset))) + }; + + let text = coll + .iter() + .map(|msg| match msg { + GraphicalMessage::Log(log_msg) => Self::line_from_log_message(log_msg, scroll), + GraphicalMessage::Tx { timestamp, message } => { + Self::line_from_message(timestamp, message, Color::Cyan, scroll) + } + GraphicalMessage::Rx { timestamp, message } => { + Self::line_from_message(timestamp, message, Color::Reset, scroll) + } + }) + .collect::>(); + let paragraph = Paragraph::new(text).block(block); + frame.render_widget(paragraph, rect); + } + + pub fn draw_command_bar( + inputs_shared: &Shared, + serial_shared: &Shared, + frame: &mut Frame, + rect: Rect, + ) { + let (port, baudrate, is_connected) = { + let serial_shared = serial_shared + .read() + .expect("Cannot get serial lock for read"); + ( + serial_shared.port.clone(), + serial_shared.baudrate, + matches!(serial_shared.mode, SerialMode::Connected), + ) + }; + let (text, cursor, history_len, current_hint) = { + let inputs_shared = inputs_shared + .read() + .expect("Cannot get inputs lock for read"); + ( + inputs_shared.command_line.clone(), + inputs_shared.cursor as u16, + inputs_shared.history_len, + inputs_shared.current_hint.clone(), + ) + }; + + let cursor = (rect.x + cursor + 2, rect.y + 1); + let bar_color = if is_connected { + Color::Green + } else { + Color::Red + }; + + let block = Block::default() + .title(format!( + "[{:03}] Serial {}:{}bps", + history_len, port, baudrate + )) + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .border_style(Style::default().fg(bar_color)); + + let paragraph = Paragraph::new(Span::from({ + " ".to_string() + + if let Some(hint) = current_hint.as_ref() { + hint + } else { + &text + } + })) + .style(Style::default().fg(if current_hint.is_some() { + Color::DarkGray + } else { + Color::Reset + })) + .block(block); + + frame.render_widget(paragraph, rect); + frame.set_cursor(cursor.0, cursor.1); + } + + pub fn draw_autocomplete_list( + inputs_shared: &Shared, + frame: &mut Frame, + command_bar_y: u16, + ) { + let (autocomplete_list, pattern) = { + let inputs_shared = inputs_shared + .read() + .expect("Cannot get inputs lock for read"); + + ( + inputs_shared.autocomplete_list.clone(), + inputs_shared.pattern.clone(), + ) + }; + + if autocomplete_list.is_empty() { + return; + } + + let max_entries = min(frame.size().height as usize / 2, autocomplete_list.len()); + let mut entries = autocomplete_list[..max_entries].to_vec(); + if entries.len() < autocomplete_list.len() { + entries.push(Arc::new("...".to_string())); + } + + let longest_entry_len = entries + .iter() + .fold(0u16, |len, x| max(len, x.chars().count() as u16)); + let area_size = (longest_entry_len + 5, entries.len() as u16 + 2); + let area = Rect::new( + frame.size().x + 2, + command_bar_y - area_size.1, + area_size.0, + area_size.1, + ); + + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .style(Style::default().fg(Color::Magenta)); + let text = entries + .iter() + .map(|x| { + let is_last = + (x == entries.last().unwrap()) && (entries.len() < autocomplete_list.len()); + + Line::from(vec![ + Span::styled( + format!(" {}", if !is_last { &pattern } else { "" }), + Style::default().fg(Color::Magenta), + ), + Span::styled( + x[pattern.len() - 1..].to_string(), + Style::default().fg(Color::DarkGray), + ), + ]) + }) + .collect::>(); + let paragraph = Paragraph::new(text).block(block); + + frame.render_widget(Clear, area); + frame.render_widget(paragraph, area); + } + + pub fn task( + shared: Arc>, + mut private: GraphicsConnections, + cmd_receiver: Receiver, + ) { + enable_raw_mode().expect("Cannot enable terminal raw mode"); + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture) + .expect("Cannot enable alternate screen and mouse capture"); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).expect("Cannot create terminal backend"); + + let mut blink_color = None; + let mut blink_amounts = 0; + let mut blink_max_amounts = 2; + let mut blink_timer_on = Timer::new(Duration::from_millis(200), || {}); + let mut blink_timer_off = Timer::new(Duration::from_millis(200), || {}); + + 'draw_loop: loop { + blink_timer_on.tick(); + blink_timer_off.tick(); + + if let Ok(cmd) = cmd_receiver.try_recv() { + match cmd { + GraphicsCommand::SaveData => { + if private.recorder.is_recording() { + warning!(private.logger, "Cannot save file while recording."); + continue; + } + + blink_timer_on.start(); + } + GraphicsCommand::RecordData => { + if private.recorder.is_recording() { + private.recorder.stop_record(); + } else { + private.recorder.start_record(); + } + } + GraphicsCommand::Exit => break 'draw_loop, + } + } + + terminal + .draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(f.size().height - Self::COMMAND_BAR_HEIGHT), + Constraint::Length(Self::COMMAND_BAR_HEIGHT), + ]) + .split(f.size()); + + Self::draw_history(&private, f, chunks[0], blink_color); + Self::draw_command_bar( + &private.inputs_shared, + &private.serial_shared, + f, + chunks[1], + ); + Self::draw_autocomplete_list(&private.inputs_shared, f, chunks[1].y); + }) + .expect("Error to draw"); + } + + disable_raw_mode().expect("Cannot disable terminal raw mode"); + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ) + .expect("Cannot disable alternate screen and mouse capture"); + terminal.show_cursor().expect("Cannot show mouse cursor"); + } + + fn timestamp_span(timestamp: &DateTime) -> Span { + Span::styled( + format!("{}", timestamp.format("%H:%M:%S.%3f")), + Style::default().fg(Color::DarkGray), + ) + } + + fn line_from_log_message( + LogMessage { + timestamp, + message, + level, + }: &LogMessage, + (_scroll_y, scroll_x): (u16, u16), + ) -> Line { + let scroll_x = scroll_x as usize; + let mut spans = vec![Self::timestamp_span(timestamp)]; + let (bg, fg) = match level { + crate::infra::LogLevel::Error => (Color::Red, Color::White), + crate::infra::LogLevel::Warning => (Color::Yellow, Color::DarkGray), + crate::infra::LogLevel::Success => (Color::LightGreen, Color::DarkGray), + crate::infra::LogLevel::Info => (Color::White, Color::DarkGray), + crate::infra::LogLevel::Debug => (Color::Reset, Color::DarkGray), + }; + + if scroll_x < message.chars().count() { + spans.push(Span::styled( + &message[scroll_x..], + Style::default().fg(fg).bg(bg), + )); + } + + Line::from(spans) + } + + fn line_from_message<'a>( + timestamp: &'a DateTime, + message: &'a [(String, Color)], + bg: Color, + (_scroll_y, scroll_x): (u16, u16), + ) -> Line<'a> { + let scroll_x = scroll_x as usize; + let mut offset = 0; + let mut spans = vec![Self::timestamp_span(×tamp)]; + + for (msg, fg) in message { + if scroll_x >= msg.len() + offset { + offset += msg.len(); + continue; + } + + let cropped_message = if scroll_x < (msg.len() + offset) { + &msg[(scroll_x - offset)..] + } else { + &msg + }; + offset += msg.len(); + + spans.push(Span::styled( + cropped_message, + Style::default().fg(*fg).bg(bg), + )); + } + + Line::from(spans) + } + + fn max_main_axis(frame_height: u16, history: &Vec) -> u16 { + let main_axis_length = frame_height - Self::COMMAND_BAR_HEIGHT - 2; + let history_len = history.len() as u16; + + if history_len > main_axis_length { + history_len - main_axis_length + } else { + 0 + } + } +} + +impl GraphicsConnections { + pub fn new( + logger: Logger, + logger_receiver: Receiver, + tx: Consumer>, + rx: Consumer>, + inputs_shared: Shared, + serial_shared: Shared, + storage_base_filename: String, + capacity: usize, + ) -> Self { + Self { + logger, + logger_receiver, + tx, + rx, + inputs_shared, + serial_shared, + history: vec![], + typewriter: TypeWriter::new(storage_base_filename.clone()), + recorder: Recorder::new(storage_base_filename).expect("Cannot create Recorder"), + capacity, + auto_scroll: true, + scroll: (0, 0), + } + } +} diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs new file mode 100644 index 0000000..842261c --- /dev/null +++ b/src/graphics/mod.rs @@ -0,0 +1,2 @@ +pub mod graphics_task; +pub mod rich_string; diff --git a/src/rich_string.rs b/src/graphics/rich_string.rs similarity index 98% rename from src/rich_string.rs rename to src/graphics/rich_string.rs index af172c6..301a137 100644 --- a/src/rich_string.rs +++ b/src/graphics/rich_string.rs @@ -1,4 +1,3 @@ -use crate::text::ViewData; use chrono::{DateTime, Local}; use ratatui::style::{Color, Style}; use ratatui::text::Span; @@ -74,6 +73,17 @@ pub struct RichTextWithInvisible { rich_texts: Vec, } +pub struct ViewData { + timestamp: DateTime, + data: Vec, +} + +impl ViewData { + pub fn new(timestamp: DateTime, data: Vec) -> Self { + Self { timestamp, data } + } +} + impl RichTextWithInvisible { pub fn into_view_data(self, timestamp: DateTime) -> ViewData { ViewData::new(timestamp, self.rich_texts) diff --git a/src/infra/blink.rs b/src/infra/blink.rs new file mode 100644 index 0000000..1500ac5 --- /dev/null +++ b/src/infra/blink.rs @@ -0,0 +1,53 @@ +use std::time::Duration; + +use super::timer::Timer; + +pub struct Blink { + on: T, + off: T, + current: Option, + timer_on: Timer, + timer_off: Timer, + total_blinks: usize, + num_blinks: usize, +} + +impl Blink { + pub fn new(duration: Duration, total_blinks: usize, on: T, off: T) -> Self { + Self { + on, + off, + current: None, + timer_on: Timer::new(duration), + timer_off: Timer::new(duration), + total_blinks, + num_blinks: 0, + } + } + + fn timer_on_timeout(&mut self) { + self.timer_off.start(); + self.current = Some(self.off.clone()); + } + + fn timer_off_timeout(&mut self) { + self.timer_on.start(); + self.current = Some(self.on.clone()); + } + + pub fn start(&mut self) { + self.timer_on.start(); + self.current = Some(self.on.clone()); + + self.timer_on.set_action(|| self.timer_on_timeout()); + } + + pub fn tick(&mut self) { + self.timer_on.tick(); + self.timer_off.tick(); + } + + pub fn get_current(&self) -> Option<&T> { + self.current.as_ref() + } +} diff --git a/src/infra/logger.rs b/src/infra/logger.rs new file mode 100644 index 0000000..24201e1 --- /dev/null +++ b/src/infra/logger.rs @@ -0,0 +1,77 @@ +use std::sync::mpsc::{channel, Receiver, Sender}; + +use chrono::{DateTime, Local}; + +#[derive(Clone)] +pub struct Logger { + sender: Sender, +} + +pub struct LogMessage { + pub timestamp: DateTime, + pub message: String, + pub level: LogLevel, +} + +pub enum LogLevel { + Error, + Warning, + Success, + Info, + Debug, +} + +impl Logger { + pub fn new() -> (Self, Receiver) { + let (sender, receiver) = channel(); + + (Self { sender }, receiver) + } + + pub fn write( + &self, + message: String, + level: LogLevel, + ) -> Result<(), std::sync::mpsc::SendError> { + self.sender.send(LogMessage { + timestamp: Local::now(), + message, + level, + }) + } +} + +#[macro_export] +macro_rules! debug { + ($logger:expr, $($arg:tt)+) => { + $logger.write(format!($($arg)+), LogLevel::Debug) + }; +} + +#[macro_export] +macro_rules! info { + ($logger:expr, $($arg:tt)+) => { + $logger.write(format!($($arg)+), LogLevel::Info) + }; +} + +#[macro_export] +macro_rules! success { + ($logger:expr, $($arg:tt)+) => { + $logger.write(format!($($arg)+), LogLevel::Success) + }; +} + +#[macro_export] +macro_rules! warning { + ($logger:expr, $($arg:tt)+) => { + $logger.write(format!($($arg)+), LogLevel::Warning) + }; +} + +#[macro_export] +macro_rules! error { + ($logger:expr, $($arg:tt)+) => { + $logger.write(format!($($arg)+), LogLevel::Error) + }; +} diff --git a/src/infra/messages.rs b/src/infra/messages.rs new file mode 100644 index 0000000..ea085e5 --- /dev/null +++ b/src/infra/messages.rs @@ -0,0 +1,7 @@ +use chrono::{DateTime, Local}; + +#[derive(Default)] +pub struct TimedBytes { + pub timestamp: DateTime, + pub message: Vec, +} diff --git a/src/infra/mod.rs b/src/infra/mod.rs new file mode 100644 index 0000000..55d5fa1 --- /dev/null +++ b/src/infra/mod.rs @@ -0,0 +1,21 @@ +pub mod blink; +pub mod logger; +pub mod messages; +pub mod mpmc; +pub mod recorder; +pub mod task; +pub mod timer; +pub mod typewriter; + +pub use logger::LogLevel; + +fn into_byte_format(size: u128) -> String { + let (size, unit) = match size { + x if x < 1024 => return format!("{} Bytes", size), + x if x < 1024 * 1024 => (size as f32 / 1024.0, "KB"), + x if x < 1024 * 1024 * 1024 => (size as f32 / (1024.0 * 1024.0), "MB"), + _ => (size as f32 / (1024.0 * 1024.0 * 1024.0), "GB"), + }; + + format!("{:.1} {}", size, unit) +} diff --git a/src/infra/mpmc.rs b/src/infra/mpmc.rs new file mode 100644 index 0000000..eb8132c --- /dev/null +++ b/src/infra/mpmc.rs @@ -0,0 +1,64 @@ +use std::sync::{mpsc, Arc}; + +pub type Id = usize; + +#[derive(Default)] +pub struct Channel { + channels: Vec<(Id, mpsc::Sender)>, +} + +pub struct Consumer { + id: Id, + receiver: mpsc::Receiver, +} + +pub struct Producer { + channel: Arc>, +} + +impl Channel { + pub fn new_consumer(&mut self) -> Consumer { + let id = self.channels.len() + 1; + let (sender, receiver) = mpsc::channel(); + self.channels.push((id, sender)); + + Consumer { id, receiver } + } + + pub fn new_producer(self: Arc) -> Producer { + Producer { channel: self } + } + + fn send_data(self: Arc, data: T, id: Option) { + self.channels + .iter() + .filter(|(item_id, _sender)| *item_id != id.unwrap_or(0)) + .for_each(|(_id, sender)| { + let _ = sender.send(data.clone()); + }); + } +} + +impl Consumer { + pub fn id(&self) -> Id { + self.id + } + + pub fn recv(&self) -> Result { + self.receiver.recv() + } + + pub fn try_recv(&self) -> Result { + self.receiver.try_recv() + } +} + +impl Producer { + pub fn produce(&self, data: T) { + self.channel.clone().send_data(data, None) + } + + pub fn produce_without_loopback(&self, data: T, id: Id) { + self.channel.clone().send_data(data, Some(id)) + } +} diff --git a/src/recorder.rs b/src/infra/recorder.rs similarity index 98% rename from src/recorder.rs rename to src/infra/recorder.rs index 9f980e8..de289ef 100644 --- a/src/recorder.rs +++ b/src/infra/recorder.rs @@ -1,8 +1,9 @@ -use crate::text::into_byte_format; use std::path::PathBuf; use tokio::fs::File; use tokio::io::AsyncWriteExt; +use super::into_byte_format; + pub struct Recorder { base_filename: String, suffix: usize, diff --git a/src/infra/task.rs b/src/infra/task.rs new file mode 100644 index 0000000..47d68f2 --- /dev/null +++ b/src/infra/task.rs @@ -0,0 +1,78 @@ +use std::{ + sync::{ + mpsc::{Receiver, Sender}, + Arc, RwLock, + }, + thread::{self, JoinHandle}, +}; + +pub struct Task { + shared: Arc>, + cmd_sender: Sender, + handler: JoinHandle<()>, +} + +pub struct Shared { + shared: Arc>, +} + +impl Task +where + M: Send + 'static, + S: Send + Sync + 'static, +{ + pub fn new( + shared: S, + private: P, + run_fn: fn(Arc>, P, Receiver), + cmd_sender: Sender, + cmd_receiver: Receiver, + ) -> Self { + let shared = Arc::new(RwLock::new(shared)); + let shared_clone = shared.clone(); + + let handler = thread::spawn(move || { + run_fn(shared_clone, private, cmd_receiver); + }); + + Self { + shared, + cmd_sender, + handler, + } + } + + pub fn join(self) { + let _ = self.handler.join(); + } + + pub fn shared_ref(&self) -> Shared { + Shared { + shared: self.shared.clone(), + } + } + + pub fn cmd_sender(&self) -> Sender { + self.cmd_sender.clone() + } +} + +impl Shared { + pub fn read( + &self, + ) -> Result< + std::sync::RwLockReadGuard<'_, S>, + std::sync::PoisonError>, + > { + self.shared.read() + } + + pub fn try_read( + &self, + ) -> Result< + std::sync::RwLockReadGuard<'_, S>, + std::sync::TryLockError>, + > { + self.shared.try_read() + } +} diff --git a/src/infra/timer.rs b/src/infra/timer.rs new file mode 100644 index 0000000..8589fc4 --- /dev/null +++ b/src/infra/timer.rs @@ -0,0 +1,45 @@ +use std::time::{Duration, Instant}; + +pub struct Timer { + duration: Duration, + action: Option, + now: Option, +} + +impl Timer { + pub fn new(duration: Duration) -> Self { + Self { + duration, + action: None, + now: None, + } + } + + pub fn set_action(&mut self, action: F) { + self.action = Some(action); + } + + pub fn start(&mut self) { + self.now = Some(Instant::now()); + } + + pub fn is_action_empty(&self) -> bool { + self.action.as_ref().is_none() + } + + pub fn tick(&mut self) { + let Some(now) = self.now.as_ref() else { + return; + }; + + if now.elapsed() < self.duration { + return; + } + + if let Some(action) = self.action.as_mut() { + (action)(); + } + + self.now.take(); + } +} diff --git a/src/typewriter.rs b/src/infra/typewriter.rs similarity index 97% rename from src/typewriter.rs rename to src/infra/typewriter.rs index c37d5ed..a2fa1ff 100644 --- a/src/typewriter.rs +++ b/src/infra/typewriter.rs @@ -1,8 +1,9 @@ -use crate::text::into_byte_format; use std::ops::AddAssign; use tokio::fs::OpenOptions; use tokio::io::AsyncWriteExt; +use super::into_byte_format; + pub struct TypeWriter { contents: Vec, filename: String, diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs new file mode 100644 index 0000000..dd86e34 --- /dev/null +++ b/src/inputs/inputs_task.rs @@ -0,0 +1,73 @@ +use std::sync::{ + mpsc::{Receiver, Sender}, + Arc, RwLock, +}; + +use crate::{ + graphics::graphics_task::GraphicsCommand, + infra::{logger::Logger, messages::TimedBytes, mpmc::Producer, task::Task}, + plugin::plugin_engine::PluginEngineCommand, + serial::serial_if::SerialCommand, +}; + +pub type InputsTask = Task; + +#[derive(Default)] +pub struct InputsShared { + pub command_line: String, + pub cursor: usize, + pub history_len: usize, + pub current_hint: Option, + pub autocomplete_list: Vec>, + pub pattern: String, +} + +pub struct InputsConnections { + logger: Logger, + tx: Producer>, + graphics_cmd_sender: Sender, + serial_if_cmd_sender: Sender, + plugin_engine_cmd_sender: Sender, +} + +impl InputsTask { + pub fn spawn_inputs_task( + inputs_connections: InputsConnections, + inputs_cmd_sender: Sender<()>, + inputs_cmd_receiver: Receiver<()>, + ) -> Self { + Self::new( + InputsShared::default(), + inputs_connections, + Self::task, + inputs_cmd_sender, + inputs_cmd_receiver, + ) + } + + pub fn task( + _shared: Arc>, + _private: InputsConnections, + _inputs_cmd_receiver: Receiver<()>, + ) { + todo!() + } +} + +impl InputsConnections { + pub fn new( + logger: Logger, + tx: Producer>, + graphics_cmd_sender: Sender, + serial_if_cmd_sender: Sender, + plugin_engine_cmd_sender: Sender, + ) -> Self { + Self { + logger, + tx, + graphics_cmd_sender, + serial_if_cmd_sender, + plugin_engine_cmd_sender, + } + } +} diff --git a/src/inputs/mod.rs b/src/inputs/mod.rs new file mode 100644 index 0000000..9d3fada --- /dev/null +++ b/src/inputs/mod.rs @@ -0,0 +1 @@ +pub mod inputs_task; diff --git a/src/main.rs b/src/main.rs index db82588..5324f68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,133 +1,113 @@ #![deny(warnings)] +// TODO remove this allow after migration +#![allow(unused)] extern crate core; -use crate::command_bar::CommandBar; -use crate::serial::SerialIF; -use chrono::Local; -use clap::{Parser, Subcommand}; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::execute; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -}; -use ratatui::backend::{Backend, CrosstermBackend}; -use ratatui::Terminal; -use std::io; -use std::io::Stdout; -use std::path::PathBuf; - -mod blink_color; -mod command_bar; -mod error_pop_up; -mod messages; -mod mpmc; -mod recorder; -mod rich_string; +mod graphics; +mod infra; +mod inputs; +mod plugin; mod serial; -mod task_bridge; -mod text; -mod typewriter; - -pub type ConcreteBackend = CrosstermBackend; -#[derive(Subcommand)] -pub enum Commands { - Serial { - port: Option, - baudrate: Option, - }, - Ble { - name_device: String, - mtu: u32, - }, -} +use chrono::Local; +use graphics::graphics_task::{GraphicsConnections, GraphicsTask}; +use infra::logger::Logger; +use infra::mpmc::Channel; +use inputs::inputs_task::{InputsConnections, InputsTask}; +use plugin::plugin_engine::{PluginEngine, PluginEngineConnections}; +use serial::serial_if::{SerialConnections, SerialInterface, SerialSetup}; +use std::sync::mpsc::channel; +use std::sync::Arc; + +fn app(capacity: usize) -> Result<(), String> { + let (logger, logger_receiver) = Logger::new(); + let mut tx_channel = Channel::default(); + let mut rx_channel = Channel::default(); + + let mut tx_channel_consumers = (0..3) + .map(|_| tx_channel.new_consumer()) + .collect::>(); + let mut rx_channel_consumers = (0..2) + .map(|_| rx_channel.new_consumer()) + .collect::>(); + + let rx_channel = Arc::new(rx_channel); + let tx_channel = Arc::new(tx_channel); + + let (serial_if_cmd_sender, serial_if_cmd_receiver) = channel(); + let (inputs_cmd_sender, inputs_cmd_receiver) = channel(); + let (graphics_cmd_sender, graphics_cmd_receiver) = channel(); + let (plugin_engine_cmd_sender, plugin_engine_cmd_receiver) = channel(); + + let serial_connections = SerialConnections::new( + logger.clone(), + tx_channel_consumers.pop().unwrap(), + rx_channel.clone().new_producer(), + ); + let inputs_connections = InputsConnections::new( + logger.clone(), + tx_channel.clone().new_producer(), + graphics_cmd_sender.clone(), + serial_if_cmd_sender.clone(), + plugin_engine_cmd_sender.clone(), + ); + let plugin_engine_connections = PluginEngineConnections::new( + logger.clone(), + tx_channel.new_producer(), + tx_channel_consumers.pop().unwrap(), + rx_channel_consumers.pop().unwrap(), + ); + + let serial_if = SerialInterface::spawn_serial_interface( + serial_connections, + serial_if_cmd_sender, + serial_if_cmd_receiver, + SerialSetup::default(), + ); + let inputs_task = + InputsTask::spawn_inputs_task(inputs_connections, inputs_cmd_sender, inputs_cmd_receiver); + + let inputs_shared = inputs_task.shared_ref(); + let serial_shared = serial_if.shared_ref(); + + let now_str = Local::now().format("%Y%m%d_%H%M%S"); + let storage_base_filename = format!("{}.txt", now_str); + let graphics_connections = GraphicsConnections::new( + logger.clone(), + logger_receiver, + tx_channel_consumers.pop().unwrap(), + rx_channel_consumers.pop().unwrap(), + inputs_shared, + serial_shared, + storage_base_filename, + capacity, + ); + let text_view = GraphicsTask::spawn_graphics_task( + graphics_connections, + graphics_cmd_sender, + graphics_cmd_receiver, + ); + let plugin_engine = PluginEngine::spawn_plugin_engine( + plugin_engine_connections, + plugin_engine_cmd_sender, + plugin_engine_cmd_receiver, + ); + + serial_if.join(); + inputs_task.join(); + text_view.join(); + plugin_engine.join(); -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, - #[clap(short, long)] - view_length: Option, - #[clap(short, long)] - cmd_file: Option, + Ok(()) } -const CMD_FILEPATH: &str = "cmds.yaml"; -const CAPACITY: usize = 2000; - -async fn app() -> Result<(), String> { +fn main() { #[cfg(target_os = "windows")] ctrlc::set_handler(|| { /* Do nothing on user ctrl+c */ }) .expect("Error setting Ctrl-C handler"); - let cli = Cli::parse(); - - let view_length = cli.view_length.unwrap_or(CAPACITY); - let cmd_file = cli.cmd_file.unwrap_or(PathBuf::from(CMD_FILEPATH)); - let interface; - - match cli.command { - Commands::Serial { port, baudrate } => { - let port_select = port.unwrap_or("".to_string()); - let baudrate_select = baudrate.unwrap_or(0); - - interface = SerialIF::build_and_connect(&port_select, baudrate_select); - } - _ => { - unimplemented!() - } - } - - enable_raw_mode().map_err(|_| "Cannot enable terminal raw mode".to_string())?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture) - .map_err(|_| "Cannot enable alternate screen and mouse capture".to_string())?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = - Terminal::new(backend).map_err(|_| "Cannot create terminal backend".to_string())?; - - let datetime = Local::now().format("%Y%m%d_%H%M%S"); - let mut command_bar = CommandBar::new(interface, view_length, format!("{}.txt", datetime)) - .with_command_file(cmd_file.as_path().to_str().unwrap()); - - 'main: loop { - { - let text_view = command_bar.get_text_view().await; - let interface = command_bar.get_interface().await; - - terminal - .draw(|f| command_bar.draw(f, &text_view, &interface)) - .map_err(|_| "Fail at terminal draw".to_string())?; - } - - if command_bar - .update(terminal.backend().size().unwrap()) - .await - .is_err() - { - break 'main; - } - } - - disable_raw_mode().map_err(|_| "Cannot disable terminal raw mode".to_string())?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ) - .map_err(|_| "Cannot disable alternate screen and mouse capture".to_string())?; - terminal - .show_cursor() - .map_err(|_| "Cannot show mouse cursor".to_string())?; - - Ok(()) -} - -#[tokio::main] -async fn main() { - if let Err(err) = app().await { + if let Err(err) = app() { println!("[\x1b[31mERR\x1b[0m] {}", err); } } diff --git a/src/blink_color.rs b/src/old/blink_color.rs similarity index 100% rename from src/blink_color.rs rename to src/old/blink_color.rs diff --git a/src/command_bar.rs b/src/old/command_bar.rs similarity index 100% rename from src/command_bar.rs rename to src/old/command_bar.rs diff --git a/src/messages.rs b/src/old/messages.rs similarity index 100% rename from src/messages.rs rename to src/old/messages.rs diff --git a/src/serial.rs b/src/old/serial.rs similarity index 100% rename from src/serial.rs rename to src/old/serial.rs diff --git a/src/task_bridge.rs b/src/old/task_bridge.rs similarity index 100% rename from src/task_bridge.rs rename to src/old/task_bridge.rs diff --git a/src/text.rs b/src/old/text.rs similarity index 100% rename from src/text.rs rename to src/old/text.rs diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 0000000..73e9077 --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1 @@ +pub mod plugin_engine; diff --git a/src/plugin/plugin_engine.rs b/src/plugin/plugin_engine.rs new file mode 100644 index 0000000..fc657ea --- /dev/null +++ b/src/plugin/plugin_engine.rs @@ -0,0 +1,58 @@ +use std::sync::{ + mpsc::{Receiver, Sender}, + Arc, RwLock, +}; + +use crate::infra::{ + logger::Logger, + messages::TimedBytes, + mpmc::{Consumer, Producer}, + task::Task, +}; + +pub type PluginEngine = Task<(), PluginEngineCommand>; + +pub enum PluginEngineCommand { + Exit, +} + +pub struct PluginEngineConnections { + logger: Logger, + tx_producer: Producer>, + tx_consumer: Consumer>, + rx: Consumer>, +} + +impl PluginEngine { + pub fn spawn_plugin_engine( + connections: PluginEngineConnections, + sender: Sender, + receiver: Receiver, + ) -> Self { + Self::new((), connections, Self::task, sender, receiver) + } + + pub fn task( + _shared: Arc>, + _private: PluginEngineConnections, + _receiver: Receiver, + ) { + todo!() + } +} + +impl PluginEngineConnections { + pub fn new( + logger: Logger, + tx_producer: Producer>, + tx_consumer: Consumer>, + rx: Consumer>, + ) -> Self { + Self { + logger, + tx_producer, + tx_consumer, + rx, + } + } +} diff --git a/src/serial/mod.rs b/src/serial/mod.rs new file mode 100644 index 0000000..eab5cca --- /dev/null +++ b/src/serial/mod.rs @@ -0,0 +1 @@ +pub mod serial_if; diff --git a/src/serial/serial_if.rs b/src/serial/serial_if.rs new file mode 100644 index 0000000..4ae5419 --- /dev/null +++ b/src/serial/serial_if.rs @@ -0,0 +1,284 @@ +use crate::{ + error, + infra::{ + logger::{self, LogLevel, Logger}, + messages::TimedBytes, + mpmc::{Consumer, Producer}, + task::Task, + }, +}; +use chrono::Local; +use serialport::{DataBits, FlowControl, Parity, SerialPortBuilder, StopBits, TTYPort}; +use std::{ + io::{self, Read, Write}, + sync::{ + mpsc::{Receiver, Sender}, + Arc, RwLock, + }, + time::{Duration, Instant}, +}; + +pub type SerialInterface = Task; + +#[cfg(target_os = "linux")] +type SerialPort = TTYPort; +#[cfg(target_os = "windows")] +type SerialPort = COMPort; + +pub struct SerialShared { + pub port: String, + pub baudrate: u32, + pub mode: SerialMode, + pub data_bits: DataBits, + pub flow_control: FlowControl, + pub parity: Parity, + pub stop_bits: StopBits, +} + +#[derive(Default)] +pub struct SerialSetup { + pub port: Option, + pub baudrate: Option, + pub data_bits: Option, + pub flow_control: Option, + pub parity: Option, + pub stop_bits: Option, +} + +pub struct SerialConnections { + logger: Logger, + tx: Consumer>, + rx: Producer>, +} + +pub enum SerialCommand { + Connect, + Disconnect, + Exit, + Setup(SerialSetup), +} + +#[derive(Copy, Clone)] +pub enum SerialMode { + DoNotConnect, + Reconnecting, + Connected, +} + +impl SerialInterface { + const NEW_LINE_TIMEOUT_MS: u128 = 1_000; + const SERIAL_TIMEOUT_MS: u64 = 100; + + pub fn spawn_serial_interface( + connections: SerialConnections, + cmd_sender: Sender, + cmd_receiver: Receiver, + setup: SerialSetup, + ) -> Self { + Self::new( + SerialShared { + port: setup.port.clone().unwrap_or("".to_string()), + baudrate: setup.baudrate.unwrap_or(0), + data_bits: setup.data_bits.unwrap_or(DataBits::Eight), + flow_control: setup.flow_control.unwrap_or(FlowControl::None), + parity: setup.parity.unwrap_or(Parity::None), + stop_bits: setup.stop_bits.unwrap_or(StopBits::One), + mode: if !setup.port.unwrap_or("".to_string()).is_empty() + && setup.baudrate.unwrap_or(0) != 0 + { + SerialMode::Reconnecting + } else { + SerialMode::DoNotConnect + }, + }, + connections, + Self::task, + cmd_sender, + cmd_receiver, + ) + } + + fn task( + shared: Arc>, + connections: SerialConnections, + cmd_receiver: Receiver, + ) { + let SerialConnections { logger, tx, rx } = connections; + let mut line = vec![]; + let mut buffer = [0u8]; + let mut mode = shared + .read() + .expect("Cannot get serial shared for read") + .mode; + #[cfg(target_os = "linux")] + let mut serial: Option = None; + let mut now = Instant::now(); + + 'task_loop: loop { + if let Ok(cmd) = cmd_receiver.try_recv() { + match cmd { + SerialCommand::Connect => mode = Self::connect(shared.clone(), &mut serial), + SerialCommand::Disconnect => { + mode = Self::disconnect(shared.clone(), &mut serial); + } + SerialCommand::Exit => break 'task_loop, + SerialCommand::Setup(setup) => { + if let Some(new_mode) = Self::setup(shared.clone(), setup, &mut serial) { + mode = new_mode; + } + } + } + } + + match mode { + SerialMode::DoNotConnect => { + std::thread::yield_now(); + continue; + } + SerialMode::Reconnecting => { + mode = Self::connect(shared.clone(), &mut serial); + } + SerialMode::Connected => { /* Do nothing. It's already connected. */ } + } + + let Some(mut ser) = serial.take() else { + std::thread::yield_now(); + continue; + }; + + if let Ok(data_to_sent) = tx.try_recv() { + if ser.write_all(data_to_sent.message.as_slice()).is_err() { + error!(logger, "Cannot sent: {:?}", data_to_sent.message); + } + } + + match ser.read(&mut buffer) { + Ok(_) => { + now = Instant::now(); + line.push(buffer[0]); + if buffer[0] == b'\n' { + rx.produce(Arc::new(TimedBytes { + timestamp: Local::now(), + message: line.drain(..).collect(), + })); + now = Instant::now(); + } + } + Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {} + Err(ref e) + if e.kind() == io::ErrorKind::PermissionDenied + || e.kind() == io::ErrorKind::BrokenPipe => + { + mode = Self::disconnect(shared.clone(), &mut Some(ser)); + continue; + } + Err(_) => {} + } + + if now.elapsed().as_millis() > Self::NEW_LINE_TIMEOUT_MS { + now = Instant::now(); + + if !line.is_empty() { + rx.produce(Arc::new(TimedBytes { + timestamp: Local::now(), + message: line.drain(..).collect(), + })); + } + } + + serial = Some(ser); + } + } + + fn connect(shared: Arc>, serial: &mut Option) -> SerialMode { + let sw = shared + .read() + .expect("Cannot get serial share lock for write"); + + let connect_res = serialport::new(sw.port.clone(), sw.baudrate) + .data_bits(sw.data_bits) + .flow_control(sw.flow_control) + .parity(sw.parity) + .stop_bits(sw.stop_bits) + .timeout(Duration::from_millis(Self::SERIAL_TIMEOUT_MS)) + .open_native(); + match connect_res { + Ok(ser) => { + *serial = Some(ser); + SerialMode::Connected + } + Err(_) => { + let _ = serial.take(); + SerialMode::Reconnecting + } + } + } + + fn disconnect( + shared: Arc>, + serial: &mut Option, + ) -> SerialMode { + let _ = serial.take(); + let mut sw = shared.write().expect(""); + sw.mode = SerialMode::DoNotConnect; + sw.mode + } + + fn setup( + shared: Arc>, + setup: SerialSetup, + serial: &mut Option, + ) -> Option { + let mut has_changes = false; + let mut sw = shared + .write() + .expect("Cannot get serial shared lock for write"); + + if let Some(port) = setup.port { + sw.port = port; + has_changes = true; + } + + if let Some(baudrate) = setup.baudrate { + sw.baudrate = baudrate; + has_changes = true; + } + + if let Some(databits) = setup.data_bits { + sw.data_bits = databits; + has_changes = true; + } + + if let Some(flow_control) = setup.flow_control { + sw.flow_control = flow_control; + has_changes = true; + } + + if let Some(parity) = setup.parity { + sw.parity = parity; + has_changes = true; + } + + if let Some(stop_bits) = setup.stop_bits { + sw.stop_bits = stop_bits; + has_changes = true; + } + + if has_changes { + Self::disconnect(shared.clone(), serial); + Some(SerialMode::Reconnecting) + } else { + None + } + } +} + +impl SerialConnections { + pub fn new( + logger: Logger, + tx: Consumer>, + rx: Producer>, + ) -> Self { + Self { logger, tx, rx } + } +} From 5270d2291d6f4c78f9443151ddb1578939a90162 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 13 Jul 2024 22:18:21 -0300 Subject: [PATCH 11/49] feat: finish of new archtecture migration --- src/graphics/graphics_task.rs | 276 ++++++++++++-- src/graphics/mod.rs | 5 +- src/graphics/rich_string.rs | 398 -------------------- src/infra/blink.rs | 73 ++-- src/infra/logger.rs | 11 +- src/infra/mpmc.rs | 3 + src/infra/recorder.rs | 32 +- src/infra/task.rs | 2 + src/infra/timer.rs | 29 +- src/infra/typewriter.rs | 30 +- src/inputs/inputs_task.rs | 444 +++++++++++++++++++++- src/main.rs | 71 +++- src/old/blink_color.rs | 71 ---- src/old/command_bar.rs | 680 ---------------------------------- src/old/messages.rs | 243 ------------ src/old/serial.rs | 276 -------------- src/old/task_bridge.rs | 70 ---- src/old/text.rs | 249 ------------- src/plugin/plugin_engine.rs | 22 +- src/serial/serial_if.rs | 116 ++++-- 20 files changed, 948 insertions(+), 2153 deletions(-) delete mode 100644 src/graphics/rich_string.rs delete mode 100644 src/old/blink_color.rs delete mode 100644 src/old/command_bar.rs delete mode 100644 src/old/messages.rs delete mode 100644 src/old/serial.rs delete mode 100644 src/old/task_bridge.rs delete mode 100644 src/old/text.rs diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index c0dc807..a68ee92 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -1,11 +1,12 @@ +use crate::{error, info, success}; use crate::{ infra::{ + blink::Blink, logger::{LogLevel, LogMessage, Logger}, messages::TimedBytes, mpmc::Consumer, recorder::Recorder, task::{Shared, Task}, - timer::Timer, typewriter::TypeWriter, }, inputs::inputs_task::InputsShared, @@ -21,17 +22,17 @@ use crossterm::{ use ratatui::{ backend::CrosstermBackend, layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style, Stylize}, + style::{Color, Style}, text::{Line, Span}, widgets::{block::Title, Block, BorderType, Borders, Clear, Paragraph}, Frame, Terminal, }; +use std::collections::VecDeque; use std::{ cmp::{max, min}, time::Duration, }; use std::{ - fs::File, io, sync::{ mpsc::{Receiver, Sender}, @@ -39,7 +40,7 @@ use std::{ }, }; -use super::rich_string::RichText; +use super::Serialize; pub type GraphicsTask = Task<(), GraphicsCommand>; @@ -50,17 +51,27 @@ pub struct GraphicsConnections { rx: Consumer>, inputs_shared: Shared, serial_shared: Shared, - history: Vec, + history: VecDeque, typewriter: TypeWriter, recorder: Recorder, capacity: usize, auto_scroll: bool, scroll: (u16, u16), + last_frame_height: u16, } pub enum GraphicsCommand { SaveData, RecordData, + ScrollLeft, + ScrollRight, + ScrollUp, + ScrollDown, + JumpToStart, + JumpToEnd, + PageUp, + PageDown, + Clear, Exit, } @@ -88,22 +99,20 @@ impl GraphicsTask { } fn draw_history( - private: &GraphicsConnections, + private: &mut GraphicsConnections, frame: &mut Frame, rect: Rect, - blink_color: Option, + blink_color: Color, ) { + private.last_frame_height = frame.size().height; let scroll = if private.auto_scroll { - ( - Self::max_main_axis(frame.size().height, &private.history), - private.scroll.1, - ) + (Self::max_main_axis(&private), private.scroll.1) } else { private.scroll }; let (coll, coll_size) = ( - &private.history[(scroll.0 as usize)..], + private.history.range(scroll.0 as usize..), private.history.len(), ); let border_type = if private.auto_scroll { @@ -139,11 +148,10 @@ impl GraphicsTask { ) .borders(Borders::ALL) .border_type(border_type) - .border_style(Style::default().fg(blink_color.unwrap_or(Color::Reset))) + .border_style(Style::default().fg(blink_color)) }; let text = coll - .iter() .map(|msg| match msg { GraphicalMessage::Log(log_msg) => Self::line_from_log_message(log_msg, scroll), GraphicalMessage::Tx { timestamp, message } => { @@ -185,6 +193,11 @@ impl GraphicsTask { inputs_shared.current_hint.clone(), ) }; + let port = if port.is_empty() { + "\"\"".to_string() + } else { + port + }; let cursor = (rect.x + cursor + 2, rect.y + 1); let bar_color = if is_connected { @@ -195,7 +208,7 @@ impl GraphicsTask { let block = Block::default() .title(format!( - "[{:03}] Serial {}:{}bps", + "[{:03}] Serial {}:{:04}bps", history_len, port, baudrate )) .borders(Borders::ALL) @@ -287,7 +300,7 @@ impl GraphicsTask { } pub fn task( - shared: Arc>, + _shared: Arc>, mut private: GraphicsConnections, cmd_receiver: Receiver, ) { @@ -297,16 +310,14 @@ impl GraphicsTask { .expect("Cannot enable alternate screen and mouse capture"); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend).expect("Cannot create terminal backend"); - - let mut blink_color = None; - let mut blink_amounts = 0; - let mut blink_max_amounts = 2; - let mut blink_timer_on = Timer::new(Duration::from_millis(200), || {}); - let mut blink_timer_off = Timer::new(Duration::from_millis(200), || {}); + let blink = Blink::new(Duration::from_millis(200), 2, Color::Reset, Color::Black); + let mut new_messages = vec![]; 'draw_loop: loop { - blink_timer_on.tick(); - blink_timer_off.tick(); + { + let mut blk = blink.lock().unwrap(); + blk.tick(); + } if let Ok(cmd) = cmd_receiver.try_recv() { match cmd { @@ -316,19 +327,155 @@ impl GraphicsTask { continue; } - blink_timer_on.start(); + { + let mut blk = blink.lock().unwrap(); + blk.start(); + } + let filename = private.typewriter.get_filename(); + + match private.typewriter.flush() { + Ok(_) => success!(private.logger, "Content save on \"{}\"", filename), + Err(err) => { + error!(private.logger, "Cannot save on \"{}\": {}", filename, err) + } + } } GraphicsCommand::RecordData => { + let filename = private.recorder.get_filename(); + if private.recorder.is_recording() { private.recorder.stop_record(); + success!(private.logger, "Content recoded on \"{}\"", filename); + } else { + match private.recorder.start_record() { + Ok(_) => info!( + private.logger, + "Recording content on \"{}\"...", filename + ), + Err(err) => error!( + private.logger, + "Cannot start record the content on \"{}\": {}", filename, err + ), + } + } + } + GraphicsCommand::Clear => { + private.auto_scroll = true; + private.scroll = (0, 0); + private.history.clear(); + } + GraphicsCommand::ScrollLeft => { + if private.scroll.1 < 3 { + private.scroll.1 = 0; + } else { + private.scroll.1 -= 3; + } + } + GraphicsCommand::ScrollRight => private.scroll.1 += 3, + GraphicsCommand::ScrollUp => { + if Self::max_main_axis(&private) > 0 { + private.auto_scroll = false; + } + + if private.scroll.0 < 3 { + private.scroll.0 = 0; + } else { + private.scroll.0 -= 3; + } + } + GraphicsCommand::ScrollDown => { + let max_main_axis = Self::max_main_axis(&private); + + private.scroll.0 += 3; + private.scroll.0 = private.scroll.0.clamp(0, max_main_axis); + + if private.scroll.0 == max_main_axis { + private.auto_scroll = true; + } + } + GraphicsCommand::JumpToStart => { + if Self::max_main_axis(&private) > 0 { + private.auto_scroll = false; + } + + private.scroll.0 = 0; + } + GraphicsCommand::JumpToEnd => { + let max_main_axis = Self::max_main_axis(&private); + + private.scroll.0 = max_main_axis; + private.auto_scroll = true; + } + GraphicsCommand::PageUp => { + if Self::max_main_axis(&private) > 0 { + private.auto_scroll = false; + } + + let page_height = private.last_frame_height - 5; + + if private.scroll.0 < page_height { + private.scroll.0 = 0; } else { - private.recorder.start_record(); + private.scroll.0 -= page_height; + } + } + GraphicsCommand::PageDown => { + if Self::max_main_axis(&private) > 0 { + private.auto_scroll = false; } + + private.scroll.0 = 0; } GraphicsCommand::Exit => break 'draw_loop, } } + while let Ok(rx_msg) = private.rx.try_recv() { + if private.history.len() + new_messages.len() >= private.capacity { + private.history.remove(0); + } + + new_messages.push(GraphicalMessage::Rx { + timestamp: rx_msg.timestamp, + message: vec![(Self::bytes_to_string(&rx_msg.message), Color::Reset)], + }); + } + + while let Ok(tx_msg) = private.tx.try_recv() { + if private.history.len() + new_messages.len() >= private.capacity { + private.history.remove(0); + } + + new_messages.push(GraphicalMessage::Tx { + timestamp: tx_msg.timestamp, + message: vec![(Self::bytes_to_string(&tx_msg.message), Color::Black)], + }); + } + + while let Ok(log_msg) = private.logger_receiver.try_recv() { + if private.history.len() + new_messages.len() >= private.capacity { + private.history.remove(0); + } + + new_messages.push(GraphicalMessage::Log(log_msg)); + } + + if !new_messages.is_empty() { + new_messages + .sort_by(|a, b| a.get_timestamp().partial_cmp(b.get_timestamp()).unwrap()); + if private.recorder.is_recording() { + if let Err(err) = private + .recorder + .add_bulk_content(new_messages.iter().map(|gm| gm.serialize()).collect()) + { + error!(private.logger, "{}", err); + } + } + private.typewriter += new_messages.iter().map(|gm| gm.serialize()).collect(); + private.history.extend(new_messages.into_iter()); + new_messages = vec![]; + } + terminal .draw(|f| { let chunks = Layout::default() @@ -339,7 +486,11 @@ impl GraphicsTask { ]) .split(f.size()); - Self::draw_history(&private, f, chunks[0], blink_color); + let blink_color = { + let blk = blink.lock().unwrap(); + blk.get_current() + }; + Self::draw_history(&mut private, f, chunks[0], blink_color); Self::draw_command_bar( &private.inputs_shared, &private.serial_shared, @@ -361,9 +512,24 @@ impl GraphicsTask { terminal.show_cursor().expect("Cannot show mouse cursor"); } + fn bytes_to_string(msg: &[u8]) -> String { + let mut output = "".to_string(); + + for byte in msg { + match *byte { + x if 0x20 <= x && x <= 0x7E => output.push(x as char), + 0x0a => output += "\\n", + 0x0d => output += "\\r", + _ => output += &format!("\\x{:02x}", byte), + } + } + + output + } + fn timestamp_span(timestamp: &DateTime) -> Span { Span::styled( - format!("{}", timestamp.format("%H:%M:%S.%3f")), + format!("{} ", timestamp.format("%H:%M:%S.%3f")), Style::default().fg(Color::DarkGray), ) } @@ -380,10 +546,10 @@ impl GraphicsTask { let mut spans = vec![Self::timestamp_span(timestamp)]; let (bg, fg) = match level { crate::infra::LogLevel::Error => (Color::Red, Color::White), - crate::infra::LogLevel::Warning => (Color::Yellow, Color::DarkGray), - crate::infra::LogLevel::Success => (Color::LightGreen, Color::DarkGray), - crate::infra::LogLevel::Info => (Color::White, Color::DarkGray), - crate::infra::LogLevel::Debug => (Color::Reset, Color::DarkGray), + crate::infra::LogLevel::Warning => (Color::Yellow, Color::Black), + crate::infra::LogLevel::Success => (Color::LightGreen, Color::Black), + crate::infra::LogLevel::Info => (Color::White, Color::Black), + crate::infra::LogLevel::Debug => (Color::Reset, Color::Black), }; if scroll_x < message.chars().count() { @@ -428,9 +594,9 @@ impl GraphicsTask { Line::from(spans) } - fn max_main_axis(frame_height: u16, history: &Vec) -> u16 { - let main_axis_length = frame_height - Self::COMMAND_BAR_HEIGHT - 2; - let history_len = history.len() as u16; + fn max_main_axis(private: &GraphicsConnections) -> u16 { + let main_axis_length = private.last_frame_height - Self::COMMAND_BAR_HEIGHT - 2; + let history_len = private.history.len() as u16; if history_len > main_axis_length { history_len - main_axis_length @@ -458,12 +624,50 @@ impl GraphicsConnections { rx, inputs_shared, serial_shared, - history: vec![], + history: VecDeque::new(), typewriter: TypeWriter::new(storage_base_filename.clone()), recorder: Recorder::new(storage_base_filename).expect("Cannot create Recorder"), capacity, auto_scroll: true, scroll: (0, 0), + last_frame_height: u16::MAX, + } + } +} + +impl Serialize for GraphicalMessage { + fn serialize(&self) -> String { + match self { + GraphicalMessage::Log(log) => { + let log_level = match log.level { + LogLevel::Error => "ERR", + LogLevel::Warning => "WRN", + LogLevel::Success => " OK", + LogLevel::Info => "INF", + LogLevel::Debug => "DBG", + }; + + format!("[{}][{}] {}", log.timestamp, log_level, log.message) + } + GraphicalMessage::Tx { timestamp, message } => { + let msg = message.iter().fold(String::new(), |acc, x| acc + &x.0); + + format!("[{}][ =>] {}", timestamp, msg) + } + GraphicalMessage::Rx { timestamp, message } => { + let msg = message.iter().fold(String::new(), |acc, x| acc + &x.0); + format!("[{}][ <=] {}", timestamp, msg) + } + } + } +} + +impl GraphicalMessage { + pub fn get_timestamp(&self) -> &DateTime { + match self { + GraphicalMessage::Log(LogMessage { timestamp, .. }) => timestamp, + GraphicalMessage::Tx { timestamp, .. } => timestamp, + GraphicalMessage::Rx { timestamp, .. } => timestamp, } } } diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs index 842261c..2bc294f 100644 --- a/src/graphics/mod.rs +++ b/src/graphics/mod.rs @@ -1,2 +1,5 @@ pub mod graphics_task; -pub mod rich_string; + +pub trait Serialize { + fn serialize(&self) -> String; +} diff --git a/src/graphics/rich_string.rs b/src/graphics/rich_string.rs deleted file mode 100644 index 301a137..0000000 --- a/src/graphics/rich_string.rs +++ /dev/null @@ -1,398 +0,0 @@ -use chrono::{DateTime, Local}; -use ratatui::style::{Color, Style}; -use ratatui::text::Span; -use std::collections::HashMap; - -#[derive(Clone)] -pub struct RichText { - content: Vec, - fg: Color, - bg: Color, -} - -impl RichText { - pub fn new(content: Vec, fg: Color, bg: Color) -> Self { - Self { content, fg, bg } - } - - pub fn from_string(content: String, fg: Color, bg: Color) -> Self { - Self { - content: content.as_bytes().to_vec(), - fg, - bg, - } - } - - pub fn decode_ansi_color(self) -> RichTextAnsi { - RichTextAnsi::new(self) - } - - pub fn highlight_invisible(self) -> RichTextWithInvisible { - RichTextWithInvisible::new(self) - } - - pub fn to_span<'a>(&self) -> Span<'a> { - Span::styled( - String::from_utf8_lossy(&self.content).to_string(), - Style::default().bg(self.bg).fg(self.fg), - ) - } - - pub fn crop_rich_texts(rich_texts: &[RichText], cursor: usize) -> Vec { - rich_texts - .iter() - .fold((cursor, vec![]), |(mut cursor, mut acc), text| { - if cursor == 0 { - acc.push(text.clone().to_span()); - return (0, acc); - } - - match cursor { - x if x < text.content.len() => { - acc.push( - Self { - content: text.content[cursor..].to_vec(), - fg: text.fg, - bg: text.bg, - } - .to_span(), - ); - cursor = 0; - } - x if x == text.content.len() => cursor = 0, - _ => cursor -= text.content.len(), - } - - (cursor, acc) - }) - .1 - } -} - -pub struct RichTextWithInvisible { - rich_texts: Vec, -} - -pub struct ViewData { - timestamp: DateTime, - data: Vec, -} - -impl ViewData { - pub fn new(timestamp: DateTime, data: Vec) -> Self { - Self { timestamp, data } - } -} - -impl RichTextWithInvisible { - pub fn into_view_data(self, timestamp: DateTime) -> ViewData { - ViewData::new(timestamp, self.rich_texts) - } - - fn new(rich_text: RichText) -> Self { - if rich_text.content.is_empty() { - return Self { rich_texts: vec![] }; - } - - enum State { - None, - Visible, - Invisible, - } - - let (fg, bg) = (rich_text.fg, rich_text.bg); - let (hl_fg, hl_bg) = Self::get_colors(rich_text.fg, rich_text.bg, true); - - let (buffer, state, acc) = rich_text.content.into_iter().fold( - (vec![], State::None, vec![]), - |(buffer, state, acc), byte| match state { - State::None => ( - vec![byte], - if Self::is_visible(byte) { - State::Visible - } else { - State::Invisible - }, - acc, - ), - State::Visible => { - if Self::is_visible(byte) { - ( - buffer.into_iter().chain([byte]).collect(), - State::Visible, - acc, - ) - } else { - ( - vec![byte], - State::Invisible, - acc.into_iter() - .chain([RichText::new(buffer, rich_text.fg, rich_text.bg)]) - .collect(), - ) - } - } - State::Invisible => { - if Self::is_visible(byte) { - ( - vec![byte], - State::Visible, - acc.into_iter() - .chain([RichText::new(Self::bytes_to_rich(buffer), hl_fg, hl_bg)]) - .collect(), - ) - } else { - ( - buffer.into_iter().chain([byte]).collect(), - State::Invisible, - acc, - ) - } - } - }, - ); - - Self { - rich_texts: if buffer.is_empty() { - acc - } else { - let (fg, bg) = Self::get_colors(fg, bg, matches!(state, State::Invisible)); - let buffer = if matches!(state, State::Invisible) { - Self::bytes_to_rich(buffer) - } else { - buffer - }; - - acc.into_iter() - .chain([RichText::new(buffer, fg, bg)]) - .collect() - }, - } - } - - fn is_visible(byte: u8) -> bool { - (0x20..=0x7E).contains(&byte) - } - - fn bytes_to_rich(bytes: Vec) -> Vec { - bytes - .into_iter() - .flat_map(|b| match b { - b'\n' => b"\\n".to_vec(), - b'\r' => b"\\r".to_vec(), - b'\0' => b"\\0".to_vec(), - x => format!("\\x{:02x}", x).as_bytes().to_vec(), - }) - .collect() - } - - fn get_colors(fg: Color, bg: Color, is_highlight: bool) -> (Color, Color) { - if !is_highlight { - return (fg, bg); - } - - if bg == Color::Magenta - || bg == Color::LightMagenta - || fg == Color::Magenta - || fg == Color::LightMagenta - { - (Color::LightYellow, bg) - } else { - (Color::LightMagenta, bg) - } - } -} - -pub struct RichTextAnsi { - rich_texts: Vec, -} - -impl RichTextAnsi { - pub fn highlight_invisible(self) -> RichTextWithInvisible { - let rich_texts = self - .rich_texts - .into_iter() - .flat_map(|rich_text| RichTextWithInvisible::new(rich_text).rich_texts) - .collect::>(); - - RichTextWithInvisible { rich_texts } - } - - fn new(rich_text: RichText) -> Self { - if rich_text.content.is_empty() { - return RichTextAnsi { rich_texts: vec![] }; - } - - enum State { - None, - Escape, - Normal, - } - - let (fg, bg) = (rich_text.fg, rich_text.bg); - let (text_buffer, _color_pattern, state, acc, current_fg, current_bg) = - rich_text.content.into_iter().fold( - (vec![], vec![], State::None, Vec::::new(), fg, bg), - |(text_buffer, color_pattern, state, acc, current_fg, current_bg), byte| match state - { - State::None => { - if byte == 0x1B { - ( - vec![], - vec![byte], - State::Escape, - acc, - current_fg, - current_bg, - ) - } else { - ( - vec![byte], - vec![], - State::Normal, - acc, - current_fg, - current_bg, - ) - } - } - State::Escape => { - let color_pattern_ext = - color_pattern.into_iter().chain([byte]).collect::>(); - match RichTextAnsi::match_color_pattern(&color_pattern_ext) { - Ok(Some((new_fg, new_bg))) => ( - vec![], - vec![], - State::Normal, - acc.into_iter() - .chain([RichText { - content: text_buffer, - fg: current_fg, - bg: current_bg, - }]) - .collect::>(), - new_fg, - new_bg, - ), - Ok(None) => ( - text_buffer, - color_pattern_ext, - State::Escape, - acc, - current_fg, - current_bg, - ), - Err(_) => ( - text_buffer - .into_iter() - .chain(color_pattern_ext) - .collect::>(), - vec![], - State::Normal, - acc, - current_fg, - current_bg, - ), - } - } - State::Normal => { - if byte == 0x1B { - ( - text_buffer, - vec![byte], - State::Escape, - acc, - current_fg, - current_bg, - ) - } else { - ( - text_buffer.into_iter().chain([byte]).collect(), - vec![], - State::Normal, - acc, - current_fg, - current_bg, - ) - } - } - }, - ); - - Self { - rich_texts: if text_buffer.is_empty() { - acc - } else { - acc.into_iter() - .chain([match state { - State::None => unreachable!(), - State::Normal | State::Escape => RichText { - content: text_buffer, - fg: current_fg, - bg: current_bg, - }, - }]) - .collect() - }, - } - } - - fn match_color_pattern(pattern: &[u8]) -> Result, ()> { - let pattern_lut = HashMap::new() - .into_iter() - .chain([ - (b"\x1B[m".to_vec(), (Color::Reset, Color::Reset)), - (b"\x1B[0m".to_vec(), (Color::Reset, Color::Reset)), - (b"\x1B[1m".to_vec(), (Color::Reset, Color::Reset)), - (b"\x1B[30m".to_vec(), (Color::Black, Color::Reset)), - (b"\x1B[0;30m".to_vec(), (Color::Black, Color::Reset)), - (b"\x1B[1;30m".to_vec(), (Color::Black, Color::Reset)), - (b"\x1B[31m".to_vec(), (Color::Red, Color::Reset)), - (b"\x1B[0;31m".to_vec(), (Color::Red, Color::Reset)), - (b"\x1B[1;31m".to_vec(), (Color::Red, Color::Reset)), - (b"\x1B[32m".to_vec(), (Color::Green, Color::Reset)), - (b"\x1B[0;32m".to_vec(), (Color::Green, Color::Reset)), - (b"\x1B[1;32m".to_vec(), (Color::Green, Color::Reset)), - (b"\x1B[33m".to_vec(), (Color::Yellow, Color::Reset)), - (b"\x1B[0;33m".to_vec(), (Color::Yellow, Color::Reset)), - (b"\x1B[1;33m".to_vec(), (Color::Yellow, Color::Reset)), - (b"\x1B[34m".to_vec(), (Color::Blue, Color::Reset)), - (b"\x1B[0;34m".to_vec(), (Color::Blue, Color::Reset)), - (b"\x1B[1;34m".to_vec(), (Color::Blue, Color::Reset)), - (b"\x1B[35m".to_vec(), (Color::Magenta, Color::Reset)), - (b"\x1B[0;35m".to_vec(), (Color::Magenta, Color::Reset)), - (b"\x1B[1;35m".to_vec(), (Color::Magenta, Color::Reset)), - (b"\x1B[36m".to_vec(), (Color::Cyan, Color::Reset)), - (b"\x1B[0;36m".to_vec(), (Color::Cyan, Color::Reset)), - (b"\x1B[1;36m".to_vec(), (Color::Cyan, Color::Reset)), - (b"\x1B[37m".to_vec(), (Color::Gray, Color::Reset)), - (b"\x1B[0;37m".to_vec(), (Color::Gray, Color::Reset)), - (b"\x1B[1;37m".to_vec(), (Color::Gray, Color::Reset)), - ]) - .collect::, (Color, Color)>>(); - - if let Some((fg, bg)) = pattern_lut.get(pattern) { - return Ok(Some((*fg, *bg))); - } - - if pattern_lut.keys().any(|x| x.starts_with(pattern)) { - return Ok(None); - } - - if pattern.starts_with(b"\x1b[") && pattern[2..].iter().all(|x| x.is_ascii_digit()) { - return Ok(None); - } - - if pattern == b"\x1b[J" { - return Ok(Some((Color::Reset, Color::Reset))); - } - - if pattern.starts_with(b"\x1b[") - && [b'A', b'B', b'C', b'D'].contains(pattern.last().unwrap()) - { - return Ok(Some((Color::Reset, Color::Reset))); - } - - Err(()) - } -} diff --git a/src/infra/blink.rs b/src/infra/blink.rs index 1500ac5..fffb929 100644 --- a/src/infra/blink.rs +++ b/src/infra/blink.rs @@ -1,45 +1,50 @@ -use std::time::Duration; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; -use super::timer::Timer; +use super::timer::{Timeout, Timer}; -pub struct Blink { +#[derive(Default)] +struct TimerOn; +#[derive(Default)] +struct TimerOff; + +pub struct Blink { on: T, off: T, - current: Option, - timer_on: Timer, - timer_off: Timer, + current: T, + timer_on: Timer, + timer_off: Timer, total_blinks: usize, num_blinks: usize, } -impl Blink { - pub fn new(duration: Duration, total_blinks: usize, on: T, off: T) -> Self { - Self { - on, +impl Blink { + pub fn new(duration: Duration, total_blinks: usize, on: T, off: T) -> Arc> { + let obj = Self { + on: on.clone(), off, - current: None, + current: on, timer_on: Timer::new(duration), timer_off: Timer::new(duration), total_blinks, num_blinks: 0, + }; + let obj = Arc::new(Mutex::new(obj)); + { + let mut o = obj.lock().unwrap(); + o.timer_on.set_action(obj.clone()); + o.timer_off.set_action(obj.clone()); } - } - - fn timer_on_timeout(&mut self) { - self.timer_off.start(); - self.current = Some(self.off.clone()); - } - fn timer_off_timeout(&mut self) { - self.timer_on.start(); - self.current = Some(self.on.clone()); + obj } pub fn start(&mut self) { + self.num_blinks = 0; self.timer_on.start(); - self.current = Some(self.on.clone()); - - self.timer_on.set_action(|| self.timer_on_timeout()); + self.current = self.on.clone(); } pub fn tick(&mut self) { @@ -47,7 +52,25 @@ impl Blink { self.timer_off.tick(); } - pub fn get_current(&self) -> Option<&T> { - self.current.as_ref() + pub fn get_current(&self) -> T { + self.current.clone() + } +} + +impl Timeout for Blink { + fn action(&mut self) { + self.timer_off.start(); + self.current = self.off.clone(); + } +} + +impl Timeout for Blink { + fn action(&mut self) { + self.num_blinks += 1; + self.current = self.on.clone(); + + if self.num_blinks < self.total_blinks { + self.timer_on.start(); + } } } diff --git a/src/infra/logger.rs b/src/infra/logger.rs index 24201e1..9f4fcc8 100644 --- a/src/infra/logger.rs +++ b/src/infra/logger.rs @@ -13,6 +13,7 @@ pub struct LogMessage { pub level: LogLevel, } +#[allow(unused)] pub enum LogLevel { Error, Warning, @@ -44,34 +45,34 @@ impl Logger { #[macro_export] macro_rules! debug { ($logger:expr, $($arg:tt)+) => { - $logger.write(format!($($arg)+), LogLevel::Debug) + {let _ = $logger.write(format!($($arg)+), LogLevel::Debug);} }; } #[macro_export] macro_rules! info { ($logger:expr, $($arg:tt)+) => { - $logger.write(format!($($arg)+), LogLevel::Info) + {let _ = $logger.write(format!($($arg)+), LogLevel::Info);} }; } #[macro_export] macro_rules! success { ($logger:expr, $($arg:tt)+) => { - $logger.write(format!($($arg)+), LogLevel::Success) + {let _ = $logger.write(format!($($arg)+), LogLevel::Success);} }; } #[macro_export] macro_rules! warning { ($logger:expr, $($arg:tt)+) => { - $logger.write(format!($($arg)+), LogLevel::Warning) + {let _ = $logger.write(format!($($arg)+), LogLevel::Warning);} }; } #[macro_export] macro_rules! error { ($logger:expr, $($arg:tt)+) => { - $logger.write(format!($($arg)+), LogLevel::Error) + {let _ = $logger.write(format!($($arg)+), LogLevel::Error);} }; } diff --git a/src/infra/mpmc.rs b/src/infra/mpmc.rs index eb8132c..7da9c75 100644 --- a/src/infra/mpmc.rs +++ b/src/infra/mpmc.rs @@ -40,10 +40,12 @@ impl Channel { } impl Consumer { + #[allow(unused)] pub fn id(&self) -> Id { self.id } + #[allow(unused)] pub fn recv(&self) -> Result { self.receiver.recv() } @@ -58,6 +60,7 @@ impl Producer { self.channel.clone().send_data(data, None) } + #[allow(unused)] pub fn produce_without_loopback(&self, data: T, id: Id) { self.channel.clone().send_data(data, Some(id)) } diff --git a/src/infra/recorder.rs b/src/infra/recorder.rs index de289ef..10a6386 100644 --- a/src/infra/recorder.rs +++ b/src/infra/recorder.rs @@ -1,6 +1,4 @@ -use std::path::PathBuf; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; +use std::{fs::File, io::Write, path::PathBuf}; use super::into_byte_format; @@ -41,10 +39,8 @@ impl Recorder { self.file_handler.is_some() } - pub async fn start_record(&mut self) -> Result<(), String> { - let file = File::create(self.get_filename()) - .await - .map_err(|err| err.to_string())?; + pub fn start_record(&mut self) -> Result<(), String> { + let file = File::create(self.get_filename()).map_err(|err| err.to_string())?; self.file_handler = Some(file); Ok(()) @@ -59,22 +55,22 @@ impl Recorder { self.file_size = 0; } - pub async fn add_content(&mut self, content: String) -> Result<(), String> { + pub fn add_bulk_content(&mut self, contents: Vec) -> Result<(), String> { let Some(file) = self.file_handler.as_mut() else { return Err("No file recording now".to_string()); }; - let content = if !content.ends_with('\n') { - content + "\r\n" - } else { - content - }; - - file.write_all(content.as_bytes()) - .await - .map_err(|err| err.to_string())?; - self.file_size += content.len() as u128; + for content in contents { + let content = if !content.ends_with('\n') { + content.to_string() + "\r\n" + } else { + content.to_string() + }; + file.write_all(content.as_bytes()) + .map_err(|err| err.to_string())?; + self.file_size += content.len() as u128; + } Ok(()) } } diff --git a/src/infra/task.rs b/src/infra/task.rs index 47d68f2..94cc4e2 100644 --- a/src/infra/task.rs +++ b/src/infra/task.rs @@ -52,6 +52,7 @@ where } } + #[allow(unused)] pub fn cmd_sender(&self) -> Sender { self.cmd_sender.clone() } @@ -67,6 +68,7 @@ impl Shared { self.shared.read() } + #[allow(unused)] pub fn try_read( &self, ) -> Result< diff --git a/src/infra/timer.rs b/src/infra/timer.rs index 8589fc4..4f0b7f6 100644 --- a/src/infra/timer.rs +++ b/src/infra/timer.rs @@ -1,21 +1,31 @@ -use std::time::{Duration, Instant}; +use std::{ + marker::PhantomData, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; + +pub trait Timeout { + fn action(&mut self); +} -pub struct Timer { +pub struct Timer> { duration: Duration, - action: Option, + action: Option>>, now: Option, + marker: PhantomData, } -impl Timer { +impl> Timer { pub fn new(duration: Duration) -> Self { Self { duration, action: None, now: None, + marker: PhantomData, } } - pub fn set_action(&mut self, action: F) { + pub fn set_action(&mut self, action: Arc>) { self.action = Some(action); } @@ -23,10 +33,6 @@ impl Timer { self.now = Some(Instant::now()); } - pub fn is_action_empty(&self) -> bool { - self.action.as_ref().is_none() - } - pub fn tick(&mut self) { let Some(now) = self.now.as_ref() else { return; @@ -36,8 +42,9 @@ impl Timer { return; } - if let Some(action) = self.action.as_mut() { - (action)(); + if let Some(action) = self.action.as_ref() { + let mut action = action.lock().unwrap(); + action.action(); } self.now.take(); diff --git a/src/infra/typewriter.rs b/src/infra/typewriter.rs index a2fa1ff..e03c127 100644 --- a/src/infra/typewriter.rs +++ b/src/infra/typewriter.rs @@ -1,6 +1,4 @@ -use std::ops::AddAssign; -use tokio::fs::OpenOptions; -use tokio::io::AsyncWriteExt; +use std::{fs::OpenOptions, io::Write, ops::AddAssign}; use super::into_byte_format; @@ -27,33 +25,33 @@ impl TypeWriter { into_byte_format(self.file_size) } - pub async fn flush(&mut self) -> Result<(), String> { + pub fn flush(&mut self) -> Result<(), String> { let mut file = OpenOptions::new() .append(true) .create(true) .open(self.get_filename()) - .await .map_err(|err| err.to_string())?; let content = self.contents.drain(..).collect::>().join(""); file.write_all(content.as_bytes()) - .await .map_err(|err| err.to_string())?; Ok(()) } } -impl AddAssign for TypeWriter { - fn add_assign(&mut self, rhs: String) { - let rhs = if !rhs.ends_with('\n') { - rhs + "\r\n" - } else { - rhs - }; - - self.file_size += rhs.len() as u128; - self.contents.push(rhs); +impl AddAssign> for TypeWriter { + fn add_assign(&mut self, rhs: Vec) { + for content in rhs { + let content = if !content.ends_with('\n') { + content.to_string() + "\r\n" + } else { + content.to_string() + }; + + self.file_size += content.len() as u128; + self.contents.push(content); + } } } diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index dd86e34..d67880e 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -1,13 +1,25 @@ -use std::sync::{ - mpsc::{Receiver, Sender}, - Arc, RwLock, -}; - use crate::{ + error, graphics::graphics_task::GraphicsCommand, - infra::{logger::Logger, messages::TimedBytes, mpmc::Producer, task::Task}, + infra::{ + logger::{LogLevel, Logger}, + messages::TimedBytes, + mpmc::Producer, + task::Task, + }, plugin::plugin_engine::PluginEngineCommand, - serial::serial_if::SerialCommand, + serial::serial_if::{SerialCommand, SerialSetup}, +}; +use chrono::Local; +use core::panic; +use crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use rand::seq::SliceRandom; +use std::{ + path::PathBuf, + sync::{ + mpsc::{Receiver, Sender}, + Arc, RwLock, + }, }; pub type InputsTask = Task; @@ -28,6 +40,16 @@ pub struct InputsConnections { graphics_cmd_sender: Sender, serial_if_cmd_sender: Sender, plugin_engine_cmd_sender: Sender, + hints: Vec<&'static str>, + history_index: Option, + history: Vec, + backup_command_line: String, + tag_file: PathBuf, +} + +enum LoopStatus { + Continue, + Break, } impl InputsTask { @@ -45,12 +67,404 @@ impl InputsTask { ) } + fn handle_key_input( + private: &mut InputsConnections, + shared: Arc>, + key: KeyEvent, + ) -> LoopStatus { + match key.code { + KeyCode::Esc => { + return LoopStatus::Break; + } + KeyCode::Char('l') | KeyCode::Char('L') if key.modifiers == KeyModifiers::CONTROL => { + let _ = private.graphics_cmd_sender.send(GraphicsCommand::Clear); + } + KeyCode::Char('s') | KeyCode::Char('S') if key.modifiers == KeyModifiers::CONTROL => { + let _ = private.graphics_cmd_sender.send(GraphicsCommand::SaveData); + } + KeyCode::Char('r') | KeyCode::Char('R') if key.modifiers == KeyModifiers::CONTROL => { + let _ = private + .graphics_cmd_sender + .send(GraphicsCommand::RecordData); + } + KeyCode::Char(c) => { + let mut sw = shared.write().expect("Cannot get input lock for write"); + + sw.current_hint = None; + + if sw.cursor >= sw.command_line.chars().count() { + sw.command_line.push(c); + } else { + sw.command_line = sw.command_line.chars().enumerate().fold( + "".to_string(), + |mut acc, (i, x)| { + if i == sw.cursor { + acc.push(c); + } + + acc.push(x); + acc + }, + ); + } + + sw.cursor += 1; + Self::update_tag_list(); + private.history_index = None; + } + KeyCode::PageUp if key.modifiers == KeyModifiers::CONTROL => { + let _ = private + .graphics_cmd_sender + .send(GraphicsCommand::JumpToStart); + } + KeyCode::PageDown if key.modifiers == KeyModifiers::CONTROL => { + let _ = private.graphics_cmd_sender.send(GraphicsCommand::JumpToEnd); + } + KeyCode::PageUp => { + let _ = private.graphics_cmd_sender.send(GraphicsCommand::PageUp); + } + KeyCode::PageDown => { + let _ = private.graphics_cmd_sender.send(GraphicsCommand::PageDown); + } + KeyCode::Backspace => { + let mut sw = shared.write().expect("Cannot get input lock for write"); + + if sw.command_line.chars().count() == 1 { + Self::set_hint(&mut sw.current_hint, &private.hints); + } + + if sw.cursor > 0 { + sw.cursor -= 1; + sw.command_line = sw + .command_line + .chars() + .enumerate() + .filter_map(|(i, c)| if i != sw.cursor { Some(c) } else { None }) + .collect(); + Self::update_tag_list(); + } + + if sw.command_line.chars().count() > 0 + && sw.command_line.chars().all(|x| x.is_whitespace()) + { + sw.command_line.clear(); + sw.cursor = 0; + Self::set_hint(&mut sw.current_hint, &private.hints); + } + } + KeyCode::Delete => { + let mut sw = shared.write().expect("Cannot get input lock for write"); + + sw.command_line = sw + .command_line + .chars() + .enumerate() + .filter_map(|(i, c)| if i != sw.cursor { Some(c) } else { None }) + .collect(); + Self::update_tag_list(); + + if sw.command_line.chars().count() > 0 + && sw.command_line.chars().all(|x| x.is_whitespace()) + { + sw.command_line.clear(); + sw.cursor = 0; + Self::set_hint(&mut sw.current_hint, &private.hints); + } + } + KeyCode::Right => { + let mut sw = shared.write().expect("Cannot get input lock for write"); + + if sw.cursor < sw.command_line.chars().count() { + sw.cursor += 1; + } + } + KeyCode::Left => { + let mut sw = shared.write().expect("Cannot get input lock for write"); + + if sw.cursor > 0 { + sw.cursor -= 1; + } + } + KeyCode::Up => { + if private.history.is_empty() { + return LoopStatus::Continue; + } + + let mut sw = shared.write().expect("Cannot get input lock for write"); + + match &mut private.history_index { + None => { + private.history_index = Some(private.history.len() - 1); + private.backup_command_line.clone_from(&sw.command_line); + } + Some(0) => {} + Some(idx) => *idx -= 1, + } + + sw.current_hint = None; + sw.command_line + .clone_from(&private.history[private.history_index.unwrap()]); + sw.cursor = sw.command_line.chars().count(); + Self::update_tag_list(); + } + KeyCode::Down => { + if private.history.is_empty() { + return LoopStatus::Continue; + } + + let mut sw = shared.write().expect("Cannot get input lock for write"); + + match &mut private.history_index { + None => {} + Some(idx) if *idx == (private.history.len() - 1) => { + private.history_index = None; + sw.command_line.clone_from(&private.backup_command_line); + if sw.command_line.is_empty() { + Self::set_hint(&mut sw.current_hint, &private.hints); + } + } + Some(idx) => { + *idx += 1; + sw.command_line.clone_from(&private.history[*idx]); + } + } + + sw.cursor = sw.command_line.chars().count(); + Self::update_tag_list(); + } + KeyCode::Enter => { + let mut sw = shared.write().expect("Cannot get input lock for write"); + + if sw.command_line.is_empty() { + return LoopStatus::Continue; + } + + let command_line = sw.command_line.drain(..).collect::(); + Self::set_hint(&mut sw.current_hint, &private.hints); + + let empty_string = "".to_string(); + let last_command = private.history.last().unwrap_or(&empty_string); + if last_command != &command_line { + private.history.push(command_line.clone()); + } + + Self::clear_tag_list(); + private.history_index = None; + sw.cursor = 0; + drop(sw); + + if command_line.starts_with("!") { + let command_line_split = command_line + .strip_prefix('!') + .unwrap() + .split_whitespace() + .map(|arg| arg.to_string()) + .collect(); + + Self::handle_user_command(command_line_split, &private); + } else { + let command_line = Self::replace_hex_sequence(command_line); + let command_line = Self::replace_tag_sequence(command_line, &private.tag_file); + + private.tx.produce(Arc::new(TimedBytes { + timestamp: Local::now(), + message: command_line.as_bytes().to_vec(), + })); + } + } + _ => {} + } + + LoopStatus::Continue + } + + fn handle_user_command(command_line_split: Vec, private: &InputsConnections) { + let Some(cmd_name) = command_line_split.get(0) else { + private.tx.produce(Arc::new(TimedBytes { + timestamp: Local::now(), + message: vec!['!' as u8], + })); + return; + }; + + match cmd_name.as_str() { + "connect" => { + fn mount_setup(option: &str, setup: Option) -> SerialSetup { + if option.chars().all(|x| x.is_digit(10)) { + SerialSetup { + baudrate: Some(u32::from_str_radix(option, 10).unwrap()), + ..setup.unwrap_or(SerialSetup::default()) + } + } else { + SerialSetup { + port: Some(option.to_string()), + ..setup.unwrap_or(SerialSetup::default()) + } + } + } + + match command_line_split.len() { + x if x < 2 => { + let _ = private.serial_if_cmd_sender.send(SerialCommand::Connect); + } + 2 => { + let setup = SerialCommand::Setup(mount_setup(&command_line_split[1], None)); + let _ = private.serial_if_cmd_sender.send(setup); + } + _ => { + let setup = mount_setup(&command_line_split[1], None); + let setup = mount_setup(&command_line_split[2], Some(setup)); + + let _ = private + .serial_if_cmd_sender + .send(SerialCommand::Setup(setup)); + } + } + } + "disconnect" => { + let _ = private.serial_if_cmd_sender.send(SerialCommand::Disconnect); + } + _ => error!(private.logger, "Invalid command \"{}\"", cmd_name), + } + } + pub fn task( - _shared: Arc>, - _private: InputsConnections, + shared: Arc>, + mut private: InputsConnections, _inputs_cmd_receiver: Receiver<()>, ) { - todo!() + { + let mut sw = shared.write().expect("Cannot get input lock for write"); + Self::set_hint(&mut sw.current_hint, &private.hints); + } + + 'input_loop: loop { + let evt = match event::read() { + Ok(evt) => evt, + Err(err) => panic!("Error at input task: {:?}", err), + }; + + match evt { + event::Event::FocusGained => {} + event::Event::FocusLost => {} + event::Event::Key(key) if key.kind == KeyEventKind::Press => { + if let LoopStatus::Break = + Self::handle_key_input(&mut private, shared.clone(), key) + { + let _ = private + .plugin_engine_cmd_sender + .send(PluginEngineCommand::Exit); + let _ = private.serial_if_cmd_sender.send(SerialCommand::Exit); + let _ = private.graphics_cmd_sender.send(GraphicsCommand::Exit); + break 'input_loop; + } + } + event::Event::Mouse(mouse_evt) => match mouse_evt.kind { + event::MouseEventKind::ScrollUp + if mouse_evt.modifiers == KeyModifiers::CONTROL => + { + let _ = private + .graphics_cmd_sender + .send(GraphicsCommand::ScrollLeft); + } + event::MouseEventKind::ScrollDown + if mouse_evt.modifiers == KeyModifiers::CONTROL => + { + let _ = private + .graphics_cmd_sender + .send(GraphicsCommand::ScrollRight); + } + event::MouseEventKind::ScrollDown => { + let _ = private + .graphics_cmd_sender + .send(GraphicsCommand::ScrollDown); + } + event::MouseEventKind::ScrollUp => { + let _ = private.graphics_cmd_sender.send(GraphicsCommand::ScrollUp); + } + _ => {} + }, + event::Event::Paste(_) => {} + event::Event::Resize(_, _) => {} + _ => {} + } + } + } + + fn set_hint(current_hint: &mut Option, hints: &[&'static str]) { + *current_hint = Some(hints.choose(&mut rand::thread_rng()).unwrap().to_string()); + } + + fn replace_hex_sequence(command_line: String) -> String { + let mut output = String::new(); + let mut in_hex_seq = false; + let valid = "0123456789abcdefABCDEF,_-."; + let mut hex_shift = 0; + let mut hex_val = None; + + for c in command_line.chars() { + if !in_hex_seq { + if c == '$' { + in_hex_seq = true; + hex_shift = 0; + hex_val = Some(0); + continue; + } + + output.push(c); + } else { + if !valid.contains(c) { + in_hex_seq = false; + output.push(c); + continue; + } + + match c { + '0'..='9' => { + *hex_val.get_or_insert(0) <<= hex_shift; + *hex_val.get_or_insert(0) |= c as u8 - '0' as u8; + } + 'a'..='f' => { + *hex_val.get_or_insert(0) <<= hex_shift; + *hex_val.get_or_insert(0) |= c as u8 - 'a' as u8; + } + 'A'..='F' => { + *hex_val.get_or_insert(0) <<= hex_shift; + *hex_val.get_or_insert(0) |= c as u8 - 'A' as u8; + } + _ => { + if let Some(hex) = hex_val.take() { + output.push(hex as char); + } + hex_shift = 0; + } + } + + if hex_shift == 0 { + hex_shift = 4; + } else { + if let Some(hex) = hex_val.take() { + output.push(hex as char); + } + hex_shift = 0; + } + } + } + + output + } + + fn replace_tag_sequence(command_line: String, _tag_file: &PathBuf) -> String { + // TODO + command_line + } + + fn update_tag_list() { + // TODO + } + + fn clear_tag_list() { + // TODO } } @@ -61,6 +475,7 @@ impl InputsConnections { graphics_cmd_sender: Sender, serial_if_cmd_sender: Sender, plugin_engine_cmd_sender: Sender, + tag_file: PathBuf, ) -> Self { Self { logger, @@ -68,6 +483,15 @@ impl InputsConnections { graphics_cmd_sender, serial_if_cmd_sender, plugin_engine_cmd_sender, + history_index: None, + hints: vec![ + "Type @ to place a tag", + "Type $ to start a hex sequence", + "Type here and hit to send the text", + ], + history: vec![], + backup_command_line: String::new(), + tag_file, } } } diff --git a/src/main.rs b/src/main.rs index 5324f68..5efba40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,4 @@ #![deny(warnings)] -// TODO remove this allow after migration -#![allow(unused)] extern crate core; @@ -11,16 +9,49 @@ mod plugin; mod serial; use chrono::Local; +use clap::{Parser, Subcommand}; use graphics::graphics_task::{GraphicsConnections, GraphicsTask}; use infra::logger::Logger; use infra::mpmc::Channel; use inputs::inputs_task::{InputsConnections, InputsTask}; use plugin::plugin_engine::{PluginEngine, PluginEngineConnections}; use serial::serial_if::{SerialConnections, SerialInterface, SerialSetup}; +use std::path::PathBuf; use std::sync::mpsc::channel; use std::sync::Arc; -fn app(capacity: usize) -> Result<(), String> { +const DEFAULT_CAPACITY: usize = 2000; +const DEFAULT_TAG_FILE: &str = "tags.yml"; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, + #[clap(short, long)] + capacity: Option, + #[clap(short, long)] + tag_file: Option, +} + +#[derive(Subcommand)] +pub enum Commands { + Serial { + port: Option, + baudrate: Option, + }, + Ble { + name_device: String, + mtu: u32, + }, +} + +fn app( + capacity: usize, + tag_file: PathBuf, + port: Option, + baudrate: Option, +) -> Result<(), String> { let (logger, logger_receiver) = Logger::new(); let mut tx_channel = Channel::default(); let mut rx_channel = Channel::default(); @@ -40,6 +71,12 @@ fn app(capacity: usize) -> Result<(), String> { let (graphics_cmd_sender, graphics_cmd_receiver) = channel(); let (plugin_engine_cmd_sender, plugin_engine_cmd_receiver) = channel(); + let _ = serial_if_cmd_sender.send(serial::serial_if::SerialCommand::Setup(SerialSetup { + port, + baudrate, + ..SerialSetup::default() + })); + let serial_connections = SerialConnections::new( logger.clone(), tx_channel_consumers.pop().unwrap(), @@ -51,6 +88,7 @@ fn app(capacity: usize) -> Result<(), String> { graphics_cmd_sender.clone(), serial_if_cmd_sender.clone(), plugin_engine_cmd_sender.clone(), + tag_file, ); let plugin_engine_connections = PluginEngineConnections::new( logger.clone(), @@ -102,12 +140,33 @@ fn app(capacity: usize) -> Result<(), String> { Ok(()) } -fn main() { +fn main() -> Result<(), String> { #[cfg(target_os = "windows")] ctrlc::set_handler(|| { /* Do nothing on user ctrl+c */ }) .expect("Error setting Ctrl-C handler"); - if let Err(err) = app() { - println!("[\x1b[31mERR\x1b[0m] {}", err); + let cli = Cli::parse(); + + if cli.tag_file.is_some() { + return Err("Sorry! We're developing tag files and it's not available yet".to_string()); } + + let (port, baudrate) = match cli.command { + Commands::Serial { port, baudrate } => (port, baudrate), + Commands::Ble { .. } => { + return Err( + "Sorry! We're developing BLE interface and it's not available yet".to_string(), + ); + } + }; + + let capacity = cli.capacity.unwrap_or(DEFAULT_CAPACITY); + let tag_file = cli.tag_file.unwrap_or(PathBuf::from(DEFAULT_TAG_FILE)); + + if let Err(err) = app(capacity, tag_file, port, baudrate) { + return Err(format!("[\x1b[31mERR\x1b[0m] {}", err)); + } + + println!("See you later ^^"); + Ok(()) } diff --git a/src/old/blink_color.rs b/src/old/blink_color.rs deleted file mode 100644 index 6d7e478..0000000 --- a/src/old/blink_color.rs +++ /dev/null @@ -1,71 +0,0 @@ -use ratatui::style::Color; -use std::time::Duration; -use tokio::time::Instant; - -pub struct BlinkColor { - color: Color, - duration: Duration, - blinks: usize, - action: BlinkAction, -} - -enum BlinkAction { - None, - On { timeout: Instant, blink: usize }, - Off { timeout: Instant, blink: usize }, -} - -impl BlinkColor { - pub fn new(color: Color, duration: Duration, blinks: usize) -> Self { - Self { - color, - duration, - blinks, - action: BlinkAction::None, - } - } - - pub fn start(&mut self) { - self.action = BlinkAction::On { - timeout: Instant::now() + self.duration, - blink: 1, - } - } - - pub fn get_color(&self) -> Option { - match self.action { - BlinkAction::None | BlinkAction::Off { .. } => None, - BlinkAction::On { .. } => Some(self.color), - } - } - - pub fn update(&mut self) { - match self.action { - BlinkAction::None => {} - BlinkAction::On { timeout, blink } => { - if Instant::now() >= timeout { - if self.blinks <= blink { - self.action = BlinkAction::None; - } else { - self.action = BlinkAction::Off { - timeout: Instant::now() + self.duration, - blink, - }; - } - } - } - BlinkAction::Off { timeout, blink } => { - if Instant::now() >= timeout { - if self.blinks <= blink { - self.action = BlinkAction::None; - } else { - self.action = BlinkAction::On { - timeout: Instant::now() + self.duration, - blink: blink + 1, - }; - } - } - } - } - } -} diff --git a/src/old/command_bar.rs b/src/old/command_bar.rs deleted file mode 100644 index e6ca71c..0000000 --- a/src/old/command_bar.rs +++ /dev/null @@ -1,680 +0,0 @@ -use crate::blink_color::BlinkColor; -use crate::command_bar::InputEvent::{HorizontalScroll, Key, VerticalScroll}; -use crate::error_pop_up::ErrorPopUp; -use crate::messages::{SerialRxData, UserTxData}; -use crate::serial::SerialIF; -use crate::text::TextView; -use chrono::Local; -use crossterm::event::{ - Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind, -}; -use futures::StreamExt; -use rand::seq::SliceRandom; -use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; -use ratatui::Frame; -use std::cmp::{max, min}; -use std::collections::btree_map::BTreeMap; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tokio::sync::{Mutex, MutexGuard}; -use tokio::time::sleep; - -pub struct CommandBar { - interface: Arc>, - text_view: Arc>, - command_line: String, - command_line_idx: usize, - command_filepath: Option, - history: Vec, - history_index: Option, - backup_command_line: String, - error_pop_up: Option, - command_list: CommandList, - key_receiver: UnboundedReceiver, - current_hint: Option<&'static str>, - hints: Vec<&'static str>, - blink_color: BlinkColor, -} - -impl CommandBar { - const HEIGHT: u16 = 3; - - pub fn new(interface: SerialIF, view_capacity: usize, save_filename: String) -> Self { - let (key_sender, key_receiver) = tokio::sync::mpsc::unbounded_channel(); - - tokio::spawn(async move { - CommandBar::task(key_sender).await; - }); - - let hints = vec![ - "Type / to send a command", - "Type $ to start a hex sequence", - "Type here and hit to send", - ]; - - let interface = Arc::new(Mutex::new(interface)); - let text_view = Arc::new(Mutex::new(TextView::new(view_capacity, save_filename))); - - Self { - interface, - text_view, - command_line: String::new(), - command_line_idx: 0, - history: vec![], - history_index: None, - backup_command_line: String::new(), - key_receiver, - error_pop_up: None, - command_filepath: None, - command_list: CommandList::new(), - hints: hints.clone(), - current_hint: Some(hints.choose(&mut rand::thread_rng()).unwrap()), - blink_color: BlinkColor::new(Color::Black, Duration::from_millis(200), 2), - } - } - - pub fn with_command_file(mut self, filepath: &str) -> Self { - self.command_filepath = Some(PathBuf::from(filepath)); - self - } - - async fn task(sender: UnboundedSender) { - let mut reader = EventStream::new(); - - loop { - let event = reader.next().await; - - match event { - Some(Ok(event)) => match event { - Event::Mouse(mouse_evt) if mouse_evt.modifiers == KeyModifiers::CONTROL => { - match mouse_evt.kind { - MouseEventKind::ScrollUp => sender.send(HorizontalScroll(-1)).unwrap(), - MouseEventKind::ScrollDown => sender.send(HorizontalScroll(1)).unwrap(), - _ => {} - } - } - Event::Key(key) if key.kind == KeyEventKind::Press => { - sender.send(Key(key)).unwrap() - } - Event::Mouse(mouse_evt) => match mouse_evt.kind { - MouseEventKind::ScrollUp => sender.send(VerticalScroll(-1)).unwrap(), - MouseEventKind::ScrollDown => sender.send(VerticalScroll(1)).unwrap(), - _ => {} - }, - _ => {} - }, - Some(Err(e)) => panic!("Error at command bar task: {:?}", e), - None => break, - } - } - } - - pub async fn get_text_view(&self) -> MutexGuard { - self.text_view.lock().await - } - - pub async fn get_interface(&self) -> MutexGuard { - self.interface.lock().await - } - - pub fn draw(&self, f: &mut Frame<'_>, text_view: &TextView, interface: &SerialIF) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(f.size().height - CommandBar::HEIGHT), - Constraint::Length(CommandBar::HEIGHT), - ] - .as_ref(), - ) - .split(f.size()); - - text_view.draw(f, chunks[0], self.blink_color.get_color()); - - let (description, is_connected) = (interface.description(), interface.is_connected()); - - let cursor_pos = ( - chunks[1].x + self.command_line_idx as u16 + 2, - chunks[1].y + 1, - ); - let bar_color = if is_connected { - Color::LightGreen - } else { - Color::LightRed - }; - let block = Block::default() - .title(format!("[{:03}] {}", self.history.len(), description)) - .borders(Borders::ALL) - .border_type(BorderType::Thick) - .border_style(Style::default().fg(bar_color)); - let paragraph = Paragraph::new(Span::from({ - " ".to_string() - + if let Some(hint) = self.current_hint { - hint - } else { - &self.command_line - } - })) - .style(Style::default().fg(if self.current_hint.is_some() { - Color::DarkGray - } else { - Color::Reset - })) - .block(block); - f.render_widget(paragraph, chunks[1]); - f.set_cursor(cursor_pos.0, cursor_pos.1); - - self.command_list.draw(f, chunks[1].y, Color::LightGreen); - - if let Some(pop_up) = self.error_pop_up.as_ref() { - pop_up.draw(f, chunks[1].y); - } - } - - fn set_error_pop_up(&mut self, message: String) { - self.error_pop_up = Some(ErrorPopUp::new(message)); - } - - fn update_command_list(&mut self) { - if !self.command_line.starts_with('/') { - self.command_list.clear(); - return; - } - - let Some(filepath) = self.command_filepath.clone() else { - self.set_error_pop_up("No YAML command file loaded!".to_string()); - return; - }; - - let yaml_content = self.load_commands(&filepath); - if yaml_content.is_empty() { - return; - } - - let cmd_line = self.command_line.strip_prefix('/').unwrap(); - let cmds = yaml_content - .keys() - .filter_map(|x| { - if x.starts_with(cmd_line) { - Some(x.clone()) - } else { - None - } - }) - .collect::>(); - - self.command_list - .update_params(cmds, self.command_line.clone()); - } - - fn hex_string_to_bytes(hex_string: &str) -> Result, ()> { - if hex_string.len() % 2 != 0 { - return Err(()); - } - - if !hex_string - .chars() - .all(|x| "0123456789abcdefABCDEF".contains(x)) - { - return Err(()); - } - - let res = (0..hex_string.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&hex_string[i..(i + 2)], 16).unwrap()) - .collect(); - - Ok(res) - } - - fn show_hint(&mut self) { - self.current_hint = Some(self.hints.choose(&mut rand::thread_rng()).unwrap()); - } - - fn clear_hint(&mut self) { - self.current_hint = None; - } - - async fn handle_key_input(&mut self, key: KeyEvent, _term_size: Rect) -> Result<(), ()> { - match key.code { - KeyCode::Char('l') if key.modifiers == KeyModifiers::CONTROL => { - let mut text_view = self.text_view.lock().await; - text_view.clear() - } - KeyCode::Char('q') if key.modifiers == KeyModifiers::CONTROL => { - self.error_pop_up.take(); - } - KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => { - let is_recording = { - let mut text_view = self.text_view.lock().await; - text_view.get_mut_recorder().is_recording() - }; - - if is_recording { - self.set_error_pop_up("Cannot save file while recording.".to_string()); - return Ok(()); - } - - self.blink_color.start(); - let mut text_view = self.text_view.lock().await; - let typewriter = text_view.get_mut_typewriter(); - let filename = typewriter.get_filename(); - let save_result = typewriter.flush().await; - text_view - .add_data_out(SerialRxData::Plugin { - plugin_name: "SAVE".to_string(), - timestamp: Local::now(), - content: if let Err(err) = &save_result { - format!("Cannot save on \"{}\": {}", filename, err) - } else { - format!("Content saved on \"{}\"", filename) - }, - is_successful: save_result.is_ok(), - }) - .await; - } - KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { - let mut text_view = self.text_view.lock().await; - let recorder = text_view.get_mut_recorder(); - let filename = recorder.get_filename(); - let record_msg = if !recorder.is_recording() { - let record_result = recorder.start_record().await; - SerialRxData::Plugin { - plugin_name: "REC".to_string(), - timestamp: Local::now(), - content: if let Err(err) = &record_result { - format!( - "Cannot start content recording on \"{}\": {}", - filename, err - ) - } else { - format!("Recording content on \"{}\"...", filename) - }, - is_successful: record_result.is_ok(), - } - } else { - recorder.stop_record(); - SerialRxData::Plugin { - plugin_name: "REC".to_string(), - timestamp: Local::now(), - content: format!("Content recorded on \"{}\"", filename), - is_successful: true, - } - }; - text_view.add_data_out(record_msg).await; - } - KeyCode::Char(c) => { - self.clear_hint(); - - if self.command_line_idx >= self.command_line.chars().count() { - self.command_line.push(c); - } else { - self.command_line = self.command_line.chars().enumerate().fold( - "".to_string(), - |mut acc, (i, x)| { - if i == self.command_line_idx { - acc.push(c); - } - - acc.push(x); - acc - }, - ); - } - - self.command_line_idx += 1; - self.update_command_list(); - self.history_index = None; - } - KeyCode::PageUp if key.modifiers == KeyModifiers::CONTROL => { - self.text_view.lock().await.scroll_to_start(); - } - KeyCode::PageUp => self.text_view.lock().await.page_up(), - KeyCode::PageDown if key.modifiers == KeyModifiers::CONTROL => { - self.text_view.lock().await.scroll_to_end(); - } - KeyCode::PageDown => self.text_view.lock().await.page_down(), - KeyCode::Backspace => { - if self.command_line.chars().count() == 1 { - self.show_hint(); - } - - if self.command_line_idx > 0 { - self.command_line_idx -= 1; - self.command_line = self - .command_line - .chars() - .enumerate() - .filter_map(|(i, c)| { - if i != self.command_line_idx { - Some(c) - } else { - None - } - }) - .collect(); - self.update_command_list(); - } - - if self.command_line.chars().count() > 0 - && self.command_line.chars().all(|x| x.is_whitespace()) - { - self.command_line.clear(); - self.command_line_idx = 0; - self.show_hint(); - } - } - KeyCode::Delete => { - if self.command_line.chars().count() == 0 { - self.show_hint(); - } - - self.command_line = self - .command_line - .chars() - .enumerate() - .filter_map(|(i, c)| { - if i != self.command_line_idx { - Some(c) - } else { - None - } - }) - .collect(); - self.update_command_list(); - - if self.command_line.chars().count() > 0 - && self.command_line.chars().all(|x| x.is_whitespace()) - { - self.command_line.clear(); - self.command_line_idx = 0; - self.show_hint(); - } - } - KeyCode::Right => { - if self.command_line_idx == self.command_line.chars().count() { - return Ok(()); - } - - self.command_line_idx += 1; - } - KeyCode::Left => { - if self.command_line_idx == 0 { - return Ok(()); - } - - self.command_line_idx -= 1; - } - KeyCode::Up => { - if self.history.is_empty() { - return Ok(()); - } - - match &mut self.history_index { - None => { - self.history_index = Some(self.history.len() - 1); - self.backup_command_line.clone_from(&self.command_line); - } - Some(0) => {} - Some(idx) => { - *idx -= 1; - } - } - - self.clear_hint(); - self.command_line - .clone_from(&self.history[self.history_index.unwrap()]); - self.command_line_idx = self.command_line.chars().count(); - self.update_command_list(); - } - KeyCode::Down => { - if self.history.is_empty() { - return Ok(()); - } - - match &mut self.history_index { - None => {} - Some(idx) if *idx == (self.history.len() - 1) => { - self.history_index = None; - self.command_line.clone_from(&self.backup_command_line); - if self.command_line.is_empty() { - self.show_hint(); - } - } - Some(idx) => { - *idx += 1; - self.command_line.clone_from(&self.history[*idx]); - } - } - - self.command_line_idx = self.command_line.chars().count(); - self.update_command_list(); - } - KeyCode::Esc => { - let interface = self.interface.lock().await; - interface.exit().await; - sleep(Duration::from_millis(100)).await; - return Err(()); - } - KeyCode::Enter if !self.command_line.is_empty() => { - let command_line = self.command_line.clone(); - self.show_hint(); - self.history.push(self.command_line.clone()); - self.command_line.clear(); - self.command_list.clear(); - self.history_index = None; - self.command_line_idx = 0; - - match command_line.chars().next().unwrap() { - '/' => { - let Some(filepath) = self.command_filepath.clone() else { - self.set_error_pop_up("No YAML command file loaded!".to_string()); - return Ok(()); - }; - - let yaml_content = self.load_commands(&filepath); - if yaml_content.is_empty() { - return Ok(()); - } - - let key = command_line.strip_prefix('/').unwrap(); - - if !yaml_content.contains_key(key) { - self.set_error_pop_up(format!("Command not found", key)); - return Ok(()); - } - - let data_to_send = yaml_content.get(key).unwrap(); - let data_to_send = data_to_send.replace("\\r", "\r").replace("\\n", "\n"); - let interface = self.interface.lock().await; - interface.send(UserTxData::Command { - command_name: key.to_string(), - content: data_to_send, - }); - } - '$' => { - let command_line = command_line - .strip_prefix('$') - .unwrap() - .replace([',', ' '], "") - .to_uppercase(); - - let Ok(bytes) = CommandBar::hex_string_to_bytes(&command_line) else { - self.set_error_pop_up(format!("Invalid hex string: {}", command_line)); - return Ok(()); - }; - - let interface = self.interface.lock().await; - interface.send(UserTxData::HexString { content: bytes }); - } - _ => { - let interface = self.interface.lock().await; - interface.send(UserTxData::Data { - content: command_line, - }); - } - } - - self.error_pop_up.take(); - } - _ => {} - } - - Ok(()) - } - - pub async fn update(&mut self, term_size: Rect) -> Result<(), ()> { - { - let mut text_view = self.text_view.lock().await; - text_view.set_frame_height(term_size.height); - text_view.update_scroll(); - } - - if let Some(error_pop_up) = self.error_pop_up.as_ref() { - if error_pop_up.is_timeout() { - self.error_pop_up.take(); - } - } - - self.blink_color.update(); - - { - let mut interface = self.interface.lock().await; - if let Ok(data_out) = interface.try_recv() { - let mut text_view = self.text_view.lock().await; - text_view.add_data_out(data_out.clone()).await; - } - } - - let Ok(input_evt) = self.key_receiver.try_recv() else { - return Ok(()); - }; - - match input_evt { - Key(key) => return self.handle_key_input(key, term_size).await, - VerticalScroll(direction) => { - let mut text_view = self.text_view.lock().await; - if direction < 0 { - text_view.up_scroll(); - } else { - text_view.down_scroll(); - } - } - HorizontalScroll(direction) => { - let mut text_view = self.text_view.lock().await; - if direction < 0 { - text_view.left_scroll(); - } else { - text_view.right_scroll(); - } - } - } - - Ok(()) - } - - fn load_commands(&mut self, filepath: &PathBuf) -> BTreeMap { - let Ok(yaml) = std::fs::read(filepath) else { - self.set_error_pop_up(format!("Cannot find {:?} filepath", filepath)); - return BTreeMap::new(); - }; - - let Ok(yaml_str) = std::str::from_utf8(yaml.as_slice()) else { - self.set_error_pop_up(format!("The file {:?} has non UTF-8 characters", filepath)); - return BTreeMap::new(); - }; - - let Ok(commands) = serde_yaml::from_str(yaml_str) else { - self.set_error_pop_up(format!( - "The YAML from {:?} has an incorret format", - filepath - )); - return BTreeMap::new(); - }; - - commands - } -} - -enum InputEvent { - Key(KeyEvent), - VerticalScroll(i8), - HorizontalScroll(i8), -} - -struct CommandList { - commands: Vec, - pattern: String, -} - -impl CommandList { - pub fn new() -> Self { - Self { - commands: vec![], - pattern: String::new(), - } - } - - pub fn clear(&mut self) { - self.commands.clear(); - self.pattern.clear(); - } - - pub fn update_params(&mut self, commands: Vec, pattern: String) { - self.commands = commands; - self.pattern = pattern; - } - - pub fn draw(&self, f: &mut Frame, command_bar_y: u16, color: Color) { - if self.commands.is_empty() { - return; - } - - let max_commands = min(f.size().height as usize / 2, self.commands.len()); - let mut commands = self.commands[..max_commands].to_vec(); - if commands.len() < self.commands.len() { - commands.push("...".to_string()); - } - - let longest_command_len = commands - .iter() - .fold(0u16, |len, x| max(len, x.chars().count() as u16)); - let area_size = (longest_command_len + 5, commands.len() as u16 + 2); - let area = Rect::new( - f.size().x + 2, - command_bar_y - area_size.1, - area_size.0, - area_size.1, - ); - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Thick) - .style(Style::default().fg(color)); - let text = commands - .iter() - .map(|x| { - let is_last = - (x == commands.last().unwrap()) && (commands.len() < self.commands.len()); - - Line::from(vec![ - Span::styled( - format!(" {}", if !is_last { &self.pattern } else { "" }), - Style::default().fg(color), - ), - Span::styled( - x[self.pattern.len() - 1..].to_string(), - Style::default().fg(Color::DarkGray), - ), - ]) - }) - .collect::>(); - let paragraph = Paragraph::new(text).block(block); - f.render_widget(Clear, area); - f.render_widget(paragraph, area); - } -} diff --git a/src/old/messages.rs b/src/old/messages.rs deleted file mode 100644 index f7f2603..0000000 --- a/src/old/messages.rs +++ /dev/null @@ -1,243 +0,0 @@ -use crate::rich_string::RichText; -use crate::text::ViewData; -use chrono::{DateTime, Local}; -use ratatui::style::Color; -use std::borrow::Cow; - -pub enum UserTxData { - Data { - content: String, - }, - Command { - command_name: String, - content: String, - }, - HexString { - content: Vec, - }, -} - -#[derive(Clone)] -pub enum SerialRxData { - RxData { - timestamp: DateTime, - content: Vec, - }, - TxData { - timestamp: DateTime, - content: String, - is_successful: bool, - }, - Command { - timestamp: DateTime, - command_name: String, - content: String, - is_successful: bool, - }, - HexString { - timestamp: DateTime, - content: Vec, - is_successful: bool, - }, - Plugin { - timestamp: DateTime, - plugin_name: String, - content: String, - is_successful: bool, - }, -} - -impl SerialRxData { - fn from_utf8_print_invalid(v: &[u8]) -> Cow<'_, str> { - let mut iter = v.utf8_chunks(); - - let chunk = if let Some(chunk) = iter.next() { - let valid = chunk.valid(); - if chunk.invalid().is_empty() { - debug_assert_eq!(valid.len(), v.len()); - return Cow::Borrowed(valid); - } - chunk - } else { - return Cow::Borrowed(""); - }; - - let mut res = String::with_capacity(v.len()); - res.push_str(chunk.valid()); - res.extend(chunk.invalid().iter().map(|ch| format!("\\x{:02x}", ch))); - - for chunk in iter { - res.push_str(chunk.valid()); - res.extend(chunk.invalid().iter().map(|ch| format!("\\x{:02x}", ch))); - } - - Cow::Owned(res) - } - - pub fn serialize(&self) -> String { - let success = " OK"; - let fail = "ERR"; - - match self { - SerialRxData::RxData { timestamp, content } => { - format!( - "[{}|<=| OK]{}", - timestamp.format("%H:%M:%S.%3f"), - Self::from_utf8_print_invalid(content) - ) - } - SerialRxData::TxData { - timestamp, - content, - is_successful, - } => { - format!( - "[{}|=>|{}]{}", - timestamp.format("%H:%M:%S.%3f"), - if *is_successful { success } else { fail }, - content - ) - } - SerialRxData::Command { - timestamp, - command_name, - content, - is_successful, - } => { - format!( - "[{}|=>|{}|/{}]{}", - timestamp.format("%H:%M:%S.%3f"), - if *is_successful { success } else { fail }, - command_name, - content - ) - } - SerialRxData::HexString { - timestamp, - content, - is_successful, - } => { - format!( - "[{}|=>|{}]{:?}", - timestamp.format("%H:%M:%S.%3f"), - if *is_successful { success } else { fail }, - content - ) - } - SerialRxData::Plugin { - timestamp, - content, - is_successful, - plugin_name, - } => { - format!( - "[{}| P|{}|!{}]{}", - timestamp.format("%H:%M:%S.%3f"), - if *is_successful { success } else { fail }, - plugin_name, - content - ) - } - } - } -} - -#[allow(clippy::from_over_into)] -impl Into for SerialRxData { - fn into(self) -> ViewData { - match self { - SerialRxData::RxData { timestamp, content } => { - RichText::new(content, Color::Reset, Color::Reset) - .decode_ansi_color() - .highlight_invisible() - .into_view_data(timestamp) - } - SerialRxData::TxData { - timestamp, - content, - is_successful, - } => { - if is_successful { - RichText::from_string(content, Color::Black, Color::LightCyan) - .highlight_invisible() - .into_view_data(timestamp) - } else { - RichText::from_string( - format!("Cannot send \"{}\"", content), - Color::White, - Color::LightRed, - ) - .highlight_invisible() - .into_view_data(timestamp) - } - } - SerialRxData::Command { - timestamp, - command_name, - content, - is_successful, - } => { - if is_successful { - RichText::from_string( - format!(" {}", command_name, content), - Color::Black, - Color::LightGreen, - ) - .highlight_invisible() - .into_view_data(timestamp) - } else { - RichText::from_string( - format!("Cannot send ", command_name), - Color::White, - Color::LightRed, - ) - .highlight_invisible() - .into_view_data(timestamp) - } - } - SerialRxData::HexString { - timestamp, - content, - is_successful, - } => { - if is_successful { - RichText::from_string(format!("{:02x?}", &content), Color::Black, Color::Yellow) - .highlight_invisible() - .into_view_data(timestamp) - } else { - RichText::from_string( - format!("Cannot send {:02x?}", &content), - Color::White, - Color::LightRed, - ) - .highlight_invisible() - .into_view_data(timestamp) - } - } - SerialRxData::Plugin { - timestamp, - plugin_name, - content, - is_successful, - } => { - if is_successful { - RichText::from_string( - format!(" [{plugin_name}] {content} "), - Color::Black, - Color::White, - ) - .highlight_invisible() - .into_view_data(timestamp) - } else { - RichText::from_string( - format!(" [{plugin_name}] {content} "), - Color::White, - Color::Red, - ) - .highlight_invisible() - .into_view_data(timestamp) - } - } - } - } -} diff --git a/src/old/serial.rs b/src/old/serial.rs deleted file mode 100644 index cd4c5b3..0000000 --- a/src/old/serial.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::messages::{SerialRxData, UserTxData}; -use crate::task_bridge::{Task, TaskBridge}; -use chrono::Local; -use std::io; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tokio::sync::Mutex; -use tokio::time::Instant; - -pub type SerialIF = TaskBridge; - -pub struct SerialIFShared { - info: SerialInfo, - is_connected: Arc, - reconnect: bool, - is_exit: bool, - disconnect: Option, -} - -pub struct SerialIFSynchronized { - info: SerialInfo, - is_connected: Arc, -} - -impl SerialIF { - pub fn build_and_connect(port: &str, baudrate: u32) -> Self { - let info = SerialInfo { - port: port.to_string(), - baudrate, - }; - let info2 = info.clone(); - - let is_connected = Arc::new(AtomicBool::new(false)); - let is_connected2 = is_connected.clone(); - - Self::new::( - SerialIFShared { - is_exit: false, - is_connected, - disconnect: None, - reconnect: true, - info, - }, - SerialIFSynchronized { - info: info2, - is_connected: is_connected2, - }, - ) - } - - pub fn is_connected(&self) -> bool { - self.synchronized().is_connected.load(Ordering::SeqCst) - } - - pub fn description(&self) -> String { - let info = &self.synchronized().info; - - format!("Serial {}:{}bps", info.port, info.baudrate) - } - - pub async fn exit(&self) { - self.shared().await.is_exit = true; - } -} - -struct SerialTask; - -impl SerialTask { - const SERIAL_TIMEOUT: Duration = Duration::from_millis(100); - - fn set_is_connected( - info: SerialInfo, - is_connected: &AtomicBool, - to_bridge: &UnboundedSender, - val: bool, - ) { - is_connected.store(val, Ordering::SeqCst); - - if is_connected.load(Ordering::SeqCst) { - to_bridge - .send(SerialRxData::Plugin { - is_successful: true, - timestamp: Local::now(), - plugin_name: "serial".to_string(), - content: format!("Connected at \"{}\" with {}bps", info.port, info.baudrate), - }) - .expect("Cannot forward message read from serial") - } else { - to_bridge - .send(SerialRxData::Plugin { - is_successful: true, - timestamp: Local::now(), - plugin_name: "serial".to_string(), - content: format!( - "Disconnected from \"{}\" with {}bps", - info.port, info.baudrate - ), - }) - .expect("Cannot forward message read from serial"); - } - } -} - -impl Task for SerialTask { - async fn run( - shared: Arc>, - mut from_bridge: UnboundedReceiver, - to_bridge: UnboundedSender, - ) { - let mut line = vec![]; - let mut buffer = [0u8]; - let mut now = Instant::now(); - let mut serial_wrapper = None; - - 'task: loop { - let mut shared = shared.lock().await; - - if shared.is_exit { - break 'task; - } - - if let Some(old_serial_info) = shared.disconnect.take() { - let _ = serial_wrapper.take(); - Self::set_is_connected(old_serial_info, &shared.is_connected, &to_bridge, false); - continue 'task; - } - - if shared.reconnect { - let _ = serial_wrapper.take(); - shared.is_connected.store(false, Ordering::SeqCst); - let Ok(serial) = tokio_serial::new(&shared.info.port, shared.info.baudrate) - .data_bits(tokio_serial::DataBits::Eight) - .flow_control(tokio_serial::FlowControl::None) - .parity(tokio_serial::Parity::None) - .stop_bits(tokio_serial::StopBits::One) - .timeout(Self::SERIAL_TIMEOUT) - .open() - else { - continue 'task; - }; - - Self::set_is_connected(shared.info.clone(), &shared.is_connected, &to_bridge, true); - shared.reconnect = false; - let _ = serial_wrapper.insert(serial); - } - - let Some(mut serial) = serial_wrapper.take() else { - continue 'task; - }; - - if let Ok(data_to_send) = from_bridge.try_recv() { - match data_to_send { - UserTxData::Data { content } => { - let content = format!("{content}\r\n"); - - match serial.write_all(content.as_bytes()) { - Ok(_) => { - to_bridge - .send(SerialRxData::TxData { - timestamp: Local::now(), - content, - is_successful: true, - }) - .expect("Cannot send data confirm"); - } - Err(err) => { - to_bridge - .send(SerialRxData::TxData { - timestamp: Local::now(), - content: content + &err.to_string(), - is_successful: false, - }) - .expect("Cannot send data fail"); - } - } - } - UserTxData::Command { - command_name, - content, - } => { - let content = format!("{content}\r\n"); - - match serial.write_all(content.as_bytes()) { - Ok(_) => { - to_bridge - .send(SerialRxData::Command { - timestamp: Local::now(), - command_name, - content, - is_successful: true, - }) - .expect("Cannot send command confirm"); - } - Err(_) => { - to_bridge - .send(SerialRxData::Command { - timestamp: Local::now(), - command_name, - content, - is_successful: false, - }) - .expect("Cannot send command fail"); - } - } - } - UserTxData::HexString { content } => match serial.write(&content) { - Ok(_) => to_bridge - .send(SerialRxData::HexString { - timestamp: Local::now(), - content, - is_successful: true, - }) - .expect("Cannot send hex string comfirm"), - Err(_) => to_bridge - .send(SerialRxData::HexString { - timestamp: Local::now(), - content, - is_successful: false, - }) - .expect("Cannot send hex string fail"), - }, - } - } - - match serial.read(&mut buffer) { - Ok(_) => { - now = Instant::now(); - line.push(buffer[0]); - if buffer[0] == b'\n' { - to_bridge - .send(SerialRxData::RxData { - timestamp: Local::now(), - content: line.clone(), - }) - .expect("Cannot forward message read from serial"); - line.clear(); - now = Instant::now(); - } - } - Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {} - Err(ref e) - if e.kind() == io::ErrorKind::PermissionDenied - || e.kind() == io::ErrorKind::BrokenPipe => - { - shared.reconnect = true; - continue 'task; - } - Err(_) => {} - } - - if now.elapsed().as_millis() > 1_000 { - now = Instant::now(); - - if !line.is_empty() { - to_bridge - .send(SerialRxData::RxData { - timestamp: Local::now(), - content: line.clone(), - }) - .expect("Cannot forward message read from serial"); - line.clear(); - } - } - - serial_wrapper = Some(serial); - } - } -} - -#[derive(Clone)] -pub struct SerialInfo { - port: String, - baudrate: u32, -} diff --git a/src/old/task_bridge.rs b/src/old/task_bridge.rs deleted file mode 100644 index 39323b7..0000000 --- a/src/old/task_bridge.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::future::Future; -use std::sync::Arc; -use tokio::sync::mpsc::error::TryRecvError; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tokio::sync::{Mutex, MutexGuard}; - -pub struct TaskBridge -where - Shared: Send + 'static, - ToTask: Send + 'static, - FromTask: Send + 'static, -{ - shared: Arc>, - synchronized: Synchronized, - to_task: UnboundedSender, - from_task: UnboundedReceiver, -} - -impl TaskBridge -where - Shared: Send + 'static, - ToTask: Send + 'static, - FromTask: Send + 'static, -{ - pub fn new(shared: Shared, synchronized: Synchronized) -> Self - where - T: Task, - { - let shared = Arc::new(Mutex::new(shared)); - let (to_task, to_task_rx) = tokio::sync::mpsc::unbounded_channel(); - let (from_task_tx, from_task) = tokio::sync::mpsc::unbounded_channel(); - - let shared2 = shared.clone(); - - tokio::spawn(async move { - T::run(shared2, to_task_rx, from_task_tx).await; - }); - - Self { - shared, - synchronized, - to_task, - from_task, - } - } - - pub fn synchronized(&self) -> &Synchronized { - &self.synchronized - } - - pub async fn shared(&self) -> MutexGuard { - self.shared.lock().await - } - - pub fn send(&self, data: ToTask) { - self.to_task.send(data).unwrap() - } - - pub fn try_recv(&mut self) -> Result { - self.from_task.try_recv() - } -} - -pub trait Task { - fn run( - shared: Arc>, - from_bridge: UnboundedReceiver, - to_bridge: UnboundedSender, - ) -> impl Future + Send; -} diff --git a/src/old/text.rs b/src/old/text.rs deleted file mode 100644 index 7e82e67..0000000 --- a/src/old/text.rs +++ /dev/null @@ -1,249 +0,0 @@ -use crate::messages::SerialRxData; -use crate::recorder::Recorder; -use crate::rich_string::RichText; -use crate::typewriter::TypeWriter; -use chrono::{DateTime, Local}; -use ratatui::layout::{Alignment, Rect}; -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::block::Title; -use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; -use ratatui::Frame; - -pub struct TextView { - history: Vec, - typewriter: TypeWriter, - recorder: Recorder, - capacity: usize, - auto_scroll: bool, - scroll: (u16, u16), - frame_height: u16, -} - -impl TextView { - pub fn new(capacity: usize, filename: String) -> Self { - Self { - history: vec![], - typewriter: TypeWriter::new(filename.clone()), - recorder: Recorder::new(filename).expect("Cannot create Recorder"), - capacity, - auto_scroll: true, - scroll: (0, 0), - frame_height: u16::MAX, - } - } - - fn max_main_axis(&self) -> u16 { - let main_axis_length = self.frame_height - 5; - let history_len = self.history.len() as u16; - - if history_len > main_axis_length { - history_len - main_axis_length - } else { - 0 - } - } - - pub fn get_mut_typewriter(&mut self) -> &mut TypeWriter { - &mut self.typewriter - } - - pub fn get_mut_recorder(&mut self) -> &mut Recorder { - &mut self.recorder - } - - pub fn draw(&self, f: &mut Frame, rect: Rect, blink_color: Option) { - let scroll = if self.auto_scroll { - (self.max_main_axis(), self.scroll.1) - } else { - self.scroll - }; - - let (coll, coll_size) = (&self.history[(scroll.0 as usize)..], self.history.len()); - let border_type = if self.auto_scroll { - BorderType::Thick - } else { - BorderType::Double - }; - - let block = if self.recorder.is_recording() { - Block::default() - .title(format!( - "[{:03}][ASCII] ◉ {}", - coll_size, - self.recorder.get_filename() - )) - .title( - Title::from(format!("[{}]", self.recorder.get_size())) - .alignment(Alignment::Right), - ) - .borders(Borders::ALL) - .border_type(border_type) - .border_style(Style::default().fg(Color::Yellow)) - } else { - Block::default() - .title(format!( - "[{:03}][ASCII] {}", - coll_size, - self.typewriter.get_filename() - )) - .title( - Title::from(format!("[{}]", self.typewriter.get_size())) - .alignment(Alignment::Right), - ) - .borders(Borders::ALL) - .border_type(border_type) - .border_style(Style::default().fg(blink_color.unwrap_or(Color::Reset))) - }; - - let text = coll - .iter() - .map(|ViewData { data, timestamp }| { - let timestamp_span = Span::styled( - format!("{} ", timestamp.format("%H:%M:%S.%3f")), - Style::default().fg(Color::DarkGray), - ); - let content = vec![timestamp_span] - .into_iter() - .chain(RichText::crop_rich_texts(data, scroll.1 as usize)) - .collect::>(); - - Line::from(content) - }) - .collect::>(); - let paragraph = Paragraph::new(text).block(block); - f.render_widget(paragraph, rect); - } - - pub async fn add_data_out(&mut self, data: SerialRxData) { - if self.history.len() >= self.capacity { - self.history.remove(0); - } - - if self.recorder.is_recording() { - self.recorder - .add_content(data.serialize()) - .await - .expect("Cannot record data"); - } else { - self.typewriter += data.serialize(); - } - self.history.push(data.into()); - } - - pub fn clear(&mut self) { - self.scroll = (0, 0); - self.auto_scroll = true; - self.history.clear(); - } - - pub fn up_scroll(&mut self) { - if self.max_main_axis() > 0 { - self.auto_scroll = false; - } - - if self.scroll.0 < 3 { - self.scroll.0 = 0; - } else { - self.scroll.0 -= 3; - } - } - - pub fn down_scroll(&mut self) { - let max_main_axis = self.max_main_axis(); - - self.scroll.0 += 3; - self.scroll.0 = self.scroll.0.clamp(0, max_main_axis); - - if self.scroll.0 == max_main_axis { - self.auto_scroll = true; - } - } - - pub fn page_up(&mut self) { - if self.max_main_axis() > 0 { - self.auto_scroll = false; - } - - let page_height = self.frame_height - 5; - - if self.scroll.0 < page_height { - self.scroll.0 = 0; - } else { - self.scroll.0 -= page_height; - } - } - - pub fn page_down(&mut self) { - let max_main_axis = self.max_main_axis(); - let page_height = self.frame_height - 5; - - self.scroll.0 += page_height; - self.scroll.0 = self.scroll.0.clamp(0, max_main_axis); - - if self.scroll.0 == max_main_axis { - self.auto_scroll = true; - } - } - - pub fn scroll_to_start(&mut self) { - if self.max_main_axis() > 0 { - self.auto_scroll = false; - } - - self.scroll.0 = 0; - } - - pub fn scroll_to_end(&mut self) { - let max_main_axis = self.max_main_axis(); - - self.scroll.0 = max_main_axis; - self.auto_scroll = true; - } - - pub fn left_scroll(&mut self) { - if self.scroll.1 < 3 { - self.scroll.1 = 0; - } else { - self.scroll.1 -= 3; - } - } - - pub fn right_scroll(&mut self) { - self.scroll.1 += 3; - } - - pub fn set_frame_height(&mut self, frame_height: u16) { - self.frame_height = frame_height; - } - - pub fn update_scroll(&mut self) { - self.scroll = if self.auto_scroll { - (self.max_main_axis(), self.scroll.1) - } else { - self.scroll - }; - } -} - -pub struct ViewData { - timestamp: DateTime, - data: Vec, -} - -impl ViewData { - pub fn new(timestamp: DateTime, data: Vec) -> Self { - Self { timestamp, data } - } -} - -pub fn into_byte_format(size: u128) -> String { - let (size, unit) = match size { - x if x < 1024 => return format!("{} Bytes", size), - x if x < 1024 * 1024 => (size as f32 / 1024.0, "KB"), - x if x < 1024 * 1024 * 1024 => (size as f32 / (1024.0 * 1024.0), "MB"), - _ => (size as f32 / (1024.0 * 1024.0 * 1024.0), "GB"), - }; - - format!("{:.1} {}", size, unit) -} diff --git a/src/plugin/plugin_engine.rs b/src/plugin/plugin_engine.rs index fc657ea..22f6d71 100644 --- a/src/plugin/plugin_engine.rs +++ b/src/plugin/plugin_engine.rs @@ -1,6 +1,9 @@ -use std::sync::{ - mpsc::{Receiver, Sender}, - Arc, RwLock, +use std::{ + sync::{ + mpsc::{Receiver, Sender}, + Arc, RwLock, + }, + thread::yield_now, }; use crate::infra::{ @@ -16,6 +19,7 @@ pub enum PluginEngineCommand { Exit, } +#[allow(unused)] pub struct PluginEngineConnections { logger: Logger, tx_producer: Producer>, @@ -35,9 +39,17 @@ impl PluginEngine { pub fn task( _shared: Arc>, _private: PluginEngineConnections, - _receiver: Receiver, + cmd_receiver: Receiver, ) { - todo!() + 'plugin_engine_loop: loop { + if let Ok(cmd) = cmd_receiver.try_recv() { + match cmd { + PluginEngineCommand::Exit => break 'plugin_engine_loop, + } + } + + yield_now(); + } } } diff --git a/src/serial/serial_if.rs b/src/serial/serial_if.rs index 4ae5419..a9475ee 100644 --- a/src/serial/serial_if.rs +++ b/src/serial/serial_if.rs @@ -1,14 +1,15 @@ use crate::{ error, infra::{ - logger::{self, LogLevel, Logger}, + logger::{LogLevel, Logger}, messages::TimedBytes, mpmc::{Consumer, Producer}, task::Task, }, + success, warning, }; use chrono::Local; -use serialport::{DataBits, FlowControl, Parity, SerialPortBuilder, StopBits, TTYPort}; +use serialport::{DataBits, FlowControl, Parity, StopBits, TTYPort}; use std::{ io::{self, Read, Write}, sync::{ @@ -98,6 +99,15 @@ impl SerialInterface { ) } + fn set_mode(shared: Arc>, mode: Option) { + let Some(mode) = mode else { + return; + }; + + let mut sw = shared.write().expect("Cannot get serial lock for write"); + sw.mode = mode; + } + fn task( shared: Arc>, connections: SerialConnections, @@ -106,39 +116,41 @@ impl SerialInterface { let SerialConnections { logger, tx, rx } = connections; let mut line = vec![]; let mut buffer = [0u8]; - let mut mode = shared - .read() - .expect("Cannot get serial shared for read") - .mode; - #[cfg(target_os = "linux")] - let mut serial: Option = None; + let mut serial = None; let mut now = Instant::now(); 'task_loop: loop { if let Ok(cmd) = cmd_receiver.try_recv() { - match cmd { - SerialCommand::Connect => mode = Self::connect(shared.clone(), &mut serial), + let new_mode = match cmd { + SerialCommand::Connect => Self::connect(shared.clone(), &mut serial, &logger), SerialCommand::Disconnect => { - mode = Self::disconnect(shared.clone(), &mut serial); + Self::disconnect(shared.clone(), &mut serial, &logger) } SerialCommand::Exit => break 'task_loop, SerialCommand::Setup(setup) => { - if let Some(new_mode) = Self::setup(shared.clone(), setup, &mut serial) { - mode = new_mode; - } + Self::setup(shared.clone(), setup, &mut serial, &logger) } - } + }; + Self::set_mode(shared.clone(), new_mode); } - match mode { - SerialMode::DoNotConnect => { - std::thread::yield_now(); - continue; - } - SerialMode::Reconnecting => { - mode = Self::connect(shared.clone(), &mut serial); + { + let mode = shared + .read() + .expect("Cannot get serial shared for read") + .mode; + + match mode { + SerialMode::DoNotConnect => { + std::thread::yield_now(); + continue; + } + SerialMode::Reconnecting => { + let new_mode = Self::connect(shared.clone(), &mut serial, &logger); + Self::set_mode(shared.clone(), new_mode); + } + SerialMode::Connected => { /* Do nothing. It's already connected. */ } } - SerialMode::Connected => { /* Do nothing. It's already connected. */ } } let Some(mut ser) = serial.take() else { @@ -169,7 +181,8 @@ impl SerialInterface { if e.kind() == io::ErrorKind::PermissionDenied || e.kind() == io::ErrorKind::BrokenPipe => { - mode = Self::disconnect(shared.clone(), &mut Some(ser)); + let _ = Self::disconnect(shared.clone(), &mut Some(ser), &logger); + Self::set_mode(shared.clone(), Some(SerialMode::Reconnecting)); continue; } Err(_) => {} @@ -190,11 +203,19 @@ impl SerialInterface { } } - fn connect(shared: Arc>, serial: &mut Option) -> SerialMode { + fn connect( + shared: Arc>, + serial: &mut Option, + logger: &Logger, + ) -> Option { let sw = shared .read() .expect("Cannot get serial share lock for write"); + if let SerialMode::Connected = sw.mode { + return None; + } + let connect_res = serialport::new(sw.port.clone(), sw.baudrate) .data_bits(sw.data_bits) .flow_control(sw.flow_control) @@ -202,14 +223,24 @@ impl SerialInterface { .stop_bits(sw.stop_bits) .timeout(Duration::from_millis(Self::SERIAL_TIMEOUT_MS)) .open_native(); + match connect_res { Ok(ser) => { *serial = Some(ser); - SerialMode::Connected + success!( + logger, + "Connected at \"{}\" with {}bps", + sw.port, + sw.baudrate + ); + Some(SerialMode::Connected) } Err(_) => { let _ = serial.take(); - SerialMode::Reconnecting + match sw.mode { + SerialMode::Reconnecting => None, + _ => Some(SerialMode::Reconnecting), + } } } } @@ -217,17 +248,30 @@ impl SerialInterface { fn disconnect( shared: Arc>, serial: &mut Option, - ) -> SerialMode { + logger: &Logger, + ) -> Option { let _ = serial.take(); - let mut sw = shared.write().expect(""); - sw.mode = SerialMode::DoNotConnect; - sw.mode + let sw = shared.read().expect("Cannot get serial lock for read"); + if let SerialMode::Connected = sw.mode { + warning!( + logger, + "Disconnected from \"{}\" with {}bps", + sw.port, + sw.baudrate + ); + } + + match sw.mode { + SerialMode::DoNotConnect => None, + _ => Some(SerialMode::DoNotConnect), + } } fn setup( shared: Arc>, setup: SerialSetup, serial: &mut Option, + logger: &Logger, ) -> Option { let mut has_changes = false; let mut sw = shared @@ -264,9 +308,15 @@ impl SerialInterface { has_changes = true; } + let last_mode = sw.mode; if has_changes { - Self::disconnect(shared.clone(), serial); - Some(SerialMode::Reconnecting) + drop(sw); + let _ = Self::disconnect(shared.clone(), serial, &logger); + + match last_mode { + SerialMode::Reconnecting => None, + _ => Some(SerialMode::Reconnecting), + } } else { None } From 8586f7e1c6a5606fc52c4dde8363ac3f29eb6d27 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 17 Jul 2024 22:17:13 -0300 Subject: [PATCH 12/49] feat: add color to special characters --- src/graphics/graphics_task.rs | 40 +++++++++++++++++++++++++++-------- src/inputs/inputs_task.rs | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index a68ee92..32b2696 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -437,7 +437,7 @@ impl GraphicsTask { new_messages.push(GraphicalMessage::Rx { timestamp: rx_msg.timestamp, - message: vec![(Self::bytes_to_string(&rx_msg.message), Color::Reset)], + message: Self::bytes_to_string(&rx_msg.message, Color::Reset), }); } @@ -448,7 +448,7 @@ impl GraphicsTask { new_messages.push(GraphicalMessage::Tx { timestamp: tx_msg.timestamp, - message: vec![(Self::bytes_to_string(&tx_msg.message), Color::Black)], + message: Self::bytes_to_string(&tx_msg.message, Color::Black), }); } @@ -512,18 +512,40 @@ impl GraphicsTask { terminal.show_cursor().expect("Cannot show mouse cursor"); } - fn bytes_to_string(msg: &[u8]) -> String { - let mut output = "".to_string(); + fn bytes_to_string(msg: &[u8], color: Color) -> Vec<(String, Color)> { + let mut output = vec![]; + let mut buffer = "".to_string(); + let mut in_plain_text = true; for byte in msg { match *byte { - x if 0x20 <= x && x <= 0x7E => output.push(x as char), - 0x0a => output += "\\n", - 0x0d => output += "\\r", - _ => output += &format!("\\x{:02x}", byte), + x if 0x20 <= x && x <= 0x7E => { + if !in_plain_text { + output.push((buffer.drain(..).collect(), Color::Magenta)); + in_plain_text = true; + } + + buffer.push(x as char); + } + x => { + if in_plain_text { + output.push((buffer.drain(..).collect(), color)); + in_plain_text = false; + } + + match x { + 0x0a => buffer += "\\n", + 0x0d => buffer += "\\r", + _ => buffer += &format!("\\x{:02x}", byte), + } + } } } + if !buffer.is_empty() { + output.push((buffer, if in_plain_text { color } else { Color::Magenta })); + } + output } @@ -578,7 +600,7 @@ impl GraphicsTask { continue; } - let cropped_message = if scroll_x < (msg.len() + offset) { + let cropped_message = if scroll_x < (msg.len() + offset) && scroll_x >= offset { &msg[(scroll_x - offset)..] } else { &msg diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index d67880e..02adf31 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -268,7 +268,7 @@ impl InputsTask { private.tx.produce(Arc::new(TimedBytes { timestamp: Local::now(), - message: command_line.as_bytes().to_vec(), + message: (command_line + "\r\n").as_bytes().to_vec(), })); } } From e57e2ef7d0396fd41100c8bce0974169e6467f3a Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 19 Jul 2024 20:56:04 -0300 Subject: [PATCH 13/49] feat: add ansi colors and fix other colors --- Cargo.lock | 1 - Cargo.toml | 1 - src/graphics/graphics_task.rs | 58 ++++++++++++++++++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ced417..6acd9dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -980,7 +980,6 @@ dependencies = [ "mlua", "rand", "ratatui", - "regex", "serde", "serde_yaml", "serialport", diff --git a/Cargo.toml b/Cargo.toml index 46da825..1917ed3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ clap = { version = "4.1.9", features = ["derive"] } mlua = { version = "0.9.6", features = ["lua54", "vendored", "async", "send"] } anyhow = "1.0.79" homedir = "0.2.1" -regex = { version = "1.10.3", features = [] } tokio = { version = "1.36.0", features = ["full"] } [target.'cfg(windows)'.dependencies] diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index 32b2696..8890b3f 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -155,7 +155,7 @@ impl GraphicsTask { .map(|msg| match msg { GraphicalMessage::Log(log_msg) => Self::line_from_log_message(log_msg, scroll), GraphicalMessage::Tx { timestamp, message } => { - Self::line_from_message(timestamp, message, Color::Cyan, scroll) + Self::line_from_message(timestamp, message, Color::LightCyan, scroll) } GraphicalMessage::Rx { timestamp, message } => { Self::line_from_message(timestamp, message, Color::Reset, scroll) @@ -312,6 +312,17 @@ impl GraphicsTask { let mut terminal = Terminal::new(backend).expect("Cannot create terminal backend"); let blink = Blink::new(Duration::from_millis(200), 2, Color::Reset, Color::Black); let mut new_messages = vec![]; + let patterns = [ + ("\x1b[0m", Color::Reset), + ("\x1b[30m", Color::Black), + ("\x1b[31m", Color::Red), + ("\x1b[32m", Color::Green), + ("\x1b[33m", Color::Yellow), + ("\x1b[34m", Color::Blue), + ("\x1b[35m", Color::Magenta), + ("\x1b[36m", Color::Cyan), + ("\x1b[37m", Color::White), + ]; 'draw_loop: loop { { @@ -437,7 +448,7 @@ impl GraphicsTask { new_messages.push(GraphicalMessage::Rx { timestamp: rx_msg.timestamp, - message: Self::bytes_to_string(&rx_msg.message, Color::Reset), + message: Self::ansi_colors(&patterns, &rx_msg.message), }); } @@ -512,16 +523,55 @@ impl GraphicsTask { terminal.show_cursor().expect("Cannot show mouse cursor"); } + fn ansi_colors(patterns: &[(&'static str, Color)], msg: &[u8]) -> Vec<(String, Color)> { + let mut output = vec![]; + let mut buffer = "".to_string(); + let mut color = Color::Reset; + + for byte in msg { + buffer.push(*byte as char); + + if (*byte as char) != 'm' { + continue; + } + + 'pattern_loop: for (pattern, new_color) in patterns { + if buffer.contains(pattern) { + output.push((buffer.replace(pattern, ""), color)); + + buffer.clear(); + color = *new_color; + + break 'pattern_loop; + } + } + } + + if !buffer.is_empty() { + output.push((buffer, color)); + } + + output + .into_iter() + .flat_map(|(msg, color)| Self::bytes_to_string(msg.as_bytes(), color)) + .collect() + } + fn bytes_to_string(msg: &[u8], color: Color) -> Vec<(String, Color)> { let mut output = vec![]; let mut buffer = "".to_string(); let mut in_plain_text = true; + let accent_color = if color == Color::Magenta { + Color::DarkGray + } else { + Color::Magenta + }; for byte in msg { match *byte { x if 0x20 <= x && x <= 0x7E => { if !in_plain_text { - output.push((buffer.drain(..).collect(), Color::Magenta)); + output.push((buffer.drain(..).collect(), accent_color)); in_plain_text = true; } @@ -543,7 +593,7 @@ impl GraphicsTask { } if !buffer.is_empty() { - output.push((buffer, if in_plain_text { color } else { Color::Magenta })); + output.push((buffer, if in_plain_text { color } else { accent_color })); } output From d481cf3fd7e3ac922939c6539d3ad550d306f116 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 19 Jul 2024 21:25:46 -0300 Subject: [PATCH 14/49] fix: fix blink dead lock bug and timestamp format on files --- src/graphics/graphics_task.rs | 29 ++++++++++++---------------- src/infra/blink.rs | 36 ++++++++++++++--------------------- src/infra/timer.rs | 33 +++++++++----------------------- 3 files changed, 35 insertions(+), 63 deletions(-) diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index 8890b3f..d54ff26 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -310,7 +310,7 @@ impl GraphicsTask { .expect("Cannot enable alternate screen and mouse capture"); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend).expect("Cannot create terminal backend"); - let blink = Blink::new(Duration::from_millis(200), 2, Color::Reset, Color::Black); + let mut blink = Blink::new(Duration::from_millis(200), 2, Color::Reset, Color::Black); let mut new_messages = vec![]; let patterns = [ ("\x1b[0m", Color::Reset), @@ -325,10 +325,7 @@ impl GraphicsTask { ]; 'draw_loop: loop { - { - let mut blk = blink.lock().unwrap(); - blk.tick(); - } + blink.tick(); if let Ok(cmd) = cmd_receiver.try_recv() { match cmd { @@ -338,10 +335,7 @@ impl GraphicsTask { continue; } - { - let mut blk = blink.lock().unwrap(); - blk.start(); - } + blink.start(); let filename = private.typewriter.get_filename(); match private.typewriter.flush() { @@ -497,11 +491,7 @@ impl GraphicsTask { ]) .split(f.size()); - let blink_color = { - let blk = blink.lock().unwrap(); - blk.get_current() - }; - Self::draw_history(&mut private, f, chunks[0], blink_color); + Self::draw_history(&mut private, f, chunks[0], blink.get_current()); Self::draw_command_bar( &private.inputs_shared, &private.serial_shared, @@ -719,16 +709,21 @@ impl Serialize for GraphicalMessage { LogLevel::Debug => "DBG", }; - format!("[{}][{}] {}", log.timestamp, log_level, log.message) + format!( + "[{}][{}] {}", + log.timestamp.format("%H:%M:%S.%3f"), + log_level, + log.message + ) } GraphicalMessage::Tx { timestamp, message } => { let msg = message.iter().fold(String::new(), |acc, x| acc + &x.0); - format!("[{}][ =>] {}", timestamp, msg) + format!("[{}][ =>] {}", timestamp.format("%H:%M:%S.%3f"), msg) } GraphicalMessage::Rx { timestamp, message } => { let msg = message.iter().fold(String::new(), |acc, x| acc + &x.0); - format!("[{}][ <=] {}", timestamp, msg) + format!("[{}][ <=] {}", timestamp.format("%H:%M:%S.%3f"), msg) } } } diff --git a/src/infra/blink.rs b/src/infra/blink.rs index fffb929..476cce9 100644 --- a/src/infra/blink.rs +++ b/src/infra/blink.rs @@ -1,28 +1,23 @@ -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +use std::time::Duration; use super::timer::{Timeout, Timer}; -#[derive(Default)] struct TimerOn; -#[derive(Default)] struct TimerOff; pub struct Blink { on: T, off: T, current: T, - timer_on: Timer, - timer_off: Timer, + timer_on: Timer, + timer_off: Timer, total_blinks: usize, num_blinks: usize, } impl Blink { - pub fn new(duration: Duration, total_blinks: usize, on: T, off: T) -> Arc> { - let obj = Self { + pub fn new(duration: Duration, total_blinks: usize, on: T, off: T) -> Self { + Self { on: on.clone(), off, current: on, @@ -30,15 +25,7 @@ impl Blink { timer_off: Timer::new(duration), total_blinks, num_blinks: 0, - }; - let obj = Arc::new(Mutex::new(obj)); - { - let mut o = obj.lock().unwrap(); - o.timer_on.set_action(obj.clone()); - o.timer_off.set_action(obj.clone()); } - - obj } pub fn start(&mut self) { @@ -48,8 +35,13 @@ impl Blink { } pub fn tick(&mut self) { - self.timer_on.tick(); - self.timer_off.tick(); + if self.timer_on.tick() { + self.action(TimerOn); + } + + if self.timer_off.tick() { + self.action(TimerOff); + } } pub fn get_current(&self) -> T { @@ -58,14 +50,14 @@ impl Blink { } impl Timeout for Blink { - fn action(&mut self) { + fn action(&mut self, _id: TimerOn) { self.timer_off.start(); self.current = self.off.clone(); } } impl Timeout for Blink { - fn action(&mut self) { + fn action(&mut self, _id: TimerOff) { self.num_blinks += 1; self.current = self.on.clone(); diff --git a/src/infra/timer.rs b/src/infra/timer.rs index 4f0b7f6..d3a9d45 100644 --- a/src/infra/timer.rs +++ b/src/infra/timer.rs @@ -1,52 +1,37 @@ -use std::{ - marker::PhantomData, - sync::{Arc, Mutex}, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; pub trait Timeout { - fn action(&mut self); + fn action(&mut self, id: Id); } -pub struct Timer> { +pub struct Timer { duration: Duration, - action: Option>>, now: Option, - marker: PhantomData, } -impl> Timer { +impl Timer { pub fn new(duration: Duration) -> Self { Self { duration, - action: None, now: None, - marker: PhantomData, } } - pub fn set_action(&mut self, action: Arc>) { - self.action = Some(action); - } - pub fn start(&mut self) { self.now = Some(Instant::now()); } - pub fn tick(&mut self) { + pub fn tick(&mut self) -> bool { let Some(now) = self.now.as_ref() else { - return; + return false; }; if now.elapsed() < self.duration { - return; - } - - if let Some(action) = self.action.as_ref() { - let mut action = action.lock().unwrap(); - action.action(); + return false; } self.now.take(); + + true } } From e8e8e40fb33231d26fcb33801d850c7f0ae15980 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 19 Jul 2024 21:42:56 -0300 Subject: [PATCH 15/49] feat: fix hex sequence --- src/inputs/inputs_task.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index 02adf31..7cdf8fa 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -437,6 +437,7 @@ impl InputsTask { output.push(hex as char); } hex_shift = 0; + continue; } } @@ -495,3 +496,29 @@ impl InputsConnections { } } } + +#[cfg(test)] +mod tests { + use super::InputsTask; + + #[test] + fn test_rhs_one() { + let res = InputsTask::replace_hex_sequence("$61".to_string()); + + assert_eq!(&res, "a"); + } + + #[test] + fn test_rhs_two_no_sep() { + let res = InputsTask::replace_hex_sequence("$6161".to_string()); + + assert_eq!(&res, "aa"); + } + + #[test] + fn test_rhs_two_comma() { + let res = InputsTask::replace_hex_sequence("$61,61".to_string()); + + assert_eq!(&res, "aa"); + } +} From f166a0a46610dfdd46492a885431b5b754570dcd Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 19 Jul 2024 22:01:07 -0300 Subject: [PATCH 16/49] feat: add a command to setup flow control --- src/graphics/graphics_task.rs | 14 +++++++++--- src/inputs/inputs_task.rs | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index d54ff26..d61eadd 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -172,13 +172,14 @@ impl GraphicsTask { frame: &mut Frame, rect: Rect, ) { - let (port, baudrate, is_connected) = { + let (port, baudrate, flow_control, is_connected) = { let serial_shared = serial_shared .read() .expect("Cannot get serial lock for read"); ( serial_shared.port.clone(), serial_shared.baudrate, + serial_shared.flow_control, matches!(serial_shared.mode, SerialMode::Connected), ) }; @@ -208,8 +209,15 @@ impl GraphicsTask { let block = Block::default() .title(format!( - "[{:03}] Serial {}:{:04}bps", - history_len, port, baudrate + "[{:03}] Serial {}:{:04}bps{}", + history_len, + port, + baudrate, + match flow_control { + serialport::FlowControl::None => "", + serialport::FlowControl::Software => ":SW", + serialport::FlowControl::Hardware => ":HW", + } )) .borders(Borders::ALL) .border_type(BorderType::Thick) diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index 7cdf8fa..6f186b4 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -9,11 +9,13 @@ use crate::{ }, plugin::plugin_engine::PluginEngineCommand, serial::serial_if::{SerialCommand, SerialSetup}, + success, }; use chrono::Local; use core::panic; use crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use rand::seq::SliceRandom; +use serialport::FlowControl; use std::{ path::PathBuf, sync::{ @@ -324,6 +326,44 @@ impl InputsTask { "disconnect" => { let _ = private.serial_if_cmd_sender.send(SerialCommand::Disconnect); } + "flow" => { + if command_line_split.len() < 2 { + error!( + private.logger, + "Insufficient arguments for \"!flow\" command" + ); + return; + } + + let flow_control = match command_line_split[1].as_str() { + "none" => FlowControl::None, + "sw" => FlowControl::Software, + "hw" => FlowControl::Hardware, + _ => { + error!( + private.logger, + "Invalid flow control. Choose one of these options: none, sw, hw" + ); + return; + } + }; + + let res = private + .serial_if_cmd_sender + .send(SerialCommand::Setup(SerialSetup { + flow_control: Some(flow_control), + ..SerialSetup::default() + })); + + match res { + Ok(_) => success!( + private.logger, + "Flow control setted to \"{}\"", + command_line_split[1] + ), + Err(err) => error!(private.logger, "Cannot set flow control: {}", err), + } + } _ => error!(private.logger, "Invalid command \"{}\"", cmd_name), } } From a3e82aa9df358081ff720983641eb12d92a2b66f Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 19 Jul 2024 22:31:50 -0300 Subject: [PATCH 17/49] feat: add a command to set log level --- src/graphics/graphics_task.rs | 33 ++++++++++++++++++++++++++++++--- src/infra/logger.rs | 2 +- src/inputs/inputs_task.rs | 34 +++++++++++++++++++++++++++++++++- src/plugin/plugin_engine.rs | 22 ++++++++++++++-------- 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index d61eadd..b8a12dc 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -47,6 +47,7 @@ pub type GraphicsTask = Task<(), GraphicsCommand>; pub struct GraphicsConnections { logger: Logger, logger_receiver: Receiver, + system_log_level: LogLevel, tx: Consumer>, rx: Consumer>, inputs_shared: Shared, @@ -61,6 +62,7 @@ pub struct GraphicsConnections { } pub enum GraphicsCommand { + SetLogLevel(LogLevel), SaveData, RecordData, ScrollLeft, @@ -112,8 +114,14 @@ impl GraphicsTask { }; let (coll, coll_size) = ( - private.history.range(scroll.0 as usize..), - private.history.len(), + private.history.range(scroll.0 as usize..).filter(|msg| { + if let GraphicalMessage::Log(log) = msg { + log.level as u32 <= private.system_log_level as u32 + } else { + true + } + }), + Self::history_length(private), ); let border_type = if private.auto_scroll { BorderType::Thick @@ -337,6 +345,10 @@ impl GraphicsTask { if let Ok(cmd) = cmd_receiver.try_recv() { match cmd { + GraphicsCommand::SetLogLevel(level) => { + private.system_log_level = level; + success!(private.logger, "Log setted to {:?}", level); + } GraphicsCommand::SaveData => { if private.recorder.is_recording() { warning!(private.logger, "Cannot save file while recording."); @@ -664,9 +676,23 @@ impl GraphicsTask { Line::from(spans) } + fn history_length(private: &GraphicsConnections) -> usize { + private + .history + .iter() + .filter(|msg| { + if let GraphicalMessage::Log(log) = msg { + log.level as u32 <= private.system_log_level as u32 + } else { + true + } + }) + .count() + } + fn max_main_axis(private: &GraphicsConnections) -> u16 { let main_axis_length = private.last_frame_height - Self::COMMAND_BAR_HEIGHT - 2; - let history_len = private.history.len() as u16; + let history_len = Self::history_length(private) as u16; if history_len > main_axis_length { history_len - main_axis_length @@ -701,6 +727,7 @@ impl GraphicsConnections { auto_scroll: true, scroll: (0, 0), last_frame_height: u16::MAX, + system_log_level: LogLevel::Debug, } } } diff --git a/src/infra/logger.rs b/src/infra/logger.rs index 9f4fcc8..0fef007 100644 --- a/src/infra/logger.rs +++ b/src/infra/logger.rs @@ -13,7 +13,7 @@ pub struct LogMessage { pub level: LogLevel, } -#[allow(unused)] +#[derive(Debug, Clone, Copy)] pub enum LogLevel { Error, Warning, diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index 6f186b4..f11bb70 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -342,7 +342,7 @@ impl InputsTask { _ => { error!( private.logger, - "Invalid flow control. Choose one of these options: none, sw, hw" + "Invalid flow control. Please, chose one of these options: none, sw, hw" ); return; } @@ -364,6 +364,38 @@ impl InputsTask { Err(err) => error!(private.logger, "Cannot set flow control: {}", err), } } + "log" => { + if command_line_split.len() < 3 { + error!( + private.logger, + "Insufficient arguments for \"!log\" command" + ); + return; + } + + let module = command_line_split[1].as_str(); + let level = match command_line_split[2].as_str() { + "debug" | "dbg" | "all" => LogLevel::Debug, + "info" | "inf" => LogLevel::Info, + "success" | "ok" => LogLevel::Success, + "warning" | "wrn" => LogLevel::Warning, + "error" | "err" => LogLevel::Error, + _ => { + error!(private.logger, "Invalid log level. Please, chose one of these options: debug, info, success, warning, error"); + return; + } + }; + + if module == "system" || module == "sys" { + let _ = private + .graphics_cmd_sender + .send(GraphicsCommand::SetLogLevel(level)); + } else { + let _ = private + .plugin_engine_cmd_sender + .send(PluginEngineCommand::SetLogLevel(level)); + } + } _ => error!(private.logger, "Invalid command \"{}\"", cmd_name), } } diff --git a/src/plugin/plugin_engine.rs b/src/plugin/plugin_engine.rs index 22f6d71..5451f51 100644 --- a/src/plugin/plugin_engine.rs +++ b/src/plugin/plugin_engine.rs @@ -1,3 +1,12 @@ +use crate::{ + infra::{ + logger::{LogLevel, Logger}, + messages::TimedBytes, + mpmc::{Consumer, Producer}, + task::Task, + }, + warning, +}; use std::{ sync::{ mpsc::{Receiver, Sender}, @@ -6,16 +15,10 @@ use std::{ thread::yield_now, }; -use crate::infra::{ - logger::Logger, - messages::TimedBytes, - mpmc::{Consumer, Producer}, - task::Task, -}; - pub type PluginEngine = Task<(), PluginEngineCommand>; pub enum PluginEngineCommand { + SetLogLevel(LogLevel), Exit, } @@ -38,12 +41,15 @@ impl PluginEngine { pub fn task( _shared: Arc>, - _private: PluginEngineConnections, + private: PluginEngineConnections, cmd_receiver: Receiver, ) { 'plugin_engine_loop: loop { if let Ok(cmd) = cmd_receiver.try_recv() { match cmd { + PluginEngineCommand::SetLogLevel(_level) => { + warning!(private.logger, "Sorry, but we're building this feature..."); + } PluginEngineCommand::Exit => break 'plugin_engine_loop, } } From 3d9413372848c2ef55e57249d1ce488dae2925dc Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 27 Jul 2024 17:53:25 -0300 Subject: [PATCH 18/49] feat: add plugin engine base infrastructure --- Cargo.lock | 5 +- Cargo.toml | 1 + plugins/api.md | 6 +- plugins/scope.lua | 13 +- plugins/shell.lua | 20 +- src/graphics/graphics_task.rs | 7 +- src/inputs/inputs_task.rs | 18 +- src/main.rs | 18 +- src/plugin/bridge.rs | 38 ++++ src/plugin/engine.rs | 395 ++++++++++++++++++++++++++++++++++ src/plugin/messages.rs | 208 ++++++++++++++++++ src/plugin/method_call.rs | 238 ++++++++++++++++++++ src/plugin/mod.rs | 7 +- src/plugin/plugin.rs | 144 +++++++++++++ src/plugin/plugin_engine.rs | 76 ------- src/plugin/shell.rs | 12 ++ src/serial/serial_if.rs | 9 +- 17 files changed, 1099 insertions(+), 116 deletions(-) create mode 100644 src/plugin/bridge.rs create mode 100644 src/plugin/engine.rs create mode 100644 src/plugin/messages.rs create mode 100644 src/plugin/method_call.rs create mode 100644 src/plugin/plugin.rs delete mode 100644 src/plugin/plugin_engine.rs create mode 100644 src/plugin/shell.rs diff --git a/Cargo.lock b/Cargo.lock index 6acd9dc..70189f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,9 +902,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -980,6 +980,7 @@ dependencies = [ "mlua", "rand", "ratatui", + "regex", "serde", "serde_yaml", "serialport", diff --git a/Cargo.toml b/Cargo.toml index 1917ed3..d56d206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ mlua = { version = "0.9.6", features = ["lua54", "vendored", "async", "send"] } anyhow = "1.0.79" homedir = "0.2.1" tokio = { version = "1.36.0", features = ["full"] } +regex = "1.10.5" [target.'cfg(windows)'.dependencies] ctrlc = "3.4.3" diff --git a/plugins/api.md b/plugins/api.md index 619f409..f5a5814 100644 --- a/plugins/api.md +++ b/plugins/api.md @@ -70,7 +70,7 @@ end ### `on_serial_disconnect` ```lua -function M.on_serial_disconnect() +function M.on_serial_disconnect(port, baudrate) -- ... end ``` @@ -131,10 +131,10 @@ function M.on_ble_indicate(serv, char, val) end ``` -### `on_mtu_change` +### `on_ble_mtu_change` ```lua -function M.on_mtu_change(uuid, val) +function M.on_ble_mtu_change(uuid, val) -- ... end ``` diff --git a/plugins/scope.lua b/plugins/scope.lua index 4368f52..fd7ef36 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -34,7 +34,7 @@ function M.log.error(msg) end function M.serial.info() - local _, port, baud_rate = coroutine.yield({":serial.info"}) + local port, baud_rate = coroutine.yield({":serial.info"}) return port, baud_rate end @@ -43,7 +43,7 @@ function M.serial.send(msg) end function M.serial.recv(opts) - local _, err, msg = coroutine.yield({":serial.recv", opts}) + local err, msg = coroutine.yield({":serial.recv", opts}) return err, msg end @@ -60,12 +60,21 @@ function M.sys.sleep_ms(time) end function M.re.literal(str) + return coroutine.yield({":re.literal", str}) end function M.re.matches(str, pattern_table) + local fn_name = coroutine.yield({":re.matches", str, pattern_table}) + if fn_name ~= nil then + local fn = pattern_table[fn_name] + fn(str) + end end function M.re.match(str, pattern, code) + if coroutine.yield({":re.match", str, pattern}) then + code(str) + end end return M diff --git a/plugins/shell.lua b/plugins/shell.lua index c198fe5..5332bac 100644 --- a/plugins/shell.lua +++ b/plugins/shell.lua @@ -1,22 +1,12 @@ -local Shell = { - pid = nil, -} +local M = {} -function Shell.new() - local _, pid = coroutine.yield({":Shell.new"}) - local self = setmetatable({}, Shell) - self.pid = pid - - return self -end - -function Shell:run(cmd, opts) - local _, stdout, stderr = coroutine.yield({":Shell:run", self, cmd, opts}) +function M.run(cmd) + local stdout, stderr = coroutine.yield({":shell.run", cmd}) return stdout, stderr end -function Shell:exist(program) - local _, res = coroutine.yield({"Shell:run", self, program}) +function M.exist(program) + local res = coroutine.yield({":shell.exist", program}) return res end diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index b8a12dc..c5c5e8b 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -352,7 +352,8 @@ impl GraphicsTask { GraphicsCommand::SaveData => { if private.recorder.is_recording() { warning!(private.logger, "Cannot save file while recording."); - continue; + /* don't yield here, because we need to put this warning message on display */ + continue 'draw_loop; } blink.start(); @@ -482,6 +483,8 @@ impl GraphicsTask { private.history.remove(0); } + // TODO brake \n into multiple logs + new_messages.push(GraphicalMessage::Log(log_msg)); } @@ -521,6 +524,8 @@ impl GraphicsTask { Self::draw_autocomplete_list(&private.inputs_shared, f, chunks[1].y); }) .expect("Error to draw"); + + std::thread::yield_now(); } disable_raw_mode().expect("Cannot disable terminal raw mode"); diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index f11bb70..00ec91b 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -7,7 +7,7 @@ use crate::{ mpmc::Producer, task::Task, }, - plugin::plugin_engine::PluginEngineCommand, + plugin::engine::PluginEngineCommand, serial::serial_if::{SerialCommand, SerialSetup}, success, }; @@ -374,7 +374,7 @@ impl InputsTask { } let module = command_line_split[1].as_str(); - let level = match command_line_split[2].as_str() { + let log_level = match command_line_split[2].as_str() { "debug" | "dbg" | "all" => LogLevel::Debug, "info" | "inf" => LogLevel::Info, "success" | "ok" => LogLevel::Success, @@ -389,11 +389,15 @@ impl InputsTask { if module == "system" || module == "sys" { let _ = private .graphics_cmd_sender - .send(GraphicsCommand::SetLogLevel(level)); + .send(GraphicsCommand::SetLogLevel(log_level)); } else { - let _ = private - .plugin_engine_cmd_sender - .send(PluginEngineCommand::SetLogLevel(level)); + let _ = + private + .plugin_engine_cmd_sender + .send(PluginEngineCommand::SetLogLevel { + plugin_name: module.to_string(), + log_level, + }); } } _ => error!(private.logger, "Invalid command \"{}\"", cmd_name), @@ -460,6 +464,8 @@ impl InputsTask { event::Event::Resize(_, _) => {} _ => {} } + + std::thread::yield_now(); } } diff --git a/src/main.rs b/src/main.rs index 5efba40..8505513 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use graphics::graphics_task::{GraphicsConnections, GraphicsTask}; use infra::logger::Logger; use infra::mpmc::Channel; use inputs::inputs_task::{InputsConnections, InputsTask}; -use plugin::plugin_engine::{PluginEngine, PluginEngineConnections}; +use plugin::engine::{PluginEngine, PluginEngineConnections}; use serial::serial_if::{SerialConnections, SerialInterface, SerialSetup}; use std::path::PathBuf; use std::sync::mpsc::channel; @@ -90,12 +90,6 @@ fn app( plugin_engine_cmd_sender.clone(), tag_file, ); - let plugin_engine_connections = PluginEngineConnections::new( - logger.clone(), - tx_channel.new_producer(), - tx_channel_consumers.pop().unwrap(), - rx_channel_consumers.pop().unwrap(), - ); let serial_if = SerialInterface::spawn_serial_interface( serial_connections, @@ -103,6 +97,16 @@ fn app( serial_if_cmd_receiver, SerialSetup::default(), ); + let serial_shared = serial_if.shared_ref(); + + let plugin_engine_connections = PluginEngineConnections::new( + logger.clone(), + tx_channel.new_producer(), + tx_channel_consumers.pop().unwrap(), + rx_channel_consumers.pop().unwrap(), + serial_shared, + ); + let inputs_task = InputsTask::spawn_inputs_task(inputs_connections, inputs_cmd_sender, inputs_cmd_receiver); diff --git a/src/plugin/bridge.rs b/src/plugin/bridge.rs new file mode 100644 index 0000000..06905e9 --- /dev/null +++ b/src/plugin/bridge.rs @@ -0,0 +1,38 @@ +use tokio::sync::{broadcast, mpsc}; + +use super::messages::{PluginExternalRequest, PluginMethodMessage, PluginResponse}; + +pub struct PluginEngineGate { + pub sender: broadcast::Sender>, + pub receiver: mpsc::Receiver>, + pub method_call_gate: PluginMethodCallGate, +} + +pub struct PluginMethodCallGate { + pub sender: mpsc::Sender>, + pub receiver: broadcast::Receiver>, +} + +impl PluginEngineGate { + pub fn new(size: usize) -> Self { + let (sender_req, receiver_req) = mpsc::channel(size); + let (sender_rsp, receiver_rsp) = broadcast::channel(size); + + let method_call_gate = PluginMethodCallGate { + sender: sender_req, + receiver: receiver_rsp, + }; + Self { + sender: sender_rsp, + receiver: receiver_req, + method_call_gate, + } + } + + pub fn new_method_call_gate(&self) -> PluginMethodCallGate { + PluginMethodCallGate { + sender: self.method_call_gate.sender.clone(), + receiver: self.sender.subscribe(), + } + } +} diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs new file mode 100644 index 0000000..357624e --- /dev/null +++ b/src/plugin/engine.rs @@ -0,0 +1,395 @@ +use super::{ + bridge::{PluginEngineGate, PluginMethodCallGate}, + messages::{self, PluginExternalRequest, PluginMethodMessage, PluginResponse}, + plugin::{Plugin, PluginUnloadMode}, +}; +use crate::{ + error, + infra::{ + logger::{LogLevel, Logger}, + messages::TimedBytes, + mpmc::{Consumer, Producer}, + task::{Shared, Task}, + }, + serial::serial_if::SerialShared, + success, +}; +use chrono::Local; +use std::{ + collections::HashMap, + os::unix::ffi::OsStrExt, + path::PathBuf, + str::FromStr, + sync::{Arc, RwLock}, + time::Instant, +}; +use std::{path::Path, sync::mpsc::Receiver}; +use tokio::{runtime::Runtime, task::yield_now}; +pub type PluginEngine = Task<(), PluginEngineCommand>; + +#[allow(unused)] +pub enum PluginEngineCommand { + SetLogLevel { + plugin_name: String, + log_level: LogLevel, + }, + LoadPlugin { + filepath: String, + }, + UnloadPlugin { + plugin_name: String, + }, + UserCommand { + plugin_name: String, + command: String, + options: Vec, + }, + SerialConnected { + port: String, + baudrate: u32, + }, + SerialDisconnected { + port: String, + baudrate: u32, + }, + Exit, +} + +pub struct PluginEngineConnections { + logger: Logger, + tx_producer: Producer>, + tx_consumer: Consumer>, + rx: Consumer>, + serial_shared: Shared, +} + +impl PluginEngine { + pub fn spawn_plugin_engine( + connections: PluginEngineConnections, + sender: std::sync::mpsc::Sender, + receiver: std::sync::mpsc::Receiver, + ) -> Self { + Self::new((), connections, Self::task, sender, receiver) + } + + pub fn task( + shared: Arc>, + private: PluginEngineConnections, + cmd_receiver: Receiver, + ) { + let rt = Runtime::new().expect("Cannot create tokio runtime"); + + let _ = rt.block_on(async { + Self::task_async(shared, private, cmd_receiver).await; + }); + } + + pub async fn task_async( + _shared: Arc>, + private: PluginEngineConnections, + cmd_receiver: Receiver, + ) { + let mut plugin_list: HashMap, Plugin> = HashMap::new(); + let mut engine_gate = PluginEngineGate::new(32); + let mut serial_recv_reqs = vec![]; + + 'plugin_engine_loop: loop { + if let Ok(cmd) = cmd_receiver.try_recv() { + match cmd { + PluginEngineCommand::Exit => break 'plugin_engine_loop, + PluginEngineCommand::SetLogLevel { + plugin_name, + log_level, + } => { + let Some(plugin) = plugin_list.get_mut(&plugin_name) else { + error!(private.logger, "Plugin \"{}\" not loaded", plugin_name); + continue 'plugin_engine_loop; + }; + + plugin.set_log_level(log_level); + + success!( + private.logger, + "Log level setted to {:?}, on plugin {}", + log_level, + plugin_name + ); + } + PluginEngineCommand::LoadPlugin { filepath } => { + let Some(plugin_name) = Self::get_plugin_name(&filepath) else { + continue 'plugin_engine_loop; + }; + + if let Some(plugin) = plugin_list.get_mut(&plugin_name) { + plugin.spawn_method_call( + engine_gate.new_method_call_gate(), + "on_unload", + (), + ); + plugin.set_unload_mode(super::plugin::PluginUnloadMode::Reload); + + continue 'plugin_engine_loop; + } + + let Ok(filepath) = PathBuf::from_str(&filepath) else { + error!(private.logger, "Invalid filepath \"{}\"", filepath); + return; + }; + + Self::load_plugin( + engine_gate.new_method_call_gate(), + Arc::new(plugin_name), + filepath, + &mut plugin_list, + &private.logger, + ) + .await; + } + PluginEngineCommand::UnloadPlugin { plugin_name } => { + let Some(plugin) = plugin_list.get_mut(&plugin_name) else { + continue 'plugin_engine_loop; + }; + + plugin.spawn_method_call( + engine_gate.new_method_call_gate(), + "on_unload", + (), + ); + plugin.set_unload_mode(super::plugin::PluginUnloadMode::Unload); + } + PluginEngineCommand::UserCommand { + plugin_name, + command, + options, + } => { + let Some(plugin) = plugin_list.get_mut(&plugin_name) else { + error!(private.logger, "Plugin \"{}\" not loaded", plugin_name); + continue 'plugin_engine_loop; + }; + + plugin.spawn_method_call( + engine_gate.new_method_call_gate(), + &command, + options, + ); + } + PluginEngineCommand::SerialConnected { port, baudrate } => { + for plugin in plugin_list.values_mut() { + plugin.spawn_method_call( + engine_gate.new_method_call_gate(), + "on_serial_connect", + (port.clone(), baudrate), + ); + } + } + PluginEngineCommand::SerialDisconnected { port, baudrate } => { + for plugin in plugin_list.values_mut() { + plugin.spawn_method_call( + engine_gate.new_method_call_gate(), + "on_serial_disconnect", + (port.clone(), baudrate), + ); + } + } + } + } + + while let Ok(PluginMethodMessage { + plugin_name, + method_id, + data, + }) = engine_gate.receiver.try_recv() + { + let Some(plugin) = plugin_list.remove(&plugin_name) else { + continue; + }; + + let rsp = match data { + super::messages::PluginExternalRequest::SerialInfo => { + let (port, baudrate) = { + let serial_shared = private + .serial_shared + .read() + .expect("Cannot get serial shared for read"); + (serial_shared.port.clone(), serial_shared.baudrate) + }; + + Some(PluginResponse::SerialInfo { port, baudrate }) + } + super::messages::PluginExternalRequest::SerialSend { message } => { + let id = private.tx_consumer.id(); + private.tx_producer.produce_without_loopback( + Arc::new(TimedBytes { + timestamp: Local::now(), + message, + }), + id, + ); + + Some(PluginResponse::SerialSend) + } + super::messages::PluginExternalRequest::SerialRecv { timeout } => { + if Instant::now() >= timeout { + Some(PluginResponse::SerialRecv { + err: "timeout".to_string(), + message: vec![], + }) + } else { + serial_recv_reqs.push(PluginMethodMessage { + plugin_name: plugin_name.clone(), + method_id, + data: PluginExternalRequest::SerialRecv { timeout }, + }); + + None + } + } + super::messages::PluginExternalRequest::Log { level, message } => { + if level as u32 <= plugin.log_level() as u32 { + let _ = private.logger.write(message, level); + } + + Some(PluginResponse::Log) + } + messages::PluginExternalRequest::Finish { fn_name } => { + if fn_name.as_str() == "on_unload" + && matches!(plugin.unload_mode(), PluginUnloadMode::Reload) + { + Self::load_plugin( + engine_gate.new_method_call_gate(), + plugin_name, + plugin.filepath(), + &mut plugin_list, + &private.logger, + ) + .await; + } + + /* don't yield here! Because this request doesn't have response and we don't want to reinsert the plugin. */ + continue; + } + }; + + plugin_list.insert(plugin_name.clone(), plugin); + + let Some(rsp) = rsp else { + /* don't yield here! Because the request doesn't have response. */ + continue; + }; + + let _ = engine_gate.sender.send(PluginMethodMessage { + plugin_name, + method_id, + data: rsp, + }); + } + + if let Ok(tx_msg) = private.tx_consumer.try_recv() { + for plugin in plugin_list.values_mut() { + plugin.spawn_method_call( + engine_gate.new_method_call_gate(), + "on_serial_send", + tx_msg.message.clone(), + ); + } + } + + serial_recv_reqs = serial_recv_reqs + .into_iter() + .filter(|PluginMethodMessage { data, .. }| { + if let PluginExternalRequest::SerialRecv { timeout } = data { + Instant::now() < *timeout + } else { + false + } + }) + .collect(); + + if let Ok(rx_msg) = private.rx.try_recv() { + for plugin in plugin_list.values_mut() { + plugin.spawn_method_call( + engine_gate.new_method_call_gate(), + "on_serial_recv", + rx_msg.message.clone(), + ); + } + + for serial_recv_req in serial_recv_reqs.drain(..) { + let PluginMethodMessage { + plugin_name, + method_id, + .. + } = serial_recv_req; + + let _ = engine_gate.sender.send(PluginMethodMessage { + plugin_name, + method_id, + data: PluginResponse::SerialRecv { + err: "".to_string(), + message: rx_msg.message.clone(), + }, + }); + } + } + + yield_now().await; + } + } + + fn get_plugin_name(filepath: &str) -> Option { + Path::new(filepath) + .file_name() + .map(|filename| filename.to_str()) + .flatten() + .map(|filename| filename.to_string()) + } + + async fn load_plugin( + gate: PluginMethodCallGate, + plugin_name: Arc, + filepath: PathBuf, + plugin_list: &mut HashMap, Plugin>, + logger: &Logger, + ) { + let filepath = match filepath.extension() { + Some(extension) if extension.as_bytes() != b"lua" => { + error!(logger, "Invalid plugin extension: {:?}", extension); + return; + } + Some(_extension) => filepath, + None => filepath.with_extension("lua"), + }; + + if !filepath.exists() { + error!(logger, "Filepath \"{:?}\" doesn't exist!", filepath); + return; + } + + let res = Plugin::new(plugin_name.clone(), filepath); + let Ok(mut plugin) = res else { + error!(logger, "{}", res.err().unwrap()); + return; + }; + plugin.spawn_method_call(gate, "on_load", ()); + + plugin_list.insert(plugin_name, plugin); + } +} + +impl PluginEngineConnections { + pub fn new( + logger: Logger, + tx_producer: Producer>, + tx_consumer: Consumer>, + rx: Consumer>, + serial_shared: Shared, + ) -> Self { + Self { + logger, + tx_producer, + tx_consumer, + rx, + serial_shared, + } + } +} diff --git a/src/plugin/messages.rs b/src/plugin/messages.rs new file mode 100644 index 0000000..cfd0d4a --- /dev/null +++ b/src/plugin/messages.rs @@ -0,0 +1,208 @@ +use std::{sync::Arc, time::Instant}; + +use mlua::{Table, Value}; +use std::time::Duration; + +use crate::infra::LogLevel; + +#[derive(Clone)] +pub struct PluginMethodMessage { + pub plugin_name: Arc, + pub method_id: u64, + pub data: T, +} + +pub enum PluginRequest { + Internal(PluginInternalRequest), + External(PluginExternalRequest), +} + +#[derive(Clone)] +pub enum PluginExternalRequest { + Finish { fn_name: Arc }, + SerialInfo, + SerialSend { message: Vec }, + SerialRecv { timeout: Instant }, + Log { level: LogLevel, message: String }, +} + +pub enum PluginInternalRequest { + SysSleep { + time: Duration, + }, + ReLiteral { + string: String, + }, + ReMatches { + string: String, + pattern_table: Vec, + }, + ReMatch { + string: String, + pattern: String, + }, + ShellRun { + cmd: String, + }, + ShellExist { + program: String, + }, +} + +#[derive(Clone)] +pub enum PluginResponse { + Log, + SerialInfo { port: String, baudrate: u32 }, + SerialSend, + SerialRecv { err: String, message: Vec }, + SysSleep, + ReLiteral { literal: String }, + ReMatches { pattern: Option }, + ReMatch { is_match: bool }, + ShellRun { stdout: String, stderr: String }, + ShellExist { exist: bool }, +} + +impl<'lua> TryFrom> for PluginRequest { + type Error = String; + + fn try_from(value: Table<'lua>) -> Result { + let req_id: String = value + .get(1) + .map_err(|_| "Cannot get first table entry as String".to_string())?; + + let req = match req_id.as_str() { + ":log.debug" => { + let message: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::External(PluginExternalRequest::Log { + level: LogLevel::Debug, + message, + }) + } + ":log.info" => { + let message: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::External(PluginExternalRequest::Log { + level: LogLevel::Info, + message, + }) + } + ":log.success" => { + let message: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::External(PluginExternalRequest::Log { + level: LogLevel::Success, + message, + }) + } + ":log.warning" => { + let message: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::External(PluginExternalRequest::Log { + level: LogLevel::Warning, + message, + }) + } + ":log.error" => { + let message: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::External(PluginExternalRequest::Log { + level: LogLevel::Error, + message, + }) + } + ":serial.info" => PluginRequest::External(PluginExternalRequest::SerialInfo), + ":serial.send" => { + let message: Vec = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::External(PluginExternalRequest::SerialSend { message }) + } + ":serial.recv" => { + let opts: Table = value + .get(2) + .map_err(|_| "Cannot get second table entry as Table".to_string())?; + + let timeout_ms = opts.get("timeout_ms").unwrap_or(u64::MAX); + + PluginRequest::External(PluginExternalRequest::SerialRecv { + timeout: Instant::now() + Duration::from_millis(timeout_ms), + }) + } + ":sys.sleep" => { + let time: u64 = value + .get(2) + .map_err(|_| "Cannot get second table entry as Number".to_string())?; + + PluginRequest::Internal(PluginInternalRequest::SysSleep { + time: Duration::from_millis(time), + }) + } + ":shell.run" => { + let cmd: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::Internal(PluginInternalRequest::ShellRun { cmd }) + } + ":shell.exist" => { + let program: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::Internal(PluginInternalRequest::ShellExist { program }) + } + ":re.literal" => { + let string: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + + PluginRequest::Internal(PluginInternalRequest::ReLiteral { string }) + } + ":re.matches" => { + let string: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + let pattern_table: Table<'_> = value + .get(3) + .map_err(|_| "Cannot get third table entry as String".to_string())?; + let pattern_table = pattern_table + .pairs() + .into_iter() + .filter_map(|res| res.ok()) + .map(|(k, _v): (String, Value<'_>)| k) + .collect(); + + PluginRequest::Internal(PluginInternalRequest::ReMatches { + string, + pattern_table, + }) + } + "re.match" => { + let string: String = value + .get(2) + .map_err(|_| "Cannot get second table entry as String".to_string())?; + let pattern: String = value + .get(3) + .map_err(|_| "Cannot get third table entry as String".to_string())?; + + PluginRequest::Internal(PluginInternalRequest::ReMatch { string, pattern }) + } + _ => return Err("Invalid Plugin Request ID".to_string()), + }; + + Ok(req) + } +} diff --git a/src/plugin/method_call.rs b/src/plugin/method_call.rs new file mode 100644 index 0000000..ebe0f6e --- /dev/null +++ b/src/plugin/method_call.rs @@ -0,0 +1,238 @@ +use super::{ + bridge::PluginMethodCallGate, + messages::{PluginInternalRequest, PluginMethodMessage, PluginRequest, PluginResponse}, + shell::Shell, +}; +use mlua::{Function, IntoLuaMulti, Lua, Table, Thread, Value}; +use regex::Regex; +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + sync::Arc, +}; + +pub struct PluginMethodCall { + plugin_name: Arc, + fn_name: Arc, + id: u64, + gate: PluginMethodCallGate, +} + +impl PluginMethodCall { + pub fn spawn( + plugin_name: Arc, + fn_name: String, + index: u128, + lua: Arc, + initial_args: impl for<'a> IntoLuaMulti<'a> + 'static, + gate: PluginMethodCallGate, + ) { + let mut hasher = DefaultHasher::new(); + plugin_name.hash(&mut hasher); + fn_name.hash(&mut hasher); + index.hash(&mut hasher); + + let id = hasher.finish(); + + let fn_name = Arc::new(fn_name); + + let sender = gate.sender.clone(); + let pmc = Self { + plugin_name: plugin_name.clone(), + fn_name: fn_name.clone(), + id, + gate, + }; + + tokio::task::spawn_local(async move { + let _ = pmc.call_fn(&lua, initial_args).await; + let _ = sender + .send(PluginMethodMessage { + plugin_name, + method_id: id, + data: super::messages::PluginExternalRequest::Finish { fn_name }, + }) + .await; + }); + } + + async fn call_fn<'a>( + mut self, + lua: &'a Lua, + initial_args: impl IntoLuaMulti<'a>, + ) -> Result<(), String> { + let plugin_table: Table = lua.globals().get("M").unwrap(); + + let plugin_fn: Function = plugin_table + .get(self.fn_name.as_str()) + .map_err(|err| err.to_string())?; + + let thread = lua + .create_thread(plugin_fn) + .map_err(|err| err.to_string())?; + + let Some(mut table) = self.call_fn_inner(&lua, &thread, initial_args).await? else { + return Ok(()); + }; + + 'run_loop: loop { + match self.call_fn_inner(&lua, &thread, table).await { + Ok(Some(t)) => table = t, + Ok(None) => break 'run_loop Ok(()), + Err(err) => break 'run_loop Err(err), + } + } + } + + async fn call_fn_inner<'a>( + &mut self, + lua: &'a Lua, + thread: &Thread<'a>, + plugin_fn_args: impl IntoLuaMulti<'a>, + ) -> Result>, String> { + let plugin_req: Table = match thread.resume(plugin_fn_args) { + Ok(plugin_req) => plugin_req, + Err(mlua::Error::CoroutineInactive) => return Ok(None), + Err(err) => return Err(err.to_string()), + }; + + let plugin_req: PluginRequest = plugin_req + .try_into() + .map_err(|err: String| err.to_string())?; + + let rsp = match plugin_req { + PluginRequest::Internal(internal_req) => { + self.handle_internal_plugin_request(internal_req).await + } + PluginRequest::External(external_req) => { + self.gate + .sender + .send(PluginMethodMessage { + plugin_name: self.plugin_name.clone(), + method_id: self.id, + data: external_req, + }) + .await + .map_err(|err| err.to_string())?; + + 'rsp_loop: loop { + let PluginMethodMessage { + plugin_name: _plugin_name, + method_id, + data, + } = self + .gate + .receiver + .recv() + .await + .map_err(|err| err.to_string())?; + + if method_id != self.id { + continue 'rsp_loop; + } + + break 'rsp_loop data; + } + } + }; + + let next_table = self.rsp_decode(lua, rsp)?; + + Ok(Some(next_table)) + } + + async fn handle_internal_plugin_request(&self, req: PluginInternalRequest) -> PluginResponse { + match req { + PluginInternalRequest::SysSleep { time } => { + tokio::time::sleep(time).await; + PluginResponse::SysSleep + } + PluginInternalRequest::ReLiteral { string } => { + let special_chars = "/.*+?|[](){}\\"; + let literal = string + .chars() + .map(|c| { + if special_chars.contains(c) { + format!("\\{}", c) + } else { + c.to_string() + } + }) + .collect(); + + PluginResponse::ReLiteral { literal } + } + PluginInternalRequest::ReMatches { + string, + pattern_table, + } => { + let pos = pattern_table + .iter() + .filter_map(|pattern| Regex::new(&pattern).ok()) + .position(|re| re.is_match(&string)); + let pattern = pos + .and_then(|pos| pattern_table.get(pos)) + .map(|pattern| pattern.to_string()); + + PluginResponse::ReMatches { pattern } + } + PluginInternalRequest::ReMatch { string, pattern } => { + let is_match = Regex::new(&pattern) + .ok() + .and_then(|regex| regex.is_match(&string).then_some(())) + .is_some(); + + PluginResponse::ReMatch { is_match } + } + PluginInternalRequest::ShellRun { cmd } => { + let (stdout, stderr) = Shell::run(cmd).await; + + PluginResponse::ShellRun { stdout, stderr } + } + PluginInternalRequest::ShellExist { program } => { + let exist = Shell::exist(program).await; + + PluginResponse::ShellExist { exist } + } + } + } + + fn rsp_decode<'a>(&self, lua: &'a Lua, rsp: PluginResponse) -> Result, String> { + let table = lua.create_table().map_err(|err| err.to_string())?; + + match rsp { + PluginResponse::Log | PluginResponse::SerialSend | PluginResponse::SysSleep => {} + PluginResponse::ReMatches { pattern } => { + table + .push(if let Some(pattern) = pattern { + Value::String(lua.create_string(pattern).map_err(|err| err.to_string())?) + } else { + Value::Nil + }) + .map_err(|err| err.to_string())?; + } + PluginResponse::ReMatch { is_match } => { + table.push(is_match).map_err(|err| err.to_string())? + } + PluginResponse::SerialInfo { port, baudrate } => { + table.push(port).map_err(|err| err.to_string())?; + table.push(baudrate).map_err(|err| err.to_string())?; + } + PluginResponse::SerialRecv { err, message } => { + table.push(err).map_err(|err| err.to_string())?; + table.push(message).map_err(|err| err.to_string())?; + } + PluginResponse::ReLiteral { literal } => { + table.push(literal).map_err(|err| err.to_string())?; + } + PluginResponse::ShellRun { stdout, stderr } => { + table.push(stdout).map_err(|err| err.to_string())?; + table.push(stderr).map_err(|err| err.to_string())?; + } + PluginResponse::ShellExist { exist } => { + table.push(exist).map_err(|err| err.to_string())?; + } + } + + Ok(table) + } +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 73e9077..10b329b 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -1 +1,6 @@ -pub mod plugin_engine; +pub mod bridge; +pub mod engine; +pub mod messages; +pub mod method_call; +pub mod plugin; +pub mod shell; diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs new file mode 100644 index 0000000..b754650 --- /dev/null +++ b/src/plugin/plugin.rs @@ -0,0 +1,144 @@ +use mlua::{IntoLuaMulti, Lua, LuaOptions, Table}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::infra::LogLevel; + +use super::bridge::PluginMethodCallGate; +use super::method_call::PluginMethodCall; + +pub struct Plugin { + name: Arc, + filepath: PathBuf, + lua: Arc, + log_level: LogLevel, + index: u128, + unload_mode: PluginUnloadMode, +} + +#[derive(Clone, Copy)] +pub enum PluginUnloadMode { + None, + Unload, + Reload, +} + +impl Plugin { + pub fn new(name: Arc, filepath: PathBuf) -> Result { + let lua = Lua::new_with(mlua::StdLib::ALL_SAFE, LuaOptions::default()) + .map_err(|err| err.to_string())?; + let plugin_dir = filepath.parent().unwrap_or(Path::new("/")); + let code = std::fs::read_to_string(&filepath).map_err(|err| err.to_string())?; + lua.load(format!( + "package.path = package.path .. ';{:?}/?.lua'", + plugin_dir + )) + .exec() + .map_err(|err| err.to_string())?; + let plugin_table: Table = lua.load(code).eval().map_err(|err| err.to_string())?; + lua.globals() + .set("M", plugin_table) + .map_err(|err| err.to_string())?; + + Ok(Self { + name, + filepath, + lua: Arc::new(lua), + index: 0, + log_level: LogLevel::Info, + unload_mode: PluginUnloadMode::None, + }) + } + + pub fn log_level(&self) -> LogLevel { + self.log_level + } + + pub fn set_log_level(&mut self, log_level: LogLevel) { + self.log_level = log_level; + } + + pub fn unload_mode(&self) -> PluginUnloadMode { + self.unload_mode + } + + pub fn set_unload_mode(&mut self, mode: PluginUnloadMode) { + self.unload_mode = mode; + } + + pub fn filepath(self) -> PathBuf { + self.filepath + } + + pub fn spawn_method_call( + &mut self, + gate: PluginMethodCallGate, + fn_name: &str, + initial_args: impl for<'a> IntoLuaMulti<'a> + 'static, + ) { + if !matches!(self.unload_mode, PluginUnloadMode::None) { + return; + } + + PluginMethodCall::spawn( + self.name.clone(), + fn_name.to_string(), + self.index, + self.lua.clone(), + initial_args, + gate, + ); + + self.index = self.index.overflowing_add_signed(0).0; + } +} + +#[cfg(test)] +mod tests { + use std::{path::PathBuf, sync::Arc}; + + use super::Plugin; + use mlua::{Lua, LuaOptions, Table, Value}; + + fn print_table(lua: Lua) { + let table: Table = lua.globals().get("M").unwrap(); + let mut keys = vec![]; + + for r in table.pairs() { + let (k, _): (String, Value) = r.unwrap(); + + keys.push(k); + } + keys.sort(); + + assert_eq!( + keys, + ["data", "level", "on_serial_recv"] + .into_iter() + .map(|x| x.to_string()) + .collect::>() + ) + } + + #[test] + fn test_lua_load_plugin() { + let lua = Lua::new_with(mlua::StdLib::ALL_SAFE, LuaOptions::default()).unwrap(); + let code = std::fs::read_to_string("plugins/echo.lua").unwrap(); + + lua.load("package.path = package.path .. ';plugins/?.lua'") + .exec() + .unwrap(); + let table: Table = lua.load(code).eval().unwrap(); + lua.globals().set("M", table).unwrap(); + + print_table(lua); + } + + #[test] + fn test_plugin_new() { + let _plugin = Plugin::new( + Arc::new("echo".to_string()), + Arc::new(PathBuf::from("plugins/echo.lua")), + ); + } +} diff --git a/src/plugin/plugin_engine.rs b/src/plugin/plugin_engine.rs deleted file mode 100644 index 5451f51..0000000 --- a/src/plugin/plugin_engine.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::{ - infra::{ - logger::{LogLevel, Logger}, - messages::TimedBytes, - mpmc::{Consumer, Producer}, - task::Task, - }, - warning, -}; -use std::{ - sync::{ - mpsc::{Receiver, Sender}, - Arc, RwLock, - }, - thread::yield_now, -}; - -pub type PluginEngine = Task<(), PluginEngineCommand>; - -pub enum PluginEngineCommand { - SetLogLevel(LogLevel), - Exit, -} - -#[allow(unused)] -pub struct PluginEngineConnections { - logger: Logger, - tx_producer: Producer>, - tx_consumer: Consumer>, - rx: Consumer>, -} - -impl PluginEngine { - pub fn spawn_plugin_engine( - connections: PluginEngineConnections, - sender: Sender, - receiver: Receiver, - ) -> Self { - Self::new((), connections, Self::task, sender, receiver) - } - - pub fn task( - _shared: Arc>, - private: PluginEngineConnections, - cmd_receiver: Receiver, - ) { - 'plugin_engine_loop: loop { - if let Ok(cmd) = cmd_receiver.try_recv() { - match cmd { - PluginEngineCommand::SetLogLevel(_level) => { - warning!(private.logger, "Sorry, but we're building this feature..."); - } - PluginEngineCommand::Exit => break 'plugin_engine_loop, - } - } - - yield_now(); - } - } -} - -impl PluginEngineConnections { - pub fn new( - logger: Logger, - tx_producer: Producer>, - tx_consumer: Consumer>, - rx: Consumer>, - ) -> Self { - Self { - logger, - tx_producer, - tx_consumer, - rx, - } - } -} diff --git a/src/plugin/shell.rs b/src/plugin/shell.rs new file mode 100644 index 0000000..9c0c8f9 --- /dev/null +++ b/src/plugin/shell.rs @@ -0,0 +1,12 @@ +#![allow(unused)] +pub struct Shell; + +impl Shell { + pub async fn run(cmd: String) -> (String, String) { + todo!() + } + + pub async fn exist(program: String) -> bool { + todo!() + } +} diff --git a/src/serial/serial_if.rs b/src/serial/serial_if.rs index a9475ee..a35ceee 100644 --- a/src/serial/serial_if.rs +++ b/src/serial/serial_if.rs @@ -143,7 +143,7 @@ impl SerialInterface { match mode { SerialMode::DoNotConnect => { std::thread::yield_now(); - continue; + continue 'task_loop; } SerialMode::Reconnecting => { let new_mode = Self::connect(shared.clone(), &mut serial, &logger); @@ -155,7 +155,7 @@ impl SerialInterface { let Some(mut ser) = serial.take() else { std::thread::yield_now(); - continue; + continue 'task_loop; }; if let Ok(data_to_sent) = tx.try_recv() { @@ -183,7 +183,8 @@ impl SerialInterface { { let _ = Self::disconnect(shared.clone(), &mut Some(ser), &logger); Self::set_mode(shared.clone(), Some(SerialMode::Reconnecting)); - continue; + std::thread::yield_now(); + continue 'task_loop; } Err(_) => {} } @@ -200,6 +201,8 @@ impl SerialInterface { } serial = Some(ser); + + std::thread::yield_now(); } } From c6b57dddd1aabf52263756d20d90d58aefc22893 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 4 Aug 2024 12:44:36 -0300 Subject: [PATCH 19/49] feat: add input task and serial if integration with the plugin engine --- src/inputs/inputs_task.rs | 56 +++++++++++++++++++++++++++++-- src/main.rs | 1 + src/plugin/engine.rs | 1 - src/plugin/method_call.rs | 5 ++- src/plugin/shell.rs | 68 ++++++++++++++++++++++++++++++++++--- src/serial/serial_if.rs | 70 ++++++++++++++++++++++++++++++++------- 6 files changed, 181 insertions(+), 20 deletions(-) diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index 00ec91b..bd5f395 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -381,7 +381,7 @@ impl InputsTask { "warning" | "wrn" => LogLevel::Warning, "error" | "err" => LogLevel::Error, _ => { - error!(private.logger, "Invalid log level. Please, chose one of these options: debug, info, success, warning, error"); + error!(private.logger, "Invalid log level. Please, choose one of these options: debug, info, success, warning, error"); return; } }; @@ -400,7 +400,59 @@ impl InputsTask { }); } } - _ => error!(private.logger, "Invalid command \"{}\"", cmd_name), + "plugin" => { + if command_line_split.len() < 3 { + error!( + private.logger, + "Insufficient arguments for \"!plugin\" command" + ); + return; + } + + let command = command_line_split[1].as_str(); + + match command { + "load" | "reload" => { + let filepath = command_line_split[2].clone(); + + let _ = private + .plugin_engine_cmd_sender + .send(PluginEngineCommand::LoadPlugin { filepath }); + } + "unload" => { + let plugin_name = command_line_split[2].clone(); + + let _ = private + .plugin_engine_cmd_sender + .send(PluginEngineCommand::UnloadPlugin { plugin_name }); + } + _ => { + error!(private.logger, "Invalid command. Please, choose one of these options: load, reload, unload"); + return; + } + } + } + _ => { + if command_line_split.len() < 2 { + error!(private.logger, "Insufficient arguments"); + return; + } + + let plugin_name = command_line_split[0].clone(); + let command = command_line_split[1].clone(); + let options = command_line_split + .get(2..) + .map(|v| v.to_vec()) + .unwrap_or(vec![]); + + let _ = private + .plugin_engine_cmd_sender + .send(PluginEngineCommand::UserCommand { + plugin_name, + command, + options, + }); + } } } diff --git a/src/main.rs b/src/main.rs index 8505513..49a9e63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,6 +81,7 @@ fn app( logger.clone(), tx_channel_consumers.pop().unwrap(), rx_channel.clone().new_producer(), + plugin_engine_cmd_sender.clone(), ); let inputs_connections = InputsConnections::new( logger.clone(), diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 357624e..4d80fc7 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -27,7 +27,6 @@ use std::{path::Path, sync::mpsc::Receiver}; use tokio::{runtime::Runtime, task::yield_now}; pub type PluginEngine = Task<(), PluginEngineCommand>; -#[allow(unused)] pub enum PluginEngineCommand { SetLogLevel { plugin_name: String, diff --git a/src/plugin/method_call.rs b/src/plugin/method_call.rs index ebe0f6e..f1fbcf8 100644 --- a/src/plugin/method_call.rs +++ b/src/plugin/method_call.rs @@ -184,7 +184,10 @@ impl PluginMethodCall { PluginResponse::ReMatch { is_match } } PluginInternalRequest::ShellRun { cmd } => { - let (stdout, stderr) = Shell::run(cmd).await; + let (stdout, stderr) = match Shell::run(cmd).await { + Ok(r) => r, + Err(err) => ("".to_string(), err), + }; PluginResponse::ShellRun { stdout, stderr } } diff --git a/src/plugin/shell.rs b/src/plugin/shell.rs index 9c0c8f9..be7c481 100644 --- a/src/plugin/shell.rs +++ b/src/plugin/shell.rs @@ -1,12 +1,72 @@ -#![allow(unused)] +use std::process::Stdio; +use tokio::process::Command; + pub struct Shell; impl Shell { - pub async fn run(cmd: String) -> (String, String) { - todo!() + pub async fn run(cmd: String) -> Result<(String, String), String> { + let child = if cfg!(target_os = "windows") { + Command::new("cmd") + .arg("/C") + .arg(cmd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .spawn() + .map_err(|err| err.to_string())? + } else { + Command::new("sh") + .arg("-c") + .arg(cmd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|err| err.to_string())? + }; + + let output = child + .wait_with_output() + .await + .map_err(|err| err.to_string())?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + Ok((stdout.to_string(), stderr.to_string())) } pub async fn exist(program: String) -> bool { - todo!() + let mut child = if cfg!(target_os = "windows") { + let Ok(res) = Command::new("cmd") + .arg("/C") + .arg(program) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .spawn() + else { + return false; + }; + + res + } else { + let Ok(res) = Command::new("sh") + .arg("-c") + .arg(program) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + else { + return false; + }; + + res + }; + + let Ok(res) = child.wait().await else { + return false; + }; + + res.success() } } diff --git a/src/serial/serial_if.rs b/src/serial/serial_if.rs index a35ceee..d39f6b3 100644 --- a/src/serial/serial_if.rs +++ b/src/serial/serial_if.rs @@ -6,6 +6,7 @@ use crate::{ mpmc::{Consumer, Producer}, task::Task, }, + plugin::engine::PluginEngineCommand, success, warning, }; use chrono::Local; @@ -50,6 +51,7 @@ pub struct SerialConnections { logger: Logger, tx: Consumer>, rx: Producer>, + plugin_engine_cmd_sender: Sender, } pub enum SerialCommand { @@ -113,7 +115,12 @@ impl SerialInterface { connections: SerialConnections, cmd_receiver: Receiver, ) { - let SerialConnections { logger, tx, rx } = connections; + let SerialConnections { + logger, + tx, + rx, + plugin_engine_cmd_sender, + } = connections; let mut line = vec![]; let mut buffer = [0u8]; let mut serial = None; @@ -122,14 +129,26 @@ impl SerialInterface { 'task_loop: loop { if let Ok(cmd) = cmd_receiver.try_recv() { let new_mode = match cmd { - SerialCommand::Connect => Self::connect(shared.clone(), &mut serial, &logger), - SerialCommand::Disconnect => { - Self::disconnect(shared.clone(), &mut serial, &logger) - } + SerialCommand::Connect => Self::connect( + shared.clone(), + &mut serial, + &logger, + &plugin_engine_cmd_sender, + ), + SerialCommand::Disconnect => Self::disconnect( + shared.clone(), + &mut serial, + &logger, + &plugin_engine_cmd_sender, + ), SerialCommand::Exit => break 'task_loop, - SerialCommand::Setup(setup) => { - Self::setup(shared.clone(), setup, &mut serial, &logger) - } + SerialCommand::Setup(setup) => Self::setup( + shared.clone(), + setup, + &mut serial, + &logger, + &plugin_engine_cmd_sender, + ), }; Self::set_mode(shared.clone(), new_mode); } @@ -146,7 +165,12 @@ impl SerialInterface { continue 'task_loop; } SerialMode::Reconnecting => { - let new_mode = Self::connect(shared.clone(), &mut serial, &logger); + let new_mode = Self::connect( + shared.clone(), + &mut serial, + &logger, + &plugin_engine_cmd_sender, + ); Self::set_mode(shared.clone(), new_mode); } SerialMode::Connected => { /* Do nothing. It's already connected. */ } @@ -181,7 +205,12 @@ impl SerialInterface { if e.kind() == io::ErrorKind::PermissionDenied || e.kind() == io::ErrorKind::BrokenPipe => { - let _ = Self::disconnect(shared.clone(), &mut Some(ser), &logger); + let _ = Self::disconnect( + shared.clone(), + &mut Some(ser), + &logger, + &plugin_engine_cmd_sender, + ); Self::set_mode(shared.clone(), Some(SerialMode::Reconnecting)); std::thread::yield_now(); continue 'task_loop; @@ -210,6 +239,7 @@ impl SerialInterface { shared: Arc>, serial: &mut Option, logger: &Logger, + plugin_engine_cmd_sender: &Sender, ) -> Option { let sw = shared .read() @@ -236,6 +266,10 @@ impl SerialInterface { sw.port, sw.baudrate ); + let _ = plugin_engine_cmd_sender.send(PluginEngineCommand::SerialConnected { + port: sw.port.clone(), + baudrate: sw.baudrate, + }); Some(SerialMode::Connected) } Err(_) => { @@ -252,6 +286,7 @@ impl SerialInterface { shared: Arc>, serial: &mut Option, logger: &Logger, + plugin_engine_cmd_sender: &Sender, ) -> Option { let _ = serial.take(); let sw = shared.read().expect("Cannot get serial lock for read"); @@ -262,6 +297,10 @@ impl SerialInterface { sw.port, sw.baudrate ); + let _ = plugin_engine_cmd_sender.send(PluginEngineCommand::SerialDisconnected { + port: sw.port.clone(), + baudrate: sw.baudrate, + }); } match sw.mode { @@ -275,6 +314,7 @@ impl SerialInterface { setup: SerialSetup, serial: &mut Option, logger: &Logger, + plugin_engine_cmd_sender: &Sender, ) -> Option { let mut has_changes = false; let mut sw = shared @@ -314,7 +354,7 @@ impl SerialInterface { let last_mode = sw.mode; if has_changes { drop(sw); - let _ = Self::disconnect(shared.clone(), serial, &logger); + let _ = Self::disconnect(shared.clone(), serial, &logger, &plugin_engine_cmd_sender); match last_mode { SerialMode::Reconnecting => None, @@ -331,7 +371,13 @@ impl SerialConnections { logger: Logger, tx: Consumer>, rx: Producer>, + plugin_engine_cmd_sender: Sender, ) -> Self { - Self { logger, tx, rx } + Self { + logger, + tx, + rx, + plugin_engine_cmd_sender, + } } } From a4d45c78d47411be44361f3e0836238620d8a749 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 4 Aug 2024 12:54:04 -0300 Subject: [PATCH 20/49] feat: break logs with newline inside --- src/graphics/graphics_task.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index c5c5e8b..1574d80 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -478,14 +478,33 @@ impl GraphicsTask { }); } - while let Ok(log_msg) = private.logger_receiver.try_recv() { + while let Ok(LogMessage { + timestamp, + message, + level, + }) = private.logger_receiver.try_recv() + { if private.history.len() + new_messages.len() >= private.capacity { private.history.remove(0); } - // TODO brake \n into multiple logs - - new_messages.push(GraphicalMessage::Log(log_msg)); + let log_msg_splited = message + .split('\n') + .enumerate() + .map(|(i, msg)| { + GraphicalMessage::Log(LogMessage { + timestamp, + message: if i == 0 { + msg.to_owned() + } else { + " ".to_string() + msg + }, + level, + }) + }) + .collect::>(); + + new_messages.extend(log_msg_splited); } if !new_messages.is_empty() { From d35c85de21c05e06e1ff2d16990acc2ab265d82c Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 4 Aug 2024 15:11:36 -0300 Subject: [PATCH 21/49] fix: fix load plugins bugs --- plugins/echo.lua | 5 ++- src/graphics/graphics_task.rs | 2 +- src/plugin/engine.rs | 77 +++++++++++++++++++++-------------- src/plugin/plugin.rs | 6 +-- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/plugins/echo.lua b/plugins/echo.lua index b4bb7a2..b2aba21 100644 --- a/plugins/echo.lua +++ b/plugins/echo.lua @@ -24,9 +24,12 @@ end --- @param lvl string The level of echo message function M.level(lvl) if not (lvl == "debug" or lvl == "info" or lvl == "success" or lvl == "warning" or lvl == "error") then + log.error("Level invalid: " .. lvl) return end - + + log.debug("Level setted as " .. lvl) + M.data.level = level end diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index 1574d80..9b18e6e 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -655,7 +655,7 @@ impl GraphicsTask { crate::infra::LogLevel::Warning => (Color::Yellow, Color::Black), crate::infra::LogLevel::Success => (Color::LightGreen, Color::Black), crate::infra::LogLevel::Info => (Color::White, Color::Black), - crate::infra::LogLevel::Debug => (Color::Reset, Color::Black), + crate::infra::LogLevel::Debug => (Color::Reset, Color::DarkGray), }; if scroll_x < message.chars().count() { diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 4d80fc7..4e7c19e 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -24,7 +24,10 @@ use std::{ time::Instant, }; use std::{path::Path, sync::mpsc::Receiver}; -use tokio::{runtime::Runtime, task::yield_now}; +use tokio::{ + runtime::Runtime, + task::{self, yield_now}, +}; pub type PluginEngine = Task<(), PluginEngineCommand>; pub enum PluginEngineCommand { @@ -79,7 +82,13 @@ impl PluginEngine { let rt = Runtime::new().expect("Cannot create tokio runtime"); let _ = rt.block_on(async { - Self::task_async(shared, private, cmd_receiver).await; + let local = task::LocalSet::new(); + + local + .run_until(async move { + Self::task_async(shared, private, cmd_receiver).await; + }) + .await; }); } @@ -131,18 +140,25 @@ impl PluginEngine { } let Ok(filepath) = PathBuf::from_str(&filepath) else { - error!(private.logger, "Invalid filepath \"{}\"", filepath); - return; + error!(private.logger, "Invalid filepath {}", filepath); + continue 'plugin_engine_loop; }; - Self::load_plugin( + let plugin_name = Arc::new(plugin_name); + + match Self::load_plugin( engine_gate.new_method_call_gate(), - Arc::new(plugin_name), + plugin_name.clone(), filepath, &mut plugin_list, - &private.logger, ) - .await; + .await + { + Ok(_) => { + success!(private.logger, "Plugin \"{}\" loaded", plugin_name); + } + Err(err) => error!(private.logger, "{}", err), + } } PluginEngineCommand::UnloadPlugin { plugin_name } => { let Some(plugin) = plugin_list.get_mut(&plugin_name) else { @@ -251,17 +267,22 @@ impl PluginEngine { Some(PluginResponse::Log) } messages::PluginExternalRequest::Finish { fn_name } => { - if fn_name.as_str() == "on_unload" - && matches!(plugin.unload_mode(), PluginUnloadMode::Reload) - { - Self::load_plugin( - engine_gate.new_method_call_gate(), - plugin_name, - plugin.filepath(), - &mut plugin_list, - &private.logger, - ) - .await; + if fn_name.as_str() == "on_unload" { + if matches!(plugin.unload_mode(), PluginUnloadMode::Reload) { + success!(private.logger, "Plugin \"{}\" unloaded", plugin_name); + if let Err(err) = Self::load_plugin( + engine_gate.new_method_call_gate(), + plugin_name.clone(), + plugin.filepath(), + &mut plugin_list, + ) + .await + { + error!(private.logger, "{}", err); + } + } + } else { + plugin_list.insert(plugin_name.clone(), plugin); } /* don't yield here! Because this request doesn't have response and we don't want to reinsert the plugin. */ @@ -337,6 +358,7 @@ impl PluginEngine { fn get_plugin_name(filepath: &str) -> Option { Path::new(filepath) + .with_extension("") .file_name() .map(|filename| filename.to_str()) .flatten() @@ -348,30 +370,25 @@ impl PluginEngine { plugin_name: Arc, filepath: PathBuf, plugin_list: &mut HashMap, Plugin>, - logger: &Logger, - ) { + ) -> Result<(), String> { let filepath = match filepath.extension() { Some(extension) if extension.as_bytes() != b"lua" => { - error!(logger, "Invalid plugin extension: {:?}", extension); - return; + return Err(format!("Invalid plugin extension: {:?}", extension)); } Some(_extension) => filepath, None => filepath.with_extension("lua"), }; if !filepath.exists() { - error!(logger, "Filepath \"{:?}\" doesn't exist!", filepath); - return; + return Err(format!("Filepath \"{:?}\" doesn't exist!", filepath)); } - let res = Plugin::new(plugin_name.clone(), filepath); - let Ok(mut plugin) = res else { - error!(logger, "{}", res.err().unwrap()); - return; - }; + let mut plugin = Plugin::new(plugin_name.clone(), filepath)?; plugin.spawn_method_call(gate, "on_load", ()); plugin_list.insert(plugin_name, plugin); + + Ok(()) } } diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index b754650..64b5aa2 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -30,8 +30,8 @@ impl Plugin { let plugin_dir = filepath.parent().unwrap_or(Path::new("/")); let code = std::fs::read_to_string(&filepath).map_err(|err| err.to_string())?; lua.load(format!( - "package.path = package.path .. ';{:?}/?.lua'", - plugin_dir + "package.path = package.path .. ';{}/?.lua'", + plugin_dir.to_str().unwrap_or("") )) .exec() .map_err(|err| err.to_string())?; @@ -138,7 +138,7 @@ mod tests { fn test_plugin_new() { let _plugin = Plugin::new( Arc::new("echo".to_string()), - Arc::new(PathBuf::from("plugins/echo.lua")), + PathBuf::from("plugins/echo.lua"), ); } } From edfe3ad66da4ca2f498325d3a28f400ad3239a7e Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 4 Aug 2024 15:44:10 -0300 Subject: [PATCH 22/49] fix: fix plugin execution bugs --- plugins/echo.lua | 22 ++++++++++++++-------- src/plugin/engine.rs | 5 ++++- src/plugin/messages.rs | 6 ++++-- src/plugin/method_call.rs | 28 +++++++++++++++++++++++----- src/plugin/plugin.rs | 11 +++++++++-- 5 files changed, 54 insertions(+), 18 deletions(-) diff --git a/plugins/echo.lua b/plugins/echo.lua index b2aba21..832f8e4 100644 --- a/plugins/echo.lua +++ b/plugins/echo.lua @@ -6,29 +6,35 @@ local M = { } } -function M.on_serial_recv(msg) - if M.data.level == "debug" then +local function print_with_level(msg, level) + if level == "debug" then log.debug(msg) - elseif M.data.level == "info" then + elseif level == "info" then log.info(msg) - elseif M.data.level == "success" then + elseif level == "success" then log.success(msg) - elseif M.data.level == "warning" then + elseif level == "warning" then log.warning(msg) - elseif M.data.level == "error" then + elseif level == "error" then log.error(msg) end end +function M.on_serial_recv(msg) + print_with_level(msg, M.data.level) +end + --- Set up the level of echo message --- @param lvl string The level of echo message -function M.level(lvl) +function M.level(args) + local lvl = args[1] + if not (lvl == "debug" or lvl == "info" or lvl == "success" or lvl == "warning" or lvl == "error") then log.error("Level invalid: " .. lvl) return end - log.debug("Level setted as " .. lvl) + print_with_level("Level setted as " .. lvl, lvl) M.data.level = level end diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 4e7c19e..cf68477 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -151,6 +151,7 @@ impl PluginEngine { plugin_name.clone(), filepath, &mut plugin_list, + private.logger.clone(), ) .await { @@ -275,6 +276,7 @@ impl PluginEngine { plugin_name.clone(), plugin.filepath(), &mut plugin_list, + private.logger.clone(), ) .await { @@ -370,6 +372,7 @@ impl PluginEngine { plugin_name: Arc, filepath: PathBuf, plugin_list: &mut HashMap, Plugin>, + logger: Logger, ) -> Result<(), String> { let filepath = match filepath.extension() { Some(extension) if extension.as_bytes() != b"lua" => { @@ -383,7 +386,7 @@ impl PluginEngine { return Err(format!("Filepath \"{:?}\" doesn't exist!", filepath)); } - let mut plugin = Plugin::new(plugin_name.clone(), filepath)?; + let mut plugin = Plugin::new(plugin_name.clone(), filepath, logger)?; plugin.spawn_method_call(gate, "on_load", ()); plugin_list.insert(plugin_name, plugin); diff --git a/src/plugin/messages.rs b/src/plugin/messages.rs index cfd0d4a..c57f5a5 100644 --- a/src/plugin/messages.rs +++ b/src/plugin/messages.rs @@ -12,12 +12,13 @@ pub struct PluginMethodMessage { pub data: T, } +#[derive(Debug)] pub enum PluginRequest { Internal(PluginInternalRequest), External(PluginExternalRequest), } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum PluginExternalRequest { Finish { fn_name: Arc }, SerialInfo, @@ -26,6 +27,7 @@ pub enum PluginExternalRequest { Log { level: LogLevel, message: String }, } +#[derive(Debug)] pub enum PluginInternalRequest { SysSleep { time: Duration, @@ -49,7 +51,7 @@ pub enum PluginInternalRequest { }, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum PluginResponse { Log, SerialInfo { port: String, baudrate: u32 }, diff --git a/src/plugin/method_call.rs b/src/plugin/method_call.rs index f1fbcf8..4cb08ed 100644 --- a/src/plugin/method_call.rs +++ b/src/plugin/method_call.rs @@ -1,3 +1,10 @@ +#[allow(unused)] +use crate::debug; +use crate::{ + error, + infra::logger::{LogLevel, Logger}, +}; + use super::{ bridge::PluginMethodCallGate, messages::{PluginInternalRequest, PluginMethodMessage, PluginRequest, PluginResponse}, @@ -15,6 +22,8 @@ pub struct PluginMethodCall { fn_name: Arc, id: u64, gate: PluginMethodCallGate, + #[allow(unused)] + logger: Logger, } impl PluginMethodCall { @@ -25,6 +34,7 @@ impl PluginMethodCall { lua: Arc, initial_args: impl for<'a> IntoLuaMulti<'a> + 'static, gate: PluginMethodCallGate, + logger: Logger, ) { let mut hasher = DefaultHasher::new(); plugin_name.hash(&mut hasher); @@ -41,10 +51,14 @@ impl PluginMethodCall { fn_name: fn_name.clone(), id, gate, + logger: logger.clone(), }; tokio::task::spawn_local(async move { - let _ = pmc.call_fn(&lua, initial_args).await; + if let Err(err) = pmc.call_fn(&lua, initial_args).await { + error!(logger, "{}", err); + } + let _ = sender .send(PluginMethodMessage { plugin_name, @@ -62,9 +76,12 @@ impl PluginMethodCall { ) -> Result<(), String> { let plugin_table: Table = lua.globals().get("M").unwrap(); - let plugin_fn: Function = plugin_table - .get(self.fn_name.as_str()) - .map_err(|err| err.to_string())?; + let Ok(plugin_fn) = plugin_table + .get::<_, Function>(self.fn_name.as_str()) + .map_err(|err| err.to_string()) + else { + return Ok(()); + }; let thread = lua .create_thread(plugin_fn) @@ -92,7 +109,8 @@ impl PluginMethodCall { let plugin_req: Table = match thread.resume(plugin_fn_args) { Ok(plugin_req) => plugin_req, Err(mlua::Error::CoroutineInactive) => return Ok(None), - Err(err) => return Err(err.to_string()), + Err(mlua::Error::FromLuaConversionError { .. }) => return Ok(None), + Err(err) => return Err(format!("Cannot get plugin_req: {}", err)), }; let plugin_req: PluginRequest = plugin_req diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 64b5aa2..517af43 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -2,6 +2,7 @@ use mlua::{IntoLuaMulti, Lua, LuaOptions, Table}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::infra::logger::Logger; use crate::infra::LogLevel; use super::bridge::PluginMethodCallGate; @@ -14,6 +15,7 @@ pub struct Plugin { log_level: LogLevel, index: u128, unload_mode: PluginUnloadMode, + logger: Logger, } #[derive(Clone, Copy)] @@ -24,7 +26,7 @@ pub enum PluginUnloadMode { } impl Plugin { - pub fn new(name: Arc, filepath: PathBuf) -> Result { + pub fn new(name: Arc, filepath: PathBuf, logger: Logger) -> Result { let lua = Lua::new_with(mlua::StdLib::ALL_SAFE, LuaOptions::default()) .map_err(|err| err.to_string())?; let plugin_dir = filepath.parent().unwrap_or(Path::new("/")); @@ -47,6 +49,7 @@ impl Plugin { index: 0, log_level: LogLevel::Info, unload_mode: PluginUnloadMode::None, + logger, }) } @@ -87,9 +90,10 @@ impl Plugin { self.lua.clone(), initial_args, gate, + self.logger.clone(), ); - self.index = self.index.overflowing_add_signed(0).0; + self.index = self.index.overflowing_add_signed(1).0; } } @@ -97,6 +101,8 @@ impl Plugin { mod tests { use std::{path::PathBuf, sync::Arc}; + use crate::infra::logger::Logger; + use super::Plugin; use mlua::{Lua, LuaOptions, Table, Value}; @@ -139,6 +145,7 @@ mod tests { let _plugin = Plugin::new( Arc::new("echo".to_string()), PathBuf::from("plugins/echo.lua"), + Logger::new().0, ); } } From d2638c8065987355aa88c6f7b72c9d95451a5660 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 4 Aug 2024 22:41:12 -0300 Subject: [PATCH 23/49] fix: improve plugin messages and usage --- Cargo.lock | 573 ++++++++++++++++++++------------------ plugins/echo.lua | 5 +- src/plugin/engine.rs | 23 +- src/plugin/method_call.rs | 13 +- src/plugin/thread.lua | 7 + 5 files changed, 335 insertions(+), 286 deletions(-) create mode 100644 src/plugin/thread.lua diff --git a/Cargo.lock b/Cargo.lock index 70189f2..b3d9609 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -31,18 +31,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -61,47 +61,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.12" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -109,21 +110,21 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -142,15 +143,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bstr" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" dependencies = [ "memchr", "serde", @@ -158,15 +159,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] -name = "bytes" +name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cassowary" @@ -176,18 +183,18 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.0.86" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" [[package]] name = "cfg-if" @@ -203,9 +210,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -213,14 +220,14 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.3", + "windows-targets 0.52.6", ] [[package]] name = "clap" -version = "4.5.1" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", "clap_derive", @@ -228,9 +235,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstream", "anstyle", @@ -240,27 +247,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "compact_str" @@ -287,11 +294,11 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "crossterm_winapi", "futures-core", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -309,9 +316,9 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.3" +version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ba08eff649795412705351c11358001781278102196f49623d9c8d240ed8b9" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ "nix 0.28.0", "windows-sys 0.52.0", @@ -319,9 +326,9 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" @@ -331,9 +338,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -395,7 +402,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] [[package]] @@ -430,9 +437,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -441,15 +448,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -457,9 +464,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -515,20 +522,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown", ] -[[package]] -name = "indoc" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" - [[package]] name = "io-kit-sys" version = "0.4.1" @@ -539,6 +540,12 @@ dependencies = [ "mach2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -548,26 +555,35 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libudev" @@ -591,15 +607,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -607,33 +623,33 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ "hashbrown", ] [[package]] name = "lua-src" -version = "546.0.2" +version = "547.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da0daa7eee611a4c30c8f5ee31af55266e26e573971ba9336d2993e2da129b2" +checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42" dependencies = [ "cc", ] [[package]] name = "luajit-src" -version = "210.5.6+9cc2e42" +version = "210.5.9+04dca79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b365d859c9ffc187f48bb3e25ec80c3b40cf3f68f53544f4adeaee70554157" +checksum = "6e03d48e8d8c11c297d49ea6d2cf6cc0d7657eb3d175219bba47d59a601b7ca9" dependencies = [ "cc", "which", @@ -650,9 +666,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -665,18 +681,18 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", @@ -684,11 +700,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "mlua" -version = "0.9.6" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868d02cb5eb97761bbf6bd6922c1c7a88b8ea252bbf43bd8350a0bf8497a1fc0" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" dependencies = [ "bstr", "futures-util", @@ -700,9 +728,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2847b42764435201d8cbee1f517edb79c4cca4181877b90047587c89e1b7bce4" +checksum = "3ab7a5b4756b8177a2dfa8e0bbcde63bd4000afbc4ab20cbb68d114a25470f29" dependencies = [ "cc", "cfg-if", @@ -730,7 +758,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -738,28 +766,18 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.2" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" dependencies = [ "memchr", ] @@ -772,9 +790,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -782,28 +800,28 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -819,24 +837,27 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -873,38 +894,38 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cassowary", "compact_str", "crossterm", - "indoc", - "itertools", + "itertools 0.12.1", "lru", "paste", "stability", "strum", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -914,9 +935,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -925,29 +946,29 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -956,15 +977,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scope-monitor" @@ -995,29 +1016,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] [[package]] name = "serde_yaml" -version = "0.9.32" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", @@ -1032,7 +1053,7 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de7c4f0cce25b9b3518eea99618112f9ee4549f974480c8f43d3c06f03c131a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "core-foundation-sys", "io-kit-sys", @@ -1057,20 +1078,20 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -1086,15 +1107,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1102,12 +1123,12 @@ dependencies = [ [[package]] name = "stability" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -1118,48 +1139,37 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.50", + "syn", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -1168,52 +1178,51 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] [[package]] name = "tokio" -version = "1.36.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.1", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] [[package]] @@ -1237,29 +1246,40 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unsafe-libyaml" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -1269,9 +1289,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1279,24 +1299,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.50", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1304,41 +1324,40 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "which" -version = "6.0.0" +version = "6.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +checksum = "3d9c5ed668ee1f17edb3b627225343d210006a90bb1e3745ce1f30b1fb115075" dependencies = [ "either", "home", - "once_cell", "rustix", - "windows-sys 0.52.0", + "winsafe", ] [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -1371,7 +1390,7 @@ dependencies = [ "windows-core", "windows-implement", "windows-interface", - "windows-targets 0.52.3", + "windows-targets 0.52.6", ] [[package]] @@ -1380,7 +1399,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.6", ] [[package]] @@ -1391,7 +1410,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] [[package]] @@ -1402,7 +1421,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] [[package]] @@ -1420,7 +1439,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.6", ] [[package]] @@ -1440,17 +1459,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.3", - "windows_aarch64_msvc 0.52.3", - "windows_i686_gnu 0.52.3", - "windows_i686_msvc 0.52.3", - "windows_x86_64_gnu 0.52.3", - "windows_x86_64_gnullvm 0.52.3", - "windows_x86_64_msvc 0.52.3", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1461,9 +1481,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1473,9 +1493,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1485,9 +1505,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1497,9 +1523,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1509,9 +1535,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1521,9 +1547,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1533,15 +1559,21 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winsafe" +version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wmi" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff298e96fd8ef6bb55dcb2a7fd2f26969f962bf428ffa6b267457dd804d64d8" +checksum = "fc2f0a4062ca522aad4705a2948fd4061b3857537990202a8ddd5af21607f79a" dependencies = [ "chrono", "futures", @@ -1553,20 +1585,21 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn", ] diff --git a/plugins/echo.lua b/plugins/echo.lua index 832f8e4..4e44190 100644 --- a/plugins/echo.lua +++ b/plugins/echo.lua @@ -26,15 +26,14 @@ end --- Set up the level of echo message --- @param lvl string The level of echo message -function M.level(args) - local lvl = args[1] - +function M.level(lvl, hello) if not (lvl == "debug" or lvl == "info" or lvl == "success" or lvl == "warning" or lvl == "error") then log.error("Level invalid: " .. lvl) return end print_with_level("Level setted as " .. lvl, lvl) + print_with_level("hello: " .. hello, lvl) M.data.level = level end diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index cf68477..802d696 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -12,7 +12,7 @@ use crate::{ task::{Shared, Task}, }, serial::serial_if::SerialShared, - success, + success, warning, }; use chrono::Local; use std::{ @@ -155,14 +155,13 @@ impl PluginEngine { ) .await { - Ok(_) => { - success!(private.logger, "Plugin \"{}\" loaded", plugin_name); - } + Ok(_) => success!(private.logger, "Plugin \"{}\" loaded", plugin_name), Err(err) => error!(private.logger, "{}", err), } } PluginEngineCommand::UnloadPlugin { plugin_name } => { let Some(plugin) = plugin_list.get_mut(&plugin_name) else { + error!(private.logger, "Plugin \"{}\" not loaded", plugin_name); continue 'plugin_engine_loop; }; @@ -269,9 +268,8 @@ impl PluginEngine { } messages::PluginExternalRequest::Finish { fn_name } => { if fn_name.as_str() == "on_unload" { - if matches!(plugin.unload_mode(), PluginUnloadMode::Reload) { - success!(private.logger, "Plugin \"{}\" unloaded", plugin_name); - if let Err(err) = Self::load_plugin( + if let PluginUnloadMode::Reload = plugin.unload_mode() { + match Self::load_plugin( engine_gate.new_method_call_gate(), plugin_name.clone(), plugin.filepath(), @@ -280,8 +278,15 @@ impl PluginEngine { ) .await { - error!(private.logger, "{}", err); + Ok(_) => success!( + private.logger, + "Plugin \"{}\" reloaded", + plugin_name + ), + Err(err) => error!(private.logger, "{}", err), } + } else { + warning!(private.logger, "Plugin \"{}\" unloaded", plugin_name); } } else { plugin_list.insert(plugin_name.clone(), plugin); @@ -389,7 +394,7 @@ impl PluginEngine { let mut plugin = Plugin::new(plugin_name.clone(), filepath, logger)?; plugin.spawn_method_call(gate, "on_load", ()); - plugin_list.insert(plugin_name, plugin); + plugin_list.insert(plugin_name.clone(), plugin); Ok(()) } diff --git a/src/plugin/method_call.rs b/src/plugin/method_call.rs index 4cb08ed..413611b 100644 --- a/src/plugin/method_call.rs +++ b/src/plugin/method_call.rs @@ -76,15 +76,20 @@ impl PluginMethodCall { ) -> Result<(), String> { let plugin_table: Table = lua.globals().get("M").unwrap(); - let Ok(plugin_fn) = plugin_table + let Ok(_plugin_fn) = plugin_table .get::<_, Function>(self.fn_name.as_str()) .map_err(|err| err.to_string()) else { return Ok(()); }; - let thread = lua - .create_thread(plugin_fn) + let thread: Thread = lua + .load(format!( + include_str!("thread.lua"), + self.fn_name, self.fn_name + )) + .eval_async() + .await .map_err(|err| err.to_string())?; let Some(mut table) = self.call_fn_inner(&lua, &thread, initial_args).await? else { @@ -110,7 +115,7 @@ impl PluginMethodCall { Ok(plugin_req) => plugin_req, Err(mlua::Error::CoroutineInactive) => return Ok(None), Err(mlua::Error::FromLuaConversionError { .. }) => return Ok(None), - Err(err) => return Err(format!("Cannot get plugin_req: {}", err)), + Err(err) => return Err(err.to_string()), }; let plugin_req: PluginRequest = plugin_req diff --git a/src/plugin/thread.lua b/src/plugin/thread.lua new file mode 100644 index 0000000..99e48b8 --- /dev/null +++ b/src/plugin/thread.lua @@ -0,0 +1,7 @@ +coroutine.create(function(t) + if t == nil then + return M.{}() + else + return M.{}(table.unpack(t)) + end +end) From 070ef68c37277fdf217ec6ef0356e1e553e7561d Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 7 Aug 2024 14:49:05 -0300 Subject: [PATCH 24/49] feat: add to_str implementation in lua --- plugins/scope.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/scope.lua b/plugins/scope.lua index fd7ef36..7ad4935 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -8,6 +8,20 @@ local M = { } function M.fmt.to_str(val) + if type(val) == "table" then + local bytearr = {} + for _, v in ipairs(val) do + local utf8byte = v < 0 and (0xff + v + 1) or v + table.insert(bytearr, string.char(utf8byte)) + end + return table.concat(bytearr) + elseif type(val) == "string" then + return val + elseif type(val) == "nil" then + return "nil" + else + return tostring(val) + end end function M.fmt.to_bytes(val) From 5271d4fd6daccb56b23b14efef9cef2a1b9da121 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 7 Aug 2024 14:50:26 -0300 Subject: [PATCH 25/49] feat: add unpack to pass arguments to lua plugins --- src/plugin/engine.rs | 9 ++++++++- src/plugin/method_call.rs | 14 +++++++++----- src/plugin/plugin.rs | 2 ++ src/plugin/thread.lua | 6 +----- src/plugin/unpack.lua | 3 +++ 5 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 src/plugin/unpack.lua diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 802d696..662ec0c 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -133,6 +133,7 @@ impl PluginEngine { engine_gate.new_method_call_gate(), "on_unload", (), + false, ); plugin.set_unload_mode(super::plugin::PluginUnloadMode::Reload); @@ -169,6 +170,7 @@ impl PluginEngine { engine_gate.new_method_call_gate(), "on_unload", (), + false, ); plugin.set_unload_mode(super::plugin::PluginUnloadMode::Unload); } @@ -186,6 +188,7 @@ impl PluginEngine { engine_gate.new_method_call_gate(), &command, options, + true, ); } PluginEngineCommand::SerialConnected { port, baudrate } => { @@ -194,6 +197,7 @@ impl PluginEngine { engine_gate.new_method_call_gate(), "on_serial_connect", (port.clone(), baudrate), + false, ); } } @@ -203,6 +207,7 @@ impl PluginEngine { engine_gate.new_method_call_gate(), "on_serial_disconnect", (port.clone(), baudrate), + false, ); } } @@ -317,6 +322,7 @@ impl PluginEngine { engine_gate.new_method_call_gate(), "on_serial_send", tx_msg.message.clone(), + false, ); } } @@ -338,6 +344,7 @@ impl PluginEngine { engine_gate.new_method_call_gate(), "on_serial_recv", rx_msg.message.clone(), + false, ); } @@ -392,7 +399,7 @@ impl PluginEngine { } let mut plugin = Plugin::new(plugin_name.clone(), filepath, logger)?; - plugin.spawn_method_call(gate, "on_load", ()); + plugin.spawn_method_call(gate, "on_load", (), false); plugin_list.insert(plugin_name.clone(), plugin); diff --git a/src/plugin/method_call.rs b/src/plugin/method_call.rs index 413611b..a32bd3d 100644 --- a/src/plugin/method_call.rs +++ b/src/plugin/method_call.rs @@ -35,6 +35,7 @@ impl PluginMethodCall { initial_args: impl for<'a> IntoLuaMulti<'a> + 'static, gate: PluginMethodCallGate, logger: Logger, + has_unpack: bool, ) { let mut hasher = DefaultHasher::new(); plugin_name.hash(&mut hasher); @@ -55,7 +56,7 @@ impl PluginMethodCall { }; tokio::task::spawn_local(async move { - if let Err(err) = pmc.call_fn(&lua, initial_args).await { + if let Err(err) = pmc.call_fn(&lua, initial_args, has_unpack).await { error!(logger, "{}", err); } @@ -73,6 +74,7 @@ impl PluginMethodCall { mut self, lua: &'a Lua, initial_args: impl IntoLuaMulti<'a>, + has_unpack: bool, ) -> Result<(), String> { let plugin_table: Table = lua.globals().get("M").unwrap(); @@ -83,11 +85,13 @@ impl PluginMethodCall { return Ok(()); }; + let thread_code = if has_unpack { + format!(include_str!("unpack.lua"), self.fn_name) + } else { + format!(include_str!("thread.lua"), self.fn_name) + }; let thread: Thread = lua - .load(format!( - include_str!("thread.lua"), - self.fn_name, self.fn_name - )) + .load(thread_code) .eval_async() .await .map_err(|err| err.to_string())?; diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 517af43..3691c91 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -78,6 +78,7 @@ impl Plugin { gate: PluginMethodCallGate, fn_name: &str, initial_args: impl for<'a> IntoLuaMulti<'a> + 'static, + has_unpack: bool, ) { if !matches!(self.unload_mode, PluginUnloadMode::None) { return; @@ -91,6 +92,7 @@ impl Plugin { initial_args, gate, self.logger.clone(), + has_unpack, ); self.index = self.index.overflowing_add_signed(1).0; diff --git a/src/plugin/thread.lua b/src/plugin/thread.lua index 99e48b8..8acd44c 100644 --- a/src/plugin/thread.lua +++ b/src/plugin/thread.lua @@ -1,7 +1,3 @@ coroutine.create(function(t) - if t == nil then - return M.{}() - else - return M.{}(table.unpack(t)) - end + return M.{}(t) end) diff --git a/src/plugin/unpack.lua b/src/plugin/unpack.lua new file mode 100644 index 0000000..5731447 --- /dev/null +++ b/src/plugin/unpack.lua @@ -0,0 +1,3 @@ +coroutine.create(function(t) + return M.{}(table.unpack(t)) +end) From 534650560bfacd2a5204de58fb99242fd1c4073e Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 7 Aug 2024 14:51:01 -0300 Subject: [PATCH 26/49] fix: fix graphics bugs --- plugins/echo.lua | 10 +++++----- src/graphics/graphics_task.rs | 25 ++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/plugins/echo.lua b/plugins/echo.lua index 4e44190..9823e07 100644 --- a/plugins/echo.lua +++ b/plugins/echo.lua @@ -1,4 +1,5 @@ local log = require("scope").log +local fmt = require("scope").fmt local M = { data = { @@ -21,21 +22,20 @@ local function print_with_level(msg, level) end function M.on_serial_recv(msg) - print_with_level(msg, M.data.level) + print_with_level(fmt.to_str(msg), M.data.level) end --- Set up the level of echo message --- @param lvl string The level of echo message -function M.level(lvl, hello) +function M.level(lvl) if not (lvl == "debug" or lvl == "info" or lvl == "success" or lvl == "warning" or lvl == "error") then log.error("Level invalid: " .. lvl) return end - print_with_level("Level setted as " .. lvl, lvl) - print_with_level("hello: " .. hello, lvl) + M.data.level = lvl - M.data.level = level + print_with_level("Level setted as " .. M.data.level, lvl) end return M diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index 9b18e6e..b5dbb2b 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -488,8 +488,11 @@ impl GraphicsTask { private.history.remove(0); } + let message = message.split("\n").collect::>(); + let message_len = message.len(); let log_msg_splited = message - .split('\n') + .into_iter() + .filter(|msg| !msg.is_empty()) .enumerate() .map(|(i, msg)| { GraphicalMessage::Log(LogMessage { @@ -498,7 +501,10 @@ impl GraphicsTask { msg.to_owned() } else { " ".to_string() + msg - }, + } + .replace('\r', "") + .replace('\t', " ") + + if i < (message_len - 1) { "\r\n" } else { "" }, level, }) }) @@ -658,11 +664,24 @@ impl GraphicsTask { crate::infra::LogLevel::Debug => (Color::Reset, Color::DarkGray), }; + let end_index = if message.ends_with("\r\n") { + message.len() - 2 + } else { + message.len() + }; + if scroll_x < message.chars().count() { spans.push(Span::styled( - &message[scroll_x..], + &message[scroll_x..end_index], Style::default().fg(fg).bg(bg), )); + + if message.ends_with("\r\n") { + spans.push(Span::styled( + "\\r\\n", + Style::default().fg(Color::Magenta).bg(bg), + )); + } } Line::from(spans) From 22ca2fdf3d00585354f6f0546db029522a8172c4 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 7 Aug 2024 15:41:55 -0300 Subject: [PATCH 27/49] feat: add a plugin for basic tests --- plugins/scope.lua | 11 ++++++-- plugins/test.lua | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 plugins/test.lua diff --git a/plugins/scope.lua b/plugins/scope.lua index 7ad4935..136f776 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -25,6 +25,13 @@ function M.fmt.to_str(val) end function M.fmt.to_bytes(val) + if type(val) == "string" then + return { string.byte(val, 1, -1) } + elseif type(val) == "table" then + return val + else + return {} + end end function M.log.debug(msg) @@ -48,7 +55,7 @@ function M.log.error(msg) end function M.serial.info() - local port, baud_rate = coroutine.yield({":serial.info"}) + local port, baud_rate = table.unpack(coroutine.yield({":serial.info"})) return port, baud_rate end @@ -57,7 +64,7 @@ function M.serial.send(msg) end function M.serial.recv(opts) - local err, msg = coroutine.yield({":serial.recv", opts}) + local err, msg = table.unpack(coroutine.yield({":serial.recv", opts})) return err, msg end diff --git a/plugins/test.lua b/plugins/test.lua new file mode 100644 index 0000000..8acf64b --- /dev/null +++ b/plugins/test.lua @@ -0,0 +1,64 @@ +local log = require("scope").log +local fmt = require("scope").fmt +local serial = require("scope").serial +local sys = require("scope").sys + +local M = { + is_loaded = false +} + +function M.on_load() + log.info("Running on " .. sys.os_name()) + sys.sleep_ms(3000) + log.success("Plugin is ready now!") + M.is_loaded = true +end + +function M.on_unload() + log.warning("Cleaning up resources...") + sys.sleep_ms(2000) + log.success("Resources clean") + M.is_loaded = false +end + +function M.on_serial_send(msg) + if not M.is_loaded then + log.error("Plugin not loaded yet") + return + end + + log.info("Sending " .. fmt.to_str(msg) .. " ...") + serial.send(fmt.to_bytes("AT\r\n")) + local _, data = serial.recv({timeout_ms=2000}) + log.info("Receive sent message: " .. fmt.to_str(data)) + local _, data = serial.recv({timeout_ms=2000}) + log.info("Receive AT: " .. fmt.to_str(data)) +end + +function M.on_serial_recv(msg) + log.info("Receive pkt: " .. fmt.to_str(msg)) +end + +function M.on_serial_connect(port, baudrate) + log.success("Connected to " .. port .. "@" .. fmt.to_str(baudrate)) +end + +function M.on_serial_disconnect(port, baudrate) + log.warning("Disconnected from " .. port .. "@" .. fmt.to_str(baudrate)) +end + +function M.hello(name, age) + name = name or "???" + age = age or "???" + log.info("Hello, " .. name .. ". Do you have " .. age .. " years?") +end + +function M.logs() + log.debug("debug log level") + log.info("info log level") + log.success("success log level") + log.warning("warning log level") + log.error("error log level") +end + +return M From f00ab5853c85ad5d74822ef659302c7989c886c3 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 9 Aug 2024 11:07:32 -0300 Subject: [PATCH 28/49] feat: add a option to send a \r\n with shift and enter --- src/inputs/inputs_task.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index bd5f395..d3c55f2 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -238,6 +238,13 @@ impl InputsTask { let mut sw = shared.write().expect("Cannot get input lock for write"); if sw.command_line.is_empty() { + if let KeyModifiers::SHIFT = key.modifiers { + private.tx.produce(Arc::new(TimedBytes { + timestamp: Local::now(), + message: b"\r\n".to_vec(), + })); + } + return LoopStatus::Continue; } From b2dbb5d8f645bd375ca8d9c01544db0c789f27fc Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 9 Aug 2024 11:08:14 -0300 Subject: [PATCH 29/49] fix: fix hex sequence --- src/inputs/inputs_task.rs | 63 ++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index d3c55f2..6d06835 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -273,11 +273,20 @@ impl InputsTask { Self::handle_user_command(command_line_split, &private); } else { let command_line = Self::replace_hex_sequence(command_line); - let command_line = Self::replace_tag_sequence(command_line, &private.tag_file); + let mut command_line = + Self::replace_tag_sequence(command_line, &private.tag_file); + + let end_bytes = if let KeyModifiers::SHIFT = key.modifiers { + b"\r\n".as_slice() + } else { + b"".as_slice() + }; + + command_line.extend_from_slice(end_bytes); private.tx.produce(Arc::new(TimedBytes { timestamp: Local::now(), - message: (command_line + "\r\n").as_bytes().to_vec(), + message: command_line, })); } } @@ -532,8 +541,8 @@ impl InputsTask { *current_hint = Some(hints.choose(&mut rand::thread_rng()).unwrap().to_string()); } - fn replace_hex_sequence(command_line: String) -> String { - let mut output = String::new(); + fn replace_hex_sequence(command_line: String) -> Vec { + let mut output = vec![]; let mut in_hex_seq = false; let valid = "0123456789abcdefABCDEF,_-."; let mut hex_shift = 0; @@ -548,11 +557,11 @@ impl InputsTask { continue; } - output.push(c); + output.push(c as u8); } else { if !valid.contains(c) { in_hex_seq = false; - output.push(c); + output.push(c as u8); continue; } @@ -563,15 +572,15 @@ impl InputsTask { } 'a'..='f' => { *hex_val.get_or_insert(0) <<= hex_shift; - *hex_val.get_or_insert(0) |= c as u8 - 'a' as u8; + *hex_val.get_or_insert(0) |= c as u8 - 'a' as u8 + 0x0a; } 'A'..='F' => { *hex_val.get_or_insert(0) <<= hex_shift; - *hex_val.get_or_insert(0) |= c as u8 - 'A' as u8; + *hex_val.get_or_insert(0) |= c as u8 - 'A' as u8 + 0x0A; } _ => { if let Some(hex) = hex_val.take() { - output.push(hex as char); + output.push(hex); } hex_shift = 0; continue; @@ -582,7 +591,7 @@ impl InputsTask { hex_shift = 4; } else { if let Some(hex) = hex_val.take() { - output.push(hex as char); + output.push(hex); } hex_shift = 0; } @@ -592,7 +601,7 @@ impl InputsTask { output } - fn replace_tag_sequence(command_line: String, _tag_file: &PathBuf) -> String { + fn replace_tag_sequence(command_line: Vec, _tag_file: &PathBuf) -> Vec { // TODO command_line } @@ -642,20 +651,46 @@ mod tests { fn test_rhs_one() { let res = InputsTask::replace_hex_sequence("$61".to_string()); - assert_eq!(&res, "a"); + assert_eq!(&res, b"a"); } #[test] fn test_rhs_two_no_sep() { let res = InputsTask::replace_hex_sequence("$6161".to_string()); - assert_eq!(&res, "aa"); + assert_eq!(&res, b"aa"); } #[test] fn test_rhs_two_comma() { let res = InputsTask::replace_hex_sequence("$61,61".to_string()); - assert_eq!(&res, "aa"); + assert_eq!(&res, b"aa"); + } + + #[test] + fn test_all_bytes() { + let mut command_line = "$".to_string(); + let mut expected = vec![]; + for b in 0u8..=0xff { + command_line.push_str(&format!("{:02x},", b)); + expected.push(b); + } + for b in 0u8..=0xff { + command_line.push_str(&format!("{:02X},", b)); + expected.push(b); + } + + let res = InputsTask::replace_hex_sequence(command_line.clone()); + let mut it = res.iter().enumerate(); + + for (i, b) in &mut it { + assert_eq!(*b, i as u8); + } + for (i, b) in it { + assert_eq!(*b, i as u8); + } + + assert_eq!(&res, &expected); } } From 558c233231d84b78e0ebc00e2a44fbbecfa6a373 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 9 Aug 2024 11:08:37 -0300 Subject: [PATCH 30/49] feat: add more ansi color decode option --- src/graphics/graphics_task.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index b5dbb2b..4da4023 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -331,9 +331,12 @@ impl GraphicsTask { let patterns = [ ("\x1b[0m", Color::Reset), ("\x1b[30m", Color::Black), - ("\x1b[31m", Color::Red), ("\x1b[32m", Color::Green), + ("\x1b[1;32m", Color::Green), + ("\x1b[31m", Color::Red), + ("\x1b[1;31m", Color::Red), ("\x1b[33m", Color::Yellow), + ("\x1b[1;33m", Color::Yellow), ("\x1b[34m", Color::Blue), ("\x1b[35m", Color::Magenta), ("\x1b[36m", Color::Cyan), From 1823f03802546fa01de5bca25e2f2d9445c31c00 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 9 Aug 2024 11:19:03 -0300 Subject: [PATCH 31/49] feat: change shift to alt to send without new line --- src/inputs/inputs_task.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index 6d06835..f8dc379 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -238,7 +238,7 @@ impl InputsTask { let mut sw = shared.write().expect("Cannot get input lock for write"); if sw.command_line.is_empty() { - if let KeyModifiers::SHIFT = key.modifiers { + if let KeyModifiers::ALT = key.modifiers { private.tx.produce(Arc::new(TimedBytes { timestamp: Local::now(), message: b"\r\n".to_vec(), @@ -276,10 +276,10 @@ impl InputsTask { let mut command_line = Self::replace_tag_sequence(command_line, &private.tag_file); - let end_bytes = if let KeyModifiers::SHIFT = key.modifiers { - b"\r\n".as_slice() - } else { + let end_bytes = if let KeyModifiers::ALT = key.modifiers { b"".as_slice() + } else { + b"\r\n".as_slice() }; command_line.extend_from_slice(end_bytes); From 2bc4d490d927249220dab04675bb1a66376bed70 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 9 Aug 2024 20:40:23 -0300 Subject: [PATCH 32/49] fix: fix hex string print --- src/graphics/bytes.rs | 58 +++++++++++++++++++++++++++++++++++ src/graphics/graphics_task.rs | 38 +++++++++++------------ src/graphics/mod.rs | 1 + 3 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 src/graphics/bytes.rs diff --git a/src/graphics/bytes.rs b/src/graphics/bytes.rs new file mode 100644 index 0000000..c3bfc4b --- /dev/null +++ b/src/graphics/bytes.rs @@ -0,0 +1,58 @@ +pub trait SliceExt { + fn contains(&self, pat: &[T]) -> bool; + fn replace(&self, from: &[T], to: &[T]) -> Self; +} + +impl SliceExt for Vec { + fn contains(&self, pat: &[u8]) -> bool { + self.iter() + .enumerate() + .any(|(i, byte)| pat.get(0) == Some(byte) && Some(pat) == self.get(i..(i + pat.len()))) + } + + fn replace(&self, from: &[u8], to: &[u8]) -> Self { + let mut result = vec![]; + let mut last_end = 0; + for (start, part) in match_indices(self, from) { + result.extend_from_slice(unsafe { self.get_unchecked(last_end..start) }); + result.extend_from_slice(to); + last_end = start + part.len(); + } + result.extend_from_slice(unsafe { self.get_unchecked(last_end..self.len()) }); + result + } +} + +fn match_indices<'a>(vec: &'a Vec, from: &'a [u8]) -> BytesMatchIndices<'a> { + BytesMatchIndices { + vec: Some(vec.as_slice()), + from, + } +} + +struct BytesMatchIndices<'a> { + vec: Option<&'a [u8]>, + from: &'a [u8], +} + +impl<'a> Iterator for BytesMatchIndices<'a> { + type Item = (usize, &'a [u8]); + + fn next(&mut self) -> Option { + let Some(vec) = self.vec.take() else { + return None; + }; + + for (i, _byte) in vec.iter().enumerate() { + let start_idx = i; + let end_idx = i + self.from.len(); + + if vec.get(start_idx..end_idx) == Some(self.from) { + self.vec = vec.get(end_idx..); + return Some((i, self.from)); + } + } + + None + } +} diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index 4da4023..c89c240 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -1,3 +1,5 @@ +use super::bytes::SliceExt; +use super::Serialize; use crate::{error, info, success}; use crate::{ infra::{ @@ -40,8 +42,6 @@ use std::{ }, }; -use super::Serialize; - pub type GraphicsTask = Task<(), GraphicsCommand>; pub struct GraphicsConnections { @@ -329,18 +329,18 @@ impl GraphicsTask { let mut blink = Blink::new(Duration::from_millis(200), 2, Color::Reset, Color::Black); let mut new_messages = vec![]; let patterns = [ - ("\x1b[0m", Color::Reset), - ("\x1b[30m", Color::Black), - ("\x1b[32m", Color::Green), - ("\x1b[1;32m", Color::Green), - ("\x1b[31m", Color::Red), - ("\x1b[1;31m", Color::Red), - ("\x1b[33m", Color::Yellow), - ("\x1b[1;33m", Color::Yellow), - ("\x1b[34m", Color::Blue), - ("\x1b[35m", Color::Magenta), - ("\x1b[36m", Color::Cyan), - ("\x1b[37m", Color::White), + (b"\x1b[0m".as_slice(), Color::Reset), + (b"\x1b[30m", Color::Black), + (b"\x1b[32m", Color::Green), + (b"\x1b[1;32m", Color::Green), + (b"\x1b[31m", Color::Red), + (b"\x1b[1;31m", Color::Red), + (b"\x1b[33m", Color::Yellow), + (b"\x1b[1;33m", Color::Yellow), + (b"\x1b[34m", Color::Blue), + (b"\x1b[35m", Color::Magenta), + (b"\x1b[36m", Color::Cyan), + (b"\x1b[37m", Color::White), ]; 'draw_loop: loop { @@ -566,13 +566,13 @@ impl GraphicsTask { terminal.show_cursor().expect("Cannot show mouse cursor"); } - fn ansi_colors(patterns: &[(&'static str, Color)], msg: &[u8]) -> Vec<(String, Color)> { + fn ansi_colors(patterns: &[(&[u8], Color)], msg: &[u8]) -> Vec<(String, Color)> { let mut output = vec![]; - let mut buffer = "".to_string(); + let mut buffer = vec![]; let mut color = Color::Reset; for byte in msg { - buffer.push(*byte as char); + buffer.push(*byte); if (*byte as char) != 'm' { continue; @@ -580,7 +580,7 @@ impl GraphicsTask { 'pattern_loop: for (pattern, new_color) in patterns { if buffer.contains(pattern) { - output.push((buffer.replace(pattern, ""), color)); + output.push((buffer.replace(pattern, b""), color)); buffer.clear(); color = *new_color; @@ -596,7 +596,7 @@ impl GraphicsTask { output .into_iter() - .flat_map(|(msg, color)| Self::bytes_to_string(msg.as_bytes(), color)) + .flat_map(|(msg, color)| Self::bytes_to_string(&msg, color)) .collect() } diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs index 2bc294f..d1fa791 100644 --- a/src/graphics/mod.rs +++ b/src/graphics/mod.rs @@ -1,3 +1,4 @@ +pub mod bytes; pub mod graphics_task; pub trait Serialize { From 6606b2714da9666e72e0cbe8b615722ca5622ce5 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 9 Aug 2024 20:41:41 -0300 Subject: [PATCH 33/49] fix: remove code from re.match function --- plugins/scope.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/scope.lua b/plugins/scope.lua index 136f776..98b74a6 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -92,10 +92,8 @@ function M.re.matches(str, pattern_table) end end -function M.re.match(str, pattern, code) - if coroutine.yield({":re.match", str, pattern}) then - code(str) - end +function M.re.match(str, pattern) + return coroutine.yield({":re.match", str, pattern}) end return M From 923f9a2b8c797fe31aa958243bc84a71b2a8a1c2 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 9 Aug 2024 21:18:47 -0300 Subject: [PATCH 34/49] refactor: change special characters' color and user input on history --- src/graphics/graphics_task.rs | 24 +++++++++++++++++------- src/main.rs | 6 +++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index c89c240..f43b1d1 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -59,6 +59,7 @@ pub struct GraphicsConnections { auto_scroll: bool, scroll: (u16, u16), last_frame_height: u16, + is_true_color: bool, } pub enum GraphicsCommand { @@ -162,9 +163,16 @@ impl GraphicsTask { let text = coll .map(|msg| match msg { GraphicalMessage::Log(log_msg) => Self::line_from_log_message(log_msg, scroll), - GraphicalMessage::Tx { timestamp, message } => { - Self::line_from_message(timestamp, message, Color::LightCyan, scroll) - } + GraphicalMessage::Tx { timestamp, message } => Self::line_from_message( + timestamp, + message, + if private.is_true_color { + Color::Rgb(12, 129, 123) + } else { + Color::Blue + }, + scroll, + ), GraphicalMessage::Rx { timestamp, message } => { Self::line_from_message(timestamp, message, Color::Reset, scroll) } @@ -477,7 +485,7 @@ impl GraphicsTask { new_messages.push(GraphicalMessage::Tx { timestamp: tx_msg.timestamp, - message: Self::bytes_to_string(&tx_msg.message, Color::Black), + message: Self::bytes_to_string(&tx_msg.message, Color::White), }); } @@ -604,10 +612,10 @@ impl GraphicsTask { let mut output = vec![]; let mut buffer = "".to_string(); let mut in_plain_text = true; - let accent_color = if color == Color::Magenta { + let accent_color = if color == Color::Yellow { Color::DarkGray } else { - Color::Magenta + Color::Yellow }; for byte in msg { @@ -682,7 +690,7 @@ impl GraphicsTask { if message.ends_with("\r\n") { spans.push(Span::styled( "\\r\\n", - Style::default().fg(Color::Magenta).bg(bg), + Style::default().fg(Color::Yellow).bg(bg), )); } } @@ -758,6 +766,7 @@ impl GraphicsConnections { serial_shared: Shared, storage_base_filename: String, capacity: usize, + is_true_color: bool, ) -> Self { Self { logger, @@ -774,6 +783,7 @@ impl GraphicsConnections { scroll: (0, 0), last_frame_height: u16::MAX, system_log_level: LogLevel::Debug, + is_true_color, } } } diff --git a/src/main.rs b/src/main.rs index 49a9e63..633c6b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,8 @@ struct Cli { capacity: Option, #[clap(short, long)] tag_file: Option, + #[clap(short, long)] + non_colorful: bool, } #[derive(Subcommand)] @@ -51,6 +53,7 @@ fn app( tag_file: PathBuf, port: Option, baudrate: Option, + is_true_color: bool, ) -> Result<(), String> { let (logger, logger_receiver) = Logger::new(); let mut tx_channel = Channel::default(); @@ -125,6 +128,7 @@ fn app( serial_shared, storage_base_filename, capacity, + is_true_color, ); let text_view = GraphicsTask::spawn_graphics_task( graphics_connections, @@ -168,7 +172,7 @@ fn main() -> Result<(), String> { let capacity = cli.capacity.unwrap_or(DEFAULT_CAPACITY); let tag_file = cli.tag_file.unwrap_or(PathBuf::from(DEFAULT_TAG_FILE)); - if let Err(err) = app(capacity, tag_file, port, baudrate) { + if let Err(err) = app(capacity, tag_file, port, baudrate, !cli.non_colorful) { return Err(format!("[\x1b[31mERR\x1b[0m] {}", err)); } From ec83a5b9d1fc2adf7dcb2f61f616528583f3de6b Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 14 Aug 2024 19:03:43 -0300 Subject: [PATCH 35/49] fix: fix build error for macos --- src/serial/serial_if.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serial/serial_if.rs b/src/serial/serial_if.rs index d39f6b3..268c7d7 100644 --- a/src/serial/serial_if.rs +++ b/src/serial/serial_if.rs @@ -22,7 +22,7 @@ use std::{ pub type SerialInterface = Task; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] type SerialPort = TTYPort; #[cfg(target_os = "windows")] type SerialPort = COMPort; From 2bf88af813bb54c45527b37e3e28a71228743bc8 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 17 Aug 2024 15:12:29 -0300 Subject: [PATCH 36/49] fix: fix bytes replacement and ansi bytes decoding --- src/graphics/bytes.rs | 95 +++++++++++++++++++++++++++++++---- src/graphics/graphics_task.rs | 9 +++- 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/graphics/bytes.rs b/src/graphics/bytes.rs index c3bfc4b..08714b0 100644 --- a/src/graphics/bytes.rs +++ b/src/graphics/bytes.rs @@ -25,13 +25,15 @@ impl SliceExt for Vec { fn match_indices<'a>(vec: &'a Vec, from: &'a [u8]) -> BytesMatchIndices<'a> { BytesMatchIndices { - vec: Some(vec.as_slice()), + vec: vec.as_slice(), + offset: 0, from, } } struct BytesMatchIndices<'a> { - vec: Option<&'a [u8]>, + vec: &'a [u8], + offset: usize, from: &'a [u8], } @@ -39,20 +41,93 @@ impl<'a> Iterator for BytesMatchIndices<'a> { type Item = (usize, &'a [u8]); fn next(&mut self) -> Option { - let Some(vec) = self.vec.take() else { + if self.offset >= self.vec.len() { return None; - }; + } - for (i, _byte) in vec.iter().enumerate() { - let start_idx = i; - let end_idx = i + self.from.len(); + for (i, _byte) in self.vec.iter().enumerate() { + let start_idx = i + self.offset; + let end_idx = i + self.offset + self.from.len(); - if vec.get(start_idx..end_idx) == Some(self.from) { - self.vec = vec.get(end_idx..); - return Some((i, self.from)); + if self.vec.get(start_idx..end_idx) == Some(self.from) { + self.offset = end_idx; + return Some((start_idx, self.from)); } } None } } + +#[cfg(test)] +mod test { + use super::SliceExt; + + #[test] + fn test_replace_from_start() { + let expected = b"Hello".to_vec(); + let res = b"\x1b[mHello".to_vec().replace(b"\x1b[m", b""); + + assert_eq!(expected, res); + } + + #[test] + fn test_replace_from_end() { + let expected = b"Hello ".to_vec(); + let res = b"Hello \x1b[m".to_vec().replace(b"\x1b[m", b""); + + assert_eq!(expected, res); + } + + #[test] + fn test_replace_seq_eq() { + let expected = b"Hello ".to_vec(); + let res = b"Hello\x1b[m \x1b[m".to_vec().replace(b"\x1b[m", b""); + + assert_eq!(expected, res); + } + + #[test] + fn test_replace_seq_diff() { + let expected = b"Hello\x1b[8D".to_vec(); + let res = b"Hello\x1b[m\x1b[8D".to_vec().replace(b"\x1b[m", b""); + + assert_eq!(expected, res); + } + + #[test] + fn test_long_string() { + let expected = b"uart:~$ uart:~$ ".to_vec(); + let res = b"uart:~$ \x1b[m\x1b[8D\x1b[Juart:~$ \x1b[m" + .to_vec() + .replace(b"\x1b[m", b"") + .replace(b"\x1b[8D", b"") + .replace(b"\x1b[J", b""); + + assert_eq!(expected, res); + } + + #[test] + fn test_long_string_out_of_order() { + let expected = b"uart:~$ uart:~$ ".to_vec(); + let res = b"uart:~$ \x1b[m\x1b[8D\x1b[Juart:~$ \x1b[m" + .to_vec() + .replace(b"\x1b[m", b"") + .replace(b"\x1b[J", b"") + .replace(b"\x1b[8D", b""); + let res2 = b"uart:~$ \x1b[m\x1b[8D\x1b[Juart:~$ \x1b[m" + .to_vec() + .replace(b"\x1b[J", b"") + .replace(b"\x1b[8D", b"") + .replace(b"\x1b[m", b""); + let res3 = b"uart:~$ \x1b[m\x1b[8D\x1b[Juart:~$ \x1b[m" + .to_vec() + .replace(b"\x1b[8D", b"") + .replace(b"\x1b[m", b"") + .replace(b"\x1b[J", b""); + + assert_eq!(expected, res); + assert_eq!(expected, res2); + assert_eq!(expected, res3); + } +} diff --git a/src/graphics/graphics_task.rs b/src/graphics/graphics_task.rs index f43b1d1..46d0013 100644 --- a/src/graphics/graphics_task.rs +++ b/src/graphics/graphics_task.rs @@ -575,14 +575,19 @@ impl GraphicsTask { } fn ansi_colors(patterns: &[(&[u8], Color)], msg: &[u8]) -> Vec<(String, Color)> { + let msg = msg + .to_vec() + .replace(b"\x1b[m", b"") + .replace(b"\x1b[8D", b"") + .replace(b"\x1b[J", b""); let mut output = vec![]; let mut buffer = vec![]; let mut color = Color::Reset; for byte in msg { - buffer.push(*byte); + buffer.push(byte); - if (*byte as char) != 'm' { + if (byte as char) != 'm' { continue; } From 7c283167d0255991c959de2b75d862a75289f32f Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 18 Aug 2024 22:04:05 -0300 Subject: [PATCH 37/49] feat: add source and id to logger messages --- src/infra/logger.rs | 47 ++++++++++++++++++++++++++++++++++++--- src/main.rs | 10 ++++----- src/plugin/engine.rs | 20 ++++++++++++++--- src/plugin/messages.rs | 39 +++++++++++++++++++++++++------- src/plugin/method_call.rs | 9 +++++--- src/plugin/plugin.rs | 4 ++-- 6 files changed, 105 insertions(+), 24 deletions(-) diff --git a/src/infra/logger.rs b/src/infra/logger.rs index 0fef007..59cdcc0 100644 --- a/src/infra/logger.rs +++ b/src/infra/logger.rs @@ -5,6 +5,8 @@ use chrono::{DateTime, Local}; #[derive(Clone)] pub struct Logger { sender: Sender, + source: String, + id: Option, } pub struct LogMessage { @@ -23,10 +25,27 @@ pub enum LogLevel { } impl Logger { - pub fn new() -> (Self, Receiver) { + pub fn new(source: String) -> (Self, Receiver) { let (sender, receiver) = channel(); - (Self { sender }, receiver) + ( + Self { + sender, + source, + id: None, + }, + receiver, + ) + } + + pub fn with_source(mut self, source: String) -> Self { + self.source = source; + self + } + + pub fn with_id(mut self, id: String) -> Self { + self.id = Some(id); + self } pub fn write( @@ -36,7 +55,29 @@ impl Logger { ) -> Result<(), std::sync::mpsc::SendError> { self.sender.send(LogMessage { timestamp: Local::now(), - message, + message: format!( + "[{}{}] {}", + self.source, + self.id + .as_ref() + .map(|id| ":".to_string() + id) + .unwrap_or("".to_string()), + message + ), + level, + }) + } + + pub fn write_with_source_id( + &self, + message: String, + level: LogLevel, + source: String, + id: String, + ) -> Result<(), std::sync::mpsc::SendError> { + self.sender.send(LogMessage { + timestamp: Local::now(), + message: format!("[{}:{}] {}", source, id, message), level, }) } diff --git a/src/main.rs b/src/main.rs index 633c6b3..27726a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,7 @@ fn app( baudrate: Option, is_true_color: bool, ) -> Result<(), String> { - let (logger, logger_receiver) = Logger::new(); + let (logger, logger_receiver) = Logger::new("main".to_string()); let mut tx_channel = Channel::default(); let mut rx_channel = Channel::default(); @@ -81,13 +81,13 @@ fn app( })); let serial_connections = SerialConnections::new( - logger.clone(), + logger.clone().with_source("serial".to_string()), tx_channel_consumers.pop().unwrap(), rx_channel.clone().new_producer(), plugin_engine_cmd_sender.clone(), ); let inputs_connections = InputsConnections::new( - logger.clone(), + logger.clone().with_source("inputs".to_string()), tx_channel.clone().new_producer(), graphics_cmd_sender.clone(), serial_if_cmd_sender.clone(), @@ -104,7 +104,7 @@ fn app( let serial_shared = serial_if.shared_ref(); let plugin_engine_connections = PluginEngineConnections::new( - logger.clone(), + logger.clone().with_source("plugin".to_string()), tx_channel.new_producer(), tx_channel_consumers.pop().unwrap(), rx_channel_consumers.pop().unwrap(), @@ -120,7 +120,7 @@ fn app( let now_str = Local::now().format("%Y%m%d_%H%M%S"); let storage_base_filename = format!("{}.txt", now_str); let graphics_connections = GraphicsConnections::new( - logger.clone(), + logger.clone().with_source("graphics".to_string()), logger_receiver, tx_channel_consumers.pop().unwrap(), rx_channel_consumers.pop().unwrap(), diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 662ec0c..684a226 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -264,9 +264,19 @@ impl PluginEngine { None } } - super::messages::PluginExternalRequest::Log { level, message } => { + super::messages::PluginExternalRequest::Log { + level, + message, + plugin_name, + id, + } => { if level as u32 <= plugin.log_level() as u32 { - let _ = private.logger.write(message, level); + let _ = private.logger.write_with_source_id( + message, + level, + plugin_name, + id, + ); } Some(PluginResponse::Log) @@ -398,7 +408,11 @@ impl PluginEngine { return Err(format!("Filepath \"{:?}\" doesn't exist!", filepath)); } - let mut plugin = Plugin::new(plugin_name.clone(), filepath, logger)?; + let mut plugin = Plugin::new( + plugin_name.clone(), + filepath, + logger.with_source((*plugin_name).clone()), + )?; plugin.spawn_method_call(gate, "on_load", (), false); plugin_list.insert(plugin_name.clone(), plugin); diff --git a/src/plugin/messages.rs b/src/plugin/messages.rs index c57f5a5..b59eb0e 100644 --- a/src/plugin/messages.rs +++ b/src/plugin/messages.rs @@ -20,11 +20,22 @@ pub enum PluginRequest { #[derive(Clone, Debug)] pub enum PluginExternalRequest { - Finish { fn_name: Arc }, + Finish { + fn_name: Arc, + }, SerialInfo, - SerialSend { message: Vec }, - SerialRecv { timeout: Instant }, - Log { level: LogLevel, message: String }, + SerialSend { + message: Vec, + }, + SerialRecv { + timeout: Instant, + }, + Log { + level: LogLevel, + message: String, + plugin_name: String, + id: String, + }, } #[derive(Debug)] @@ -65,10 +76,12 @@ pub enum PluginResponse { ShellExist { exist: bool }, } -impl<'lua> TryFrom> for PluginRequest { - type Error = String; - - fn try_from(value: Table<'lua>) -> Result { +impl PluginRequest { + pub fn from_table<'lua>( + value: Table<'lua>, + plugin_name: String, + id: String, + ) -> Result { let req_id: String = value .get(1) .map_err(|_| "Cannot get first table entry as String".to_string())?; @@ -82,6 +95,8 @@ impl<'lua> TryFrom> for PluginRequest { PluginRequest::External(PluginExternalRequest::Log { level: LogLevel::Debug, message, + plugin_name, + id, }) } ":log.info" => { @@ -92,6 +107,8 @@ impl<'lua> TryFrom> for PluginRequest { PluginRequest::External(PluginExternalRequest::Log { level: LogLevel::Info, message, + plugin_name, + id, }) } ":log.success" => { @@ -102,6 +119,8 @@ impl<'lua> TryFrom> for PluginRequest { PluginRequest::External(PluginExternalRequest::Log { level: LogLevel::Success, message, + plugin_name, + id, }) } ":log.warning" => { @@ -112,6 +131,8 @@ impl<'lua> TryFrom> for PluginRequest { PluginRequest::External(PluginExternalRequest::Log { level: LogLevel::Warning, message, + plugin_name, + id, }) } ":log.error" => { @@ -122,6 +143,8 @@ impl<'lua> TryFrom> for PluginRequest { PluginRequest::External(PluginExternalRequest::Log { level: LogLevel::Error, message, + plugin_name, + id, }) } ":serial.info" => PluginRequest::External(PluginExternalRequest::SerialInfo), diff --git a/src/plugin/method_call.rs b/src/plugin/method_call.rs index a32bd3d..2debf4c 100644 --- a/src/plugin/method_call.rs +++ b/src/plugin/method_call.rs @@ -122,9 +122,12 @@ impl PluginMethodCall { Err(err) => return Err(err.to_string()), }; - let plugin_req: PluginRequest = plugin_req - .try_into() - .map_err(|err: String| err.to_string())?; + let plugin_req: PluginRequest = PluginRequest::from_table( + plugin_req, + (*self.plugin_name).clone(), + (*self.fn_name).clone(), + ) + .map_err(|err: String| err.to_string())?; let rsp = match plugin_req { PluginRequest::Internal(internal_req) => { diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 3691c91..3512a3d 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -91,7 +91,7 @@ impl Plugin { self.lua.clone(), initial_args, gate, - self.logger.clone(), + self.logger.clone().with_id(fn_name.to_string()), has_unpack, ); @@ -147,7 +147,7 @@ mod tests { let _plugin = Plugin::new( Arc::new("echo".to_string()), PathBuf::from("plugins/echo.lua"), - Logger::new().0, + Logger::new("test".to_string()).0, ); } } From 7c84c6b17542c5fd1a7cc3d9f92b62e8c7f4bd51 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 18 Aug 2024 22:05:00 -0300 Subject: [PATCH 38/49] feat: add zed configs to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3e868e1..857db6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .idea/* +.zed/* From b17927cb4c81f5f634bac25cdf960225bfa714ec Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sun, 18 Aug 2024 22:05:50 -0300 Subject: [PATCH 39/49] fix: fix shell plugin return table --- plugins/shell.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/shell.lua b/plugins/shell.lua index 5332bac..2e3a93a 100644 --- a/plugins/shell.lua +++ b/plugins/shell.lua @@ -1,13 +1,13 @@ local M = {} function M.run(cmd) - local stdout, stderr = coroutine.yield({":shell.run", cmd}) - return stdout, stderr + local stdout, stderr = coroutine.yield({ ":shell.run", cmd }) + return stdout, stderr end function M.exist(program) - local res = coroutine.yield({":shell.exist", program}) - return res + local res = coroutine.yield({ ":shell.exist", program }) + return res end -return Shell +return M From d642ae8bd08468330029987fb2c3caf4b4f636b4 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Tue, 27 Aug 2024 15:07:16 -0300 Subject: [PATCH 40/49] feat: print error message if the typed user command doesn't exist on plugin Signed-off-by: Matheus T. dos Santos --- src/plugin/engine.rs | 5 +++++ src/plugin/plugin.rs | 18 +++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 684a226..22d4d21 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -184,6 +184,11 @@ impl PluginEngine { continue 'plugin_engine_loop; }; + if !plugin.is_user_command_valid(&command) { + error!(private.logger, "Plugin \"{}\" doesn't have \"{}\" command", plugin_name, command); + continue 'plugin_engine_loop; + } + plugin.spawn_method_call( engine_gate.new_method_call_gate(), &command, diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 3512a3d..29d61ba 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -1,12 +1,10 @@ -use mlua::{IntoLuaMulti, Lua, LuaOptions, Table}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use crate::infra::logger::Logger; -use crate::infra::LogLevel; - use super::bridge::PluginMethodCallGate; use super::method_call::PluginMethodCall; +use crate::infra::logger::Logger; +use crate::infra::LogLevel; +use mlua::{Function, IntoLuaMulti, Lua, LuaOptions, Table}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; pub struct Plugin { name: Arc, @@ -53,6 +51,12 @@ impl Plugin { }) } + pub fn is_user_command_valid(&self, user_command: &str) -> bool { + let table: Table = self.lua.globals().get("M").unwrap(); + + table.get::<_, Function>(user_command).is_ok() + } + pub fn log_level(&self) -> LogLevel { self.log_level } From 0c5faddb67eec8faff6c308172593387093b706b Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 28 Aug 2024 00:13:27 -0300 Subject: [PATCH 41/49] fix: fixes on_serial_connect and on_serial_disconnect second argument Signed-off-by: Matheus T. dos Santos --- src/plugin/engine.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 22d4d21..382c156 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -185,7 +185,10 @@ impl PluginEngine { }; if !plugin.is_user_command_valid(&command) { - error!(private.logger, "Plugin \"{}\" doesn't have \"{}\" command", plugin_name, command); + error!( + private.logger, + "Plugin \"{}\" doesn't have \"{}\" command", plugin_name, command + ); continue 'plugin_engine_loop; } @@ -201,8 +204,8 @@ impl PluginEngine { plugin.spawn_method_call( engine_gate.new_method_call_gate(), "on_serial_connect", - (port.clone(), baudrate), - false, + [port.clone(), baudrate.to_string()], + true, ); } } @@ -211,8 +214,8 @@ impl PluginEngine { plugin.spawn_method_call( engine_gate.new_method_call_gate(), "on_serial_disconnect", - (port.clone(), baudrate), - false, + [port.clone(), baudrate.to_string()], + true, ); } } From a60a76608724ecf13ffceeb83f3831393cd5e76a Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Wed, 28 Aug 2024 22:32:27 -0300 Subject: [PATCH 42/49] feat: add serial at start of connect, disconnect and setup flow control commands. The old version of these commands are still available. Signed-off-by: Matheus T. dos Santos --- src/inputs/inputs_task.rs | 154 +++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 61 deletions(-) diff --git a/src/inputs/inputs_task.rs b/src/inputs/inputs_task.rs index f8dc379..e28abef 100644 --- a/src/inputs/inputs_task.rs +++ b/src/inputs/inputs_task.rs @@ -296,6 +296,79 @@ impl InputsTask { LoopStatus::Continue } + fn handle_connect_command(command_line_split: Vec, private: &InputsConnections) { + fn mount_setup(option: &str, setup: Option) -> SerialSetup { + if option.chars().all(|x| x.is_digit(10)) { + SerialSetup { + baudrate: Some(u32::from_str_radix(option, 10).unwrap()), + ..setup.unwrap_or(SerialSetup::default()) + } + } else { + SerialSetup { + port: Some(option.to_string()), + ..setup.unwrap_or(SerialSetup::default()) + } + } + } + + match command_line_split.len() { + x if x < 2 => { + let _ = private.serial_if_cmd_sender.send(SerialCommand::Connect); + } + 2 => { + let setup = SerialCommand::Setup(mount_setup(&command_line_split[1], None)); + let _ = private.serial_if_cmd_sender.send(setup); + } + _ => { + let setup = mount_setup(&command_line_split[1], None); + let setup = mount_setup(&command_line_split[2], Some(setup)); + + let _ = private + .serial_if_cmd_sender + .send(SerialCommand::Setup(setup)); + } + } + } + + fn handle_flow_command(command_line_split: Vec, private: &InputsConnections) { + if command_line_split.len() < 2 { + error!( + private.logger, + "Insufficient arguments for \"!flow\" command" + ); + return; + } + + let flow_control = match command_line_split[1].as_str() { + "none" => FlowControl::None, + "sw" => FlowControl::Software, + "hw" => FlowControl::Hardware, + _ => { + error!( + private.logger, + "Invalid flow control. Please, chose one of these options: none, sw, hw" + ); + return; + } + }; + + let res = private + .serial_if_cmd_sender + .send(SerialCommand::Setup(SerialSetup { + flow_control: Some(flow_control), + ..SerialSetup::default() + })); + + match res { + Ok(_) => success!( + private.logger, + "Flow control setted to \"{}\"", + command_line_split[1] + ), + Err(err) => error!(private.logger, "Cannot set flow control: {}", err), + } + } + fn handle_user_command(command_line_split: Vec, private: &InputsConnections) { let Some(cmd_name) = command_line_split.get(0) else { private.tx.produce(Arc::new(TimedBytes { @@ -306,79 +379,38 @@ impl InputsTask { }; match cmd_name.as_str() { - "connect" => { - fn mount_setup(option: &str, setup: Option) -> SerialSetup { - if option.chars().all(|x| x.is_digit(10)) { - SerialSetup { - baudrate: Some(u32::from_str_radix(option, 10).unwrap()), - ..setup.unwrap_or(SerialSetup::default()) - } - } else { - SerialSetup { - port: Some(option.to_string()), - ..setup.unwrap_or(SerialSetup::default()) - } - } + "serial" => { + if command_line_split.len() < 2 { + error!( + private.logger, + "Please, use \"connect\" or \"disconnect\" subcommands" + ); + return; } - match command_line_split.len() { - x if x < 2 => { - let _ = private.serial_if_cmd_sender.send(SerialCommand::Connect); + match command_line_split.get(1).unwrap().as_str() { + "connect" => { + Self::handle_connect_command(command_line_split[1..].to_vec(), private); } - 2 => { - let setup = SerialCommand::Setup(mount_setup(&command_line_split[1], None)); - let _ = private.serial_if_cmd_sender.send(setup); + "disconnect" => { + let _ = private.serial_if_cmd_sender.send(SerialCommand::Disconnect); + } + "flow" => { + Self::handle_flow_command(command_line_split[1..].to_vec(), private); } _ => { - let setup = mount_setup(&command_line_split[1], None); - let setup = mount_setup(&command_line_split[2], Some(setup)); - - let _ = private - .serial_if_cmd_sender - .send(SerialCommand::Setup(setup)); + error!(private.logger, "Invalid subcommand for serial"); } } } + "connect" => { + Self::handle_connect_command(command_line_split, private); + } "disconnect" => { let _ = private.serial_if_cmd_sender.send(SerialCommand::Disconnect); } "flow" => { - if command_line_split.len() < 2 { - error!( - private.logger, - "Insufficient arguments for \"!flow\" command" - ); - return; - } - - let flow_control = match command_line_split[1].as_str() { - "none" => FlowControl::None, - "sw" => FlowControl::Software, - "hw" => FlowControl::Hardware, - _ => { - error!( - private.logger, - "Invalid flow control. Please, chose one of these options: none, sw, hw" - ); - return; - } - }; - - let res = private - .serial_if_cmd_sender - .send(SerialCommand::Setup(SerialSetup { - flow_control: Some(flow_control), - ..SerialSetup::default() - })); - - match res { - Ok(_) => success!( - private.logger, - "Flow control setted to \"{}\"", - command_line_split[1] - ), - Err(err) => error!(private.logger, "Cannot set flow control: {}", err), - } + Self::handle_flow_command(command_line_split, private); } "log" => { if command_line_split.len() < 3 { From 40588e17ec990f2204330a23c627a3509356c156 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Thu, 29 Aug 2024 19:53:44 -0300 Subject: [PATCH 43/49] feat: remove rust mentions from plugin's lua code Signed-off-by: Matheus T. dos Santos --- src/plugin/engine.rs | 8 ++++++-- src/plugin/thread.lua | 8 +++++++- src/plugin/unpack.lua | 8 +++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index 382c156..a434842 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -15,6 +15,7 @@ use crate::{ success, warning, }; use chrono::Local; +use regex::Regex; use std::{ collections::HashMap, os::unix::ffi::OsStrExt, @@ -100,6 +101,7 @@ impl PluginEngine { let mut plugin_list: HashMap, Plugin> = HashMap::new(); let mut engine_gate = PluginEngineGate::new(32); let mut serial_recv_reqs = vec![]; + let err_regex = Regex::new(r#".*: \[string ".*"]:"#).unwrap(); 'plugin_engine_loop: loop { if let Ok(cmd) = cmd_receiver.try_recv() { @@ -157,7 +159,7 @@ impl PluginEngine { .await { Ok(_) => success!(private.logger, "Plugin \"{}\" loaded", plugin_name), - Err(err) => error!(private.logger, "{}", err), + Err(err) => error!(private.logger, "{}", err_regex.replace(&err, "")), } } PluginEngineCommand::UnloadPlugin { plugin_name } => { @@ -306,7 +308,9 @@ impl PluginEngine { "Plugin \"{}\" reloaded", plugin_name ), - Err(err) => error!(private.logger, "{}", err), + Err(err) => { + error!(private.logger, "{}", err_regex.replace(&err, "")); + } } } else { warning!(private.logger, "Plugin \"{}\" unloaded", plugin_name); diff --git a/src/plugin/thread.lua b/src/plugin/thread.lua index 8acd44c..58356fa 100644 --- a/src/plugin/thread.lua +++ b/src/plugin/thread.lua @@ -1,3 +1,9 @@ coroutine.create(function(t) - return M.{}(t) + local err = require("scope").log.error + local status, res = pcall(M.{}, t) + if not status then + err(res:match('%[string ".+"%]:(%d+: .+)')) + else + return res + end end) diff --git a/src/plugin/unpack.lua b/src/plugin/unpack.lua index 5731447..83b053e 100644 --- a/src/plugin/unpack.lua +++ b/src/plugin/unpack.lua @@ -1,3 +1,9 @@ coroutine.create(function(t) - return M.{}(table.unpack(t)) + local err = require("scope").log.error + local status, res = pcall(M.{}, table.unpack(t)) + if not status then + err(res:match('%[string ".+"%]:(%d+: .+)')) + else + return res + end end) From 4685089efba2f2641492f768327768bdd028ebd3 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Thu, 29 Aug 2024 20:57:19 -0300 Subject: [PATCH 44/49] feat: add helper functions to parser arguments from user command functions Signed-off-by: Matheus T. dos Santos --- plugins/scope.lua | 146 ++++++++++++++++++++++++++++-------------- plugins/test.lua | 65 ++++++++++--------- src/plugin/thread.lua | 2 +- src/plugin/unpack.lua | 2 +- 4 files changed, 134 insertions(+), 81 deletions(-) diff --git a/plugins/scope.lua b/plugins/scope.lua index 98b74a6..9f4fb6e 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -1,99 +1,147 @@ local M = { - fmt = {}, - log = {}, - serial = {}, - ble = {}, - sys = {}, - re = {}, + fmt = {}, + log = {}, + serial = {}, + ble = {}, + sys = {}, + re = {}, } function M.fmt.to_str(val) - if type(val) == "table" then - local bytearr = {} - for _, v in ipairs(val) do - local utf8byte = v < 0 and (0xff + v + 1) or v - table.insert(bytearr, string.char(utf8byte)) + if type(val) == "table" then + local bytearr = {} + for _, v in ipairs(val) do + local utf8byte = v < 0 and (0xff + v + 1) or v + table.insert(bytearr, string.char(utf8byte)) + end + return table.concat(bytearr) + elseif type(val) == "string" then + return val + elseif type(val) == "nil" then + return "nil" + else + return tostring(val) end - return table.concat(bytearr) - elseif type(val) == "string" then - return val - elseif type(val) == "nil" then - return "nil" - else - return tostring(val) - end end function M.fmt.to_bytes(val) - if type(val) == "string" then - return { string.byte(val, 1, -1) } - elseif type(val) == "table" then - return val - else - return {} - end + if type(val) == "string" then + return { string.byte(val, 1, -1) } + elseif type(val) == "table" then + return val + else + return {} + end end function M.log.debug(msg) - coroutine.yield({":log.debug", msg}) + coroutine.yield({ ":log.debug", msg }) end function M.log.info(msg) - coroutine.yield({":log.info", msg}) + coroutine.yield({ ":log.info", msg }) end function M.log.success(msg) - coroutine.yield({":log.success", msg}) + coroutine.yield({ ":log.success", msg }) end function M.log.warning(msg) - coroutine.yield({":log.warning", msg}) + coroutine.yield({ ":log.warning", msg }) end function M.log.error(msg) - coroutine.yield({":log.error", msg}) + coroutine.yield({ ":log.error", msg }) end function M.serial.info() - local port, baud_rate = table.unpack(coroutine.yield({":serial.info"})) - return port, baud_rate + local port, baud_rate = table.unpack(coroutine.yield({ ":serial.info" })) + return port, baud_rate end function M.serial.send(msg) - coroutine.yield({":serial.send", msg}) + coroutine.yield({ ":serial.send", msg }) end function M.serial.recv(opts) - local err, msg = table.unpack(coroutine.yield({":serial.recv", opts})) - return err, msg + local err, msg = table.unpack(coroutine.yield({ ":serial.recv", opts })) + return err, msg end function M.sys.os_name() - if os.getenv("OS") == "Windows_NT" then - return "windows" - else - return "unix" - end + if os.getenv("OS") == "Windows_NT" then + return "windows" + else + return "unix" + end end function M.sys.sleep_ms(time) - coroutine.yield({":sys.sleep", time}) + coroutine.yield({ ":sys.sleep", time }) +end + +local function ord(idx) + local rem = idx % 10 + if rem == 1 then + return tostring(idx) .. "st" + elseif rem == 2 then + return tostring(idx) .. "nd" + elseif rem == 3 then + return tostring(idx) .. "rd" + else + return tostring(idx) .. "th" + end +end + +local function parse_arg(idx, arg, ty, validate, default) + assert(not (ty == "nil" or ty == "function" or ty == "userdata" or ty == "thread" or ty == "table"), "Argument must not be " .. ty) + + if not arg then + if default then + return default + else + error(ord(idx) .. " argument must not be empty") + end + end + + if type(arg) ~= ty then + if ty == "number" then + arg = tonumber(arg) + assert(arg, ord(idx) .. " argument must be a number") + elseif ty == "boolean" then + arg = arg ~= "0" and arg ~= "false" + end + end + + if validate then + assert(validate(arg), ord(idx) .. " argument is invalid") + end + + return arg +end + +function M.sys.parse_args(args) + res = {} + for i, v in ipairs(args) do + table.insert(res, parse_arg(i, v.arg, v.ty, v.validate, v.default)) + end + return table.unpack(res) end function M.re.literal(str) - return coroutine.yield({":re.literal", str}) + return coroutine.yield({ ":re.literal", str }) end function M.re.matches(str, pattern_table) - local fn_name = coroutine.yield({":re.matches", str, pattern_table}) - if fn_name ~= nil then - local fn = pattern_table[fn_name] - fn(str) - end + local fn_name = coroutine.yield({ ":re.matches", str, pattern_table }) + if fn_name ~= nil then + local fn = pattern_table[fn_name] + fn(str) + end end function M.re.match(str, pattern) - return coroutine.yield({":re.match", str, pattern}) + return coroutine.yield({ ":re.match", str, pattern }) end return M diff --git a/plugins/test.lua b/plugins/test.lua index 8acf64b..77fb069 100644 --- a/plugins/test.lua +++ b/plugins/test.lua @@ -4,61 +4,66 @@ local serial = require("scope").serial local sys = require("scope").sys local M = { - is_loaded = false + is_loaded = false } function M.on_load() - log.info("Running on " .. sys.os_name()) - sys.sleep_ms(3000) - log.success("Plugin is ready now!") - M.is_loaded = true + log.info("Running on " .. sys.os_name()) + sys.sleep_ms(3000) + log.success("Plugin is ready now!") + M.is_loaded = true end function M.on_unload() - log.warning("Cleaning up resources...") - sys.sleep_ms(2000) - log.success("Resources clean") - M.is_loaded = false + log.warning("Cleaning up resources...") + sys.sleep_ms(2000) + log.success("Resources clean") + M.is_loaded = false end function M.on_serial_send(msg) - if not M.is_loaded then - log.error("Plugin not loaded yet") - return - end + if not M.is_loaded then + log.error("Plugin not loaded yet") + return + end - log.info("Sending " .. fmt.to_str(msg) .. " ...") - serial.send(fmt.to_bytes("AT\r\n")) - local _, data = serial.recv({timeout_ms=2000}) - log.info("Receive sent message: " .. fmt.to_str(data)) - local _, data = serial.recv({timeout_ms=2000}) - log.info("Receive AT: " .. fmt.to_str(data)) + log.info("Sending " .. fmt.to_str(msg) .. " ...") + serial.send(fmt.to_bytes("AT\r\n")) + local _, data = serial.recv({ timeout_ms = 2000 }) + log.info("Receive sent message: " .. fmt.to_str(data)) + local _, data = serial.recv({ timeout_ms = 2000 }) + log.info("Receive AT: " .. fmt.to_str(data)) end function M.on_serial_recv(msg) - log.info("Receive pkt: " .. fmt.to_str(msg)) + log.info("Receive pkt: " .. fmt.to_str(msg)) end function M.on_serial_connect(port, baudrate) - log.success("Connected to " .. port .. "@" .. fmt.to_str(baudrate)) + log.success("Connected to " .. port .. "@" .. fmt.to_str(baudrate)) end function M.on_serial_disconnect(port, baudrate) - log.warning("Disconnected from " .. port .. "@" .. fmt.to_str(baudrate)) + log.warning("Disconnected from " .. port .. "@" .. fmt.to_str(baudrate)) end function M.hello(name, age) - name = name or "???" - age = age or "???" - log.info("Hello, " .. name .. ". Do you have " .. age .. " years?") + name, age = sys.parse_args({ + { arg = name, ty = "string" }, + { arg = age, ty = "number", default = 0, validate = function(arg) + return arg >= 1 + end } + }) + + log.info("Hello, " .. name .. ". Do you have " .. tostring(age) .. " years?") end function M.logs() - log.debug("debug log level") - log.info("info log level") - log.success("success log level") - log.warning("warning log level") - log.error("error log level") + log.debug("debug log level") + log.info("info log level") + log.success("success log level") + log.warning("warning log level") + log.error("error log level") end return M diff --git a/src/plugin/thread.lua b/src/plugin/thread.lua index 58356fa..9846edb 100644 --- a/src/plugin/thread.lua +++ b/src/plugin/thread.lua @@ -2,7 +2,7 @@ coroutine.create(function(t) local err = require("scope").log.error local status, res = pcall(M.{}, t) if not status then - err(res:match('%[string ".+"%]:(%d+: .+)')) + err(res:match('%[string ".+"%]:(%d+: .+)') or res) else return res end diff --git a/src/plugin/unpack.lua b/src/plugin/unpack.lua index 83b053e..eefd112 100644 --- a/src/plugin/unpack.lua +++ b/src/plugin/unpack.lua @@ -2,7 +2,7 @@ coroutine.create(function(t) local err = require("scope").log.error local status, res = pcall(M.{}, table.unpack(t)) if not status then - err(res:match('%[string ".+"%]:(%d+: .+)')) + err(res:match('%[string ".+"%]:(%d+: .+)') or res) else return res end From d37b12355f7e7d240e28e840cbc09f7dc09b756d Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 30 Aug 2024 20:40:37 -0300 Subject: [PATCH 45/49] fix: fix bugs on plugin's regex API Signed-off-by: Matheus T. dos Santos --- plugins/scope.lua | 18 ++++++++++++++---- plugins/test.lua | 28 ++++++++++++++++++++++++++++ src/plugin/messages.rs | 9 ++++----- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/plugins/scope.lua b/plugins/scope.lua index 9f4fb6e..9bab3eb 100644 --- a/plugins/scope.lua +++ b/plugins/scope.lua @@ -129,11 +129,21 @@ function M.sys.parse_args(args) end function M.re.literal(str) - return coroutine.yield({ ":re.literal", str }) + return table.unpack(coroutine.yield({ ":re.literal", str })) end -function M.re.matches(str, pattern_table) - local fn_name = coroutine.yield({ ":re.matches", str, pattern_table }) +function M.re.matches(str, ...) + local args = table.pack(...) + assert(args.n % 2 == 0, "Each function need to have a name") + + local pattern_list = {} + local pattern_table = {} + for i = 1, args.n, 2 do + table.insert(pattern_list, { args[i], args[i + 1] }) + pattern_table[args[i]] = args[i + 1] + end + + local fn_name = table.unpack(coroutine.yield({ ":re.matches", str, pattern_list })) if fn_name ~= nil then local fn = pattern_table[fn_name] fn(str) @@ -141,7 +151,7 @@ function M.re.matches(str, pattern_table) end function M.re.match(str, pattern) - return coroutine.yield({ ":re.match", str, pattern }) + return table.unpack(coroutine.yield({ ":re.match", str, pattern })) end return M diff --git a/plugins/test.lua b/plugins/test.lua index 77fb069..2b312ab 100644 --- a/plugins/test.lua +++ b/plugins/test.lua @@ -2,6 +2,7 @@ local log = require("scope").log local fmt = require("scope").fmt local serial = require("scope").serial local sys = require("scope").sys +local re = require("scope").re local M = { is_loaded = false @@ -58,6 +59,33 @@ function M.hello(name, age) log.info("Hello, " .. name .. ". Do you have " .. tostring(age) .. " years?") end +function M.regex(str, pattern) + if re.match(str, pattern) then + log.success(str .. " matches with " .. pattern) + else + log.error(str .. " doesn't match with " .. pattern) + end +end + +function M.cases(arg) + assert(arg, "cases must have at least one argument") + + re.matches(arg, + re.literal(".0"), function(msg) + log.info("Got a fake decimal: " .. msg) + end, + "\\d+", function(msg) + log.info(msg .. " is a number") + end, + "\\w+", function(msg) + log.info(msg .. " is a word") + end, + ".*", function(msg) + log.warning(msg .. " is unknown for me") + end + ) +end + function M.logs() log.debug("debug log level") log.info("info log level") diff --git a/src/plugin/messages.rs b/src/plugin/messages.rs index b59eb0e..8b01b72 100644 --- a/src/plugin/messages.rs +++ b/src/plugin/messages.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Instant}; -use mlua::{Table, Value}; +use mlua::Table; use std::time::Duration; use crate::infra::LogLevel; @@ -204,10 +204,9 @@ impl PluginRequest { .get(3) .map_err(|_| "Cannot get third table entry as String".to_string())?; let pattern_table = pattern_table - .pairs() - .into_iter() + .sequence_values::>() .filter_map(|res| res.ok()) - .map(|(k, _v): (String, Value<'_>)| k) + .filter_map(|t| t.get::<_, String>(1).ok()) .collect(); PluginRequest::Internal(PluginInternalRequest::ReMatches { @@ -215,7 +214,7 @@ impl PluginRequest { pattern_table, }) } - "re.match" => { + ":re.match" => { let string: String = value .get(2) .map_err(|_| "Cannot get second table entry as String".to_string())?; From df56f309192e6e91fd1a41b6a331fa1630854b75 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 30 Aug 2024 20:49:35 -0300 Subject: [PATCH 46/49] ci: remove commit check CI's action Signed-off-by: Matheus T. dos Santos --- .github/commit_checks.py | 76 ----------------------------- .github/workflows/commit-checks.yml | 20 -------- 2 files changed, 96 deletions(-) delete mode 100644 .github/commit_checks.py delete mode 100644 .github/workflows/commit-checks.yml diff --git a/.github/commit_checks.py b/.github/commit_checks.py deleted file mode 100644 index 5e89dae..0000000 --- a/.github/commit_checks.py +++ /dev/null @@ -1,76 +0,0 @@ -""" - :file: commit_checks.py - :author: Paulo Santos (pauloroberto.santos@edge.ufal.br) - :brief: Realiza a verificação do histórico de commits. - :version: 0.1 - :date: 29-04-2024 - - :copyright: Copyright (c) 2024 -""" - -import argparse -import subprocess -import sys - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("-s", "--srcb", metavar="sbranch", help="Source branch to compare.") - parser.add_argument("-t", "--tgtb", metavar="tbranch", help="Target branch to compare.", - default="origin/main") - - source_branch = str(parser.parse_args().srcb) - target_branch = str(parser.parse_args().tgtb) - - if source_branch == "None": - print("\nSource branch is empty.") - return -1 - - invocation = f"git rev-list --count --left-right {target_branch}...{source_branch}" - - try: - out = subprocess.check_output(invocation, shell=True) - behind, ahead = [int(dist) for dist in out.decode("utf-8").rsplit("\t", maxsplit=1)] - except Exception as e: - print(f"\nError to check the history: {e}") - return -1 - - if behind > 0: - print(f"\nThere are {behind} commits to be included in PR using rebase to main.\n") - - return 1 - - if ahead > COMMIT_LIMIT: - print(f"\nThere are {ahead} commits to be added, divide PR to have less than {COMMIT_LIMIT + 1} commits.") - - return 1 - - print(f"\nHistory is updated. ({ahead} commits ahead)") - - invocation = f"git describe --all --first-parent {source_branch}~1 --abbrev=0" - - try: - out = subprocess.check_output(invocation, shell=True) - parent_head = out.decode("utf-8").split("/")[-1].strip() - except Exception as e: - print(f"\nError to check the history: {e}") - - return -1 - - if parent_head != "main": - print( - f"\nThe branch has its origin on `{parent_head}`, resolve `{parent_head}` first. (the branch must have " - f"origin on main)") - - return 1 - - print("\nThe branch has its origin on main.") - - return 0 - - -if __name__ == "__main__": - COMMIT_LIMIT = 15 - check_status = main() - - sys.exit(check_status) diff --git a/.github/workflows/commit-checks.yml b/.github/workflows/commit-checks.yml deleted file mode 100644 index 63345e0..0000000 --- a/.github/workflows/commit-checks.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: commit-checks - -on: - pull_request: - branches: [ "main" ] - -jobs: - commit-check: - name: Check the branch base commit - runs-on: ubuntu-latest - steps: - - name: Checkout para a branch a ser testada. - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - - name: Verifica os commits temporal e posicionalmente. - run: | - python .github/commit_checks.py -s origin/"${{github.head_ref}}" From cdc26c143ddc76008728662fa5b55b1daeb22d3a Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Fri, 30 Aug 2024 20:53:00 -0300 Subject: [PATCH 47/49] ci: remove nightly from CI Signed-off-by: Matheus T. dos Santos --- .github/workflows/build.yml | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59412fc..3a41d1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,45 +13,27 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - components: clippy - override: true - - name: Install libudev-sys run: | sudo apt-get install -y libudev-dev - - name: Build run: | - cargo +nightly build --release --verbose + cargo build --release --verbose build-windows: name: Check on Windows runs-on: windows-latest steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - components: clippy - override: true - name: Build run: | - cargo +nightly build --release --verbose + cargo build --release --verbose build-macos: name: Check on MacOS runs-on: macos-latest steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - components: clippy - override: true - - name: Build run: | - cargo +nightly build --release --verbose + cargo build --release --verbose From 2b731f9f40c94752f8d5671abcbb22371093ada0 Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 31 Aug 2024 11:53:55 -0300 Subject: [PATCH 48/49] fix: fix windows compiling bugs Signed-off-by: Matheus T. dos Santos --- src/plugin/engine.rs | 3 +-- src/serial/serial_if.rs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/plugin/engine.rs b/src/plugin/engine.rs index a434842..8548dab 100644 --- a/src/plugin/engine.rs +++ b/src/plugin/engine.rs @@ -18,7 +18,6 @@ use chrono::Local; use regex::Regex; use std::{ collections::HashMap, - os::unix::ffi::OsStrExt, path::PathBuf, str::FromStr, sync::{Arc, RwLock}, @@ -409,7 +408,7 @@ impl PluginEngine { logger: Logger, ) -> Result<(), String> { let filepath = match filepath.extension() { - Some(extension) if extension.as_bytes() != b"lua" => { + Some(extension) if extension.as_encoded_bytes() != b"lua" => { return Err(format!("Invalid plugin extension: {:?}", extension)); } Some(_extension) => filepath, diff --git a/src/serial/serial_if.rs b/src/serial/serial_if.rs index 268c7d7..1057fcf 100644 --- a/src/serial/serial_if.rs +++ b/src/serial/serial_if.rs @@ -10,7 +10,7 @@ use crate::{ success, warning, }; use chrono::Local; -use serialport::{DataBits, FlowControl, Parity, StopBits, TTYPort}; +use serialport::{DataBits, FlowControl, Parity, StopBits}; use std::{ io::{self, Read, Write}, sync::{ @@ -23,9 +23,9 @@ use std::{ pub type SerialInterface = Task; #[cfg(any(target_os = "linux", target_os = "macos"))] -type SerialPort = TTYPort; +type SerialPort = serialport::TTYPort; #[cfg(target_os = "windows")] -type SerialPort = COMPort; +type SerialPort =serialport::COMPort; pub struct SerialShared { pub port: String, From 75f229395f93497642af07dfda22ac8b4de5b69e Mon Sep 17 00:00:00 2001 From: "Matheus T. dos Santos" Date: Sat, 31 Aug 2024 11:55:55 -0300 Subject: [PATCH 49/49] style: fix style on serial_if.rs Signed-off-by: Matheus T. dos Santos --- src/serial/serial_if.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serial/serial_if.rs b/src/serial/serial_if.rs index 1057fcf..8d7c65e 100644 --- a/src/serial/serial_if.rs +++ b/src/serial/serial_if.rs @@ -25,7 +25,7 @@ pub type SerialInterface = Task; #[cfg(any(target_os = "linux", target_os = "macos"))] type SerialPort = serialport::TTYPort; #[cfg(target_os = "windows")] -type SerialPort =serialport::COMPort; +type SerialPort = serialport::COMPort; pub struct SerialShared { pub port: String,