new
This commit is contained in:
0
.venv/lib/python3.9/site-packages/ampy/__init__.py
Normal file
0
.venv/lib/python3.9/site-packages/ampy/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
429
.venv/lib/python3.9/site-packages/ampy/cli.py
Normal file
429
.venv/lib/python3.9/site-packages/ampy/cli.py
Normal file
@@ -0,0 +1,429 @@
|
||||
# Adafruit MicroPython Tool - Command Line Interface
|
||||
# Author: Tony DiCola
|
||||
# Copyright (c) 2016 Adafruit Industries
|
||||
#
|
||||
# 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.
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import platform
|
||||
import posixpath
|
||||
import re
|
||||
import serial.serialutil
|
||||
|
||||
import click
|
||||
import dotenv
|
||||
|
||||
# Load AMPY_PORT et al from .ampy file
|
||||
# Performed here because we need to beat click's decorators.
|
||||
config = dotenv.find_dotenv(filename=".ampy", usecwd=True)
|
||||
if config:
|
||||
dotenv.load_dotenv(dotenv_path=config)
|
||||
|
||||
import ampy.files as files
|
||||
import ampy.pyboard as pyboard
|
||||
|
||||
|
||||
_board = None
|
||||
|
||||
|
||||
def windows_full_port_name(portname):
|
||||
# Helper function to generate proper Windows COM port paths. Apparently
|
||||
# Windows requires COM ports above 9 to have a special path, where ports below
|
||||
# 9 are just referred to by COM1, COM2, etc. (wacky!) See this post for
|
||||
# more info and where this code came from:
|
||||
# http://eli.thegreenplace.net/2009/07/31/listing-all-serial-ports-on-windows-with-python/
|
||||
m = re.match("^COM(\d+)$", portname)
|
||||
if m and int(m.group(1)) < 10:
|
||||
return portname
|
||||
else:
|
||||
return "\\\\.\\{0}".format(portname)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option(
|
||||
"--port",
|
||||
"-p",
|
||||
envvar="AMPY_PORT",
|
||||
required=True,
|
||||
type=click.STRING,
|
||||
help="Name of serial port for connected board. Can optionally specify with AMPY_PORT environment variable.",
|
||||
metavar="PORT",
|
||||
)
|
||||
@click.option(
|
||||
"--baud",
|
||||
"-b",
|
||||
envvar="AMPY_BAUD",
|
||||
default=115200,
|
||||
type=click.INT,
|
||||
help="Baud rate for the serial connection (default 115200). Can optionally specify with AMPY_BAUD environment variable.",
|
||||
metavar="BAUD",
|
||||
)
|
||||
@click.option(
|
||||
"--delay",
|
||||
"-d",
|
||||
envvar="AMPY_DELAY",
|
||||
default=0,
|
||||
type=click.FLOAT,
|
||||
help="Delay in seconds before entering RAW MODE (default 0). Can optionally specify with AMPY_DELAY environment variable.",
|
||||
metavar="DELAY",
|
||||
)
|
||||
@click.version_option()
|
||||
def cli(port, baud, delay):
|
||||
"""ampy - Adafruit MicroPython Tool
|
||||
|
||||
Ampy is a tool to control MicroPython boards over a serial connection. Using
|
||||
ampy you can manipulate files on the board's internal filesystem and even run
|
||||
scripts.
|
||||
"""
|
||||
global _board
|
||||
# On Windows fix the COM port path name for ports above 9 (see comment in
|
||||
# windows_full_port_name function).
|
||||
if platform.system() == "Windows":
|
||||
port = windows_full_port_name(port)
|
||||
_board = pyboard.Pyboard(port, baudrate=baud, rawdelay=delay)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("remote_file")
|
||||
@click.argument("local_file", type=click.File("wb"), required=False)
|
||||
def get(remote_file, local_file):
|
||||
"""
|
||||
Retrieve a file from the board.
|
||||
|
||||
Get will download a file from the board and print its contents or save it
|
||||
locally. You must pass at least one argument which is the path to the file
|
||||
to download from the board. If you don't specify a second argument then
|
||||
the file contents will be printed to standard output. However if you pass
|
||||
a file name as the second argument then the contents of the downloaded file
|
||||
will be saved to that file (overwriting anything inside it!).
|
||||
|
||||
For example to retrieve the boot.py and print it out run:
|
||||
|
||||
ampy --port /board/serial/port get boot.py
|
||||
|
||||
Or to get main.py and save it as main.py locally run:
|
||||
|
||||
ampy --port /board/serial/port get main.py main.py
|
||||
"""
|
||||
# Get the file contents.
|
||||
board_files = files.Files(_board)
|
||||
contents = board_files.get(remote_file)
|
||||
# Print the file out if no local file was provided, otherwise save it.
|
||||
if local_file is None:
|
||||
print(contents.decode("utf-8"))
|
||||
else:
|
||||
local_file.write(contents)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--exists-okay", is_flag=True, help="Ignore if the directory already exists."
|
||||
)
|
||||
@click.argument("directory")
|
||||
def mkdir(directory, exists_okay):
|
||||
"""
|
||||
Create a directory on the board.
|
||||
|
||||
Mkdir will create the specified directory on the board. One argument is
|
||||
required, the full path of the directory to create.
|
||||
|
||||
Note that you cannot recursively create a hierarchy of directories with one
|
||||
mkdir command, instead you must create each parent directory with separate
|
||||
mkdir command calls.
|
||||
|
||||
For example to make a directory under the root called 'code':
|
||||
|
||||
ampy --port /board/serial/port mkdir /code
|
||||
"""
|
||||
# Run the mkdir command.
|
||||
board_files = files.Files(_board)
|
||||
board_files.mkdir(directory, exists_okay=exists_okay)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("directory", default="/")
|
||||
@click.option(
|
||||
"--long_format",
|
||||
"-l",
|
||||
is_flag=True,
|
||||
help="Print long format info including size of files. Note the size of directories is not supported and will show 0 values.",
|
||||
)
|
||||
@click.option(
|
||||
"--recursive",
|
||||
"-r",
|
||||
is_flag=True,
|
||||
help="recursively list all files and (empty) directories.",
|
||||
)
|
||||
def ls(directory, long_format, recursive):
|
||||
"""List contents of a directory on the board.
|
||||
|
||||
Can pass an optional argument which is the path to the directory. The
|
||||
default is to list the contents of the root, /, path.
|
||||
|
||||
For example to list the contents of the root run:
|
||||
|
||||
ampy --port /board/serial/port ls
|
||||
|
||||
Or to list the contents of the /foo/bar directory on the board run:
|
||||
|
||||
ampy --port /board/serial/port ls /foo/bar
|
||||
|
||||
Add the -l or --long_format flag to print the size of files (however note
|
||||
MicroPython does not calculate the size of folders and will show 0 bytes):
|
||||
|
||||
ampy --port /board/serial/port ls -l /foo/bar
|
||||
"""
|
||||
# List each file/directory on a separate line.
|
||||
board_files = files.Files(_board)
|
||||
for f in board_files.ls(directory, long_format=long_format, recursive=recursive):
|
||||
print(f)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("local", type=click.Path(exists=True))
|
||||
@click.argument("remote", required=False)
|
||||
def put(local, remote):
|
||||
"""Put a file or folder and its contents on the board.
|
||||
|
||||
Put will upload a local file or folder to the board. If the file already
|
||||
exists on the board it will be overwritten with no warning! You must pass
|
||||
at least one argument which is the path to the local file/folder to
|
||||
upload. If the item to upload is a folder then it will be copied to the
|
||||
board recursively with its entire child structure. You can pass a second
|
||||
optional argument which is the path and name of the file/folder to put to
|
||||
on the connected board.
|
||||
|
||||
For example to upload a main.py from the current directory to the board's
|
||||
root run:
|
||||
|
||||
ampy --port /board/serial/port put main.py
|
||||
|
||||
Or to upload a board_boot.py from a ./foo subdirectory and save it as boot.py
|
||||
in the board's root run:
|
||||
|
||||
ampy --port /board/serial/port put ./foo/board_boot.py boot.py
|
||||
|
||||
To upload a local folder adafruit_library and all of its child files/folders
|
||||
as an item under the board's root run:
|
||||
|
||||
ampy --port /board/serial/port put adafruit_library
|
||||
|
||||
Or to put a local folder adafruit_library on the board under the path
|
||||
/lib/adafruit_library on the board run:
|
||||
|
||||
ampy --port /board/serial/port put adafruit_library /lib/adafruit_library
|
||||
"""
|
||||
# Use the local filename if no remote filename is provided.
|
||||
if remote is None:
|
||||
remote = os.path.basename(os.path.abspath(local))
|
||||
# Check if path is a folder and do recursive copy of everything inside it.
|
||||
# Otherwise it's a file and should simply be copied over.
|
||||
if os.path.isdir(local):
|
||||
# Directory copy, create the directory and walk all children to copy
|
||||
# over the files.
|
||||
board_files = files.Files(_board)
|
||||
for parent, child_dirs, child_files in os.walk(local):
|
||||
# Create board filesystem absolute path to parent directory.
|
||||
remote_parent = posixpath.normpath(
|
||||
posixpath.join(remote, os.path.relpath(parent, local))
|
||||
)
|
||||
try:
|
||||
# Create remote parent directory.
|
||||
board_files.mkdir(remote_parent)
|
||||
# Loop through all the files and put them on the board too.
|
||||
for filename in child_files:
|
||||
with open(os.path.join(parent, filename), "rb") as infile:
|
||||
remote_filename = posixpath.join(remote_parent, filename)
|
||||
board_files.put(remote_filename, infile.read())
|
||||
except files.DirectoryExistsError:
|
||||
# Ignore errors for directories that already exist.
|
||||
pass
|
||||
|
||||
else:
|
||||
# File copy, open the file and copy its contents to the board.
|
||||
# Put the file on the board.
|
||||
with open(local, "rb") as infile:
|
||||
board_files = files.Files(_board)
|
||||
board_files.put(remote, infile.read())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("remote_file")
|
||||
def rm(remote_file):
|
||||
"""Remove a file from the board.
|
||||
|
||||
Remove the specified file from the board's filesystem. Must specify one
|
||||
argument which is the path to the file to delete. Note that this can't
|
||||
delete directories which have files inside them, but can delete empty
|
||||
directories.
|
||||
|
||||
For example to delete main.py from the root of a board run:
|
||||
|
||||
ampy --port /board/serial/port rm main.py
|
||||
"""
|
||||
# Delete the provided file/directory on the board.
|
||||
board_files = files.Files(_board)
|
||||
board_files.rm(remote_file)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--missing-okay", is_flag=True, help="Ignore if the directory does not exist."
|
||||
)
|
||||
@click.argument("remote_folder")
|
||||
def rmdir(remote_folder, missing_okay):
|
||||
"""Forcefully remove a folder and all its children from the board.
|
||||
|
||||
Remove the specified folder from the board's filesystem. Must specify one
|
||||
argument which is the path to the folder to delete. This will delete the
|
||||
directory and ALL of its children recursively, use with caution!
|
||||
|
||||
For example to delete everything under /adafruit_library from the root of a
|
||||
board run:
|
||||
|
||||
ampy --port /board/serial/port rmdir adafruit_library
|
||||
"""
|
||||
# Delete the provided file/directory on the board.
|
||||
board_files = files.Files(_board)
|
||||
board_files.rmdir(remote_folder, missing_okay=missing_okay)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("local_file")
|
||||
@click.option(
|
||||
"--no-output",
|
||||
"-n",
|
||||
is_flag=True,
|
||||
help="Run the code without waiting for it to finish and print output. Use this when running code with main loops that never return.",
|
||||
)
|
||||
def run(local_file, no_output):
|
||||
"""Run a script and print its output.
|
||||
|
||||
Run will send the specified file to the board and execute it immediately.
|
||||
Any output from the board will be printed to the console (note that this is
|
||||
not a 'shell' and you can't send input to the program).
|
||||
|
||||
Note that if your code has a main or infinite loop you should add the --no-output
|
||||
option. This will run the script and immediately exit without waiting for
|
||||
the script to finish and print output.
|
||||
|
||||
For example to run a test.py script and print any output after it finishes:
|
||||
|
||||
ampy --port /board/serial/port run test.py
|
||||
|
||||
Or to run test.py and not wait for it to finish:
|
||||
|
||||
ampy --port /board/serial/port run --no-output test.py
|
||||
"""
|
||||
# Run the provided file and print its output.
|
||||
board_files = files.Files(_board)
|
||||
try:
|
||||
output = board_files.run(local_file, not no_output)
|
||||
if output is not None:
|
||||
print(output.decode("utf-8"), end="")
|
||||
except IOError:
|
||||
click.echo(
|
||||
"Failed to find or read input file: {0}".format(local_file), err=True
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--bootloader", "mode", flag_value="BOOTLOADER", help="Reboot into the bootloader"
|
||||
)
|
||||
@click.option(
|
||||
"--hard",
|
||||
"mode",
|
||||
flag_value="NORMAL",
|
||||
help="Perform a hard reboot, including running init.py",
|
||||
)
|
||||
@click.option(
|
||||
"--repl",
|
||||
"mode",
|
||||
flag_value="SOFT",
|
||||
default=True,
|
||||
help="Perform a soft reboot, entering the REPL [default]",
|
||||
)
|
||||
@click.option(
|
||||
"--safe",
|
||||
"mode",
|
||||
flag_value="SAFE_MODE",
|
||||
help="Perform a safe-mode reboot. User code will not be run and the filesystem will be writeable over USB",
|
||||
)
|
||||
def reset(mode):
|
||||
"""Perform soft reset/reboot of the board.
|
||||
|
||||
Will connect to the board and perform a reset. Depending on the board
|
||||
and firmware, several different types of reset may be supported.
|
||||
|
||||
ampy --port /board/serial/port reset
|
||||
"""
|
||||
_board.enter_raw_repl()
|
||||
if mode == "SOFT":
|
||||
_board.exit_raw_repl()
|
||||
return
|
||||
|
||||
_board.exec_(
|
||||
"""if 1:
|
||||
def on_next_reset(x):
|
||||
try:
|
||||
import microcontroller
|
||||
except:
|
||||
if x == 'NORMAL': return ''
|
||||
return 'Reset mode only supported on CircuitPython'
|
||||
try:
|
||||
microcontroller.on_next_reset(getattr(microcontroller.RunMode, x))
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
return ''
|
||||
def reset():
|
||||
try:
|
||||
import microcontroller
|
||||
except:
|
||||
import machine as microcontroller
|
||||
microcontroller.reset()
|
||||
"""
|
||||
)
|
||||
r = _board.eval("on_next_reset({})".format(repr(mode)))
|
||||
print("here we are", repr(r))
|
||||
if r:
|
||||
click.echo(r, err=True)
|
||||
return
|
||||
|
||||
try:
|
||||
_board.exec_("reset()")
|
||||
except serial.serialutil.SerialException as e:
|
||||
# An error is expected to occur, as the board should disconnect from
|
||||
# serial when restarted via microcontroller.reset()
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
cli()
|
||||
finally:
|
||||
# Try to ensure the board serial connection is always gracefully closed.
|
||||
if _board is not None:
|
||||
try:
|
||||
_board.close()
|
||||
except:
|
||||
# Swallow errors when attempting to close as it's just a best effort
|
||||
# and shouldn't cause a new error or problem if the connection can't
|
||||
# be closed.
|
||||
pass
|
310
.venv/lib/python3.9/site-packages/ampy/files.py
Normal file
310
.venv/lib/python3.9/site-packages/ampy/files.py
Normal file
@@ -0,0 +1,310 @@
|
||||
# Adafruit MicroPython Tool - File Operations
|
||||
# Author: Tony DiCola
|
||||
# Copyright (c) 2016 Adafruit Industries
|
||||
#
|
||||
# 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.
|
||||
import ast
|
||||
import textwrap
|
||||
|
||||
from ampy.pyboard import PyboardError
|
||||
|
||||
|
||||
BUFFER_SIZE = 32 # Amount of data to read or write to the serial port at a time.
|
||||
# This is kept small because small chips and USB to serial
|
||||
# bridges usually have very small buffers.
|
||||
|
||||
|
||||
class DirectoryExistsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Files(object):
|
||||
"""Class to interact with a MicroPython board files over a serial connection.
|
||||
Provides functions for listing, uploading, and downloading files from the
|
||||
board's filesystem.
|
||||
"""
|
||||
|
||||
def __init__(self, pyboard):
|
||||
"""Initialize the MicroPython board files class using the provided pyboard
|
||||
instance. In most cases you should create a Pyboard instance (from
|
||||
pyboard.py) which connects to a board over a serial connection and pass
|
||||
it in, but you can pass in other objects for testing, etc.
|
||||
"""
|
||||
self._pyboard = pyboard
|
||||
|
||||
def get(self, filename):
|
||||
"""Retrieve the contents of the specified file and return its contents
|
||||
as a byte string.
|
||||
"""
|
||||
# Open the file and read it a few bytes at a time and print out the
|
||||
# raw bytes. Be careful not to overload the UART buffer so only write
|
||||
# a few bytes at a time, and don't use print since it adds newlines and
|
||||
# expects string data.
|
||||
command = """
|
||||
import sys
|
||||
with open('{0}', 'rb') as infile:
|
||||
while True:
|
||||
result = infile.read({1})
|
||||
if result == b'':
|
||||
break
|
||||
len = sys.stdout.write(result)
|
||||
""".format(
|
||||
filename, BUFFER_SIZE
|
||||
)
|
||||
self._pyboard.enter_raw_repl()
|
||||
try:
|
||||
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||
except PyboardError as ex:
|
||||
# Check if this is an OSError #2, i.e. file doesn't exist and
|
||||
# rethrow it as something more descriptive.
|
||||
if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
|
||||
raise RuntimeError("No such file: {0}".format(filename))
|
||||
else:
|
||||
raise ex
|
||||
self._pyboard.exit_raw_repl()
|
||||
return out
|
||||
|
||||
def ls(self, directory="/", long_format=True, recursive=False):
|
||||
"""List the contents of the specified directory (or root if none is
|
||||
specified). Returns a list of strings with the names of files in the
|
||||
specified directory. If long_format is True then a list of 2-tuples
|
||||
with the name and size (in bytes) of the item is returned. Note that
|
||||
it appears the size of directories is not supported by MicroPython and
|
||||
will always return 0 (i.e. no recursive size computation).
|
||||
"""
|
||||
|
||||
# Disabling for now, see https://github.com/adafruit/ampy/issues/55.
|
||||
# # Make sure directory ends in a slash.
|
||||
# if not directory.endswith("/"):
|
||||
# directory += "/"
|
||||
|
||||
# Make sure directory starts with slash, for consistency.
|
||||
if not directory.startswith("/"):
|
||||
directory = "/" + directory
|
||||
|
||||
command = """\
|
||||
try:
|
||||
import os
|
||||
except ImportError:
|
||||
import uos as os\n"""
|
||||
|
||||
if recursive:
|
||||
command += """\
|
||||
def listdir(directory):
|
||||
result = set()
|
||||
|
||||
def _listdir(dir_or_file):
|
||||
try:
|
||||
# if its a directory, then it should provide some children.
|
||||
children = os.listdir(dir_or_file)
|
||||
except OSError:
|
||||
# probably a file. run stat() to confirm.
|
||||
os.stat(dir_or_file)
|
||||
result.add(dir_or_file)
|
||||
else:
|
||||
# probably a directory, add to result if empty.
|
||||
if children:
|
||||
# queue the children to be dealt with in next iteration.
|
||||
for child in children:
|
||||
# create the full path.
|
||||
if dir_or_file == '/':
|
||||
next = dir_or_file + child
|
||||
else:
|
||||
next = dir_or_file + '/' + child
|
||||
|
||||
_listdir(next)
|
||||
else:
|
||||
result.add(dir_or_file)
|
||||
|
||||
_listdir(directory)
|
||||
return sorted(result)\n"""
|
||||
else:
|
||||
command += """\
|
||||
def listdir(directory):
|
||||
if directory == '/':
|
||||
return sorted([directory + f for f in os.listdir(directory)])
|
||||
else:
|
||||
return sorted([directory + '/' + f for f in os.listdir(directory)])\n"""
|
||||
|
||||
# Execute os.listdir() command on the board.
|
||||
if long_format:
|
||||
command += """
|
||||
r = []
|
||||
for f in listdir('{0}'):
|
||||
size = os.stat(f)[6]
|
||||
r.append('{{0}} - {{1}} bytes'.format(f, size))
|
||||
print(r)
|
||||
""".format(
|
||||
directory
|
||||
)
|
||||
else:
|
||||
command += """
|
||||
print(listdir('{0}'))
|
||||
""".format(
|
||||
directory
|
||||
)
|
||||
self._pyboard.enter_raw_repl()
|
||||
try:
|
||||
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||
except PyboardError as ex:
|
||||
# Check if this is an OSError #2, i.e. directory doesn't exist and
|
||||
# rethrow it as something more descriptive.
|
||||
if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
|
||||
raise RuntimeError("No such directory: {0}".format(directory))
|
||||
else:
|
||||
raise ex
|
||||
self._pyboard.exit_raw_repl()
|
||||
# Parse the result list and return it.
|
||||
return ast.literal_eval(out.decode("utf-8"))
|
||||
|
||||
def mkdir(self, directory, exists_okay=False):
|
||||
"""Create the specified directory. Note this cannot create a recursive
|
||||
hierarchy of directories, instead each one should be created separately.
|
||||
"""
|
||||
# Execute os.mkdir command on the board.
|
||||
command = """
|
||||
try:
|
||||
import os
|
||||
except ImportError:
|
||||
import uos as os
|
||||
os.mkdir('{0}')
|
||||
""".format(
|
||||
directory
|
||||
)
|
||||
self._pyboard.enter_raw_repl()
|
||||
try:
|
||||
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||
except PyboardError as ex:
|
||||
# Check if this is an OSError #17, i.e. directory already exists.
|
||||
if ex.args[2].decode("utf-8").find("OSError: [Errno 17] EEXIST") != -1:
|
||||
if not exists_okay:
|
||||
raise DirectoryExistsError(
|
||||
"Directory already exists: {0}".format(directory)
|
||||
)
|
||||
else:
|
||||
raise ex
|
||||
self._pyboard.exit_raw_repl()
|
||||
|
||||
def put(self, filename, data):
|
||||
"""Create or update the specified file with the provided data.
|
||||
"""
|
||||
# Open the file for writing on the board and write chunks of data.
|
||||
self._pyboard.enter_raw_repl()
|
||||
self._pyboard.exec_("f = open('{0}', 'wb')".format(filename))
|
||||
size = len(data)
|
||||
# Loop through and write a buffer size chunk of data at a time.
|
||||
for i in range(0, size, BUFFER_SIZE):
|
||||
chunk_size = min(BUFFER_SIZE, size - i)
|
||||
chunk = repr(data[i : i + chunk_size])
|
||||
# Make sure to send explicit byte strings (handles python 2 compatibility).
|
||||
if not chunk.startswith("b"):
|
||||
chunk = "b" + chunk
|
||||
self._pyboard.exec_("f.write({0})".format(chunk))
|
||||
self._pyboard.exec_("f.close()")
|
||||
self._pyboard.exit_raw_repl()
|
||||
|
||||
def rm(self, filename):
|
||||
"""Remove the specified file or directory."""
|
||||
command = """
|
||||
try:
|
||||
import os
|
||||
except ImportError:
|
||||
import uos as os
|
||||
os.remove('{0}')
|
||||
""".format(
|
||||
filename
|
||||
)
|
||||
self._pyboard.enter_raw_repl()
|
||||
try:
|
||||
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||
except PyboardError as ex:
|
||||
message = ex.args[2].decode("utf-8")
|
||||
# Check if this is an OSError #2, i.e. file/directory doesn't exist
|
||||
# and rethrow it as something more descriptive.
|
||||
if message.find("OSError: [Errno 2] ENOENT") != -1:
|
||||
raise RuntimeError("No such file/directory: {0}".format(filename))
|
||||
# Check for OSError #13, the directory isn't empty.
|
||||
if message.find("OSError: [Errno 13] EACCES") != -1:
|
||||
raise RuntimeError("Directory is not empty: {0}".format(filename))
|
||||
else:
|
||||
raise ex
|
||||
self._pyboard.exit_raw_repl()
|
||||
|
||||
def rmdir(self, directory, missing_okay=False):
|
||||
"""Forcefully remove the specified directory and all its children."""
|
||||
# Build a script to walk an entire directory structure and delete every
|
||||
# file and subfolder. This is tricky because MicroPython has no os.walk
|
||||
# or similar function to walk folders, so this code does it manually
|
||||
# with recursion and changing directories. For each directory it lists
|
||||
# the files and deletes everything it can, i.e. all the files. Then
|
||||
# it lists the files again and assumes they are directories (since they
|
||||
# couldn't be deleted in the first pass) and recursively clears those
|
||||
# subdirectories. Finally when finished clearing all the children the
|
||||
# parent directory is deleted.
|
||||
command = """
|
||||
try:
|
||||
import os
|
||||
except ImportError:
|
||||
import uos as os
|
||||
def rmdir(directory):
|
||||
os.chdir(directory)
|
||||
for f in os.listdir():
|
||||
try:
|
||||
os.remove(f)
|
||||
except OSError:
|
||||
pass
|
||||
for f in os.listdir():
|
||||
rmdir(f)
|
||||
os.chdir('..')
|
||||
os.rmdir(directory)
|
||||
rmdir('{0}')
|
||||
""".format(
|
||||
directory
|
||||
)
|
||||
self._pyboard.enter_raw_repl()
|
||||
try:
|
||||
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||
except PyboardError as ex:
|
||||
message = ex.args[2].decode("utf-8")
|
||||
# Check if this is an OSError #2, i.e. directory doesn't exist
|
||||
# and rethrow it as something more descriptive.
|
||||
if message.find("OSError: [Errno 2] ENOENT") != -1:
|
||||
if not missing_okay:
|
||||
raise RuntimeError("No such directory: {0}".format(directory))
|
||||
else:
|
||||
raise ex
|
||||
self._pyboard.exit_raw_repl()
|
||||
|
||||
def run(self, filename, wait_output=True):
|
||||
"""Run the provided script and return its output. If wait_output is True
|
||||
(default) then wait for the script to finish and then print its output,
|
||||
otherwise just run the script and don't wait for any output.
|
||||
"""
|
||||
self._pyboard.enter_raw_repl()
|
||||
out = None
|
||||
if wait_output:
|
||||
# Run the file and wait for output to return.
|
||||
out = self._pyboard.execfile(filename)
|
||||
else:
|
||||
# Read the file and run it using lower level pyboard functions that
|
||||
# won't wait for it to finish or return output.
|
||||
with open(filename, "rb") as infile:
|
||||
self._pyboard.exec_raw_no_follow(infile.read())
|
||||
self._pyboard.exit_raw_repl()
|
||||
return out
|
343
.venv/lib/python3.9/site-packages/ampy/pyboard.py
Normal file
343
.venv/lib/python3.9/site-packages/ampy/pyboard.py
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
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
|
||||
|
||||
_rawdelay = None
|
||||
|
||||
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 succesful
|
||||
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
|
||||
|
||||
class Pyboard:
|
||||
def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0, rawdelay=0):
|
||||
global _rawdelay
|
||||
_rawdelay = rawdelay
|
||||
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
|
||||
for attempt in range(wait + 1):
|
||||
try:
|
||||
self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=1)
|
||||
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):
|
||||
# Brief delay before sending RAW MODE char if requests
|
||||
if _rawdelay > 0:
|
||||
time.sleep(_rawdelay)
|
||||
|
||||
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.
|
||||
# Modification from original pyboard.py below:
|
||||
# Add a small delay and send Ctrl-C twice after soft reboot to ensure
|
||||
# any main program loop in main.py is interrupted.
|
||||
time.sleep(0.5)
|
||||
self.serial.write(b'\x03')
|
||||
time.sleep(0.1) # (slight delay before second interrupt
|
||||
self.serial.write(b'\x03')
|
||||
# End modification above.
|
||||
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()
|
Reference in New Issue
Block a user