Skip to content

Add new raw REPL paste mode that has flow control, and compiles as it receives data #6527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 1, 2020

Conversation

dpgeorge
Copy link
Member

@dpgeorge dpgeorge commented Oct 9, 2020

Background: the friendly/normal REPL is intended for human use whereas the raw REPL is for computer use/automation. Raw REPL is used for things like pyboard.py script_to_run.py. The normal REPL has built-in flow control because it echos back the characters. That's not so with raw REPL and flow control is just implemented by rate limiting the amount of data that goes in. Currently it's fixed at 256 byte chunks every 10ms. This is sometimes too fast for slow MCUs or systems with small stdin buffers. It's also too slow for a lot of higher-end MCUs, ie it could be a lot faster.

This PR adds a new raw REPL mode which includes flow control: the device will echo back a character after a certain number of bytes are sent to the host, and the host can use this to regulate the data going out to the device. The amount of characters is controlled by the device and sent to the host before communication starts. This flow control allows getting the maximum speed out of a serial link, regardless of the link or the device at the other end.

Also, this new raw REPL mode parses and compiles the incoming data as it comes in. It does this by creating a "stdin reader" object which is then passed to the lexer. The lexer requests bytes from this "stdin reader" which retrieves bytes from the host, and does flow control. What this means is that no memory is used to store the script (in the existing raw REPL mode the device needs a big buffer to read in the script before it can pass it on to the lexer/parser/compiler). The only memory needed on the device is enough to parse and compile.

Finally, it would be possible to extend this new raw REPL to allow bytecode (.mpy files) to be sent as well as text mode scripts (but that's not done in this PR).


Some results follow. The test was to send a large 33k script that contains mostly comments and then prints out the heap, run via pyboard.py large.py.

On PYBD-SF6, prior to this PR:

$ ./pyboard.py large.py
stack: 524 out of 23552
GC: total: 392192, used: 34464, free: 357728
 No. of 1-blocks: 12, 2-blocks: 2, max blk sz: 2075, max free sz: 22345
GC memory layout; from 2001a3f0:
00000: h=hhhh=======================================hhBShShh==h=======h
00400: =====hh=B........h==h===========================================
00800: ================================================================
00c00: ================================================================
01000: ================================================================
01400: ================================================================
01800: ================================================================
01c00: ================================================================
02000: ================================================================
02400: ================================================================
02800: ================================================================
02c00: ================================================================
03000: ================================================================
03400: ================================================================
03800: ================================================================
03c00: ================================================================
04000: ================================================================
04400: ================================================================
04800: ================================================================
04c00: ================================================================
05000: ================================================================
05400: ================================================================
05800: ================================================================
05c00: ================================================================
06000: ================================================================
06400: ================================================================
06800: ================================================================
06c00: ================================================================
07000: ================================================================
07400: ================================================================
07800: ================================================================
07c00: ================================================================
08000: ================================================================
08400: ===============================================.....h==.........
       (349 lines all free)

(the big blob of used memory is the large script).

Same but with this PR:

$ ./pyboard.py large.py
stack: 524 out of 23552
GC: total: 392192, used: 1296, free: 390896
 No. of 1-blocks: 12, 2-blocks: 3, max blk sz: 40, max free sz: 24420
GC memory layout; from 2001a3f0:
00000: h=hhhh=======================================hhBShShh==h=======h
00400: =====hh=h=B......h==.....h==....................................
       (381 lines all free)

The only thing in RAM is the compiled script (and some other unrelated items).

Time to download before this PR: 1438ms, data rate: 230,799 bits/sec.

Time to download with this PR: 119ms, data rate: 2,788,991 bits/sec.

So it's more than 10 times faster, and uses significantly less RAM.

Results are similar on other boards. On an stm32 board that connects via UART only at 115200 baud, the data rate goes from 80kbit/sec to 113kbit/sec, so gets close to saturating the UART link without loss of data.


Note that the new mode is fully backwards compatible with old pyboard.py, and new pyboard.py is fully backwards compatible with old devices (it detects if the device supports the new raw REPL mode).

The new raw REPL mode also supports a single ctrl-C to break out of this flow-control mode, so that a ctrl-C can always get back to a known state.

To summarise: this PR introduces a new raw REPL mode which 1) has flow control to make it faster, 2) compiles as data comes in to use less RAM, and 3) can be extended to support reading in .mpy files.

