././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640889926.1680648 rshell-0.0.31/0000775010432500017510000000000000000000000013270 5ustar00dhylandsdhylands././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/LICENSE0000664010432500017510000000206700000000000014302 0ustar00dhylandsdhylandsThe MIT License (MIT) Copyright (c) 2014 Dave Hylands Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/MANIFEST.in0000664010432500017510000000010100000000000015016 0ustar00dhylandsdhylandsinclude LICENSE include README.rst include tests/test-rshell.shr ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640889926.1680648 rshell-0.0.31/PKG-INFO0000664010432500017510000005235700000000000014401 0ustar00dhylandsdhylandsMetadata-Version: 1.1 Name: rshell Version: 0.0.31 Summary: A remote shell for working with MicroPython boards. Home-page: https://github.com/dhylands/rshell Author: Dave Hylands Author-email: dhylands@gmail.com License: MIT Download-URL: https://github.com/dhylands/rshell/tarball/v0.0.1 Description: rshell ========= Remote MicroPython shell. This is a simple shell which runs on the host and uses MicroPython's raw-REPL to send python snippets to the pyboard in order to get filesystem information, and to copy files to and from MicroPython's filesystem. It also has the ability to invoke the regular REPL, so rshell can be used as a terminal emulator as well. Note: With rshell you can disable USB Mass Storage and still copy files into and out of your pyboard. When using the commands, the /flash directory, and the /sdcard directory (if an sdcard is inserted) are considered to be on the pyboard, and all other directories are considered to be on the host. For an ESP based board you can only reference its directory by using the board name e.g. /pyboard etc.. NOTE: rshell requires a fairly recent version of the MicroPython firmware, specifically one which contains the ubinascii.unhexlify command which was added May 19, 2015 (v1.4.3-28-ga3a14b9 or newer). If your version of the firmware isn't new enough, then you'll see an error message something like this: :: >./rshell.py rshell needs MicroPython firmware with ubinascii.unhexlify Installation ============ You can install rshell using the command: :: sudo pip3 install rshell If you use a virtualenv, then you don't need the sudo. rshell needs Python3. All of my testing was done using version 3.4.0. Debian/Ubuntu users can get pip3 using: :: sudo apt-get install python3-pip Serial Port Permissions (linux) =============================== Under linux, the USB serial ports are owned by root and typically have a group of dialout (for ubuntu). Different unix distros may use a slightly different group name. In order for rshell (or any terminal emulator) to access the serial port, you'll need to ensure that your user account has sufficient priviledge. One way to do this is to ensure that your user is added to the dialout group, using a command like: :: sudo usermod -a -G dialout $USER Another way, is to use udev rules to modify the permissions on the serial port. There are some instructions on the [micropython wiki](https://github.com/micropython/micropython/wiki/Board-STM32F407-Discovery#programming-from-linux-via-dfu) that you can use to setup your udev rules (you don't need to install dfu-utils just to update the permissions). Sample Session ============== This shows a pyboard in its default state, copying a hello.py and then entering the repl and importing it. :: >rshell Welcome to rshell. Use Control-D to exit. /home/dhylands/Dropbox/micropython/rshell> ls -l /flash 529 May 21 17:34 README.txt 286 May 21 17:34 boot.py 34 May 21 17:34 main.py 2436 May 21 17:34 pybcdc.inf /home/dhylands/Dropbox/micropython/rshell> cp hello.py /flash /home/dhylands/Dropbox/micropython/rshell> ls -l /flash 529 May 21 17:34 README.txt 286 May 21 17:34 boot.py 21 May 21 17:35 hello.py 34 May 21 17:34 main.py 2436 May 21 17:34 pybcdc.inf /home/dhylands/Dropbox/micropython/rshell> cat /flash/hello.py print('Hello World') /home/dhylands/Dropbox/micropython/rshell> repl Entering REPL. Use Control-X to exit. Micro Python v1.4.3-28-ga3a14b9 on 2015-05-21; PYBv1.0 with STM32F405RG Type "help()" for more information. >>> >>> import hello Hello World >>> /home/dhylands/Dropbox/micropython/rshell> Command Line Options ==================== -h, --help ---------- Displays a list of the valid options. You should get something like the following displayed: :: usage: rshell [options] [command] Remote Shell for a MicroPython board. positional arguments: cmd Optional command to execute optional arguments: -h, --help show this help message and exit -b BAUD, --baud BAUD Set the baudrate used (default = 115200) --buffer-size BUFFER_SIZE Set the buffer size used for transfers (default = 512) -p PORT, --port PORT Set the serial port to use (default '/dev/ttyACM0') --rts RTS Set the RTS state (default '') --dtr DTR Set the DTR state (default '') -u USER, --user USER Set username to use (default 'micro') -w PASSWORD, --password PASSWORD Set password to use (default 'python') -e EDITOR, --editor EDITOR Set the editor to use (default 'vi') -f FILENAME, --file FILENAME Specifies a file of commands to process. -d, --debug Enable debug features -n, --nocolor Turn off colorized output --wait How long to wait for serial port --binary Enable binary file transfer --timing Print timing information about each command --quiet Turns off some output (useful for testing) You can specify the default serial port using the RSHELL_PORT environment variable. -b BAUD, --baud BAUD -------------------- Sets the baud rate to use when talking to the pyboard over a serial port. If no baud is specified, then the baudrate from the RSHELL_BAUD environment variable is used. If the RSHELL_BAUD environment variable is not defined then the default baudrate of 115200 is used. --buffer-size ------------- Sets the buffer size used when transferring files between the host and the pyboard. If no buffer size is specified, then the value from the RSHELL_BUFFER_SIZE environment variable is used. If the RSHELL_BUFFER_SIZE environment variable is not defined, then the default of 512 is used. -d, --debug ----------- Turns on debugging. This allows you to see the script which is sent over the raw REPL and the response received. -e EDITOR, --editor ------------------- Specifies the editor to use with the edit command. If no editor is specified, then the following environment variables will be searched: RSHELL_EDITOR, VISUAL, and EDITOR. If none of those environment variables is set then vi will be used. -f FILENAME, --file FILENAME ---------------------------- Specifies a file of rshell commands to process. This allows you to create a script which executes any valid rshell commands. -n, --nocolor ------------- By default, rshell uses ANSI color escape codes when displaying the prompt and ls output. This option allows colorized output to be disabled. -a --ascii ---------- On certain platforms the raw REPL mode is unreliable with particular sequences of binary characters. Specifying --ascii enables the transfer of binary files to such platforms. It does this by encoding the data as ASCII hex. --wait ------ If a port is specified defines how long rshell will wait for the port to exist and for a connection to be established. The default is 0 seconds specifying an immediate return. -p PORT, --port PORT -------------------- Specifies the serial port which should be used to talk to the MicroPython board. You can set the RSHELL\_PORT environment variable to specify the default port to be used, if --port is not specified on the command line. --dtr [0|1|True|False] ---------------------- Sets the state of the DTR line when opening the serial port. This may also be defaulted from the RSHELL_DTR environment variable. --rts [0|1|True|False] ---------------------- Sets the state of the RTS line when opening the serial port. This may also be defaulted from the RSHELL_RTS environment variable. --quiet ------- This option causes the Connecting messages printed when rshell starts to be suppressed. This is mostly useful for the test scripts. --timing -------- If the timing option is specified then rshell will print the amount of time that each command takes to execute. -u USER, --user USER -------------------- Specifies the username to use when logging into a WiPy over telnet. If no username is specified, then the username from the RSHELL_USER environment variable is used. If the RSHELL_USER environment variable doesn't exist then the default username 'micro' is used. -w PASSWORD, --password PASSWORD -------------------------------- Specified the password to use when logging into a WiPy over telnet. If no password is specified, then the password from the RSHELL_PASSWORD environment variable is used. If the RSHELL_PASSWORD environment variable doesn't exist then the default password 'python' is used. cmd --- If a command is specified, then that command will be executed and rshell will exit. Examples: :: rshell cp somefile.py /flash rshell repl ~ pyb.bootloader() ~ File System =========== rshell can be connected to multiple pyboards simultaneously. If the board module exists on the pyboard (i.e. a file named board.py somewhere in the module search path) and it contains an attribute called name (e.g. :code:`name = "myboard"`) then the pyboard will use that name. If the board module can't be imported then the board will be named, pyboard or wipy. Names will have -1 (or some other number) to make the board name unique. You can access the internal flash on the first board connected using /flash and the sd card on the first board connected can be accessed using /sd. For all other connected pyboards, you can use /board-name/flash or /board-name/sd (you can see the board names using the boards command). The boards command will show all of the connected pyboards, along with all of the directories which map onto that pyboard. Commands ======== args ---- :: args [arguments...] Debug function for verifying argument parsing. This function just prints out each argument that it receives. boards ------ :: boards Lists all of the boards that rshell is currently connected to, their names, and the connection. You can give a custom name to a board with either copying over a :code:`board.py` file or using the :code:`echo` command, e.g. :: echo 'name="myboard"' > /pyboard/board.py (Remember to exit rshell and re-enter to see the change). cat --- :: cat FILENAME... Concatenates files and sends to stdout. cd -- :: cd DIRECTORY Changes the current directory. ~ expansion is supported, and cd - goes to the previous directory. connect ------- :: connect TYPE TYPE_PARAMS connect serial port [baud] connect telnet ip-address-or-name Connects a pyboard to rshell. rshell can be connected to multiple pyboards simultaneously. cp -- :: usage: cp SOURCE DEST cp SOURCE... DIRECTORY cp [-r|--recursive] [SOURCE|SRC_DIR]... DIRECTORY cp [-r|--recursive] PATTERN DIRECTORY positional arguments: DEST A destination file SOURCE File to copy SRC_DIR Directory to copy PATTERN File or directory pattern match string e.g. foo/*.py optional arguments: -h, --help show this help message and exit -r, --recursive copy directories recursively Copies the SOURCE file to DEST. DEST may be a filename or a directory name. If more than one source file is specified, then the destination should be a directory. Directories will only be copied if -r is specified. A single pattern may be specified, in which case the destination should be a directory. Pattern matching is performed according to a subset of the Unix rules (see below). Recursive copying uses rsync (see below): where a file exists on source and destination, it will only be copied if the source is newer than the destination. echo ---- :: echo TEXT... Display a line of text. edit ---- :: edit filename If the file is on a pyboard, it copies the file to host, invokes an editor and if any changes were made to the file, it copies it back to the pyboard. The editor which is used defaults to vi, but can be overridden using either the --editor command line option when rshell.py is invoked, or by using the RSHELL\_EDITOR, VISUAL or EDITOR environment variables (they are tried in the order listed). filesize -------- :: filesize FILE Prints the size of the file, in bytes. This function is primarily testing. filetype -------- :: filetype FILE Prints the type of file (dir or file). This function is primarily for testing. help ---- :: help [COMMAND] List available commands with no arguments, or detailed help when a command is provided. ls -- :: usage: ls [-a] [-l] [FILE|DIRECTORY|PATTERN]... List directory contents. positional arguments: FILE File to list (show absolute path) DIRECTORY Directory (list contents) PATTERN File or directory pattern match string e.g. foo/*.py optional arguments: -h, --help show this help message and exit -a, --all do not ignore hidden files -l, --long use a long listing format Pattern matching is performed according to a subset of the Unix rules (see below). mkdir ----- :: mkdir DIRECTORY... Creates one or more directories. repl ---- :: repl [board-name] [~ line][ ~] Enters into the regular REPL with the MicroPython board. Use Control-X to exit REPL mode and return the shell. It may take a second or two before the REPL exits. If you provide a board-name then rshell will connect to that board, otherwise it will connect to the default board (first connected board). If you provide a tilde followed by a space (~ ) then anything after the tilde will be entered as if you typed it on the command line. If you want the repl to exit, end the line with the ~ character. For example, you could use: :: rshell.py repl ~ pyb.bootloader()~ and it will boot the pyboard into DFU. If you want to execute multiple Python commands these should be separated by the ~ character (not the ; character): :: rshell.py repl ~ import mymodule ~ mymodule.run() rm -- :: usage: rm [-f|--force] FILE... rm [-f|--force] PATTERN rm -r [-f|--force] PATTERN rm -r [-f|--force] [FILE|DIRECTORY]... Removes files or directories (including their contents). positional arguments: FILE File to remove DIRECTORY Directory to remove (-r required) PATTERN File matching pattern e.g. *.py optional arguments: -h, --help show this help message and exit -r, --recursive remove directories and their contents recursively -f, --force ignore nonexistent files and arguments A single pattern may be specified. Pattern matching is performed according to a subset of the Unix rules (see below). Directories can only be removed if the recursive argument is provided. Beware of rm -r * or worse. rsync ----- :: usage: rsync [-m|--mirror] [-n|--dry-run] [-q|--quiet] SRC_DIR DEST_DIR Recursively synchronises a source directory to a destination. Directories must exist. positional arguments: SRC_DIR Directory containing source files. DEST_DIR Directory for destination optional arguments: -h, --help show this help message and exit -m, --mirror remove files or directories from destination if absent from source. -n, --dry-run make no changes but report what would be done. Implies -v -q, --quiet don't report changes made. Synchronisation is performed by comparing the date and time of source and destination files. Files are copied if the source is newer than the destination. shell ----- The shell command can also be abbreviated using the exclamation point. :: shell some-command !some-command This will invoke a command, and return back to rshell. Example: :: !make deploy will flash the pyboard. Pattern Matching ================ This is performed according to a subset of the Unix rules. The limitations are that wildcards are only allowed in the rightmost directory of a path and curly bracket {} syntax is unsupported: :: *.py Match files in current directory with a .py extension temp/x[0-9]a.* Match temp/x1a.bmp but not temp/x00a.bmp t*/*.bmp Invalid: will produce an error message {*.doc,*.pdf} Invalid: will produce an error message Keywords: micropython shell Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Embedded Systems Classifier: Topic :: System :: Shells Classifier: Topic :: Terminals :: Serial Classifier: Topic :: Utilities ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/README.rst0000664010432500017510000003745600000000000014776 0ustar00dhylandsdhylandsrshell ========= Remote MicroPython shell. This is a simple shell which runs on the host and uses MicroPython's raw-REPL to send python snippets to the pyboard in order to get filesystem information, and to copy files to and from MicroPython's filesystem. It also has the ability to invoke the regular REPL, so rshell can be used as a terminal emulator as well. Note: With rshell you can disable USB Mass Storage and still copy files into and out of your pyboard. When using the commands, the /flash directory, and the /sdcard directory (if an sdcard is inserted) are considered to be on the pyboard, and all other directories are considered to be on the host. For an ESP based board you can only reference its directory by using the board name e.g. /pyboard etc.. NOTE: rshell requires a fairly recent version of the MicroPython firmware, specifically one which contains the ubinascii.unhexlify command which was added May 19, 2015 (v1.4.3-28-ga3a14b9 or newer). If your version of the firmware isn't new enough, then you'll see an error message something like this: :: >./rshell.py rshell needs MicroPython firmware with ubinascii.unhexlify Installation ============ You can install rshell using the command: :: sudo pip3 install rshell If you use a virtualenv, then you don't need the sudo. rshell needs Python3. All of my testing was done using version 3.4.0. Debian/Ubuntu users can get pip3 using: :: sudo apt-get install python3-pip Serial Port Permissions (linux) =============================== Under linux, the USB serial ports are owned by root and typically have a group of dialout (for ubuntu). Different unix distros may use a slightly different group name. In order for rshell (or any terminal emulator) to access the serial port, you'll need to ensure that your user account has sufficient priviledge. One way to do this is to ensure that your user is added to the dialout group, using a command like: :: sudo usermod -a -G dialout $USER Another way, is to use udev rules to modify the permissions on the serial port. There are some instructions on the [micropython wiki](https://github.com/micropython/micropython/wiki/Board-STM32F407-Discovery#programming-from-linux-via-dfu) that you can use to setup your udev rules (you don't need to install dfu-utils just to update the permissions). Sample Session ============== This shows a pyboard in its default state, copying a hello.py and then entering the repl and importing it. :: >rshell Welcome to rshell. Use Control-D to exit. /home/dhylands/Dropbox/micropython/rshell> ls -l /flash 529 May 21 17:34 README.txt 286 May 21 17:34 boot.py 34 May 21 17:34 main.py 2436 May 21 17:34 pybcdc.inf /home/dhylands/Dropbox/micropython/rshell> cp hello.py /flash /home/dhylands/Dropbox/micropython/rshell> ls -l /flash 529 May 21 17:34 README.txt 286 May 21 17:34 boot.py 21 May 21 17:35 hello.py 34 May 21 17:34 main.py 2436 May 21 17:34 pybcdc.inf /home/dhylands/Dropbox/micropython/rshell> cat /flash/hello.py print('Hello World') /home/dhylands/Dropbox/micropython/rshell> repl Entering REPL. Use Control-X to exit. Micro Python v1.4.3-28-ga3a14b9 on 2015-05-21; PYBv1.0 with STM32F405RG Type "help()" for more information. >>> >>> import hello Hello World >>> /home/dhylands/Dropbox/micropython/rshell> Command Line Options ==================== -h, --help ---------- Displays a list of the valid options. You should get something like the following displayed: :: usage: rshell [options] [command] Remote Shell for a MicroPython board. positional arguments: cmd Optional command to execute optional arguments: -h, --help show this help message and exit -b BAUD, --baud BAUD Set the baudrate used (default = 115200) --buffer-size BUFFER_SIZE Set the buffer size used for transfers (default = 512) -p PORT, --port PORT Set the serial port to use (default '/dev/ttyACM0') --rts RTS Set the RTS state (default '') --dtr DTR Set the DTR state (default '') -u USER, --user USER Set username to use (default 'micro') -w PASSWORD, --password PASSWORD Set password to use (default 'python') -e EDITOR, --editor EDITOR Set the editor to use (default 'vi') -f FILENAME, --file FILENAME Specifies a file of commands to process. -d, --debug Enable debug features -n, --nocolor Turn off colorized output --wait How long to wait for serial port --binary Enable binary file transfer --timing Print timing information about each command --quiet Turns off some output (useful for testing) You can specify the default serial port using the RSHELL_PORT environment variable. -b BAUD, --baud BAUD -------------------- Sets the baud rate to use when talking to the pyboard over a serial port. If no baud is specified, then the baudrate from the RSHELL_BAUD environment variable is used. If the RSHELL_BAUD environment variable is not defined then the default baudrate of 115200 is used. --buffer-size ------------- Sets the buffer size used when transferring files between the host and the pyboard. If no buffer size is specified, then the value from the RSHELL_BUFFER_SIZE environment variable is used. If the RSHELL_BUFFER_SIZE environment variable is not defined, then the default of 512 is used. -d, --debug ----------- Turns on debugging. This allows you to see the script which is sent over the raw REPL and the response received. -e EDITOR, --editor ------------------- Specifies the editor to use with the edit command. If no editor is specified, then the following environment variables will be searched: RSHELL_EDITOR, VISUAL, and EDITOR. If none of those environment variables is set then vi will be used. -f FILENAME, --file FILENAME ---------------------------- Specifies a file of rshell commands to process. This allows you to create a script which executes any valid rshell commands. -n, --nocolor ------------- By default, rshell uses ANSI color escape codes when displaying the prompt and ls output. This option allows colorized output to be disabled. -a --ascii ---------- On certain platforms the raw REPL mode is unreliable with particular sequences of binary characters. Specifying --ascii enables the transfer of binary files to such platforms. It does this by encoding the data as ASCII hex. --wait ------ If a port is specified defines how long rshell will wait for the port to exist and for a connection to be established. The default is 0 seconds specifying an immediate return. -p PORT, --port PORT -------------------- Specifies the serial port which should be used to talk to the MicroPython board. You can set the RSHELL\_PORT environment variable to specify the default port to be used, if --port is not specified on the command line. --dtr [0|1|True|False] ---------------------- Sets the state of the DTR line when opening the serial port. This may also be defaulted from the RSHELL_DTR environment variable. --rts [0|1|True|False] ---------------------- Sets the state of the RTS line when opening the serial port. This may also be defaulted from the RSHELL_RTS environment variable. --quiet ------- This option causes the Connecting messages printed when rshell starts to be suppressed. This is mostly useful for the test scripts. --timing -------- If the timing option is specified then rshell will print the amount of time that each command takes to execute. -u USER, --user USER -------------------- Specifies the username to use when logging into a WiPy over telnet. If no username is specified, then the username from the RSHELL_USER environment variable is used. If the RSHELL_USER environment variable doesn't exist then the default username 'micro' is used. -w PASSWORD, --password PASSWORD -------------------------------- Specified the password to use when logging into a WiPy over telnet. If no password is specified, then the password from the RSHELL_PASSWORD environment variable is used. If the RSHELL_PASSWORD environment variable doesn't exist then the default password 'python' is used. cmd --- If a command is specified, then that command will be executed and rshell will exit. Examples: :: rshell cp somefile.py /flash rshell repl ~ pyb.bootloader() ~ File System =========== rshell can be connected to multiple pyboards simultaneously. If the board module exists on the pyboard (i.e. a file named board.py somewhere in the module search path) and it contains an attribute called name (e.g. :code:`name = "myboard"`) then the pyboard will use that name. If the board module can't be imported then the board will be named, pyboard or wipy. Names will have -1 (or some other number) to make the board name unique. You can access the internal flash on the first board connected using /flash and the sd card on the first board connected can be accessed using /sd. For all other connected pyboards, you can use /board-name/flash or /board-name/sd (you can see the board names using the boards command). The boards command will show all of the connected pyboards, along with all of the directories which map onto that pyboard. Commands ======== args ---- :: args [arguments...] Debug function for verifying argument parsing. This function just prints out each argument that it receives. boards ------ :: boards Lists all of the boards that rshell is currently connected to, their names, and the connection. You can give a custom name to a board with either copying over a :code:`board.py` file or using the :code:`echo` command, e.g. :: echo 'name="myboard"' > /pyboard/board.py (Remember to exit rshell and re-enter to see the change). cat --- :: cat FILENAME... Concatenates files and sends to stdout. cd -- :: cd DIRECTORY Changes the current directory. ~ expansion is supported, and cd - goes to the previous directory. connect ------- :: connect TYPE TYPE_PARAMS connect serial port [baud] connect telnet ip-address-or-name Connects a pyboard to rshell. rshell can be connected to multiple pyboards simultaneously. cp -- :: usage: cp SOURCE DEST cp SOURCE... DIRECTORY cp [-r|--recursive] [SOURCE|SRC_DIR]... DIRECTORY cp [-r|--recursive] PATTERN DIRECTORY positional arguments: DEST A destination file SOURCE File to copy SRC_DIR Directory to copy PATTERN File or directory pattern match string e.g. foo/*.py optional arguments: -h, --help show this help message and exit -r, --recursive copy directories recursively Copies the SOURCE file to DEST. DEST may be a filename or a directory name. If more than one source file is specified, then the destination should be a directory. Directories will only be copied if -r is specified. A single pattern may be specified, in which case the destination should be a directory. Pattern matching is performed according to a subset of the Unix rules (see below). Recursive copying uses rsync (see below): where a file exists on source and destination, it will only be copied if the source is newer than the destination. echo ---- :: echo TEXT... Display a line of text. edit ---- :: edit filename If the file is on a pyboard, it copies the file to host, invokes an editor and if any changes were made to the file, it copies it back to the pyboard. The editor which is used defaults to vi, but can be overridden using either the --editor command line option when rshell.py is invoked, or by using the RSHELL\_EDITOR, VISUAL or EDITOR environment variables (they are tried in the order listed). filesize -------- :: filesize FILE Prints the size of the file, in bytes. This function is primarily testing. filetype -------- :: filetype FILE Prints the type of file (dir or file). This function is primarily for testing. help ---- :: help [COMMAND] List available commands with no arguments, or detailed help when a command is provided. ls -- :: usage: ls [-a] [-l] [FILE|DIRECTORY|PATTERN]... List directory contents. positional arguments: FILE File to list (show absolute path) DIRECTORY Directory (list contents) PATTERN File or directory pattern match string e.g. foo/*.py optional arguments: -h, --help show this help message and exit -a, --all do not ignore hidden files -l, --long use a long listing format Pattern matching is performed according to a subset of the Unix rules (see below). mkdir ----- :: mkdir DIRECTORY... Creates one or more directories. repl ---- :: repl [board-name] [~ line][ ~] Enters into the regular REPL with the MicroPython board. Use Control-X to exit REPL mode and return the shell. It may take a second or two before the REPL exits. If you provide a board-name then rshell will connect to that board, otherwise it will connect to the default board (first connected board). If you provide a tilde followed by a space (~ ) then anything after the tilde will be entered as if you typed it on the command line. If you want the repl to exit, end the line with the ~ character. For example, you could use: :: rshell.py repl ~ pyb.bootloader()~ and it will boot the pyboard into DFU. If you want to execute multiple Python commands these should be separated by the ~ character (not the ; character): :: rshell.py repl ~ import mymodule ~ mymodule.run() rm -- :: usage: rm [-f|--force] FILE... rm [-f|--force] PATTERN rm -r [-f|--force] PATTERN rm -r [-f|--force] [FILE|DIRECTORY]... Removes files or directories (including their contents). positional arguments: FILE File to remove DIRECTORY Directory to remove (-r required) PATTERN File matching pattern e.g. *.py optional arguments: -h, --help show this help message and exit -r, --recursive remove directories and their contents recursively -f, --force ignore nonexistent files and arguments A single pattern may be specified. Pattern matching is performed according to a subset of the Unix rules (see below). Directories can only be removed if the recursive argument is provided. Beware of rm -r * or worse. rsync ----- :: usage: rsync [-m|--mirror] [-n|--dry-run] [-q|--quiet] SRC_DIR DEST_DIR Recursively synchronises a source directory to a destination. Directories must exist. positional arguments: SRC_DIR Directory containing source files. DEST_DIR Directory for destination optional arguments: -h, --help show this help message and exit -m, --mirror remove files or directories from destination if absent from source. -n, --dry-run make no changes but report what would be done. Implies -v -q, --quiet don't report changes made. Synchronisation is performed by comparing the date and time of source and destination files. Files are copied if the source is newer than the destination. shell ----- The shell command can also be abbreviated using the exclamation point. :: shell some-command !some-command This will invoke a command, and return back to rshell. Example: :: !make deploy will flash the pyboard. Pattern Matching ================ This is performed according to a subset of the Unix rules. The limitations are that wildcards are only allowed in the rightmost directory of a path and curly bracket {} syntax is unsupported: :: *.py Match files in current directory with a .py extension temp/x[0-9]a.* Match temp/x1a.bmp but not temp/x00a.bmp t*/*.bmp Invalid: will produce an error message {*.doc,*.pdf} Invalid: will produce an error message ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640889926.1680648 rshell-0.0.31/rshell/0000775010432500017510000000000000000000000014561 5ustar00dhylandsdhylands././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/rshell/__init__.py0000664010432500017510000000000000000000000016660 0ustar00dhylandsdhylands././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/rshell/command_line.py0000664010432500017510000000006700000000000017563 0ustar00dhylandsdhylandsimport rshell.main def main(): rshell.main.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/rshell/getch.py0000664010432500017510000000606000000000000016227 0ustar00dhylandsdhylands#!/usr/bin/env python3 """Implements getch. From: https://gist.github.com/chao787/2652257""" # Last modified: <2012-05-10 18:04:45 Thursday by richard> # @version 0.1 # @author : Richard Wong # Email: chao787@gmail.com class _Getch: """ Gets a single character from standard input. Does not echo to the screen. """ def __init__(self): try: self.impl = _GetchWindows() except ImportError: try: self.impl = _GetchMacCarbon() except(AttributeError, ImportError): self.impl = _GetchUnix() def __call__(self): return self.impl() class _GetchUnix: def __init__(self): import tty, sys def __call__(self): import sys, tty, termios fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.buffer.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch class _GetchWindows: def __init__(self): import msvcrt # Windows sends 0xe0 followed by a single letter for many of the # special keys (like left/right arrow) so we map these to the # characters that MicroPythons readline function will work with. self.keymap = { b'G': b'\x01', # Control-A Home b'K': b'\x02', # Control-B Left Arrow b'S': b'\x04', # Control-D DEL b'O': b'\x05', # Control-E End b'M': b'\x06', # Control-F Right Arrow b'P': b'\x0e', # Control-N Down Arrow (Next line in history) b'H': b'\x10', # Control-P Up Arrow (Prev line in history) } def __call__(self): import msvcrt ch = msvcrt.getch() if ch == b'\xe0': ch = msvcrt.getch() if ch in self.keymap: ch = self.keymap[ch] return ch class _GetchMacCarbon: """ A function which returns the current ASCII key that is down; if no ASCII key is down, the null string is returned. The page http://www.mactech.com/macintosh-c/chap02-1.html was very helpful in figuring out how to do this. """ def __init__(self): import Carbon Carbon.Evt #see if it has this (in Unix, it doesn't) def __call__(self): import Carbon if Carbon.Evt.EventAvail(0x0008)[0]==0: # 0x0008 is the keyDownMask return '' else: # # The event contains the following info: # (what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1] # # The message (msg) contains the ASCII char which is # extracted with the 0x000000FF charCodeMask; this # number is converted to an ASCII character with chr() and # returned # (what,msg,when,where,mod)=Carbon.Evt.GetNextEvent(0x0008)[1] return chr(msg & 0x000000FF) getch = _Getch() if __name__ == "__main__": ch = getch() print('ch =', ch) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889485.0 rshell-0.0.31/rshell/main.py0000775010432500017510000032211100000000000016062 0ustar00dhylandsdhylands#!/usr/bin/env python3 """Implement a remote shell which talks to a MicroPython board. This program uses the raw-repl feature of the pyboard to send small programs to the pyboard to carry out the required tasks. """ # Take a look at https://repolinux.wordpress.com/2012/10/09/non-blocking-read-from-stdin-in-python/ # to see if we can use those ideas here. # from __future__ import print_function # To run rshell from the git repository, cd into the top level rshell directory # and run: # python3 -m rshell.main # # that sets things up so that the "from rshell.xxx" will import from the git # tree and not from some installed version. import sys try: from rshell.getch import getch from rshell.pyboard import Pyboard, PyboardError from rshell.version import __version__ except ImportError as err: print('sys.path =', sys.path) raise err if sys.platform == 'win32': # This is a workaround for Windows 10/Python 3.7, that allows the colorized output to # work. See: https://stackoverflow.com/questions/12492810/python-how-can-i-make-the-ansi-escape-codes-to-work-also-in-windows import subprocess subprocess.call('', shell=True) import argparse import binascii import calendar import cmd import inspect import io import fnmatch import os import re import select import serial import shutil import socket import tempfile import time import threading import token import tokenize import shlex import itertools from serial.tools import list_ports import traceback if sys.platform == 'win32': EXIT_STR = 'Use the exit command to exit rshell.' else: EXIT_STR = 'Use Control-D (or the exit command) to exit rshell.' # I got the following from: http://www.farmckon.net/2009/08/rlcompleter-how-do-i-get-it-to-work/ # Under OSX, if you call input with a prompt which contains ANSI escape # sequences for colors, and readline is installed, then the escape sequences # do not get rendered properly as colors. # # One solution would be to not use readline, but then you'd lose TAB completion. # So I opted to print the colored prompt before calling input, which makes # things work most of the time. If you try to backspace when at the first # column of the input it wipes out the prompt, but everything returns to normal # if you hit return. BROKEN_READLINE = True FAKE_INPUT_PROMPT = False import readline import rlcompleter if readline.__doc__ and 'libedit' in readline.__doc__: readline.parse_and_bind ("bind ^I rl_complete") BROKEN_READLINE = True else: readline.parse_and_bind("tab: complete") MONTH = ('', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') # Attributes # 0 Reset all attributes # 1 Bright # 2 Dim # 4 Underscore # 5 Blink # 7 Reverse # 8 Hidden LT_BLACK = "\x1b[1;30m" LT_RED = "\x1b[1;31m" LT_GREEN = "\x1b[1;32m" LT_YELLOW = "\x1b[1;33m" LT_BLUE = "\x1b[1;34m" LT_MAGENTA = "\x1b[1;35m" LT_CYAN = "\x1b[1;36m" LT_WHITE = "\x1b[1;37m" DK_BLACK = "\x1b[2;30m" DK_RED = "\x1b[2;31m" DK_GREEN = "\x1b[2;32m" DK_YELLOW = "\x1b[2;33m" DK_BLUE = "\x1b[2;34m" DK_MAGENTA = "\x1b[2;35m" DK_CYAN = "\x1b[2;36m" DK_WHITE = "\x1b[2;37m" NO_COLOR = "\x1b[0m" BG_LT_BLACK = "\x1b[1;40m" BG_LT_RED = "\x1b[1;41m" BG_LT_GREEN = "\x1b[1;42m" BG_LT_YELLOW = "\x1b[1;43m" BG_LT_BLUE = "\x1b[1;44m" BG_LT_MAGENTA = "\x1b[1;45m" BG_LT_CYAN = "\x1b[1;46m" BG_LT_WHITE = "\x1b[1;47m" BG_DK_BLACK = "\x1b[2;40m" BG_DK_RED = "\x1b[2;41m" BG_DK_GREEN = "\x1b[2;42m" BG_DK_YELLOW = "\x1b[2;43m" BG_DK_BLUE = "\x1b[2;44m" BG_DK_MAGENTA = "\x1b[2;45m" BG_DK_CYAN = "\x1b[2;46m" BG_DK_WHITE = "\x1b[2;47m" DIR_COLOR = LT_CYAN PROMPT_COLOR = LT_GREEN PY_COLOR = DK_GREEN END_COLOR = NO_COLOR cur_dir = '' HAS_BUFFER = False IS_UPY = False DEBUG = False USB_BUFFER_SIZE = 512 RPI_PICO_USB_BUFFER_SIZE = 32 UART_BUFFER_SIZE = 32 BUFFER_SIZE = USB_BUFFER_SIZE QUIET = False RTS = '' DTR = '' # It turns out that just because pyudev is installed doesn't mean that # it can actually be used. So we only bother to try if we're running # under linux. # # When running under WSL, sys.platform returns 'linux' so we do a further check # on 'Microsoft' in platform.uname().release to detect if we're running under WSL. # Currently, there is no serial port enumeration availbale under WSL. import platform USE_AUTOCONNECT = sys.platform == 'linux' and 'Microsoft' not in platform.uname().release SIX_MONTHS = 183 * 24 * 60 * 60 QUIT_REPL_CHAR = 'X' QUIT_REPL_BYTE = bytes((ord(QUIT_REPL_CHAR) - ord('@'),)) # Control-X # DELIMS is used by readline for determining word boundaries. DELIMS = ' \t\n>;' TIME_OFFSET = 0 DEVS = [] DEFAULT_DEV = None DEV_IDX = 1 DEV_LOCK = threading.RLock() def add_device(dev): """Adds a device to the list of devices we know about.""" global DEV_IDX, DEFAULT_DEV with DEV_LOCK: for idx in range(len(DEVS)): test_dev = DEVS[idx] if test_dev.dev_name_short == dev.dev_name_short: # This device is already in our list. Delete the old one if test_dev is DEFAULT_DEV: DEFAULT_DEV = None del DEVS[idx] break if find_device_by_name(dev.name): # This name is taken - make it unique dev.name += '-%d' % DEV_IDX dev.name_path = '/' + dev.name + '/' DEVS.append(dev) DEV_IDX += 1 if DEFAULT_DEV is None: DEFAULT_DEV = dev def find_device_by_name(name): """Tries to find a board by board name.""" if not name: return DEFAULT_DEV with DEV_LOCK: for dev in DEVS: if dev.name == name: return dev return None def find_serial_device_by_port(port): """Tries to find a board by port name.""" with DEV_LOCK: for dev in DEVS: if dev.is_serial_port(port): return dev return None def num_devices(): with DEV_LOCK: return len(DEVS) def is_micropython_usb_device(port): """Checks a USB device to see if it looks like a MicroPython device. """ if type(port).__name__ == 'Device': # Assume its a pyudev.device.Device if ('ID_BUS' not in port or port['ID_BUS'] != 'usb' or 'SUBSYSTEM' not in port or port['SUBSYSTEM'] != 'tty'): return False usb_id = 'usb vid:pid={}:{}'.format(port['ID_VENDOR_ID'], port['ID_MODEL_ID']) else: # Assume its a port from serial.tools.list_ports.comports() usb_id = port[2].lower() # We don't check the last digit of the PID since there are 3 possible # values. if usb_id.startswith('usb vid:pid=f055:980'): return True # Check Raspberry Pi Pico if usb_id.startswith('usb vid:pid=2e8a:0005'): global USB_BUFFER_SIZE USB_BUFFER_SIZE = RPI_PICO_USB_BUFFER_SIZE return True # Check for Teensy VID:PID if usb_id.startswith('usb vid:pid=16c0:0483'): return True # Check for LEGO Technic Large Hub if usb_id.startswith('usb vid:pid=0694:0010'): return True return False def is_micropython_usb_port(portName): """Checks to see if the indicated portname is a MicroPython device or not. """ for port in serial.tools.list_ports.comports(): if port.device == portName: return is_micropython_usb_device(port) return False def autoconnect(): """Sets up a thread to detect when USB devices are plugged and unplugged. If the device looks like a MicroPython board, then it will automatically connect to it. """ if not USE_AUTOCONNECT: return try: import pyudev except ImportError: return context = pyudev.Context() monitor = pyudev.Monitor.from_netlink(context) connect_thread = threading.Thread(target=autoconnect_thread, args=(monitor,), name='AutoConnect') connect_thread.daemon = True connect_thread.start() def autoconnect_thread(monitor): """Thread which detects USB Serial devices connecting and disconnecting.""" monitor.start() monitor.filter_by('tty') epoll = select.epoll() epoll.register(monitor.fileno(), select.POLLIN) while True: try: events = epoll.poll() except InterruptedError: continue for fileno, _ in events: if fileno == monitor.fileno(): usb_dev = monitor.poll() print('autoconnect: {} action: {}'.format(usb_dev.device_node, usb_dev.action)) dev = find_serial_device_by_port(usb_dev.device_node) if usb_dev.action == 'add': # Try connecting a few times. Sometimes the serial port # reports itself as busy, which causes the connection to fail. for i in range(8): if dev: connected = connect_serial(dev.port, dev.baud, dev.wait) elif is_micropython_usb_device(usb_dev): connected = connect_serial(usb_dev.device_node) else: connected = False if connected: break time.sleep(0.25) elif usb_dev.action == 'remove': print('') print("USB Serial device '%s' disconnected" % usb_dev.device_node) if dev: dev.close() break def autoscan(): """autoscan will check all of the serial ports to see if they have a matching VID:PID for a MicroPython board. """ for port in serial.tools.list_ports.comports(): if is_micropython_usb_device(port): connect_serial(port[0]) def extra_info(port): """Collects the serial nunber and manufacturer into a string, if the fields are available.""" extra_items = [] if port.manufacturer: extra_items.append("vendor '{}'".format(port.manufacturer)) if port.serial_number: extra_items.append("serial '{}'".format(port.serial_number)) if port.interface: extra_items.append("intf '{}'".format(port.interface)) if extra_items: return ' with ' + ' '.join(extra_items) return '' def listports(): """listports will display a list of all of the serial ports. """ detected = False for port in serial.tools.list_ports.comports(): detected = True if port.vid: micropythonPort = '' if is_micropython_usb_device(port): micropythonPort = ' *' print('USB Serial Device {:04x}:{:04x}{} found @{}{}\r'.format( port.vid, port.pid, extra_info(port), port.device, micropythonPort)) else: print('Serial Device:', port.device) if not detected: print('No serial devices detected') def escape(str): """Precede all special characters with a backslash.""" out = '' for char in str: if char in '\\ ': out += '\\' out += char return out def unescape(str): """Undoes the effects of the escape() function.""" out = '' prev_backslash = False for char in str: if not prev_backslash and char == '\\': prev_backslash = True continue out += char prev_backslash = False return out def align_cell(fmt, elem, width): """Returns an aligned element.""" if fmt == "<": return elem + ' ' * (width - len(elem)) if fmt == ">": return ' ' * (width - len(elem)) + elem return elem def column_print(fmt, rows, print_func): """Prints a formatted list, adjusting the width so everything fits. fmt contains a single character for each column. < indicates that the column should be left justified, > indicates that the column should be right justified. The last column may be a space which implies left justification and no padding. """ # Figure out the max width of each column num_cols = len(fmt) width = [max(0 if isinstance(row, str) else len(row[i]) for row in rows) for i in range(num_cols)] for row in rows: if isinstance(row, str): # Print a separator line print_func(' '.join([row * width[i] for i in range(num_cols)])) else: print_func(' '.join([align_cell(fmt[i], row[i], width[i]) for i in range(num_cols)])) def find_macthing_files(match): """Finds all of the files which match (used for completion).""" last_slash = match.rfind('/') if last_slash == -1: dirname = '.' match_prefix = match result_prefix = '' else: dirname = match[0:last_slash] match_prefix = match[last_slash + 1:] result_prefix = dirname + '/' return [result_prefix + filename for filename in os.listdir(dirname) if filename.startswith(match_prefix)] def print_err(*args, end='\n'): """Similar to print, but prints to stderr. """ print(*args, end=end, file=sys.stderr) sys.stderr.flush() def is_pattern(s): """Return True if a string contains Unix wildcard pattern characters. """ return not set('*?[{').intersection(set(s)) == set() # Disallow patterns like path/t*/bar* because handling them on remote # system is difficult without the glob library. def parse_pattern(s): """Parse a string such as 'foo/bar/*.py' Assumes is_pattern(s) has been called and returned True 1. directory to process 2. pattern to match""" if '{' in s: return None, None # Unsupported by fnmatch if s and s[0] == '~': s = os.path.expanduser(s) parts = s.split('/') absolute = len(parts) > 1 and not parts[0] if parts[-1] == '': # # Outcome of trailing / parts = parts[:-1] # discard if len(parts) == 0: directory = '' pattern = '' else: directory = '/'.join(parts[:-1]) pattern = parts[-1] if not is_pattern(directory): # Check for e.g. /abc/*/def if is_pattern(pattern): if not directory: directory = '/' if absolute else '.' return directory, pattern return None, None # Invalid or nonexistent pattern def validate_pattern(fn): """On success return an absolute path and a pattern. Otherwise print a message and return None, None """ directory, pattern = parse_pattern(fn) if directory is None: print_err("Invalid pattern {}.".format(fn)) return None, None target = resolve_path(directory) mode = auto(get_mode, target) if not mode_exists(mode): print_err("cannot access '{}': No such file or directory".format(fn)) return None, None if not mode_isdir(mode): print_err("cannot access '{}': Not a directory".format(fn)) return None, None return target, pattern def process_pattern(fn): """Return a list of paths matching a pattern (or None on error). """ directory, pattern = validate_pattern(fn) if directory is not None: filenames = fnmatch.filter(auto(listdir, directory), pattern) if filenames: return [directory + '/' + sfn for sfn in filenames] else: print_err("cannot access '{}': No such file or directory".format(fn)) def resolve_path(path): """Resolves path and converts it into an absolute path.""" if path[0] == '~': # ~ or ~user path = os.path.expanduser(path) if path[0] != '/': # Relative path if cur_dir[-1] == '/': path = cur_dir + path else: path = cur_dir + '/' + path comps = path.split('/') new_comps = [] for comp in comps: # We strip out xxx/./xxx and xxx//xxx, except that we want to keep the # leading / for absolute paths. This also removes the trailing slash # that autocompletion adds to a directory. if comp == '.' or (comp == '' and len(new_comps) > 0): continue if comp == '..': if len(new_comps) > 1: new_comps.pop() else: new_comps.append(comp) if len(new_comps) == 1 and new_comps[0] == '': return '/' return '/'.join(new_comps) def get_dev_and_path(filename): """Determines if a given file is located locally or remotely. We assume that any directories from the pyboard take precedence over local directories of the same name. /flash and /sdcard are associated with the default device. /dev_name/path where dev_name is the name of a given device is also considered to be associated with the named device. If the file is associated with a remote device, then this function returns a tuple (dev, dev_filename) where dev is the device and dev_filename is the portion of the filename relative to the device. If the file is not associated with the remote device, then the dev portion of the returned tuple will be None. """ if DEFAULT_DEV: if DEFAULT_DEV.is_root_path(filename): return (DEFAULT_DEV, filename) test_filename = filename + '/' with DEV_LOCK: for dev in DEVS: if test_filename.startswith(dev.name_path): dev_filename = filename[len(dev.name_path)-1:] if dev_filename == '': dev_filename = '/' return (dev, dev_filename) return (None, filename) def remote_repr(i): """Helper function to deal with types which we can't send to the pyboard.""" repr_str = repr(i) if repr_str and repr_str[0] == '<': return 'None' return repr_str def print_bytes(byte_str): """Prints a string or converts bytes to a string and then prints.""" if isinstance(byte_str, str): print(byte_str) else: print(str(byte_str, encoding='utf8')) def extra_funcs(*funcs): """Decorator which adds extra functions to be downloaded to the pyboard.""" def extra_funcs_decorator(real_func): def wrapper(*args, **kwargs): return real_func(*args, **kwargs) wrapper.extra_funcs = list(funcs) wrapper.source = inspect.getsource(real_func) wrapper.name = real_func.__name__ return wrapper return extra_funcs_decorator def auto(func, filename, *args, **kwargs): """If `filename` is a remote file, then this function calls func on the micropython board, otherwise it calls it locally. """ dev, dev_filename = get_dev_and_path(filename) if dev is None: if len(dev_filename) > 0 and dev_filename[0] == '~': dev_filename = os.path.expanduser(dev_filename) return func(dev_filename, *args, **kwargs) return dev.remote_eval(func, dev_filename, *args, **kwargs) def board_name(default): """Returns the boards name (if available).""" try: import board try: name = board.name except AttributeError: # There was a board.py file, but it didn't have an name attribute # We also ignore this as an error name = default except ImportError: # No board.py file on the pyboard - not an error name = default except BaseException as err: print('Error encountered executing board.py') import sys sys.print_exception(err) name = default return repr(name) def cat(src_filename, dst_file): """Copies the contents of the indicated file to an already opened file.""" (dev, dev_filename) = get_dev_and_path(src_filename) if dev is None: with open(dev_filename, 'rb') as txtfile: for line in txtfile: dst_file.write(line) else: filesize = dev.remote_eval(get_filesize, dev_filename) return dev.remote(send_file_to_host, dev_filename, dst_file, filesize, xfer_func=recv_file_from_remote) def chdir(dirname): """Changes the current working directory.""" import os os.chdir(dirname) def copy_file(src_filename, dst_filename): """Copies a file from one place to another. Both the source and destination files must exist on the same machine. """ try: with open(src_filename, 'rb') as src_file: with open(dst_filename, 'wb') as dst_file: while True: buf = src_file.read(BUFFER_SIZE) if len(buf) > 0: dst_file.write(buf) if len(buf) < BUFFER_SIZE: break return True except: return False def cp(src_filename, dst_filename): """Copies one file to another. The source file may be local or remote and the destination file may be local or remote. """ src_dev, src_dev_filename = get_dev_and_path(src_filename) dst_dev, dst_dev_filename = get_dev_and_path(dst_filename) if src_dev is dst_dev: # src and dst are either on the same remote, or both are on the host return auto(copy_file, src_filename, dst_dev_filename) filesize = auto(get_filesize, src_filename) if dst_dev is None: # Copying from remote to host with open(dst_dev_filename, 'wb') as dst_file: return src_dev.remote(send_file_to_host, src_dev_filename, dst_file, filesize, xfer_func=recv_file_from_remote) if src_dev is None: # Copying from host to remote with open(src_dev_filename, 'rb') as src_file: return dst_dev.remote(recv_file_from_host, src_file, dst_dev_filename, filesize, xfer_func=send_file_to_remote) # Copying from remote A to remote B. We first copy the file # from remote A to the host and then from the host to remote B host_temp_file = tempfile.TemporaryFile() if src_dev.remote(send_file_to_host, src_dev_filename, host_temp_file, filesize, xfer_func=recv_file_from_remote): host_temp_file.seek(0) return dst_dev.remote(recv_file_from_host, host_temp_file, dst_dev_filename, filesize, xfer_func=send_file_to_remote) return False def date(): import time tm = time.localtime() dow = ('Mon', 'Tue', 'Web', 'Thu', 'Fri', 'Sat', 'Sun') mon = ('???', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') return repr('{} {} {:2d} {:02d}:{:02d}:{:02d} {}'.format(dow[tm[6]], mon[tm[1]], tm[2], tm[3], tm[4], tm[5], tm[0])) def eval_str(string): """Executes a string containing python code.""" output = eval(string) return output def get_filesize(filename): """Returns the size of a file, in bytes.""" import os try: # Since this function runs remotely, it can't depend on other functions, # so we can't call stat_mode. return os.stat(filename)[6] except OSError: return -1 def get_mode(filename): """Returns the mode of a file, which can be used to determine if a file exists, if a file is a file or a directory. """ import os try: # Since this function runs remotely, it can't depend on other functions, # so we can't call stat_mode. return os.stat(filename)[0] except OSError: return 0 def lstat(filename): """Returns os.lstat for a given file, adjusting the timestamps as appropriate. This function will not follow symlinks.""" import os try: # on the host, lstat won't try to follow symlinks rstat = os.lstat(filename) except: rstat = os.stat(filename) return rstat[:7] + tuple(tim + TIME_OFFSET for tim in rstat[7:]) def stat(filename): """Returns os.stat for a given file, adjusting the timestamps as appropriate.""" import os rstat = os.stat(filename) return rstat[:7] + tuple(tim + TIME_OFFSET for tim in rstat[7:]) def sysname(): """Returns the os.uname().sysname field.""" try: import os return repr(os.uname().sysname) except: return repr('unknown') def is_visible(filename): """Determines if the file should be considered to be a non-hidden file.""" return filename[0] != '.' and filename[-1] != '~' @extra_funcs(stat) def get_stat(filename): """Returns the stat array for a given file. Returns all 0's if the file doesn't exist. """ try: return stat(filename) except OSError: return (0,) * 10 @extra_funcs(lstat) def get_lstat(filename): """Returns the stat array for a given file. Returns all 0's if the file doesn't exist. """ try: return lstat(filename) except OSError: return (0,) * 10 def listdir(dirname): """Returns a list of filenames contained in the named directory.""" import os return os.listdir(dirname) def listdir_matches(match): """Returns a list of filenames contained in the named directory. Only filenames which start with `match` will be returned. Directories will have a trailing slash. """ import os last_slash = match.rfind('/') if last_slash == -1: dirname = '.' match_prefix = match result_prefix = '' else: match_prefix = match[last_slash + 1:] if last_slash == 0: dirname = '/' result_prefix = '/' else: dirname = match[0:last_slash] result_prefix = dirname + '/' def add_suffix_if_dir(filename): try: if (os.stat(filename)[0] & 0x4000) != 0: return filename + '/' except FileNotFoundError: # This can happen when a symlink points to a non-existant file. pass return filename matches = [add_suffix_if_dir(result_prefix + filename) for filename in os.listdir(dirname) if filename.startswith(match_prefix)] return matches @extra_funcs(is_visible, lstat) def listdir_lstat(dirname, show_hidden=True): """Returns a list of tuples for each file contained in the named directory, or None if the directory does not exist. Each tuple contains the filename, followed by the tuple returned by calling os.stat on the filename. """ import os try: files = os.listdir(dirname) except OSError: return None if dirname == '/': return list((file, lstat('/' + file)) for file in files if is_visible(file) or show_hidden) return list((file, lstat(dirname + '/' + file)) for file in files if is_visible(file) or show_hidden) @extra_funcs(is_visible, stat) def listdir_stat(dirname, show_hidden=True): """Returns a list of tuples for each file contained in the named directory, or None if the directory does not exist. Each tuple contains the filename, followed by the tuple returned by calling os.stat on the filename. """ import os try: files = os.listdir(dirname) except OSError: return None if dirname == '/': return list((file, stat('/' + file)) for file in files if is_visible(file) or show_hidden) return list((file, stat(dirname + '/' + file)) for file in files if is_visible(file) or show_hidden) def make_directory(dirname): """Creates one or more directories.""" import os try: os.mkdir(dirname) except: return False return True def mkdir(filename): """Creates a directory.""" return auto(make_directory, filename) def remove_file(filename, recursive=False, force=False): """Removes a file or directory.""" import os try: mode = os.stat(filename)[0] if mode & 0x4000 != 0: # directory if recursive: for file in os.listdir(filename): success = remove_file(filename + '/' + file, recursive, force) if not success and not force: return False os.rmdir(filename) # PGH Work like Unix: require recursive else: if not force: return False else: os.remove(filename) except: if not force: return False return True def rm(filename, recursive=False, force=False): """Removes a file or directory tree.""" return auto(remove_file, filename, recursive, force) def make_dir(dst_dir, dry_run, print_func, recursed): """Creates a directory. Produces information in case of dry run. Issues error where necessary. """ parent = os.path.split(dst_dir.rstrip('/'))[0] # Check for nonexistent parent parent_files = auto(listdir_lstat, parent) if parent else True # Relative dir if dry_run: if recursed: # Assume success: parent not actually created yet print_func("Creating directory {}".format(dst_dir)) elif parent_files is None: print_func("Unable to create {}".format(dst_dir)) return True if not mkdir(dst_dir): print_err("Unable to create {}".format(dst_dir)) return False return True def rsync(src_dir, dst_dir, mirror, dry_run, print_func, recursed, sync_hidden): """Synchronizes 2 directory trees.""" # This test is a hack to avoid errors when accessing /flash. When the # cache synchronisation issue is solved it should be removed if not isinstance(src_dir, str) or not len(src_dir): return sstat = auto(get_stat, src_dir) smode = stat_mode(sstat) if mode_isfile(smode): print_err('Source {} is a file not a directory.'.format(src_dir)) return d_src = {} # Look up stat tuple from name in current directory src_files = auto(listdir_stat, src_dir, show_hidden=sync_hidden) if src_files is None: print_err('Source directory {} does not exist.'.format(src_dir)) return for name, stat in src_files: d_src[name] = stat d_dst = {} dst_files = auto(listdir_stat, dst_dir, show_hidden=sync_hidden) if dst_files is None: # Directory does not exist if not make_dir(dst_dir, dry_run, print_func, recursed): return else: # dest exists for name, stat in dst_files: d_dst[name] = stat set_dst = set(d_dst.keys()) set_src = set(d_src.keys()) to_add = set_src - set_dst # Files to copy to dest to_del = set_dst - set_src # To delete from dest to_upd = set_dst.intersection(set_src) # In both: may need updating for src_basename in to_add: # Name in source but absent from destination src_filename = src_dir + '/' + src_basename dst_filename = dst_dir + '/' + src_basename print_func("Adding %s" % dst_filename) src_stat = d_src[src_basename] src_mode = stat_mode(src_stat) if not dry_run: if not mode_isdir(src_mode): cp(src_filename, dst_filename) if mode_isdir(src_mode): rsync(src_filename, dst_filename, mirror=mirror, dry_run=dry_run, print_func=print_func, recursed=True, sync_hidden=sync_hidden) if mirror: # May delete for dst_basename in to_del: # In dest but not in source dst_filename = dst_dir + '/' + dst_basename print_func("Removing %s" % dst_filename) if not dry_run: rm(dst_filename, recursive=True, force=True) for src_basename in to_upd: # Names are identical src_stat = d_src[src_basename] dst_stat = d_dst[src_basename] src_filename = src_dir + '/' + src_basename dst_filename = dst_dir + '/' + src_basename src_mode = stat_mode(src_stat) dst_mode = stat_mode(dst_stat) if mode_isdir(src_mode): if mode_isdir(dst_mode): # src and dst are both directories - recurse rsync(src_filename, dst_filename, mirror=mirror, dry_run=dry_run, print_func=print_func, recursed=True, sync_hidden=sync_hidden) else: msg = "Source '{}' is a directory and destination " \ "'{}' is a file. Ignoring" print_err(msg.format(src_filename, dst_filename)) else: if mode_isdir(dst_mode): msg = "Source '{}' is a file and destination " \ "'{}' is a directory. Ignoring" print_err(msg.format(src_filename, dst_filename)) else: print_func('Checking {}'.format(dst_filename)) if stat_mtime(src_stat) > stat_mtime(dst_stat): msg = "{} is newer than {} - copying" print_func(msg.format(src_filename, dst_filename)) if not dry_run: cp(src_filename, dst_filename) # rtc_time[0] - year 4 digit # rtc_time[1] - month 1..12 # rtc_time[2] - day 1..31 # rtc_time[3] - weekday 1..7 1=Monday # rtc_time[4] - hour 0..23 # rtc_time[5] - minute 0..59 # rtc_time[6] - second 0..59 # rtc_time[7] - yearday 1..366 # rtc_time[8] - isdst 0, 1, or -1 def set_time(rtc_time): rtc = None try: # Pyboard (pyboard doesn't have machine.RTC()). # The pyb.RTC.datetime function takes the arguments in the order: # (year, month, day, weekday, hour, minute, second, subseconds) # http://docs.micropython.org/en/latest/library/pyb.RTC.html#pyb.RTC.datetime import pyb rtc = pyb.RTC() rtc.datetime(rtc_time) except: try: import pycom # PyCom's machine.RTC takes its arguments in a slightly different order # than the official machine.RTC. # (year, month, day, hour, minute, second[, microsecond[, tzinfo]]) # https://docs.pycom.io/firmwareapi/pycom/machine/rtc/#rtc-init-datetime-none-source-rtc-internal-rc rtc_time2 = (rtc_time[0], rtc_time[1], rtc_time[2], rtc_time[4], rtc_time[5], rtc_time[6]) import machine rtc = machine.RTC() rtc.init(rtc_time2) except: try: # The machine.RTC documentation was incorrect and doesn't agree with the code, so no link # is presented here. The order of the arguments is the same as the pyboard. import machine rtc = machine.RTC() try: # ESP8266 uses rtc.datetime() rather than rtc.init() rtc.datetime(rtc_time) except: # ESP32 (at least Loboris port) uses rtc.init() rtc.init(rtc_time) except: # Check for the Raspberry Pi Pico - machine.RTC doesn't exist try: import os if os.uname().sysname == 'rp2': setup_0 = rtc_time[0] << 12 | rtc_time[1] << 8 | rtc_time[2] setup_1 = (rtc_time[3] % 7) << 24 | rtc_time[4] << 16 | rtc_time[5] << 8 | rtc_time[6] machine.mem32[0x4005c004] = setup_0 machine.mem32[0x4005c008] = setup_1 machine.mem32[0x4005c00c] |= 0x10 except: pass # 0x0D's sent from the host get transformed into 0x0A's, and 0x0A sent to the # host get converted into 0x0D0A when using sys.stdin. sys.tsin.buffer does # no transformations, so if that's available, we use it, otherwise we need # to use hexlify in order to get unaltered data. def recv_file_from_host(src_file, dst_filename, filesize, dst_mode='wb'): """Function which runs on the pyboard. Matches up with send_file_to_remote.""" import sys import ubinascii import os if HAS_BUFFER: try: import micropython # We don't want 0x03 bytes in the data to be interpreted as a Control-C # This gets reset each time the REPL runs a line, so we don't need to # worry about resetting it ourselves micropython.kbd_intr(-1) except: pass try: #rp2: import time with open(dst_filename, dst_mode) as dst_file: bytes_remaining = filesize if not HAS_BUFFER: bytes_remaining *= 2 # hexlify makes each byte into 2 buf_size = BUFFER_SIZE write_buf = bytearray(buf_size) read_buf = bytearray(buf_size) while bytes_remaining > 0: # Send back an ack as a form of flow control sys.stdout.write('\x06') read_size = min(bytes_remaining, buf_size) buf_remaining = read_size buf_index = 0 while buf_remaining > 0: if HAS_BUFFER: bytes_read = sys.stdin.buffer.readinto(read_buf, read_size) else: bytes_read = sys.stdin.readinto(read_buf, read_size) # The following sleep is required for the RPi Pico #rp2: time.sleep_ms(20) if bytes_read > 0: write_buf[buf_index:bytes_read] = read_buf[0:bytes_read] buf_index += bytes_read buf_remaining -= bytes_read if HAS_BUFFER: dst_file.write(write_buf[0:read_size]) else: dst_file.write(ubinascii.unhexlify(write_buf[0:read_size])) if hasattr(os, 'sync'): os.sync() bytes_remaining -= read_size return True except: return False def send_file_to_remote(dev, src_file, dst_filename, filesize, dst_mode='wb'): """Intended to be passed to the `remote` function as the xfer_func argument. Matches up with recv_file_from_host. """ bytes_remaining = filesize save_timeout = dev.timeout dev.timeout = 2 while bytes_remaining > 0: # Wait for ack so we don't get too far ahead of the remote ack = dev.read(1) if ack is None or ack != b'\x06': sys.stderr.write("timed out or error in transfer to remote: {!r}\n".format(ack)) sys.exit(2) if HAS_BUFFER: buf_size = BUFFER_SIZE else: buf_size = BUFFER_SIZE // 2 read_size = min(bytes_remaining, buf_size) buf = src_file.read(read_size) #sys.stdout.write('\r%d/%d' % (filesize - bytes_remaining, filesize)) #sys.stdout.flush() if HAS_BUFFER: dev.write(buf) else: dev.write(binascii.hexlify(buf)) bytes_remaining -= read_size #sys.stdout.write('\r') dev.timeout = save_timeout def recv_file_from_remote(dev, src_filename, dst_file, filesize): """Intended to be passed to the `remote` function as the xfer_func argument. Matches up with send_file_to_host. """ bytes_remaining = filesize if not HAS_BUFFER: bytes_remaining *= 2 # hexlify makes each byte into 2 buf_size = BUFFER_SIZE write_buf = bytearray(buf_size) while bytes_remaining > 0: read_size = min(bytes_remaining, buf_size) buf_remaining = read_size buf_index = 0 while buf_remaining > 0: read_buf = dev.read(buf_remaining) bytes_read = len(read_buf) if bytes_read: write_buf[buf_index:bytes_read] = read_buf[0:bytes_read] buf_index += bytes_read buf_remaining -= bytes_read if HAS_BUFFER: dst_file.write(write_buf[0:read_size]) else: dst_file.write(binascii.unhexlify(write_buf[0:read_size])) # Send an ack to the remote as a form of flow control dev.write(b'\x06') # ASCII ACK is 0x06 bytes_remaining -= read_size def send_file_to_host(src_filename, dst_file, filesize): """Function which runs on the pyboard. Matches up with recv_file_from_remote.""" import sys import ubinascii try: with open(src_filename, 'rb') as src_file: bytes_remaining = filesize if HAS_BUFFER: buf_size = BUFFER_SIZE else: buf_size = BUFFER_SIZE // 2 while bytes_remaining > 0: read_size = min(bytes_remaining, buf_size) buf = src_file.read(read_size) if HAS_BUFFER: sys.stdout.buffer.write(buf) else: sys.stdout.write(ubinascii.hexlify(buf)) bytes_remaining -= read_size # Wait for an ack so we don't get ahead of the remote while True: char = sys.stdin.read(1) if char: if char == '\x06': break # This should only happen if an error occurs sys.stdout.write(char) return True except: return False def test_buffer(): """Checks the micropython firmware to see if sys.stdin.buffer exists.""" import sys try: _ = sys.stdin.buffer return True except: return False def test_readinto(): """Checks the micropython firmware to see if sys.stdin.readinto exists.""" import sys try: _ = sys.stdin.readinto return True except: return False def test_unhexlify(): """Checks the micropython firmware to see if ubinascii.unhexlify exists.""" import ubinascii try: _ = ubinascii.unhexlify return True except: return False def get_time_epoch(): """Determines the epoch used by the MicroPython board.""" import time try: return time.gmtime(0) except: """Assume its a pyboard, with an epoch of 2000.""" return (2000, 1, 1, 0, 0, 0, 0, 0) def mode_exists(mode): return mode & 0xc000 != 0 def mode_isdir(mode): return mode & 0x4000 != 0 def mode_issymlink(mode): return mode & 0xf000 == 0xa000 def mode_isfile(mode): return mode & 0x8000 != 0 def stat_mode(stat): """Returns the mode field from the results returned by os.stat().""" return stat[0] def stat_size(stat): """Returns the filesize field from the results returned by os.stat().""" return stat[6] def stat_mtime(stat): """Returns the mtime field from the results returned by os.stat().""" return stat[8] def word_len(word): """Returns the word length, minus any color codes.""" if word[0] == '\x1b': return len(word) - 11 # 7 for color, 4 for no-color return len(word) def print_cols(words, print_func, termwidth=79): """Takes a single column of words, and prints it as multiple columns that will fit in termwidth columns. """ width = max([word_len(word) for word in words]) nwords = len(words) ncols = max(1, (termwidth + 1) // (width + 1)) nrows = (nwords + ncols - 1) // ncols for row in range(nrows): for i in range(row, nwords, nrows): word = words[i] if word[0] == '\x1b': print_func('%-*s' % (width + 11, words[i]), end='\n' if i + nrows >= nwords else ' ') else: print_func('%-*s' % (width, words[i]), end='\n' if i + nrows >= nwords else ' ') def decorated_filename(filename, stat): """Takes a filename and the stat info and returns the decorated filename. The decoration takes the form of a single character which follows the filename. Currently, the only decoration is '/' for directories. """ mode = stat[0] if mode_isdir(mode): return DIR_COLOR + filename + END_COLOR + '/' if mode_issymlink(mode): return filename + '@' if filename.endswith('.py'): return PY_COLOR + filename + END_COLOR return filename def print_long(filename, stat, print_func): """Prints detailed information about the file passed in.""" size = stat_size(stat) mtime = stat_mtime(stat) file_mtime = time.localtime(mtime) curr_time = time.time() if mtime > (curr_time + SIX_MONTHS) or mtime < (curr_time - SIX_MONTHS): print_func('%6d %s %2d %04d %s' % (size, MONTH[file_mtime[1]], file_mtime[2], file_mtime[0], decorated_filename(filename, stat))) else: print_func('%6d %s %2d %02d:%02d %s' % (size, MONTH[file_mtime[1]], file_mtime[2], file_mtime[3], file_mtime[4], decorated_filename(filename, stat))) def trim(docstring): """Trims the leading spaces from docstring comments. From http://www.python.org/dev/peps/pep-0257/ """ if not docstring: return '' # Convert tabs to spaces (following the normal Python rules) # and split into a list of lines: lines = docstring.expandtabs().splitlines() # Determine minimum indentation (first line doesn't count): indent = sys.maxsize for line in lines[1:]: stripped = line.lstrip() if stripped: indent = min(indent, len(line) - len(stripped)) # Remove indentation (first line is special): trimmed = [lines[0].strip()] if indent < sys.maxsize: for line in lines[1:]: trimmed.append(line[indent:].rstrip()) # Strip off trailing and leading blank lines: while trimmed and not trimmed[-1]: trimmed.pop() while trimmed and not trimmed[0]: trimmed.pop(0) # Return a single string: return '\n'.join(trimmed) def add_arg(*args, **kwargs): """Returns a list containing args and kwargs.""" return (args, kwargs) def connect(port, baud=115200, user='micro', password='python', wait=0): """Tries to connect automagically via network or serial.""" try: ip_address = socket.gethostbyname(port) #print('Connecting to ip', ip_address) connect_telnet(port, ip_address, user=user, password=password) except socket.gaierror: # Doesn't look like a hostname or IP-address, assume its a serial port #print('connecting to serial', port) connect_serial(port, baud=baud, wait=wait) def connect_telnet(name, ip_address=None, user='micro', password='python'): """Connect to a MicroPython board via telnet.""" if ip_address is None: try: ip_address = socket.gethostbyname(name) except socket.gaierror: ip_address = name if not QUIET: if name == ip_address: print('Connecting to (%s) ...' % ip_address) else: print('Connecting to %s (%s) ...' % (name, ip_address)) dev = DeviceNet(name, ip_address, user, password) add_device(dev) def connect_serial(port, baud=115200, wait=0): """Connect to a MicroPython board via a serial port.""" if not QUIET: print('Connecting to %s (buffer-size %d)...' % (port, BUFFER_SIZE)) try: dev = DeviceSerial(port, baud, wait) except DeviceError as err: sys.stderr.write(str(err)) sys.stderr.write('\n') return False add_device(dev) return True def strip_source(source): """ Strip out comments and Docstrings from some python source code.""" mod = "" prev_toktype = token.INDENT last_lineno = -1 last_col = 0 tokgen = tokenize.generate_tokens(io.StringIO(source).readline) for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen: if 0: # Change to if 1 to see the tokens fly by. print("%10s %-14s %-20r %r" % ( tokenize.tok_name.get(toktype, toktype), "%d.%d-%d.%d" % (slineno, scol, elineno, ecol), ttext, ltext )) if slineno > last_lineno: last_col = 0 if scol > last_col: mod += " " * (scol - last_col) if toktype == token.STRING and prev_toktype == token.INDENT: # Docstring mod = mod.rstrip(' \t\n') elif toktype == tokenize.COMMENT: # Comment mod = mod.rstrip(' \t\n') else: mod += ttext prev_toktype = toktype last_col = ecol last_lineno = elineno return mod class SmartFile(object): """Class which implements a write method which can takes bytes or str.""" def __init__(self, file): self.file = file def close(self): self.file.close() def flush(self): self.file.flush() def read(self, num_bytes): return self.file.buffer.read(num_bytes) def seek(self, pos): self.file.seek(pos) def tell(self): return self.file.tell() def write(self, data): if isinstance(data, str): return self.file.write(data) return self.file.buffer.write(data) class DeviceError(Exception): """Errors that we want to report to the user and keep running.""" pass class Device(object): def __init__(self, pyb): self.pyb = pyb self.has_buffer = False # needs to be set for remote_eval to work self.time_offset = 0 self.adjust_for_timezone = False self.sysname = '' QUIET or print('Retrieving sysname ... ', end='', flush=True) self.sysname = self.remote_eval(sysname) QUIET or print(self.sysname) if not ASCII_XFER: QUIET or print('Testing if sys.stdin.buffer exists ... ', end='', flush=True) self.has_buffer = self.remote_eval(test_buffer) QUIET or print('Y' if self.has_buffer else 'N') else: QUIET or print('Testing if ubinascii.unhexlify exists ... ', end='', flush=True) unhexlify_exists = self.remote_eval(test_unhexlify) QUIET or print('Y' if unhexlify_exists else 'N') if not unhexlify_exists: raise ShellError('rshell needs MicroPython firmware with ubinascii.unhexlify') QUIET or print('Retrieving root directories ... ', end='', flush=True) self.root_dirs = ['/{}/'.format(dir) for dir in self.remote_eval(listdir, '/')] QUIET or print(' '.join(self.root_dirs)) QUIET or print('Setting time ... ', end='', flush=True) now = self.sync_time() QUIET or print(time.strftime('%b %d, %Y %H:%M:%S', now)) QUIET or print('Evaluating board_name ... ', end='', flush=True) self.name, messages = self.remote_eval_last(board_name, self.default_board_name()) QUIET or print(self.name) if (len(messages) > 0) and not QUIET: print('----- Prints from board.py ----') print(messages) print('----') self.dev_name_short = self.name QUIET or print('Retrieving time epoch ... ', end='', flush=True) epoch_tuple = self.remote_eval(get_time_epoch) if len(epoch_tuple) == 8: epoch_tuple = epoch_tuple + (0,) QUIET or print(time.strftime('%b %d, %Y', epoch_tuple)) self.time_offset = calendar.timegm(epoch_tuple) # The pyboard maintains its time as localtime, whereas unix and # esp32 maintain their time as GMT self.adjust_for_timezone = (epoch_tuple[0] != 1970) def check_pyb(self): """Raises an error if the pyb object was closed.""" if self.pyb is None: raise DeviceError('serial port %s closed' % self.dev_name_short) def close(self): """Closes the serial port.""" if self.pyb and self.pyb.serial: self.pyb.serial.close() self.pyb = None def default_board_name(self): return 'unknown' def is_root_path(self, filename): """Determines if 'filename' corresponds to a directory on this device.""" test_filename = filename + '/' for root_dir in self.root_dirs: if test_filename.startswith(root_dir): return True return False def is_serial_port(self, port): return False def read(self, num_bytes): """Reads data from the pyboard over the serial port.""" self.check_pyb() try: return self.pyb.serial.read(num_bytes) except (serial.serialutil.SerialException, TypeError): # Write failed - assume that we got disconnected self.close() raise DeviceError('serial port %s closed' % self.dev_name_short) def remote(self, func, *args, xfer_func=None, **kwargs): """Calls func with the indicated args on the micropython board.""" global HAS_BUFFER HAS_BUFFER = self.has_buffer if hasattr(func, 'extra_funcs'): func_name = func.name func_lines = [] for extra_func in func.extra_funcs: func_lines += inspect.getsource(extra_func).split('\n') func_lines += [''] func_lines += filter(lambda line: line[:1] != '@', func.source.split('\n')) func_src = '\n'.join(func_lines) else: func_name = func.__name__ func_src = inspect.getsource(func) if self.sysname == 'rp2': func_src = func_src.replace('#rp2: ', '') func_src = strip_source(func_src) args_arr = [remote_repr(i) for i in args] kwargs_arr = ["{}={}".format(k, remote_repr(v)) for k, v in kwargs.items()] func_src += 'output = ' + func_name + '(' func_src += ', '.join(args_arr + kwargs_arr) func_src += ')\n' func_src += 'if output is None:\n' func_src += ' print("None")\n' func_src += 'else:\n' func_src += ' print(output)\n' time_offset = self.time_offset if self.adjust_for_timezone: time_offset -= time.localtime().tm_gmtoff func_src = func_src.replace('TIME_OFFSET', '{}'.format(time_offset)) func_src = func_src.replace('HAS_BUFFER', '{}'.format(HAS_BUFFER)) func_src = func_src.replace('BUFFER_SIZE', '{}'.format(BUFFER_SIZE)) func_src = func_src.replace('IS_UPY', 'True') if DEBUG: print('----- About to send %d bytes of code to the pyboard -----' % len(func_src)) print(func_src) print('-----') self.check_pyb() try: self.pyb.enter_raw_repl() self.check_pyb() output = self.pyb.exec_raw_no_follow(func_src) if xfer_func: xfer_func(self, *args, **kwargs) self.check_pyb() output, _ = self.pyb.follow(timeout=20) self.check_pyb() self.pyb.exit_raw_repl() except (serial.serialutil.SerialException, TypeError): self.close() raise DeviceError('serial port %s closed' % self.dev_name_short) if DEBUG: print('-----Response-----') print(output) print('-----') return output def remote_eval(self, func, *args, **kwargs): """Calls func with the indicated args on the micropython board, and converts the response back into python by using eval. """ return eval(self.remote(func, *args, **kwargs)) def remote_eval_last(self, func, *args, **kwargs): """Calls func with the indicated args on the micropython board, and converts the response back into python by using eval. """ result = self.remote(func, *args, **kwargs).split(b'\r\n') messages = result[0:-2] messages = b'\n'.join(messages).decode('utf-8') return (eval(result[-2]), messages) def status(self): """Returns a status string to indicate whether we're connected to the pyboard or not. """ if self.pyb is None: return 'closed' return 'connected' def sync_time(self): """Sets the time on the pyboard to match the time on the host.""" now = time.localtime(time.time()) self.remote(set_time, (now.tm_year, now.tm_mon, now.tm_mday, now.tm_wday + 1, now.tm_hour, now.tm_min, now.tm_sec, 0)) return now def write(self, buf): """Writes data to the pyboard over the serial port.""" self.check_pyb() try: return self.pyb.serial.write(buf) except (serial.serialutil.SerialException, BrokenPipeError, TypeError): # Write failed - assume that we got disconnected self.close() raise DeviceError('{} closed'.format(self.dev_name_short)) class DeviceSerial(Device): def __init__(self, port, baud, wait): self.port = port self.baud = baud self.wait = wait if wait and not os.path.exists(port): toggle = False try: if not QUIET: sys.stdout.write("Waiting %d seconds for serial port '%s' to exist" % (wait, port)) sys.stdout.flush() while wait and not os.path.exists(port): if not QUIET: sys.stdout.write('.') sys.stdout.flush() time.sleep(0.5) toggle = not toggle wait = wait if not toggle else wait -1 QUIET or sys.stdout.write("\n") except KeyboardInterrupt: raise DeviceError('Interrupted') self.dev_name_long = '%s at %d baud' % (port, baud) try: pyb = Pyboard(port, baudrate=baud, wait=wait, rts=RTS, dtr=DTR) except PyboardError as err: print(err) sys.exit(1) # Bluetooth devices take some time to connect at startup, and writes # issued while the remote isn't connected will fail. So we send newlines # with pauses until one of our writes succeeds. try: # we send a Control-C which should kill the current line # assuming we're talking to the micropython repl. If we send # a newline, then the junk might get interpreted as a command # which will do who knows what. pyb.serial.write(b'\x03') except serial.serialutil.SerialException: # Write failed. Now report that we're waiting and keep trying until # a write succeeds QUIET or sys.stdout.write("Waiting for transport to be connected.") while True: time.sleep(0.5) try: pyb.serial.write(b'\x03') break except serial.serialutil.SerialException: pass if not QUIET: sys.stdout.write('.') sys.stdout.flush() QUIET or sys.stdout.write('\n') # Send Control-C followed by CR until we get a >>> prompt QUIET or print('Trying to connect to REPL ', end='', flush=True) connected = False for _ in range(20): pyb.serial.write(b'\x03\r') data = pyb.read_until(1, b'>>> ', timeout=0.1) if data.endswith(b'>>> '): connected = True break if not QUIET: sys.stdout.write('.') sys.stdout.flush() if connected: QUIET or print(' connected', flush=True) else: raise DeviceError('Unable to connect to REPL') # In theory the serial port is now ready to use Device.__init__(self, pyb) self.dev_name_short = port def default_board_name(self): return 'pyboard' def is_serial_port(self, port): return self.dev_name_short == port @property def timeout(self): """Gets the timeout associated with the serial port.""" self.check_pyb() return self.pyb.serial.timeout @timeout.setter def timeout(self, value): """Sets the timeout associated with the serial port.""" self.check_pyb() try: self.pyb.serial.timeout = value except: # timeout is a property so it calls code, and that can fail # if the serial port is closed. pass class DeviceNet(Device): def __init__(self, name, ip_address, user, password): self.dev_name_short = '{} ({})'.format(name, ip_address) self.dev_name_long = self.dev_name_short try: pyb = Pyboard(ip_address, user=user, password=password) except (socket.timeout, OSError): raise DeviceError('No response from {}'.format(ip_address)) except KeyboardInterrupt: raise DeviceError('Interrupted') Device.__init__(self, pyb) def default_board_name(self): return 'wipy' @property def timeout(self): """There is no equivalent to timeout for the telnet connection.""" return None @timeout.setter def timeout(self, value): """There is no equivalent to timeout for the telnet connection.""" pass class AutoBool(object): """A simple class which allows a boolean to be set to False in conjunction with a with: statement. """ def __init__(self): self.value = False def __enter__(self): self.value = True def __exit__(self, type, value, traceback): self.value = False def __call__(self): return self.value class ShellError(Exception): """Errors that we want to report to the user and keep running.""" pass class Shell(cmd.Cmd): """Implements the shell as a command line interpreter.""" def __init__(self, filename=None, timing=False, **kwargs): cmd.Cmd.__init__(self, **kwargs) if 'stdin' in kwargs: cmd.Cmd.use_rawinput = 0 self.real_stdout = self.stdout self.smart_stdout = SmartFile(self.stdout) self.stderr = SmartFile(sys.stderr) self.filename = filename self.line_num = 0 self.timing = timing global cur_dir cur_dir = os.getcwd() self.prev_dir = cur_dir self.columns = shutil.get_terminal_size().columns self.redirect_dev = None self.redirect_filename = '' self.redirect_mode = '' self.quit_when_no_output = False self.quit_serial_reader = False readline.set_completer_delims(DELIMS) self.set_prompt() def set_prompt(self): if self.stdin == sys.stdin: prompt = PROMPT_COLOR + cur_dir + END_COLOR + '> ' if FAKE_INPUT_PROMPT: print(prompt, end='') self.prompt = '' else: self.prompt = prompt else: # Executing commands from a file self.prompt = '' def cmdloop(self, line=None): if line: line = self.precmd(line) stop = self.onecmd(line) stop = self.postcmd(stop, line) else: cmd.Cmd.cmdloop(self) def onecmd(self, line): """Override onecmd. 1 - So we don't have to have a do_EOF method. 2 - So we can strip comments 3 - So we can track line numbers """ if DEBUG: print('Executing "%s"' % line) self.line_num += 1 if line == "EOF" or line == 'exit': if cmd.Cmd.use_rawinput: # This means that we printed a prompt, and we'll want to # print a newline to pretty things up for the caller. self.print('') return True # Strip comments comment_idx = line.find("#") if comment_idx >= 0: line = line[0:comment_idx] line = line.strip() # search multiple commands on the same line lexer = shlex.shlex(line) lexer.whitespace = '' for issemicolon, group in itertools.groupby(lexer, lambda x: x == ";"): if not issemicolon: self.onecmd_exec("".join(group)) def onecmd_exec(self, line): try: if self.timing: start_time = time.time() result = cmd.Cmd.onecmd(self, line) end_time = time.time() print('took %.3f seconds' % (end_time - start_time)) return result else: return cmd.Cmd.onecmd(self, line) except DeviceError as err: print_err(err) except ShellError as err: print_err(err) except SystemExit: # When you use -h with argparse it winds up call sys.exit, which # raises a SystemExit. We intercept it because we don't want to # exit the shell, just the command. return False def default(self, line): print_err("Unrecognized command:", line) def emptyline(self): """We want empty lines to do nothing. By default they would repeat the previous command. """ pass def precmd(self, line): self.stdout = self.smart_stdout return line def postcmd(self, stop, line): if self.stdout != self.smart_stdout: if self.redirect_dev is not None: # Redirecting to a remote device, now that we're finished the # command, we can copy the collected output to the remote. if DEBUG: print('Copy redirected output to "%s"' % self.redirect_filename) # This belongs on the remote. Copy/append now filesize = self.stdout.tell() self.stdout.seek(0) self.redirect_dev.remote(recv_file_from_host, self.stdout, self.redirect_filename, filesize, dst_mode=self.redirect_mode, xfer_func=send_file_to_remote) self.stdout.close() self.stdout = self.real_stdout if not stop: self.set_prompt() return stop def print(self, *args, end='\n', file=None): """Convenience function so you don't need to remember to put the \n at the end of the line. """ if file is None: file = self.stdout s = ' '.join(str(arg) for arg in args) + end file.write(s) def create_argparser(self, command): try: argparse_args = getattr(self, "argparse_" + command) except AttributeError: return None doc_lines = getattr(self, "do_" + command).__doc__.expandtabs().splitlines() if '' in doc_lines: blank_idx = doc_lines.index('') usage = doc_lines[:blank_idx] description = doc_lines[blank_idx+1:] else: usage = doc_lines description = [] parser = argparse.ArgumentParser( prog=command, usage='\n'.join(usage), description='\n'.join(description) ) for args, kwargs in argparse_args: parser.add_argument(*args, **kwargs) return parser def filename_complete(self, text, line, begidx, endidx): """Wrapper for catching exceptions since cmd seems to silently absorb them. """ try: return self.real_filename_complete(text, line, begidx, endidx) except: traceback.print_exc() def real_filename_complete(self, text, line, begidx, endidx): """Figure out what filenames match the completion.""" # line contains the full command line that's been entered so far. # text contains the portion of the line that readline is trying to complete # text should correspond to line[begidx:endidx] # # The way the completer works text will start after one of the characters # in DELIMS. So if the filename entered so far was "embedded\ sp" then # text will point to the s in sp. # # The following bit of logic backs up to find the real beginning of the # filename. if begidx >= len(line): # This happens when you hit TAB on an empty filename before_match = begidx else: for before_match in range(begidx, 0, -1): if line[before_match] in DELIMS and before_match >= 1 and line[before_match - 1] != '\\': break # We set fixed to be the portion of the filename which is before text # and match is the full portion of the filename that's been entered so # far (that's the part we use for matching files). # # When we return a list of completions, the bit that we return should # just be the portion that we replace 'text' with. fixed = unescape(line[before_match+1:begidx]) # fixed portion of the match match = unescape(line[before_match+1:endidx]) # portion to match filenames against # We do the following to cover the case that the current directory # is / and the path being entered is relative. strip = '' if len(match) > 0 and match[0] == '/': abs_match = match elif cur_dir == '/': abs_match = cur_dir + match strip = cur_dir else: abs_match = cur_dir + '/' + match strip = cur_dir + '/' completions = [] prepend = '' if abs_match.rfind('/') == 0: # match is in the root directory # This means that we're looking for matches in the root directory # (i.e. abs_match is /foo and the user hit TAB). # So we'll supply the matching board names as possible completions. # Since they're all treated as directories we leave the trailing slash. with DEV_LOCK: if match[0] == '/': completions += [dev.name_path for dev in DEVS if dev.name_path.startswith(abs_match)] else: completions += [dev.name_path[1:] for dev in DEVS if dev.name_path.startswith(abs_match)] if DEFAULT_DEV: # Add root directories of the default device (i.e. /flash/ and /sd/) if match[0] == '/': completions += [root_dir for root_dir in DEFAULT_DEV.root_dirs if root_dir.startswith(match)] else: completions += [root_dir[1:] for root_dir in DEFAULT_DEV.root_dirs if root_dir[1:].startswith(match)] else: # This means that there are at least 2 slashes in abs_match. If one # of them matches a board name then we need to remove the board # name from fixed. Since the results from listdir_matches won't # contain the board name, we need to prepend each of the completions. with DEV_LOCK: for dev in DEVS: if abs_match.startswith(dev.name_path): prepend = dev.name_path[:-1] break paths = sorted(auto(listdir_matches, abs_match)) for path in paths: path = prepend + path if path.startswith(strip): path = path[len(strip):] completions.append(escape(path.replace(fixed, '', 1))) return completions def directory_complete(self, text, line, begidx, endidx): """Figure out what directories match the completion.""" return [filename for filename in self.filename_complete(text, line, begidx, endidx) if filename[-1] == '/'] def line_to_args(self, line): """This will convert the line passed into the do_xxx functions into an array of arguments and handle the Output Redirection Operator. """ # Note: using shlex.split causes quoted substrings to stay together. try: args = shlex.split(line) except ValueError as err: raise DeviceError(str(err)) self.redirect_filename = '' self.redirect_dev = None redirect_index = -1 if '>' in args: redirect_index = args.index('>') elif '>>' in args: redirect_index = args.index('>>') if redirect_index >= 0: if redirect_index + 1 >= len(args): raise ShellError("> requires a filename") self.redirect_filename = resolve_path(args[redirect_index + 1]) rmode = auto(get_mode, os.path.dirname(self.redirect_filename)) if not mode_isdir(rmode): raise ShellError("Unable to redirect to '%s', directory doesn't exist" % self.redirect_filename) if args[redirect_index] == '>': self.redirect_mode = 'w' if DEBUG: print('Redirecting (write) to', self.redirect_filename) else: self.redirect_mode = 'a' if DEBUG: print('Redirecting (append) to', self.redirect_filename) self.redirect_dev, self.redirect_filename = get_dev_and_path(self.redirect_filename) try: if self.redirect_dev is None: self.stdout = SmartFile(open(self.redirect_filename, self.redirect_mode)) else: # Redirecting to a remote device. We collect the results locally # and copy them to the remote device at the end of the command. self.stdout = SmartFile(tempfile.TemporaryFile(mode='w+')) except OSError as err: raise ShellError(err) del args[redirect_index + 1] del args[redirect_index] curr_cmd, _, _ = self.parseline(self.lastcmd) parser = self.create_argparser(curr_cmd) if parser: args = parser.parse_args(args) return args def do_args(self, line): """args [arguments...] Debug function for verifying argument parsing. This function just prints out each argument that it receives. """ args = self.line_to_args(line) for idx in range(len(args)): self.print("arg[%d] = '%s'" % (idx, args[idx])) def do_boards(self, _): """boards Lists the boards that rshell is currently connected to. """ rows = [] with DEV_LOCK: for dev in DEVS: if dev is DEFAULT_DEV: dirs = [dir[:-1] for dir in dev.root_dirs] else: dirs = [] dirs += ['/{}{}'.format(dev.name, dir)[:-1] for dir in dev.root_dirs] dirs = 'Dirs: ' + ' '.join(dirs) epoch = 'Epoch: {}'.format(time.gmtime(dev.time_offset)[0]) rows.append((dev.name, '@ %s' % dev.dev_name_short, dev.status(), epoch, dirs)) if rows: column_print('<<<< ', rows, self.print) else: print('No boards connected') def complete_cat(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_cat(self, line): """cat FILENAME... Concatenates files and sends to stdout. """ # note: when we get around to supporting cat from stdin, we'll need # to write stdin to a temp file, and then copy the file # since we need to know the filesize when copying to the pyboard. args = self.line_to_args(line) for filename in args: filename = resolve_path(filename) mode = auto(get_mode, filename) if not mode_exists(mode): print_err("Cannot access '%s': No such file" % filename) continue if not mode_isfile(mode): print_err("'%s': is not a file" % filename) continue cat(filename, self.stdout) def complete_cd(self, text, line, begidx, endidx): return self.directory_complete(text, line, begidx, endidx) def do_cd(self, line): """cd DIRECTORY Changes the current directory. ~ expansion is supported, and cd - goes to the previous directory. """ args = self.line_to_args(line) if len(args) == 0: dirname = '~' else: if args[0] == '-': dirname = self.prev_dir else: dirname = args[0] dirname = resolve_path(dirname) mode = auto(get_mode, dirname) if mode_isdir(mode): global cur_dir self.prev_dir = cur_dir cur_dir = dirname auto(chdir, dirname) else: print_err("Directory '%s' does not exist" % dirname) def do_connect(self, line): """connect TYPE TYPE_PARAMS connect serial port [baud] connect telnet ip-address-or-name Connects a pyboard to rshell. """ args = self.line_to_args(line) num_args = len(args) if num_args < 1: print_err('Missing connection TYPE') return connect_type = args[0] if connect_type == 'serial': if num_args < 2: print_err('Missing serial port') return port = args[1] if num_args < 3: baud = 115200 else: try: baud = int(args[2]) except ValueError: print_err("Expecting baud to be numeric. Found '{}'".format(args[2])) return connect_serial(port, baud) elif connect_type == 'telnet': if num_args < 2: print_err('Missing hostname or ip-address') return name = args[1] connect_telnet(name) else: print_err('Unrecognized connection TYPE: {}'.format(connect_type)) def complete_cp(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_cp(self, line): """cp SOURCE DEST Copy a single SOURCE file to DEST file. cp SOURCE... DIRECTORY Copy multiple SOURCE files to a directory. cp [-r|--recursive] [SOURCE|SOURCE_DIR]... DIRECTORY cp [-r] PATTERN DIRECTORY Copy matching files to DIRECTORY. The destination must be a directory except in the case of copying a single file. To copy directories -r must be specified. This will cause directories and their contents to be recursively copied. """ args = self.line_to_args(line) if len(args.filenames) < 2: print_err('Missing destination file') return dst_dirname = resolve_path(args.filenames[-1]) dst_mode = auto(get_mode, dst_dirname) d_dst = {} # Destination directory: lookup stat by basename if args.recursive: dst_files = auto(listdir_stat, dst_dirname) if dst_files is None: err = "cp: target {} is not a directory" print_err(err.format(dst_dirname)) return for name, stat in dst_files: d_dst[name] = stat src_filenames = args.filenames[:-1] # Process PATTERN sfn = src_filenames[0] if is_pattern(sfn): if len(src_filenames) > 1: print_err("Usage: cp [-r] PATTERN DIRECTORY") return src_filenames = process_pattern(sfn) if src_filenames is None: return for src_filename in src_filenames: if is_pattern(src_filename): print_err("Only one pattern permitted.") return src_filename = resolve_path(src_filename) src_mode = auto(get_mode, src_filename) if not mode_exists(src_mode): print_err("File '{}' doesn't exist".format(src_filename)) return if mode_isdir(src_mode): if args.recursive: # Copying a directory src_basename = os.path.basename(src_filename) dst_filename = dst_dirname + '/' + src_basename if src_basename in d_dst: dst_stat = d_dst[src_basename] dst_mode = stat_mode(dst_stat) if not mode_isdir(dst_mode): err = "Destination {} is not a directory" print_err(err.format(dst_filename)) return else: if not mkdir(dst_filename): err = "Unable to create directory {}" print_err(err.format(dst_filename)) return rsync(src_filename, dst_filename, mirror=False, dry_run=False, print_func=lambda *args: None, recursed=False, sync_hidden=args.all) else: print_err("Omitting directory {}".format(src_filename)) continue if mode_isdir(dst_mode): dst_filename = dst_dirname + '/' + os.path.basename(src_filename) else: dst_filename = dst_dirname self.print("Copying '{}' to '{}' ...".format(src_filename, dst_filename)) if not cp(src_filename, dst_filename): err = "Unable to copy '{}' to '{}'" print_err(err.format(src_filename, dst_filename)) break argparse_date = ( add_arg( '-b', '--board', dest='board', action='store', help='Specify which board to get the date for', default=None ), ) def do_date(self, line): """date [-b boardname] Displays the current date and time for the board indicated by the -b option, or, if no board is specified, then it will use the current directory to determine the board. """ args = self.line_to_args(line) if args.board is None: dev, _ = get_dev_and_path(cur_dir) else: dev = find_device_by_name(args.board) if dev is None: self.print('Host:', eval(date())) else: self.print('{}: {}'.format(dev.name, trim(dev.remote_eval(date)))) def do_echo(self, line): """echo TEXT... Display a line of text. """ args = self.line_to_args(line) self.print(*args) def complete_edit(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_edit(self, line): """edit FILE Copies the file locally, launches an editor to edit the file. When the editor exits, if the file was modified then its copied back. You can specify the editor used with the --editor command line option when you start rshell, or by using the VISUAL or EDITOR environment variable. if none of those are set, then vi will be used. """ if len(line) == 0: print_err("Must provide a filename") return filename = resolve_path(line) dev, dev_filename = get_dev_and_path(filename) mode = auto(get_mode, filename) if mode_exists(mode) and mode_isdir(mode): print_err("Unable to edit directory '{}'".format(filename)) return if dev is None: # File is local os.system("{} '{}'".format(EDITOR, filename)) else: # File is remote with tempfile.TemporaryDirectory() as temp_dir: local_filename = os.path.join(temp_dir, os.path.basename(filename)) if mode_exists(mode): print('Retrieving {} ...'.format(filename)) cp(filename, local_filename) old_stat = get_stat(local_filename) if os.system("{} '{}'".format(EDITOR, local_filename)) == 0: new_stat = get_stat(local_filename) if old_stat != new_stat: self.print('Updating {} ...'.format(filename)) cp(local_filename, filename) def complete_filesize(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_exit(self, _): """exit Exits from rshell. """ return True def do_filesize(self, line): """filesize FILE Prints the size of the file, in bytes. This function is primarily for testing. """ if len(line) == 0: print_err("Must provide a filename") return filename = resolve_path(line) self.print(auto(get_filesize, filename)) def complete_filetype(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_filetype(self, line): """filetype FILE Prints the type of file (dir or file). This function is primarily for testing. """ if len(line) == 0: print_err("Must provide a filename") return filename = resolve_path(line) mode = auto(get_mode, filename) if mode_exists(mode): if mode_isdir(mode): self.print('dir') elif mode_isfile(mode): self.print('file') else: self.print('unknown') else: self.print('missing') def do_help(self, line): """help [COMMAND] List available commands with no arguments, or detailed help when a command is provided. """ # We provide a help function so that we can trim the leading spaces # from the docstrings. The builtin help function doesn't do that. if not line: cmd.Cmd.do_help(self, line) self.print(EXIT_STR) return parser = self.create_argparser(line) if parser: parser.print_help() return try: doc = getattr(self, 'do_' + line).__doc__ if doc: self.print("%s" % trim(doc)) return except AttributeError: pass self.print(str(self.nohelp % (line,))) argparse_ls = ( add_arg( '-a', '--all', dest='all', action='store_true', help='do not ignore hidden files', default=False ), add_arg( '-l', '--long', dest='long', action='store_true', help='use a long listing format', default=False ), add_arg( 'filenames', metavar='FILE', nargs='*', help='Files directories or patterns to list' ), ) def complete_ls(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_ls(self, line): """ls [-a] [-l] [FILE|DIRECTORY|PATTERN]... PATTERN supports * ? [seq] [!seq] Unix filename matching List directory contents. """ args = self.line_to_args(line) if len(args.filenames) == 0: args.filenames = ['.'] for idx, fn in enumerate(args.filenames): if not is_pattern(fn): filename = resolve_path(fn) stat = auto(get_stat, filename) mode = stat_mode(stat) if not mode_exists(mode): err = "Cannot access '{}': No such file or directory" print_err(err.format(filename)) continue if not mode_isdir(mode): if args.long: print_long(fn, stat, self.print) else: self.print(fn) continue if len(args.filenames) > 1: if idx > 0: self.print('') self.print("%s:" % filename) pattern = '*' else: # A pattern was specified filename, pattern = validate_pattern(fn) if filename is None: # An error was printed continue files = [] ldir_stat = auto(listdir_lstat, filename) if ldir_stat is None: err = "Cannot access '{}': No such file or directory" print_err(err.format(filename)) else: for filename, stat in sorted(ldir_stat, key=lambda entry: entry[0]): if is_visible(filename) or args.all: if fnmatch.fnmatch(filename, pattern): if args.long: print_long(filename, stat, self.print) else: files.append(decorated_filename(filename, stat)) if len(files) > 0: print_cols(sorted(files), self.print, self.columns) def complete_mkdir(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_mkdir(self, line): """mkdir DIRECTORY... Creates one or more directories. """ args = self.line_to_args(line) for filename in args: filename = resolve_path(filename) if not mkdir(filename): print_err('Unable to create %s' % filename) def repl_serial_to_stdout(self, dev): """Runs as a thread which has a sole purpose of readding bytes from the serial port and writing them to stdout. Used by do_repl. """ with self.serial_reader_running: try: save_timeout = dev.timeout # Set a timeout so that the read returns periodically with no data # and allows us to check whether the main thread wants us to quit. dev.timeout = 1 while not self.quit_serial_reader: try: char = dev.read(1) except serial.serialutil.SerialException: # This happens if the pyboard reboots, or a USB port # goes away. return except TypeError: # This is a bug in serialposix.py starting with python 3.3 # which causes a TypeError during the handling of the # select.error. So we treat this the same as # serial.serialutil.SerialException: return except ConnectionResetError: # This happens over a telnet session, if it resets return if not char: # This means that the read timed out. We'll check the quit # flag and return if needed if self.quit_when_no_output: break continue self.stdout.write(char) self.stdout.flush() dev.timeout = save_timeout except DeviceError: # The device is no longer present. return def do_repl(self, line): """repl [board-name] [~ line [~]] Enters into the regular REPL with the MicroPython board. Use Control-X to exit REPL mode and return the shell. It may take a second or two before the REPL exits. If you provide a line to the REPL command, then that will be executed. If you want the REPL to exit, end the line with the ~ character. """ args = self.line_to_args(line) if len(args) > 0 and line[0] != '~': name = args[0] line = ' '.join(args[1:]) else: name = '' dev = find_device_by_name(name) if not dev: print_err("Unable to find board '%s'" % name) return if line[0:2] == '~ ': line = line[2:] self.print('Entering REPL. Use Control-%c to exit.' % QUIT_REPL_CHAR) self.quit_serial_reader = False self.quit_when_no_output = False self.serial_reader_running = AutoBool() repl_thread = threading.Thread(target=self.repl_serial_to_stdout, args=(dev,), name='REPL_serial_to_stdout') repl_thread.daemon = True repl_thread.start() # Wait for reader to start while not self.serial_reader_running(): pass try: # Wake up the prompt dev.write(b'\r') if line: if line[-1] == '~': line = line[:-1] self.quit_when_no_output = True line = ';'.join(line.split('~')) dev.write(bytes(line, encoding='utf-8')) dev.write(b'\r') if not self.quit_when_no_output: while self.serial_reader_running(): char = getch() if not char: continue if char == QUIT_REPL_BYTE: self.quit_serial_reader = True # When using telnet with the WiPy, it doesn't support # an initial timeout. So for the meantime, we send a # space which should cause the wipy to echo back a # space which will wakeup our reader thread so it will # notice the quit. dev.write(b' ') # Give the reader thread a chance to detect the quit # then we don't have to call getch() above again which # means we'd need to wait for another character. time.sleep(0.5) # Print a newline so that the rshell prompt looks good. self.print('') # We stay in the loop so that we can still enter # characters until we detect the reader thread quitting # (mostly to cover off weird states). continue if char == b'\n': dev.write(b'\r') else: dev.write(char) except DeviceError as err: # The device is no longer present. self.print('') self.stdout.flush() print_err(err) repl_thread.join() argparse_cp = ( add_arg( '-a', '--all', dest='all', action='store_true', help='Don\'t ignore files starting with .', default=False ), add_arg( '-r', '--recursive', dest='recursive', action='store_true', help='Copy directories recursively', default=False ), add_arg( 'filenames', metavar='FILE', nargs='+', help='Pattern or files and directories to copy' ), ) argparse_rm = ( add_arg( '-r', '--recursive', dest='recursive', action='store_true', help='remove directories and their contents recursively', default=False ), add_arg( '-f', '--force', dest='force', action='store_true', help='ignore nonexistent files and arguments', default=False ), add_arg( 'filename', metavar='FILE', nargs='+', help='Pattern or files and directories to remove' ), ) def complete_rm(self, text, line, begidx, endidx): return self.filename_complete(text, line, begidx, endidx) def do_rm(self, line): """rm [-f|--force] FILE... Remove one or more files rm [-f|--force] PATTERN Remove multiple files rm -r [-f|--force] [FILE|DIRECTORY]... Files and/or directories rm -r [-f|--force] PATTERN Multiple files and/or directories Removes files or directories. To remove directories (and any contents) -r must be specified. """ args = self.line_to_args(line) filenames = args.filename # Process PATTERN sfn = filenames[0] if is_pattern(sfn): if len(filenames) > 1: print_err("Usage: rm [-r] [-f] PATTERN") return filenames = process_pattern(sfn) if filenames is None: return for filename in filenames: filename = resolve_path(filename) if not rm(filename, recursive=args.recursive, force=args.force): if not args.force: print_err("Unable to remove '{}'".format(filename)) break def do_shell(self, line): """!some-shell-command args Launches a shell and executes whatever command you provide. If you don't provide any commands, then it will launch a bash sub-shell and when exit from bash (Control-D) then it will return to rshell. """ if not line: line = '/bin/bash' os.system(line) argparse_rsync = ( add_arg( '-a', '--all', dest='all', action='store_true', help='Don\'t ignore files starting with .', default=False ), add_arg( '-m', '--mirror', dest='mirror', action='store_true', help="causes files in the destination which don't exist in " "the source to be removed. Without --mirror only file " "copies occur. No deletions will take place.", default=False, ), add_arg( '-n', '--dry-run', dest='dry_run', action='store_true', help='shows what would be done without actually performing ' 'any file copies. Implies --verbose.', default=False ), add_arg( '-q', '--quiet', dest='quiet', action='store_true', help='Doesn\'t show what has been done.', default=False ), add_arg( 'src_dir', metavar='SRC_DIR', help='Source directory' ), add_arg( 'dst_dir', metavar='DEST_DIR', help='Destination directory' ), ) def do_rsync(self, line): """rsync [-m|--mirror] [-n|--dry-run] [-q|--quiet] SRC_DIR DEST_DIR Synchronizes a destination directory tree with a source directory tree. """ args = self.line_to_args(line) src_dir = resolve_path(args.src_dir) dst_dir = resolve_path(args.dst_dir) verbose = not args.quiet pf = print if args.dry_run or verbose else lambda *args : None rsync(src_dir, dst_dir, mirror=args.mirror, dry_run=args.dry_run, print_func=pf, recursed=False, sync_hidden=args.all) def real_main(): """The main program.""" global RTS global DTR try: default_baud = int(os.getenv('RSHELL_BAUD')) except: default_baud = 115200 default_port = os.getenv('RSHELL_PORT') default_rts = os.getenv('RSHELL_RTS') or RTS default_dtr = os.getenv('RSHELL_DTR') or DTR default_user = os.getenv('RSHELL_USER') or 'micro' default_password = os.getenv('RSHELL_PASSWORD') or 'python' default_editor = os.getenv('RSHELL_EDITOR') or os.getenv('VISUAL') or os.getenv('EDITOR') or 'vi' default_color = sys.stdout.isatty() default_nocolor = not default_color global BUFFER_SIZE try: default_buffer_size = int(os.getenv('RSHELL_BUFFER_SIZE')) except: default_buffer_size = BUFFER_SIZE parser = argparse.ArgumentParser( prog="rshell", usage="%(prog)s [options] [command]", description="Remote Shell for a MicroPython board.", epilog=("You can specify the default serial port using the " + "RSHELL_PORT environment variable.") ) parser.add_argument( "-b", "--baud", dest="baud", action="store", type=int, help="Set the baudrate used (default = %d)" % default_baud, default=default_baud ) parser.add_argument( "--buffer-size", dest="buffer_size", action="store", type=int, help="Set the buffer size used for transfers " "(default = %d for USB, %d for UART)" % (USB_BUFFER_SIZE, UART_BUFFER_SIZE), ) parser.add_argument( "-p", "--port", dest="port", help="Set the serial port to use (default '%s')" % default_port, default=default_port ) parser.add_argument( "--rts", dest="rts", help="Set the RTS state (default '%s')" % default_rts, default=default_rts ) parser.add_argument( "--dtr", dest="dtr", help="Set the DTR state (default '%s')" % default_dtr, default=default_dtr ) parser.add_argument( "-u", "--user", dest="user", help="Set username to use (default '%s')" % default_user, default=default_user ) parser.add_argument( "-w", "--password", dest="password", help="Set password to use (default '%s')" % default_password, default=default_password ) parser.add_argument( "-e", "--editor", dest="editor", help="Set the editor to use (default '%s')" % default_editor, default=default_editor ) parser.add_argument( "-f", "--file", dest="filename", help="Specifies a file of commands to process." ) parser.add_argument( "-d", "--debug", dest="debug", action="store_true", help="Enable debug features", default=False ) parser.add_argument( "-n", "--nocolor", dest="nocolor", action="store_true", help="Turn off colorized output", default=default_nocolor ) parser.add_argument( "-l", "--list", dest="list", action="store_true", help="Display serial ports", default=False ) parser.add_argument( "-a", "--ascii", dest="ascii_xfer", action="store_true", help="ASCII encode binary files for transfer", default=False ) parser.add_argument( "--wait", dest="wait", type=int, action="store", help="Seconds to wait for serial port", default=0 ) parser.add_argument( "--timing", dest="timing", action="store_true", help="Print timing information about each command", default=False ) parser.add_argument( '-V', '--version', dest='version', action='store_true', help='Reports the version and exits.', default=False ) parser.add_argument( "--quiet", dest="quiet", action="store_true", help="Turns off some output (useful for testing)", default=False ) parser.add_argument( "cmd", nargs=argparse.REMAINDER, help="Optional command to execute" ) args = parser.parse_args(sys.argv[1:]) if args.buffer_size is not None: BUFFER_SIZE = args.buffer_size if args.debug: print("Debug = %s" % args.debug) print("Port = %s" % args.port) print("Baud = %d" % args.baud) print("User = %s" % args.user) print("Password = %s" % args.password) print("Wait = %d" % args.wait) print("List = %d" % args.list) print("nocolor = %d" % args.nocolor) print("ascii = %d" % args.ascii_xfer) print("Timing = %d" % args.timing) print("Quiet = %d" % args.quiet) print("BUFFER_SIZE = %d" % BUFFER_SIZE) print("Cmd = [%s]" % ', '.join(args.cmd)) if args.version: print(__version__) return global DEBUG DEBUG = args.debug global QUIET QUIET = args.quiet global EDITOR EDITOR = args.editor if args.nocolor: global DIR_COLOR, PROMPT_COLOR, PY_COLOR, END_COLOR DIR_COLOR = '' PROMPT_COLOR = '' PY_COLOR = '' END_COLOR = '' else: if sys.platform == 'darwin': # The readline that comes with OSX screws up colors in the prompt global FAKE_INPUT_PROMPT FAKE_INPUT_PROMPT = True global ASCII_XFER ASCII_XFER = args.ascii_xfer RTS = args.rts DTR = args.dtr if args.list: listports() return if args.port: ASCII_XFER = True if args.buffer_size is None: if is_micropython_usb_port(args.port): BUFFER_SIZE = USB_BUFFER_SIZE else: BUFFER_SIZE = UART_BUFFER_SIZE QUIET or print('Using buffer-size of', BUFFER_SIZE) try: connect(args.port, baud=args.baud, wait=args.wait, user=args.user, password=args.password) except DeviceError as err: print(err) else: autoscan() autoconnect() if args.filename: with open(args.filename) as cmd_file: shell = Shell(stdin=cmd_file, filename=args.filename, timing=args.timing) shell.cmdloop('') else: cmd_line = ' '.join(args.cmd) if cmd_line == '': print('Welcome to rshell.', EXIT_STR) if num_devices() == 0: print('') print('No MicroPython boards connected - use the connect command to add one') print('') shell = Shell(timing=args.timing) try: shell.cmdloop(cmd_line) except KeyboardInterrupt: print('') def main(): """This main function saves the stdin termios settings, calls real_main, and restores stdin termios settings when it returns. """ save_settings = None stdin_fd = -1 try: import termios stdin_fd = sys.stdin.fileno() save_settings = termios.tcgetattr(stdin_fd) except: pass try: real_main() finally: if save_settings: termios.tcsetattr(stdin_fd, termios.TCSANOW, save_settings) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/rshell/pyboard.py0000664010432500017510000002707400000000000016605 0ustar00dhylandsdhylands#!/usr/bin/env python3 """ pyboard interface This module provides the Pyboard class, used to communicate with and control the pyboard over a serial USB connection. Example usage: import pyboard pyb = pyboard.Pyboard('/dev/ttyACM0') Or: pyb = pyboard.Pyboard('192.168.1.1') Then: pyb.enter_raw_repl() pyb.exec('pyb.LED(1).on()') pyb.exit_raw_repl() Note: if using Python2 then pyb.exec must be written as pyb.exec_. To run a script from the local machine on the board and print out the results: import pyboard pyboard.execfile('test.py', device='/dev/ttyACM0') This script can also be run directly. To execute a local script, use: ./pyboard.py test.py Or: python pyboard.py test.py """ import sys import time try: stdout = sys.stdout.buffer except AttributeError: # Python2 doesn't have buffer attr stdout = sys.stdout def stdout_write_bytes(b): b = b.replace(b"\x04", b"") stdout.write(b) stdout.flush() class PyboardError(BaseException): pass class TelnetToSerial: def __init__(self, ip, user, password, read_timeout=None): import telnetlib self.tn = telnetlib.Telnet(ip, timeout=15) self.read_timeout = read_timeout if b'Login as:' in self.tn.read_until(b'Login as:', timeout=read_timeout): self.tn.write(bytes(user, 'ascii') + b"\r\n") if b'Password:' in self.tn.read_until(b'Password:', timeout=read_timeout): # needed because of internal implementation details of the telnet server time.sleep(0.2) self.tn.write(bytes(password, 'ascii') + b"\r\n") if b'for more information.' in self.tn.read_until(b'Type "help()" for more information.', timeout=read_timeout): # login successful from collections import deque self.fifo = deque() return raise PyboardError('Failed to establish a telnet connection with the board') def __del__(self): self.close() def close(self): try: self.tn.close() except: # the telnet object might not exist yet, so ignore this one pass def read(self, size=1): while len(self.fifo) < size: timeout_count = 0 data = self.tn.read_eager() if len(data): self.fifo.extend(data) timeout_count = 0 else: time.sleep(0.25) if self.read_timeout is not None and timeout_count > 4 * self.read_timeout: break timeout_count += 1 data = b'' while len(data) < size and len(self.fifo) > 0: data += bytes([self.fifo.popleft()]) return data def write(self, data): self.tn.write(data) return len(data) def inWaiting(self): n_waiting = len(self.fifo) if not n_waiting: data = self.tn.read_eager() self.fifo.extend(data) return len(data) else: return n_waiting def parse_bool(str): return str == '1' or str.lower() == 'true' class Pyboard: def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0, rts='', dtr=''): if device and device[0].isdigit() and device[-1].isdigit() and device.count('.') == 3: # device looks like an IP address self.serial = TelnetToSerial(device, user, password, read_timeout=10) else: import serial delayed = False if serial.VERSION == '3.0': self.serial = serial.Serial(baudrate=baudrate, inter_byte_timeout=1) else: self.serial = serial.Serial(baudrate=baudrate, interCharTimeout=1) if rts != '': self.serial.rts = parse_bool(rts) if dtr != '': self.serial.dtr = parse_bool(dtr) self.serial.port = device for attempt in range(wait + 1): try: # Assigning the port attribute will attempt to open the port self.serial.open() break except (OSError, IOError): # Py2 and Py3 have different errors if wait == 0: continue if attempt == 0: sys.stdout.write('Waiting {} seconds for pyboard '.format(wait)) delayed = True time.sleep(1) sys.stdout.write('.') sys.stdout.flush() else: if delayed: print('') raise PyboardError('failed to access ' + device) if delayed: print('') def close(self): self.serial.close() def read_until(self, min_num_bytes, ending, timeout=10, data_consumer=None): data = self.serial.read(min_num_bytes) if data_consumer: data_consumer(data) timeout_count = 0 while True: if data.endswith(ending): break elif self.serial.inWaiting() > 0: new_data = self.serial.read(1) data = data + new_data if data_consumer: data_consumer(new_data) timeout_count = 0 else: timeout_count += 1 if timeout is not None and timeout_count >= 100 * timeout: break time.sleep(0.01) return data def enter_raw_repl(self): self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program # flush input (without relying on serial.flushInput()) n = self.serial.inWaiting() while n > 0: self.serial.read(n) n = self.serial.inWaiting() self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>') if not data.endswith(b'raw REPL; CTRL-B to exit\r\n>'): print(data) raise PyboardError('could not enter raw repl') self.serial.write(b'\x04') # ctrl-D: soft reset data = self.read_until(1, b'soft reboot\r\n') if not data.endswith(b'soft reboot\r\n'): print(data) raise PyboardError('could not enter raw repl') # By splitting this into 2 reads, it allows boot.py to print stuff, # which will show up after the soft reboot and before the raw REPL. data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n') if not data.endswith(b'raw REPL; CTRL-B to exit\r\n'): print(data) raise PyboardError('could not enter raw repl') def exit_raw_repl(self): self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL def follow(self, timeout, data_consumer=None): # wait for normal output data = self.read_until(1, b'\x04', timeout=timeout, data_consumer=data_consumer) if not data.endswith(b'\x04'): raise PyboardError('timeout waiting for first EOF reception') data = data[:-1] # wait for error output data_err = self.read_until(1, b'\x04', timeout=timeout) if not data_err.endswith(b'\x04'): raise PyboardError('timeout waiting for second EOF reception') data_err = data_err[:-1] # return normal and error output return data, data_err def exec_raw_no_follow(self, command): if isinstance(command, bytes): command_bytes = command else: command_bytes = bytes(command, encoding='utf8') # check we have a prompt data = self.read_until(1, b'>') if not data.endswith(b'>'): raise PyboardError('could not enter raw repl') # write command for i in range(0, len(command_bytes), 256): self.serial.write(command_bytes[i:min(i + 256, len(command_bytes))]) time.sleep(0.01) self.serial.write(b'\x04') # check if we could exec command data = self.serial.read(2) if data != b'OK': raise PyboardError('could not exec command') def exec_raw(self, command, timeout=10, data_consumer=None): self.exec_raw_no_follow(command); return self.follow(timeout, data_consumer) def eval(self, expression): ret = self.exec_('print({})'.format(expression)) ret = ret.strip() return ret def exec_(self, command): ret, ret_err = self.exec_raw(command) if ret_err: raise PyboardError('exception', ret, ret_err) return ret def execfile(self, filename): with open(filename, 'rb') as f: pyfile = f.read() return self.exec_(pyfile) def get_time(self): t = str(self.eval('pyb.RTC().datetime()'), encoding='utf8')[1:-1].split(', ') return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6]) # in Python2 exec is a keyword so one must use "exec_" # but for Python3 we want to provide the nicer version "exec" setattr(Pyboard, "exec", Pyboard.exec_) def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', password='python'): pyb = Pyboard(device, baudrate, user, password) pyb.enter_raw_repl() output = pyb.execfile(filename) stdout_write_bytes(output) pyb.exit_raw_repl() pyb.close() def main(): import argparse cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.') cmd_parser.add_argument('--device', default='/dev/ttyACM0', help='the serial device or the IP address of the pyboard') cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device') cmd_parser.add_argument('-u', '--user', default='micro', help='the telnet login username') cmd_parser.add_argument('-p', '--password', default='python', help='the telnet login password') cmd_parser.add_argument('-c', '--command', help='program passed in as string') cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available') cmd_parser.add_argument('--follow', action='store_true', help='follow the output after running the scripts [default if no scripts given]') cmd_parser.add_argument('files', nargs='*', help='input files') args = cmd_parser.parse_args() def execbuffer(buf): try: pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait) pyb.enter_raw_repl() ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=stdout_write_bytes) pyb.exit_raw_repl() pyb.close() except PyboardError as er: print(er) sys.exit(1) except KeyboardInterrupt: sys.exit(1) if ret_err: stdout_write_bytes(ret_err) sys.exit(1) if args.command is not None: execbuffer(args.command.encode('utf-8')) for filename in args.files: with open(filename, 'rb') as f: pyfile = f.read() execbuffer(pyfile) if args.follow or (args.command is None and len(args.files) == 0): try: pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait) ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes) pyb.close() except PyboardError as er: print(er) sys.exit(1) except KeyboardInterrupt: sys.exit(1) if ret_err: stdout_write_bytes(ret_err) sys.exit(1) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889464.0 rshell-0.0.31/rshell/version.py0000664010432500017510000000002700000000000016617 0ustar00dhylandsdhylands__version__ = '0.0.31' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640889926.1680648 rshell-0.0.31/rshell.egg-info/0000775010432500017510000000000000000000000016253 5ustar00dhylandsdhylands././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889926.0 rshell-0.0.31/rshell.egg-info/PKG-INFO0000664010432500017510000005235700000000000017364 0ustar00dhylandsdhylandsMetadata-Version: 1.1 Name: rshell Version: 0.0.31 Summary: A remote shell for working with MicroPython boards. Home-page: https://github.com/dhylands/rshell Author: Dave Hylands Author-email: dhylands@gmail.com License: MIT Download-URL: https://github.com/dhylands/rshell/tarball/v0.0.1 Description: rshell ========= Remote MicroPython shell. This is a simple shell which runs on the host and uses MicroPython's raw-REPL to send python snippets to the pyboard in order to get filesystem information, and to copy files to and from MicroPython's filesystem. It also has the ability to invoke the regular REPL, so rshell can be used as a terminal emulator as well. Note: With rshell you can disable USB Mass Storage and still copy files into and out of your pyboard. When using the commands, the /flash directory, and the /sdcard directory (if an sdcard is inserted) are considered to be on the pyboard, and all other directories are considered to be on the host. For an ESP based board you can only reference its directory by using the board name e.g. /pyboard etc.. NOTE: rshell requires a fairly recent version of the MicroPython firmware, specifically one which contains the ubinascii.unhexlify command which was added May 19, 2015 (v1.4.3-28-ga3a14b9 or newer). If your version of the firmware isn't new enough, then you'll see an error message something like this: :: >./rshell.py rshell needs MicroPython firmware with ubinascii.unhexlify Installation ============ You can install rshell using the command: :: sudo pip3 install rshell If you use a virtualenv, then you don't need the sudo. rshell needs Python3. All of my testing was done using version 3.4.0. Debian/Ubuntu users can get pip3 using: :: sudo apt-get install python3-pip Serial Port Permissions (linux) =============================== Under linux, the USB serial ports are owned by root and typically have a group of dialout (for ubuntu). Different unix distros may use a slightly different group name. In order for rshell (or any terminal emulator) to access the serial port, you'll need to ensure that your user account has sufficient priviledge. One way to do this is to ensure that your user is added to the dialout group, using a command like: :: sudo usermod -a -G dialout $USER Another way, is to use udev rules to modify the permissions on the serial port. There are some instructions on the [micropython wiki](https://github.com/micropython/micropython/wiki/Board-STM32F407-Discovery#programming-from-linux-via-dfu) that you can use to setup your udev rules (you don't need to install dfu-utils just to update the permissions). Sample Session ============== This shows a pyboard in its default state, copying a hello.py and then entering the repl and importing it. :: >rshell Welcome to rshell. Use Control-D to exit. /home/dhylands/Dropbox/micropython/rshell> ls -l /flash 529 May 21 17:34 README.txt 286 May 21 17:34 boot.py 34 May 21 17:34 main.py 2436 May 21 17:34 pybcdc.inf /home/dhylands/Dropbox/micropython/rshell> cp hello.py /flash /home/dhylands/Dropbox/micropython/rshell> ls -l /flash 529 May 21 17:34 README.txt 286 May 21 17:34 boot.py 21 May 21 17:35 hello.py 34 May 21 17:34 main.py 2436 May 21 17:34 pybcdc.inf /home/dhylands/Dropbox/micropython/rshell> cat /flash/hello.py print('Hello World') /home/dhylands/Dropbox/micropython/rshell> repl Entering REPL. Use Control-X to exit. Micro Python v1.4.3-28-ga3a14b9 on 2015-05-21; PYBv1.0 with STM32F405RG Type "help()" for more information. >>> >>> import hello Hello World >>> /home/dhylands/Dropbox/micropython/rshell> Command Line Options ==================== -h, --help ---------- Displays a list of the valid options. You should get something like the following displayed: :: usage: rshell [options] [command] Remote Shell for a MicroPython board. positional arguments: cmd Optional command to execute optional arguments: -h, --help show this help message and exit -b BAUD, --baud BAUD Set the baudrate used (default = 115200) --buffer-size BUFFER_SIZE Set the buffer size used for transfers (default = 512) -p PORT, --port PORT Set the serial port to use (default '/dev/ttyACM0') --rts RTS Set the RTS state (default '') --dtr DTR Set the DTR state (default '') -u USER, --user USER Set username to use (default 'micro') -w PASSWORD, --password PASSWORD Set password to use (default 'python') -e EDITOR, --editor EDITOR Set the editor to use (default 'vi') -f FILENAME, --file FILENAME Specifies a file of commands to process. -d, --debug Enable debug features -n, --nocolor Turn off colorized output --wait How long to wait for serial port --binary Enable binary file transfer --timing Print timing information about each command --quiet Turns off some output (useful for testing) You can specify the default serial port using the RSHELL_PORT environment variable. -b BAUD, --baud BAUD -------------------- Sets the baud rate to use when talking to the pyboard over a serial port. If no baud is specified, then the baudrate from the RSHELL_BAUD environment variable is used. If the RSHELL_BAUD environment variable is not defined then the default baudrate of 115200 is used. --buffer-size ------------- Sets the buffer size used when transferring files between the host and the pyboard. If no buffer size is specified, then the value from the RSHELL_BUFFER_SIZE environment variable is used. If the RSHELL_BUFFER_SIZE environment variable is not defined, then the default of 512 is used. -d, --debug ----------- Turns on debugging. This allows you to see the script which is sent over the raw REPL and the response received. -e EDITOR, --editor ------------------- Specifies the editor to use with the edit command. If no editor is specified, then the following environment variables will be searched: RSHELL_EDITOR, VISUAL, and EDITOR. If none of those environment variables is set then vi will be used. -f FILENAME, --file FILENAME ---------------------------- Specifies a file of rshell commands to process. This allows you to create a script which executes any valid rshell commands. -n, --nocolor ------------- By default, rshell uses ANSI color escape codes when displaying the prompt and ls output. This option allows colorized output to be disabled. -a --ascii ---------- On certain platforms the raw REPL mode is unreliable with particular sequences of binary characters. Specifying --ascii enables the transfer of binary files to such platforms. It does this by encoding the data as ASCII hex. --wait ------ If a port is specified defines how long rshell will wait for the port to exist and for a connection to be established. The default is 0 seconds specifying an immediate return. -p PORT, --port PORT -------------------- Specifies the serial port which should be used to talk to the MicroPython board. You can set the RSHELL\_PORT environment variable to specify the default port to be used, if --port is not specified on the command line. --dtr [0|1|True|False] ---------------------- Sets the state of the DTR line when opening the serial port. This may also be defaulted from the RSHELL_DTR environment variable. --rts [0|1|True|False] ---------------------- Sets the state of the RTS line when opening the serial port. This may also be defaulted from the RSHELL_RTS environment variable. --quiet ------- This option causes the Connecting messages printed when rshell starts to be suppressed. This is mostly useful for the test scripts. --timing -------- If the timing option is specified then rshell will print the amount of time that each command takes to execute. -u USER, --user USER -------------------- Specifies the username to use when logging into a WiPy over telnet. If no username is specified, then the username from the RSHELL_USER environment variable is used. If the RSHELL_USER environment variable doesn't exist then the default username 'micro' is used. -w PASSWORD, --password PASSWORD -------------------------------- Specified the password to use when logging into a WiPy over telnet. If no password is specified, then the password from the RSHELL_PASSWORD environment variable is used. If the RSHELL_PASSWORD environment variable doesn't exist then the default password 'python' is used. cmd --- If a command is specified, then that command will be executed and rshell will exit. Examples: :: rshell cp somefile.py /flash rshell repl ~ pyb.bootloader() ~ File System =========== rshell can be connected to multiple pyboards simultaneously. If the board module exists on the pyboard (i.e. a file named board.py somewhere in the module search path) and it contains an attribute called name (e.g. :code:`name = "myboard"`) then the pyboard will use that name. If the board module can't be imported then the board will be named, pyboard or wipy. Names will have -1 (or some other number) to make the board name unique. You can access the internal flash on the first board connected using /flash and the sd card on the first board connected can be accessed using /sd. For all other connected pyboards, you can use /board-name/flash or /board-name/sd (you can see the board names using the boards command). The boards command will show all of the connected pyboards, along with all of the directories which map onto that pyboard. Commands ======== args ---- :: args [arguments...] Debug function for verifying argument parsing. This function just prints out each argument that it receives. boards ------ :: boards Lists all of the boards that rshell is currently connected to, their names, and the connection. You can give a custom name to a board with either copying over a :code:`board.py` file or using the :code:`echo` command, e.g. :: echo 'name="myboard"' > /pyboard/board.py (Remember to exit rshell and re-enter to see the change). cat --- :: cat FILENAME... Concatenates files and sends to stdout. cd -- :: cd DIRECTORY Changes the current directory. ~ expansion is supported, and cd - goes to the previous directory. connect ------- :: connect TYPE TYPE_PARAMS connect serial port [baud] connect telnet ip-address-or-name Connects a pyboard to rshell. rshell can be connected to multiple pyboards simultaneously. cp -- :: usage: cp SOURCE DEST cp SOURCE... DIRECTORY cp [-r|--recursive] [SOURCE|SRC_DIR]... DIRECTORY cp [-r|--recursive] PATTERN DIRECTORY positional arguments: DEST A destination file SOURCE File to copy SRC_DIR Directory to copy PATTERN File or directory pattern match string e.g. foo/*.py optional arguments: -h, --help show this help message and exit -r, --recursive copy directories recursively Copies the SOURCE file to DEST. DEST may be a filename or a directory name. If more than one source file is specified, then the destination should be a directory. Directories will only be copied if -r is specified. A single pattern may be specified, in which case the destination should be a directory. Pattern matching is performed according to a subset of the Unix rules (see below). Recursive copying uses rsync (see below): where a file exists on source and destination, it will only be copied if the source is newer than the destination. echo ---- :: echo TEXT... Display a line of text. edit ---- :: edit filename If the file is on a pyboard, it copies the file to host, invokes an editor and if any changes were made to the file, it copies it back to the pyboard. The editor which is used defaults to vi, but can be overridden using either the --editor command line option when rshell.py is invoked, or by using the RSHELL\_EDITOR, VISUAL or EDITOR environment variables (they are tried in the order listed). filesize -------- :: filesize FILE Prints the size of the file, in bytes. This function is primarily testing. filetype -------- :: filetype FILE Prints the type of file (dir or file). This function is primarily for testing. help ---- :: help [COMMAND] List available commands with no arguments, or detailed help when a command is provided. ls -- :: usage: ls [-a] [-l] [FILE|DIRECTORY|PATTERN]... List directory contents. positional arguments: FILE File to list (show absolute path) DIRECTORY Directory (list contents) PATTERN File or directory pattern match string e.g. foo/*.py optional arguments: -h, --help show this help message and exit -a, --all do not ignore hidden files -l, --long use a long listing format Pattern matching is performed according to a subset of the Unix rules (see below). mkdir ----- :: mkdir DIRECTORY... Creates one or more directories. repl ---- :: repl [board-name] [~ line][ ~] Enters into the regular REPL with the MicroPython board. Use Control-X to exit REPL mode and return the shell. It may take a second or two before the REPL exits. If you provide a board-name then rshell will connect to that board, otherwise it will connect to the default board (first connected board). If you provide a tilde followed by a space (~ ) then anything after the tilde will be entered as if you typed it on the command line. If you want the repl to exit, end the line with the ~ character. For example, you could use: :: rshell.py repl ~ pyb.bootloader()~ and it will boot the pyboard into DFU. If you want to execute multiple Python commands these should be separated by the ~ character (not the ; character): :: rshell.py repl ~ import mymodule ~ mymodule.run() rm -- :: usage: rm [-f|--force] FILE... rm [-f|--force] PATTERN rm -r [-f|--force] PATTERN rm -r [-f|--force] [FILE|DIRECTORY]... Removes files or directories (including their contents). positional arguments: FILE File to remove DIRECTORY Directory to remove (-r required) PATTERN File matching pattern e.g. *.py optional arguments: -h, --help show this help message and exit -r, --recursive remove directories and their contents recursively -f, --force ignore nonexistent files and arguments A single pattern may be specified. Pattern matching is performed according to a subset of the Unix rules (see below). Directories can only be removed if the recursive argument is provided. Beware of rm -r * or worse. rsync ----- :: usage: rsync [-m|--mirror] [-n|--dry-run] [-q|--quiet] SRC_DIR DEST_DIR Recursively synchronises a source directory to a destination. Directories must exist. positional arguments: SRC_DIR Directory containing source files. DEST_DIR Directory for destination optional arguments: -h, --help show this help message and exit -m, --mirror remove files or directories from destination if absent from source. -n, --dry-run make no changes but report what would be done. Implies -v -q, --quiet don't report changes made. Synchronisation is performed by comparing the date and time of source and destination files. Files are copied if the source is newer than the destination. shell ----- The shell command can also be abbreviated using the exclamation point. :: shell some-command !some-command This will invoke a command, and return back to rshell. Example: :: !make deploy will flash the pyboard. Pattern Matching ================ This is performed according to a subset of the Unix rules. The limitations are that wildcards are only allowed in the rightmost directory of a path and curly bracket {} syntax is unsupported: :: *.py Match files in current directory with a .py extension temp/x[0-9]a.* Match temp/x1a.bmp but not temp/x00a.bmp t*/*.bmp Invalid: will produce an error message {*.doc,*.pdf} Invalid: will produce an error message Keywords: micropython shell Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Embedded Systems Classifier: Topic :: System :: Shells Classifier: Topic :: Terminals :: Serial Classifier: Topic :: Utilities ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889926.0 rshell-0.0.31/rshell.egg-info/SOURCES.txt0000664010432500017510000000061500000000000020141 0ustar00dhylandsdhylandsLICENSE MANIFEST.in README.rst setup.cfg setup.py rshell/__init__.py rshell/command_line.py rshell/getch.py rshell/main.py rshell/pyboard.py rshell/version.py rshell.egg-info/PKG-INFO rshell.egg-info/SOURCES.txt rshell.egg-info/dependency_links.txt rshell.egg-info/entry_points.txt rshell.egg-info/requires.txt rshell.egg-info/top_level.txt tests/hello.py tests/make_all_bytes.py tests/tree_cmp.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889926.0 rshell-0.0.31/rshell.egg-info/dependency_links.txt0000664010432500017510000000000100000000000022321 0ustar00dhylandsdhylands ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889926.0 rshell-0.0.31/rshell.egg-info/entry_points.txt0000664010432500017510000000012300000000000021545 0ustar00dhylandsdhylands[console_scripts] pyboard = rshell.pyboard:main rshell = rshell.command_line:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889926.0 rshell-0.0.31/rshell.egg-info/requires.txt0000664010432500017510000000007500000000000020655 0ustar00dhylandsdhylandspyserial pyudev>=0.16 [:sys_platform == "win32"] pyreadline ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640889926.0 rshell-0.0.31/rshell.egg-info/top_level.txt0000664010432500017510000000001500000000000021001 0ustar00dhylandsdhylandsrshell tests ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640889926.1720645 rshell-0.0.31/setup.cfg0000664010432500017510000000011700000000000015110 0ustar00dhylandsdhylands[metadata] description-file = README.md [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/setup.py0000664010432500017510000000316300000000000015005 0ustar00dhylandsdhylandsimport os from setuptools import setup import sys if sys.version_info < (3,4): print('rshell requires Python 3.4 or newer.') sys.exit(1) from rshell.version import __version__ here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() setup( name = 'rshell', version = __version__, author = 'Dave Hylands', author_email = 'dhylands@gmail.com', description = ('A remote shell for working with MicroPython boards.'), license = 'MIT', keywords = 'micropython shell', url = 'https://github.com/dhylands/rshell', download_url = 'https://github.com/dhylands/rshell/tarball/v0.0.1', packages=['rshell', 'tests'], long_description=long_description, classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Embedded Systems', 'Topic :: System :: Shells', 'Topic :: Terminals :: Serial', 'Topic :: Utilities', ], install_requires=[ 'pyserial', 'pyudev >= 0.16', ], entry_points = { 'console_scripts': [ 'pyboard=rshell.pyboard:main', 'rshell=rshell.command_line:main' ], }, extras_require={ ':sys_platform == "win32"': [ 'pyreadline'] } ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640889926.1680648 rshell-0.0.31/tests/0000775010432500017510000000000000000000000014432 5ustar00dhylandsdhylands././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/tests/hello.py0000664010432500017510000000002500000000000016104 0ustar00dhylandsdhylandsprint('Hello World') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/tests/make_all_bytes.py0000775010432500017510000000040100000000000017755 0ustar00dhylandsdhylands#!/usr/bin/env python3 """Test program which generates an all-file.dat file which all 256 8-bit characters. """ def main(): buf = bytearray(tuple(i for i in range(256))) with open('all-bytes.dat', 'wb') as file: file.write(buf) main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637107810.0 rshell-0.0.31/tests/tree_cmp.py0000775010432500017510000000171000000000000016604 0ustar00dhylandsdhylands#!/usr/bin/env python3 """Test program which compares two directory trees on local system args: full path to directory 1 full path to directory 2 (optional) --verbose Exit value 0 on success 1 on fail """ import sys import os def main(): verbose = len(sys.argv) == 4 and sys.argv[3] == '--verbose' source = sys.argv[1] dest = sys.argv[2] source_list = [x[1:] for x in os.walk(source)] dest_list = [x[1:] for x in os.walk(dest)] lens = len(source_list) if lens != len(dest_list): if verbose: print('Length fail ', lens, len(dest_list)) sys.exit(1) for subdir in range(lens): if False in map(lambda str0, str1 : str0 == str1, source_list[subdir], dest_list[subdir]): if verbose: print('Subdir fail ', source_list[subdir], dest_list[subdir]) sys.exit(1) if verbose: print('Directories match') sys.exit(0) if __name__ == '__main__': main()