From e051a120bcd0433b209727b20d342f1faa651b8f Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 24 Oct 2023 15:27:31 +1100 Subject: [PATCH 1/4] aiorepl: Update import of asyncio. Signed-off-by: Andrew Leech --- micropython/aiorepl/README.md | 2 +- micropython/aiorepl/aiorepl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/micropython/aiorepl/README.md b/micropython/aiorepl/README.md index 4bb11083f..c1c08b899 100644 --- a/micropython/aiorepl/README.md +++ b/micropython/aiorepl/README.md @@ -21,7 +21,7 @@ To use this library, you need to import the library and then start the REPL task For example, in main.py: ```py -import uasyncio as asyncio +import asyncio import aiorepl async def demo(): diff --git a/micropython/aiorepl/aiorepl.py b/micropython/aiorepl/aiorepl.py index e7e316768..e562f9469 100644 --- a/micropython/aiorepl/aiorepl.py +++ b/micropython/aiorepl/aiorepl.py @@ -5,7 +5,7 @@ import re import sys import time -import uasyncio as asyncio +import asyncio # Import statement (needs to be global, and does not return). _RE_IMPORT = re.compile("^import ([^ ]+)( as ([^ ]+))?") From d41851ca7246470dc74f6e9140e67af74ea907e7 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 24 Oct 2023 15:34:27 +1100 Subject: [PATCH 2/4] aiorepl: Add support for paste mode (ctrl-e). Signed-off-by: Andrew Leech --- micropython/aiorepl/aiorepl.py | 39 ++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/micropython/aiorepl/aiorepl.py b/micropython/aiorepl/aiorepl.py index e562f9469..ab8f5d67e 100644 --- a/micropython/aiorepl/aiorepl.py +++ b/micropython/aiorepl/aiorepl.py @@ -19,6 +19,13 @@ _HISTORY_LIMIT = const(5 + 1) +CHAR_CTRL_A = const(1) +CHAR_CTRL_B = const(2) +CHAR_CTRL_C = const(3) +CHAR_CTRL_D = const(4) +CHAR_CTRL_E = const(5) + + async def execute(code, g, s): if not code.strip(): return @@ -43,7 +50,7 @@ async def __code(): async def kbd_intr_task(exec_task, s): while True: - if ord(await s.read(1)) == 0x03: + if ord(await s.read(1)) == CHAR_CTRL_C: exec_task.cancel() return @@ -102,7 +109,8 @@ async def task(g=None, prompt="--> "): while True: hist_b = 0 # How far back in the history are we currently. sys.stdout.write(prompt) - cmd = "" + cmd: str = "" + paste = False while True: b = await s.read(1) pc = c # save previous character @@ -112,6 +120,10 @@ async def task(g=None, prompt="--> "): if c < 0x20 or c > 0x7E: if c == 0x0A: # LF + if paste: + sys.stdout.write(b) + cmd += b + continue # If the previous character was also LF, and was less # than 20 ms ago, this was likely due to CRLF->LFLF # conversion, so ignore this linefeed. @@ -135,12 +147,12 @@ async def task(g=None, prompt="--> "): if cmd: cmd = cmd[:-1] sys.stdout.write("\x08 \x08") - elif c == 0x02: - # Ctrl-B + elif c == CHAR_CTRL_B: continue - elif c == 0x03: - # Ctrl-C - if pc == 0x03 and time.ticks_diff(t, pt) < 20: + elif c == CHAR_CTRL_C: + if paste: + break + if pc == CHAR_CTRL_C and time.ticks_diff(t, pt) < 20: # Two very quick Ctrl-C (faster than a human # typing) likely means mpremote trying to # escape. @@ -148,12 +160,21 @@ async def task(g=None, prompt="--> "): return sys.stdout.write("\n") break - elif c == 0x04: - # Ctrl-D + elif c == CHAR_CTRL_D: + if paste: + result = await execute(cmd, g, s) + if result is not None: + sys.stdout.write(repr(result)) + sys.stdout.write("\n") + break + sys.stdout.write("\n") # Shutdown asyncio. asyncio.new_event_loop() return + elif c == CHAR_CTRL_E: + sys.stdout.write("paste mode; Ctrl-C to cancel, Ctrl-D to finish\n===\n") + paste = True elif c == 0x1B: # Start of escape sequence. key = await s.read(2) From 10c9281dadb63cde38b977b4f330ea8af5faf0aa Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 24 Oct 2023 15:36:53 +1100 Subject: [PATCH 3/4] aiorepl: Add cursor left/right support. Allows modifying current line, adding/deleting characters in the middle etc. Includes home/end keys to move to start/end of current line. Signed-off-by: Andrew Leech --- micropython/aiorepl/aiorepl.py | 47 ++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/micropython/aiorepl/aiorepl.py b/micropython/aiorepl/aiorepl.py index ab8f5d67e..63e98096c 100644 --- a/micropython/aiorepl/aiorepl.py +++ b/micropython/aiorepl/aiorepl.py @@ -111,6 +111,7 @@ async def task(g=None, prompt="--> "): sys.stdout.write(prompt) cmd: str = "" paste = False + curs = 0 # cursor offset from end of cmd buffer while True: b = await s.read(1) pc = c # save previous character @@ -129,6 +130,10 @@ async def task(g=None, prompt="--> "): # conversion, so ignore this linefeed. if pc == 0x0A and time.ticks_diff(t, pt) < 20: continue + if curs: + # move cursor to end of the line + sys.stdout.write("\x1B[{}C".format(curs)) + curs = 0 sys.stdout.write("\n") if cmd: # Push current command. @@ -145,8 +150,16 @@ async def task(g=None, prompt="--> "): elif c == 0x08 or c == 0x7F: # Backspace. if cmd: - cmd = cmd[:-1] - sys.stdout.write("\x08 \x08") + if curs: + cmd = "".join((cmd[: -curs - 1], cmd[-curs:])) + sys.stdout.write( + "\x08\x1B[K" + ) # move cursor back, erase to end of line + sys.stdout.write(cmd[-curs:]) # redraw line + sys.stdout.write("\x1B[{}D".format(curs)) # reset cursor location + else: + cmd = cmd[:-1] + sys.stdout.write("\x08 \x08") elif c == CHAR_CTRL_B: continue elif c == CHAR_CTRL_C: @@ -178,7 +191,7 @@ async def task(g=None, prompt="--> "): elif c == 0x1B: # Start of escape sequence. key = await s.read(2) - if key in ("[A", "[B"): + if key in ("[A", "[B"): # up, down # Stash the current command. hist[(hist_i - hist_b) % _HISTORY_LIMIT] = cmd # Clear current command. @@ -194,12 +207,36 @@ async def task(g=None, prompt="--> "): # Update current command. cmd = hist[(hist_i - hist_b) % _HISTORY_LIMIT] sys.stdout.write(cmd) + elif key == "[D": # left + if curs < len(cmd) - 1: + curs += 1 + sys.stdout.write("\x1B") + sys.stdout.write(key) + elif key == "[C": # right + if curs: + curs -= 1 + sys.stdout.write("\x1B") + sys.stdout.write(key) + elif key == "[H": # home + pcurs = curs + curs = len(cmd) + sys.stdout.write("\x1B[{}D".format(curs - pcurs)) # move cursor left + elif key == "[F": # end + pcurs = curs + curs = 0 + sys.stdout.write("\x1B[{}C".format(pcurs)) # move cursor right else: # sys.stdout.write("\\x") # sys.stdout.write(hex(c)) pass else: - sys.stdout.write(b) - cmd += b + if curs: + # inserting into middle of line + cmd = "".join((cmd[:-curs], b, cmd[-curs:])) + sys.stdout.write(cmd[-curs - 1 :]) # redraw line to end + sys.stdout.write("\x1B[{}D".format(curs)) # reset cursor location + else: + sys.stdout.write(b) + cmd += b finally: micropython.kbd_intr(3) From f672baa92ba9c2b890c8a65fe115ec5c025c14c8 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 24 Oct 2023 15:38:00 +1100 Subject: [PATCH 4/4] aiorepl: Add support for raw mode (ctrl-a). Provides support for mpremote features like cp and mount. Signed-off-by: Andrew Leech --- micropython/aiorepl/aiorepl.py | 95 ++++++++++++++++++++++++++++++--- micropython/aiorepl/manifest.py | 2 +- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/micropython/aiorepl/aiorepl.py b/micropython/aiorepl/aiorepl.py index 63e98096c..14d5d55bc 100644 --- a/micropython/aiorepl/aiorepl.py +++ b/micropython/aiorepl/aiorepl.py @@ -160,17 +160,14 @@ async def task(g=None, prompt="--> "): else: cmd = cmd[:-1] sys.stdout.write("\x08 \x08") + elif c == CHAR_CTRL_A: + await raw_repl(s, g) + break elif c == CHAR_CTRL_B: continue elif c == CHAR_CTRL_C: if paste: break - if pc == CHAR_CTRL_C and time.ticks_diff(t, pt) < 20: - # Two very quick Ctrl-C (faster than a human - # typing) likely means mpremote trying to - # escape. - asyncio.new_event_loop() - return sys.stdout.write("\n") break elif c == CHAR_CTRL_D: @@ -240,3 +237,89 @@ async def task(g=None, prompt="--> "): cmd += b finally: micropython.kbd_intr(3) + + +async def raw_paste(s, g, window=512): + sys.stdout.write("R\x01") # supported + sys.stdout.write(bytearray([window & 0xFF, window >> 8, 0x01]).decode()) + eof = False + idx = 0 + buff = bytearray(window) + file = b"" + while not eof: + for idx in range(window): + b = await s.read(1) + c = ord(b) + if c == CHAR_CTRL_C or c == CHAR_CTRL_D: + # end of file + sys.stdout.write(chr(CHAR_CTRL_D)) + if c == CHAR_CTRL_C: + raise KeyboardInterrupt + file += buff[:idx] + eof = True + break + buff[idx] = c + + if not eof: + file += buff + sys.stdout.write("\x01") # indicate window available to host + + return file + + +async def raw_repl(s: asyncio.StreamReader, g: dict): + heading = "raw REPL; CTRL-B to exit\n" + line = "" + sys.stdout.write(heading) + + while True: + line = "" + sys.stdout.write(">") + while True: + b = await s.read(1) + c = ord(b) + if c == CHAR_CTRL_A: + rline = line + line = "" + + if len(rline) == 2 and ord(rline[0]) == CHAR_CTRL_E: + if rline[1] == "A": + line = await raw_paste(s, g) + break + else: + # reset raw REPL + sys.stdout.write(heading) + sys.stdout.write(">") + continue + elif c == CHAR_CTRL_B: + # exit raw REPL + sys.stdout.write("\n") + return 0 + elif c == CHAR_CTRL_C: + # clear line + line = "" + elif c == CHAR_CTRL_D: + # entry finished + # indicate reception of command + sys.stdout.write("OK") + break + else: + # let through any other raw 8-bit value + line += b + + if len(line) == 0: + # Normally used to trigger soft-reset but stay in raw mode. + # Fake it for aiorepl / mpremote. + sys.stdout.write("Ignored: soft reboot\n") + sys.stdout.write(heading) + + try: + result = exec(line, g) + if result is not None: + sys.stdout.write(repr(result)) + sys.stdout.write(chr(CHAR_CTRL_D)) + except Exception as ex: + print(line) + sys.stdout.write(chr(CHAR_CTRL_D)) + sys.print_exception(ex, sys.stdout) + sys.stdout.write(chr(CHAR_CTRL_D)) diff --git a/micropython/aiorepl/manifest.py b/micropython/aiorepl/manifest.py index ca88bb359..0fcc21849 100644 --- a/micropython/aiorepl/manifest.py +++ b/micropython/aiorepl/manifest.py @@ -1,5 +1,5 @@ metadata( - version="0.1.1", + version="0.2.0", description="Provides an asynchronous REPL that can run concurrently with an asyncio, also allowing await expressions.", )