There are other ways to achieve these items (and this is my attempt number 4...) and comments are welcome.

@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch 2 times, most recently from a199493 to 33d7d92 Compare October 11, 2020 06:43
@hoihu
Copy link
Contributor

hoihu commented Oct 12, 2020

We use the raw repl via USB a lot, so increasing transfer speed by 10x sounds... great!

Will test this asap when back at work.

And possibly it would also benefit other ports/boards where paste mode was not always reliable. eg on the pca10059 dev board, usb paste mode is almost unusable due to lost characters even for small scripts.

@dpgeorge
Copy link
Member Author

Will test this asap when back at work.

Thanks, would be good to get more testing on this.

And possibly it would also benefit other ports/boards where paste mode was not always reliable. eg on the pca10059 dev board, usb paste mode is almost unusable due to lost characters even for small scripts

Note that this PR won't help friendly REPL paste mode (ctrl-E, === mode) because that just accepts characters as you paste them (eg via ctrl-V) from your OS. To improve the situation there with pca10059 you can increase CFG_TUD_CDC_RX_BUFSIZE to 256, in ports/nrf/drivers/usb/tusb_config.h.

@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch from 33d7d92 to a5a209c Compare November 4, 2020 06:09
@aivarannamaa
Copy link

aivarannamaa commented Nov 20, 2020

This looks great!

When the code finishes, does the REPL go to normal prompt, just like with current paste mode?

What about errors -- is it possible to distinguish between normal stdout and stacktrace like in current raw mode or is all the output given together like in current paste mode?

Will all this work over WebREPL as well?

@dpgeorge
Copy link
Member Author

When the code finishes, does the REPL go to normal prompt, just like with current paste mode?

It goes back to raw REPL, just like normal raw REPL after executing a command (ie it does not go back to friendly REPL, it needs a ctrl-A after executing for that).

What about errors -- is it possible to distinguish between normal stdout and stacktrace like in current raw mode or is all the output given together like in current paste mode?

It's the same as current raw mode: there are ctrl-D characters delineating normal output and a stacktrace. It's very similar to normal raw mode (you can see the changes in this PR to pyboard.py are localised), just the way the code is sent to the device is different.

Will all this work over WebREPL as well?

Good question. I have not tested it but it should work, it should work with anything that plugs in to the dupterm interface.

@dpgeorge
Copy link
Member Author

Will all this work over WebREPL as well?

Testing with a command-line WebREPL client (not the browser WebREPL client), this new raw mode does work with it.

