mirror of https://github.com/ArduPilot/ardupilot
AP_Scripting: add REPL applet
Implements a reasonably comfortable REPL accessible over serial entirely as a loadable script. Also accessible over MAVLink using QGroundControl's MAVLink Console and the new mavport module.
This commit is contained in:
parent
87d2b017aa
commit
a164abaafb
|
@ -0,0 +1,358 @@
|
|||
-- Interactive REPL (read-evaluate-print-loop) for the Lua scripting engine
|
||||
-- accessible over serial, with line editing, history, and output formatting.
|
||||
|
||||
-- 0-based index of Scripting protocol port to use, or nil to use MAVLink
|
||||
local PORT_IDX = 0
|
||||
local MAX_HISTORY = 50 -- number of lines of history to keep (must be >= 1)
|
||||
local VERSION = "v1.0" -- version is convenience for the user
|
||||
|
||||
local port
|
||||
if PORT_IDX == nil then
|
||||
port = require("mavport")
|
||||
else
|
||||
port = serial:find_serial(PORT_IDX)
|
||||
end
|
||||
assert(port, "REPL scripting port not configured")
|
||||
|
||||
-- scan through parameters to find our port and grab its baud rate
|
||||
do
|
||||
local serial_info = ""
|
||||
local baud = 115200
|
||||
if PORT_IDX ~= nil then
|
||||
local port_num = 0
|
||||
while PORT_IDX >= 0 and port_num <= 9 do
|
||||
local protocol = param:get(("SERIAL%d_PROTOCOL"):format(port_num))
|
||||
port_num = port_num + 1
|
||||
if protocol == 28 then PORT_IDX = PORT_IDX - 1 end
|
||||
end
|
||||
if PORT_IDX == -1 then -- correct port index found
|
||||
port_num = port_num - 1
|
||||
baud = param:get(("SERIAL%d_BAUD"):format(port_num)) or 115200
|
||||
serial_info = (" on SERIAL%d, BAUD=%d"):format(port_num, baud)
|
||||
end
|
||||
end
|
||||
-- if we can't find the right port, the baud probably does not matter
|
||||
-- (e.g. CAN or network port)
|
||||
port:begin(baud)
|
||||
|
||||
gcs:send_text(6, "Lua REPL "..VERSION.." starting"..serial_info)
|
||||
end
|
||||
|
||||
-- grab things we use from the environment in case the user messes them up
|
||||
local string = string
|
||||
local table = table
|
||||
|
||||
-- declaration of main state variable and functions
|
||||
local state_func, state_read, state_eval, state_print
|
||||
|
||||
-- write the string s to the port, buffering if not all could be written.
|
||||
-- buffer starts of with the first message and prompt
|
||||
local tx_buf = {"\r\n\r\nLua REPL "..VERSION.." started.\r\n> "}
|
||||
local writestring
|
||||
if port.writestring then -- use more efficient method if we have it
|
||||
writestring = function(s)
|
||||
if tx_buf then -- stuff already in the buffer?
|
||||
tx_buf[#tx_buf+1] = s -- this needs to go after
|
||||
else
|
||||
local written = port:writestring(s)
|
||||
if written < #s then
|
||||
-- write was short i.e. port buffer is full. buffer the rest of the
|
||||
-- string ourselves and transmit it later
|
||||
tx_buf = { s:sub(written+1) }
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
writestring = function(s)
|
||||
if tx_buf then -- stuff already in the buffer?
|
||||
tx_buf[#tx_buf+1] = s -- this needs to go after
|
||||
else
|
||||
for ci = 1, #s do
|
||||
if port:write(s:byte(ci)) == 0 then
|
||||
-- write failed i.e. port buffer is full. we now buffer the rest of
|
||||
-- the string ourselves and transmit it later
|
||||
tx_buf = { s:sub(ci) }
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- don't use print substitute in the REPL's code (e.g. for debugging the REPL)
|
||||
local print = print -- luacheck: ignore 211 (unused variable warning)
|
||||
|
||||
-- substitute print function for within the REPL that prints to the port
|
||||
function _ENV.print(...)
|
||||
local t = table.pack(...)
|
||||
for i = 1, t.n do
|
||||
writestring(tostring(t[i]))
|
||||
writestring((i ~= t.n) and "\t" or "\r\n")
|
||||
end
|
||||
end
|
||||
|
||||
-- write the character c to the port, buffering if failed
|
||||
local function writechar(c)
|
||||
-- buffer character if stuff already in buffer or write fails
|
||||
if tx_buf or port:write(c) == 0 then
|
||||
tx_buf[#tx_buf+1] = string.char(c) -- massive overhead...
|
||||
end
|
||||
end
|
||||
|
||||
local function writeobj(o)
|
||||
if type(o) == "table" then
|
||||
writestring("{ ")
|
||||
for k, v in pairs(o) do
|
||||
if type(k) ~= "number" then k = '"'..k..'"' end
|
||||
writestring("["..k.."] = ")
|
||||
writeobj(v)
|
||||
writestring(", ")
|
||||
end
|
||||
writestring("}")
|
||||
else
|
||||
writestring(tostring(o))
|
||||
end
|
||||
end
|
||||
|
||||
local curr_line = nil -- table of line bytes, or nil if viewing history
|
||||
local curr_pos = 1 -- position the next character will be put at
|
||||
local curr_esc = nil -- table of escape sequence bytes
|
||||
|
||||
local eval_pieces = {} -- pieces of code being evaluated
|
||||
|
||||
local history_lines = {""} -- lines in the history (one is always being edited)
|
||||
local history_pos = 1 -- position in the history being edited
|
||||
|
||||
local function writeprompt()
|
||||
writestring((#eval_pieces > 0) and ">> " or "> ")
|
||||
end
|
||||
|
||||
local function movehistory(dir)
|
||||
if curr_line then -- current line was edited, store it in history
|
||||
history_lines[history_pos] = string.char(table.unpack(curr_line))
|
||||
curr_line = nil
|
||||
end
|
||||
|
||||
history_pos = history_pos + dir -- move to new position
|
||||
|
||||
writestring("\x1B[2K\r") -- erase line and return cursor to start
|
||||
writeprompt() -- draw prompt
|
||||
local line = history_lines[history_pos] -- and current line from history
|
||||
writestring(line)
|
||||
curr_pos = #line + 1
|
||||
end
|
||||
|
||||
local function readesc(c)
|
||||
assert(curr_esc) -- only called if curr_esc isn't nil
|
||||
|
||||
if c == 27 then -- another escape, clear line and buffer and exit escape mode
|
||||
curr_line = nil
|
||||
curr_pos = 1
|
||||
curr_esc = nil
|
||||
eval_pieces = {}
|
||||
history_pos = #history_lines
|
||||
history_lines[history_pos] = ""
|
||||
writestring("\r\n")
|
||||
writeprompt()
|
||||
return
|
||||
else
|
||||
curr_esc[#curr_esc+1] = c
|
||||
end
|
||||
|
||||
if curr_esc[1] ~= 91 then -- not a [, exit escape mode
|
||||
curr_esc = nil
|
||||
return
|
||||
end
|
||||
|
||||
if #curr_esc < 2 then return end -- command character not yet present
|
||||
|
||||
local line_len = #history_lines[history_pos]
|
||||
if curr_line then line_len = #curr_line end
|
||||
|
||||
-- c is now the command character
|
||||
if c == 65 then -- up
|
||||
if history_pos > 1 then
|
||||
movehistory(-1)
|
||||
end
|
||||
elseif c == 66 then -- down
|
||||
if history_pos < #history_lines then
|
||||
movehistory(1)
|
||||
end
|
||||
elseif c == 67 then -- right
|
||||
if curr_pos < line_len + 1 then
|
||||
writestring("\x1B[C")
|
||||
curr_pos = curr_pos + 1
|
||||
end
|
||||
elseif c == 68 then -- left
|
||||
if curr_pos > 1 then
|
||||
writestring("\x1B[D")
|
||||
curr_pos = curr_pos - 1
|
||||
end
|
||||
elseif c == 72 then -- home
|
||||
if curr_pos > 1 then
|
||||
writestring(("\x1B[%dD"):format(curr_pos-1))
|
||||
curr_pos = 1
|
||||
end
|
||||
elseif c == 70 then -- end
|
||||
if curr_pos < line_len + 1 then
|
||||
writestring(("\x1B[%dC"):format(line_len-curr_pos+1))
|
||||
curr_pos = line_len + 1
|
||||
end
|
||||
end
|
||||
|
||||
curr_esc = nil -- exit escape mode, handling complete
|
||||
end
|
||||
|
||||
local last_c = 0
|
||||
state_read = function ()
|
||||
while true do
|
||||
local c = port:read()
|
||||
if c == -1 then return end -- no new character, give time for more to come
|
||||
|
||||
if curr_esc then -- in escape sequence
|
||||
readesc(c)
|
||||
elseif c == 27 then -- escape, start of a control sequence
|
||||
curr_esc = {} -- engage escape sequence handler
|
||||
elseif c == 13 or c == 10 then -- line complete
|
||||
if last_c ~= 13 or c ~= 10 then -- ignore \n after \r
|
||||
writestring("\r\n")
|
||||
last_c = c
|
||||
break
|
||||
end
|
||||
elseif c == 8 or c == 127 then -- backspace
|
||||
if curr_pos > 1 then -- a character to delete?
|
||||
if curr_line == nil then -- retrieve line for editing
|
||||
curr_line = table.pack(history_lines[history_pos]:byte(1, -1))
|
||||
end
|
||||
table.remove(curr_line, curr_pos-1) -- delete the character
|
||||
writechar(8) -- back cursor up
|
||||
curr_pos = curr_pos - 1
|
||||
if curr_pos <= #curr_line then -- draw characters after deletion point
|
||||
writestring(string.char(table.unpack(curr_line, curr_pos)))
|
||||
end
|
||||
-- blank out trailing character and back cursor up to deletion point
|
||||
writestring((" \x1B[%dD"):format(#curr_line-curr_pos+2))
|
||||
end
|
||||
elseif c >= 32 and c <= 126 then -- a character to type
|
||||
if curr_line == nil then -- retrieve line for editing
|
||||
curr_line = table.pack(history_lines[history_pos]:byte(1, -1))
|
||||
end
|
||||
table.insert(curr_line, curr_pos, c) -- store character in the line
|
||||
writechar(c) -- draw the new character
|
||||
curr_pos = curr_pos + 1
|
||||
if curr_pos <= #curr_line then -- and any after
|
||||
writestring(string.char(table.unpack(curr_line, curr_pos)))
|
||||
-- back cursor up to insertion point
|
||||
writestring(("\x1B[%dD"):format(#curr_line-curr_pos+1))
|
||||
end
|
||||
end
|
||||
|
||||
last_c = c
|
||||
if tx_buf then return end -- give time to flush if buffer full
|
||||
end
|
||||
|
||||
-- loop break, line is complete!
|
||||
local line = history_lines[history_pos]
|
||||
if curr_line then
|
||||
line = string.char(table.unpack(curr_line)) -- store line for processing
|
||||
curr_line = nil
|
||||
end
|
||||
|
||||
if #line == 0 then -- line is empty, ignore it
|
||||
writeprompt()
|
||||
return
|
||||
end
|
||||
|
||||
-- if this line is different to the last one added (one before the last entry)
|
||||
if history_lines[#history_lines-1] ~= line then
|
||||
history_lines[#history_lines] = line -- insert it at history end
|
||||
history_lines[#history_lines+1] = "" -- create empty entry for next line
|
||||
if #history_lines > MAX_HISTORY then table.remove(history_lines, 1) end
|
||||
else -- don't create a new entry with a duplicate line
|
||||
history_lines[#history_lines] = "" -- just clear and reuse the last entry
|
||||
end
|
||||
history_pos = #history_lines -- now editing the last entry
|
||||
curr_pos = 1
|
||||
|
||||
eval_pieces[#eval_pieces+1] = line -- evaluate the line
|
||||
state_func = state_eval
|
||||
end
|
||||
|
||||
local function to_chunk(pieces)
|
||||
local pos = 1
|
||||
|
||||
local function next_piece()
|
||||
-- going past the last piece returns nil which signals the end
|
||||
local piece = pieces[pos]
|
||||
pos = pos + 1
|
||||
return piece
|
||||
end
|
||||
|
||||
return next_piece
|
||||
end
|
||||
|
||||
local eval_results
|
||||
state_eval = function ()
|
||||
local func, err
|
||||
-- try to compile a single line as "return %s;" assuming it could be an
|
||||
-- expression. technique borrowed from the official Lua REPL.
|
||||
if #eval_pieces == 1 then
|
||||
local expr_pieces = {"return ", eval_pieces[1], ";"}
|
||||
func = load(to_chunk(expr_pieces), "=input", "t", _ENV)
|
||||
end
|
||||
if func == nil then -- compilation unsuccessful, load normally
|
||||
func, err = load(to_chunk(eval_pieces), "=input", "t", _ENV)
|
||||
end
|
||||
|
||||
-- if there is an error at the end of the statement, assume we need more to
|
||||
-- complete it. technique borrowed from the official Lua REPL.
|
||||
-- ignore check since load defines that err is not nil if func is nil
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
if func == nil and err:sub(-5, -1) == "<eof>" then
|
||||
-- add a newline and get another line from the user
|
||||
eval_pieces[#eval_pieces+1] = "\n"
|
||||
writeprompt()
|
||||
state_func = state_read
|
||||
return
|
||||
end
|
||||
|
||||
eval_pieces = {} -- destroy to make room for result
|
||||
if func == nil then -- result is the load error message
|
||||
eval_results = { false, err, n = 2 }
|
||||
else
|
||||
eval_results = table.pack(pcall(func))
|
||||
end
|
||||
state_func = state_print
|
||||
end
|
||||
|
||||
state_print = function ()
|
||||
for i = 2, eval_results.n do -- skip pcall result
|
||||
writeobj(eval_results[i]) -- write each result separated by tabs
|
||||
writestring((i ~= eval_results.n) and "\t" or "\r\n")
|
||||
eval_results[i] = nil -- destroy to make room for stringified version
|
||||
end
|
||||
eval_results = nil
|
||||
|
||||
writeprompt()
|
||||
state_func = state_read -- loop back to read
|
||||
end
|
||||
|
||||
state_func = state_read
|
||||
local function update()
|
||||
if tx_buf then -- write out stuff in tx buffer if present
|
||||
local old_buf = tx_buf
|
||||
tx_buf = nil
|
||||
for _, s in ipairs(old_buf) do -- re-write all data
|
||||
writestring(s)
|
||||
end
|
||||
else -- otherwise we have time to process
|
||||
state_func()
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if PORT_IDX == nil then port:flush() end -- flush MAVLink port if using it
|
||||
|
||||
return update, 10
|
||||
end
|
||||
|
||||
return update()
|
|
@ -0,0 +1,63 @@
|
|||
# Lua REPL
|
||||
|
||||
This script implements an interactive REPL (read-evaluate-print-loop) for the
|
||||
Lua scripting engine accessible over serial, with line editing, history, and
|
||||
output formatting.
|
||||
|
||||
The script can also act as a client for QGroundControl's MAVLink Console
|
||||
functionality (within the Analyze view), subject to limitations detailed
|
||||
below.
|
||||
|
||||
### Basic Usage
|
||||
* Configure a serial port (e.g. `SERIALn_PROTOCOL`) to protocol 28 (Scripting).
|
||||
* By default the first such port is used; this can be adjusted in the script
|
||||
text.
|
||||
* `SERIAL6` is the alternate USB serial port on Cube Orange, and convenient
|
||||
for bench testing. CAN and network serial ports will also work.
|
||||
* Load the `repl.lua` script onto the autopilot.
|
||||
* Connect a terminal emulator to the port and enter Lua statements/expressions
|
||||
at the `> ` prompt, then press Enter to execute. Results and errors will be
|
||||
printed back.
|
||||
* A `>> ` prompt indicates that more input is needed to complete the
|
||||
statement.
|
||||
* You can use the arrow keys to edit the current and previous inputs.
|
||||
* Press ESC twice to clear the input and any incomplete statement then
|
||||
return to an empty prompt.
|
||||
|
||||
### Autopilot Connection
|
||||
* On Linux a convenient command is e.g. `minicom -w -D /dev/ttyACM1 -b 115200`,
|
||||
assuming you have the minicom terminal emulator installed.
|
||||
* Any terminal emulator on any platform should work; see notes below about
|
||||
control codes and other configuration.
|
||||
|
||||
### SITL Connection
|
||||
* Start SITL with a command like `Tools/autotest/sim_vehicle.py -A --serialN=tcp:9995:wait` to allow connection to the selected serial port.
|
||||
* Connect a terminal emulator to localhost TCP port 9995
|
||||
* On Linux a convenient command is `stty -icanon -echo -icrnl && netcat localhost 9995`.
|
||||
* Note that you must execute `reset` to turn echo back on once disconnected.
|
||||
* Scripting must be restarted after a TCP reconnection.
|
||||
|
||||
### MAVLink Connection
|
||||
* Requires at least Ardupilot 4.6.
|
||||
* Set the port in the script text to `nil` to enable.
|
||||
* In addition to `repl.lua`, copy the `mavport.lua` file and `MAVLink` directory
|
||||
from `AP_Scripting/modules` to `APM/SCRIPTS/MODULES` on your autopilot.
|
||||
* The ESC key is not supported; cause a syntax error to reset the prompt.
|
||||
* The experience over a radio link might be sub-par due to lack of any sort of
|
||||
packet loss tracking or retransmission.
|
||||
|
||||
### Notes and Limitations
|
||||
* Statements like `local x = 3` create a variable which immediately goes out of
|
||||
scope once evaluated. Names must be global to survive to the next prompt.
|
||||
* There is currently no facility for installing periodic update callbacks.
|
||||
* While theoretically impossible to accidentally crash the autopilot software,
|
||||
certain scripting APIs can cause damage to you or your vehicle if used
|
||||
improperly. Use extreme caution with an armed vehicle!
|
||||
* The script expects Enter to be `\r`, `\r\n`, or `\n`. It prints `\r\n` for a
|
||||
new line, and uses ANSI cursor control codes for line editing and history.
|
||||
Check your terminal configuration if Enter doesn't work or you see garbage
|
||||
characters. Lines longer than the terminal width likely won't edit properly.
|
||||
* Evaluating complex statements or printing complex results can cause an
|
||||
`exceeded time limit` error, stopping the script and losing variables and
|
||||
history. Increasing the vehicle's `SCR_VM_I_COUNT` parameter reduces the
|
||||
chance of this occurring.
|
Loading…
Reference in New Issue