@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch from a5a209c to 1841437 Compare November 21, 2020 05:35
STATIC void mp_reader_new_stdin(mp_reader_t *reader, mp_reader_stdin_t *reader_stdin, size_t buf_max) {
// Make flow-control window half the buffer size, and indicate to the host that 2x windows are free.
size_t window = buf_max / 2;
char reply[3] = { window & 0xff, window >> 8, 0x01 };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry if this is a stupid question, but given that window is transmitted as a 16-bit value, should this function be checking that buf_max is less than 0x20000 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yes, that should be constrained. I've now changed the type of the arg to a uint16_t so that the compiler enforces the size to a maximum of 65535 (which should be plenty).

@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch from 1841437 to 83a4d74 Compare November 24, 2020 11:12
@dpgeorge dpgeorge changed the title RFC: add new raw REPL paste mode that has flow control, and compiles as it receives data Add new raw REPL paste mode that has flow control, and compiles as it receives data Nov 24, 2020
@dpgeorge
Copy link
Member Author

I changed the title of this PR to remove the "RFC". It should be ready to go in now.

@lurch
Copy link
Contributor

lurch commented Nov 25, 2020

Would it be appropriate for this PR to also update https://github.com/micropython/micropython/blob/master/docs/reference/repl.rst with a description of this new mode? (and possibly an overview of how the different modes are entered / exited?)

Background: the friendly/normal REPL is intended for human use whereas the
raw REPL is for computer use/automation.  Raw REPL is used for things like
pyboard.py script_to_run.py.  The normal REPL has built-in flow control
because it echos back the characters.  That's not so with raw REPL and flow
control is just implemented by rate limiting the amount of data that goes
in.  Currently it's fixed at 256 byte chunks every 10ms.  This is sometimes
too fast for slow MCUs or systems with small stdin buffers.  It's also too
slow for a lot of higher-end MCUs, ie it could be a lot faster.

This commit adds a new raw REPL mode which includes flow control: the
device will echo back a character after a certain number of bytes are sent
to the host, and the host can use this to regulate the data going out to
the device.  The amount of characters is controlled by the device and sent
to the host before communication starts.  This flow control allows getting
the maximum speed out of a serial link, regardless of the link or the
device at the other end.

Also, this new raw REPL mode parses and compiles the incoming data as it
comes in.  It does this by creating a "stdin reader" object which is then
passed to the lexer.  The lexer requests bytes from this "stdin reader"
which retrieves bytes from the host, and does flow control.  What this
means is that no memory is used to store the script (in the existing raw
REPL mode the device needs a big buffer to read in the script before it can
pass it on to the lexer/parser/compiler).  The only memory needed on the
device is enough to parse and compile.

Finally, it would be possible to extend this new raw REPL to allow bytecode
(.mpy files) to be sent as well as text mode scripts (but that's not done
in this commit).

Some results follow. The test was to send a large 33k script that contains
mostly comments and then prints out the heap, run via pyboard.py large.py.

On PYBD-SF6, prior to this PR:

$ ./pyboard.py large.py
stack: 524 out of 23552
GC: total: 392192, used: 34464, free: 357728
 No. of 1-blocks: 12, 2-blocks: 2, max blk sz: 2075, max free sz: 22345
GC memory layout; from 2001a3f0:
00000: h=hhhh=======================================hhBShShh==h=======h
00400: =====hh=B........h==h===========================================
00800: ================================================================
00c00: ================================================================
01000: ================================================================
01400: ================================================================
01800: ================================================================
01c00: ================================================================
02000: ================================================================
02400: ================================================================
02800: ================================================================
02c00: ================================================================
03000: ================================================================
03400: ================================================================
03800: ================================================================
03c00: ================================================================
04000: ================================================================
04400: ================================================================
04800: ================================================================
04c00: ================================================================
05000: ================================================================
05400: ================================================================
05800: ================================================================
05c00: ================================================================
06000: ================================================================
06400: ================================================================
06800: ================================================================
06c00: ================================================================
07000: ================================================================
07400: ================================================================
07800: ================================================================
07c00: ================================================================
08000: ================================================================
08400: ===============================================.....h==.........
       (349 lines all free)

(the big blob of used memory is the large script).

Same but with this PR:

$ ./pyboard.py large.py
stack: 524 out of 23552
GC: total: 392192, used: 1296, free: 390896
 No. of 1-blocks: 12, 2-blocks: 3, max blk sz: 40, max free sz: 24420
GC memory layout; from 2001a3f0:
00000: h=hhhh=======================================hhBShShh==h=======h
00400: =====hh=h=B......h==.....h==....................................
       (381 lines all free)

The only thing in RAM is the compiled script (and some other unrelated
items).

Time to download before this PR: 1438ms, data rate: 230,799 bits/sec.

Time to download with this PR: 119ms, data rate: 2,788,991 bits/sec.

So it's more than 10 times faster, and uses significantly less RAM.

Results are similar on other boards. On an stm32 board that connects via
UART only at 115200 baud, the data rate goes from 80kbit/sec to
113kbit/sec, so gets close to saturating the UART link without loss of
data.

The new raw REPL mode also supports a single ctrl-C to break out of this
flow-control mode, so that a ctrl-C can always get back to a known state.
It's also backwards compatible with the original raw REPL mode, which is
still supported with the same sequence of commands.  The new raw REPL
mode is activated by ctrl-E, which gives an error on devices that do not
support the new mode.

Signed-off-by: Damien George <damien@micropython.org>
@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch 2 times, most recently from 528f4bf to d9d5693 Compare November 30, 2020 00:49
@dpgeorge
Copy link
Member Author

Would it be appropriate for this PR to also update https://github.com/micropython/micropython/blob/master/docs/reference/repl.rst with a description of this new mode?

Yes, good idea. Now done.

b'\r\nraw REPL; CTRL-B to exit\r\n>R\x01\x80\x00\x01\x04123\r\n\x04\x04>'

In this case the flow-control window size is 128 and there is a new window
immediately available at the start (ie up to 256 bytes can be written to begin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does a window size of 128 mean that 256 bytes can be written?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because 2 windows worth are available immediately, the implicit one and the explicitly indicated one (this is mentioned in the protocol text above in the dot points... but maybe not very clear)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to be more explicit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, documentation is much clearer now, thanks.


* While there are bytes to send, write up to the remaining window size.

* If the window size is 0, or there is a byte waiting to read, read 1 byte.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this supposed to be "if the remaining window size is 0" ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text improved

def raw_paste_write(self, command_bytes):
# Read initial header, with window size.
data = self.serial.read(2)
window_size = data[0] | data[1] << 8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be "more pythonic" to use something like struct.unpack here? 😉

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try not to depend on entire modules if it's just for a very simple thing like this... too much micro, not enough Python :)

@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch from d9d5693 to 6793108 Compare December 1, 2020 01:19
# Device supports raw-paste mode, write out the command using this mode.
return self.raw_paste_write(command_bytes)
else:
# Device doesn't support raw-paste, fall back to normal raw REPL.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this then set self.use_raw_paste = False ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's probably a good idea, so it doesn't keep trying the failing raw-paste each time. And it's not like the device can suddenly support it, you'd have to at least disconnect/reconnect to get support for raw-paste, which means resetting this variable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch from 6793108 to 1de811e Compare December 1, 2020 11:16
tools/pyboard.py Outdated
print(data)
raise PyboardError("could not enter raw repl")
# Don't try to use raw-paste mode again for this connection.
self.use_raw_paste = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errrr, whoops? 😜

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaah! Thanks for reviewing! Should be fixed now.

This commit adds support to pyboard.py for the new raw REPL paste mode.

Note that this new pyboard.py is fully backwards compatible with old
devices (it detects if the device supports the new raw REPL paste mode).

Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
@dpgeorge dpgeorge force-pushed the pyexec-raw-paste-mode-v4 branch from 1de811e to a14ca31 Compare December 1, 2020 11:35
@dpgeorge dpgeorge merged commit a14ca31 into micropython:master Dec 1, 2020
@dpgeorge dpgeorge deleted the pyexec-raw-paste-mode-v4 branch December 1, 2020 23:53
tannewt added a commit to tannewt/circuitpython that referenced this pull request Jul 7, 2023
Connects up read, write and ctrl_transfer to TinyUSB. USB Host
support is available on iMX RT and RP2040.

Fixes micropython#6527 (imx) and fixes micropython#5986 (rp2).
tannewt added a commit to tannewt/circuitpython that referenced this pull request Jul 10, 2023
Connects up read, write and ctrl_transfer to TinyUSB. USB Host
support is available on iMX RT and RP2040.

Fixes micropython#6527 (imx) and fixes micropython#5986 (rp2).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants