3
# This file is part of the MicroPython project, http://micropython.org/7
# Copyright (c) 2014-2021 Damien P. George8
# Copyright (c) 2017 Paul Sokolovsky10
# Permission is hereby granted, free of charge, to any person obtaining a copy11
# of this software and associated documentation files (the "Software"), to deal12
# in the Software without restriction, including without limitation the rights13
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell14
# copies of the Software, and to permit persons to whom the Software is15
# furnished to do so, subject to the following conditions:17
# The above copyright notice and this permission notice shall be included in18
# all copies or substantial portions of the Software.20
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR21
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,22
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE23
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER24
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,25
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN31
This module provides the Pyboard class, used to communicate with and32
control a MicroPython device over a communication channel. Both real33
boards and emulated devices (e.g. running in QEMU) are supported.34
Various communication channels are supported, including a serial35
connection, telnet-style network connection, external process41
pyb = pyboard.Pyboard('/dev/ttyACM0')45
pyb = pyboard.Pyboard('192.168.1.1')50
pyb.exec('import pyb')51
pyb.exec('pyb.LED(1).on()')54
Note: if using Python2 then pyb.exec must be written as pyb.exec_.55
To run a script from the local machine on the board and print out the results:58
pyboard.execfile('test.py', device='/dev/ttyACM0')60
This script can also be run directly. To execute a local script, use:66
python pyboard.py test.py77
from collections import namedtuple80
stdout = sys.stdout.buffer82
# Python2 doesn't have buffer attr86
def stdout_write_bytes(b):87
b = b.replace(b"\x04", b"")92
class PyboardError(Exception):93
def convert(self, info):94
if len(self.args) >= 3:95
if b"OSError" in self.args[2] and b"ENOENT" in self.args[2]:96
return OSError(errno.ENOENT, info)101
listdir_result = namedtuple("dir_result", ["name", "st_mode", "st_ino", "st_size"])105
def __init__(self, ip, user, password, read_timeout=None):109
self.tn = telnetlib.Telnet(ip, timeout=15)110
self.read_timeout = read_timeout111
if b"Login as:" in self.tn.read_until(b"Login as:", timeout=read_timeout):112
self.tn.write(bytes(user, "ascii") + b"\r\n")114
if b"Password:" in self.tn.read_until(b"Password:", timeout=read_timeout):115
# needed because of internal implementation details of the telnet server117
self.tn.write(bytes(password, "ascii") + b"\r\n")119
if b"for more information." in self.tn.read_until(120
b'Type "help()" for more information.', timeout=read_timeout123
from collections import deque128
raise PyboardError("Failed to establish a telnet connection with the board")137
def read(self, size=1):138
while len(self.fifo) < size:140
data = self.tn.read_eager()142
self.fifo.extend(data)146
if self.read_timeout is not None and timeout_count > 4 * self.read_timeout:151
while len(data) < size and len(self.fifo) > 0:152
data += bytes([self.fifo.popleft()])155
def write(self, data):160
n_waiting = len(self.fifo)162
data = self.tn.read_eager()163
self.fifo.extend(data)169
class ProcessToSerial:170
"Execute a process and emulate serial connection using its stdin/stdout."172
def __init__(self, cmd):175
self.subp = subprocess.Popen(179
preexec_fn=os.setsid,180
stdin=subprocess.PIPE,181
stdout=subprocess.PIPE,184
# Initially was implemented with selectors, but that adds Python3185
# dependency. However, there can be race conditions communicating186
# with a particular child process (like QEMU), and selectors may187
# still work better in that case, so left inplace for now.190
# self.sel = selectors.DefaultSelector()191
# self.sel.register(self.subp.stdout, selectors.EVENT_READ)195
self.poll = select.poll()196
self.poll.register(self.subp.stdout.fileno())201
os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)203
def read(self, size=1):205
while len(data) < size:206
data += self.subp.stdout.read(size - len(data))209
def write(self, data):210
self.subp.stdin.write(data)214
# res = self.sel.select(0)215
res = self.poll.poll(0)221
class ProcessPtyToTerminal:222
"""Execute a process which creates a PTY and prints slave PTY as223
first line of its output, and emulate serial connection using226
def __init__(self, cmd):231
self.subp = subprocess.Popen(235
preexec_fn=os.setsid,236
stdin=subprocess.PIPE,237
stdout=subprocess.PIPE,238
stderr=subprocess.STDOUT,240
pty_line = self.subp.stdout.readline().decode("utf-8")241
m = re.search(r"/dev/pts/[0-9]+", pty_line)243
print("Error: unable to find PTY device in startup line:", pty_line)247
# Compensate for some boards taking a bit longer to start249
# rtscts, dsrdtr params are to workaround pyserial bug:250
# http://stackoverflow.com/questions/34831131/pyserial-does-not-play-well-with-virtual-port251
self.serial = serial.Serial(pty, interCharTimeout=1, rtscts=True, dsrdtr=True)256
os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)258
def read(self, size=1):259
return self.serial.read(size)261
def write(self, data):262
return self.serial.write(data)265
return self.serial.inWaiting()280
self.in_raw_repl = False281
self.use_raw_paste = True282
if device.startswith("exec:"):283
self.serial = ProcessToSerial(device[len("exec:") :])284
elif device.startswith("execpty:"):285
self.serial = ProcessPtyToTerminal(device[len("qemupty:") :])286
elif device and device[0].isdigit() and device[-1].isdigit() and device.count(".") == 3:287
# device looks like an IP address288
self.serial = TelnetToSerial(device, user, password, read_timeout=10)291
import serial.tools.list_ports293
# Set options, and exclusive if pyserial supports it295
"baudrate": baudrate,297
"write_timeout": write_timeout,298
"interCharTimeout": 1,300
if serial.__version__ >= "3.3":301
serial_kwargs["exclusive"] = exclusive304
for attempt in range(wait + 1):307
self.serial = serial.Serial(**serial_kwargs)308
self.serial.port = device309
portinfo = list(serial.tools.list_ports.grep(device)) # type: ignore310
if portinfo and portinfo[0].manufacturer != "Microsoft":311
# ESP8266/ESP32 boards use RTS/CTS for flashing and boot mode selection.312
# DTR False: to avoid using the reset button will hang the MCU in bootloader mode313
# RTS False: to prevent pulses on rts on serial.close() that would POWERON_RESET an ESPxx314
self.serial.dtr = False # DTR False = gpio0 High = Normal boot315
self.serial.rts = False # RTS False = EN High = MCU enabled318
self.serial = serial.Serial(device, **serial_kwargs)320
except (OSError, IOError): # Py2 and Py3 have different errors324
sys.stdout.write("Waiting {} seconds for pyboard ".format(wait))327
sys.stdout.write(".")332
raise PyboardError("failed to access " + device)340
self, min_num_bytes, ending, timeout=10, data_consumer=None, timeout_overall=None343
min_num_bytes: Obsolete.344
ending: Return if 'ending' matches.345
timeout [s]: Return if timeout between characters. None: Infinite timeout.346
timeout_overall [s]: Return not later than timeout_overall. None: Infinite timeout.347
data_consumer: Use callback for incoming characters.348
If data_consumer is used then data is not accumulated and the ending must be 1 byte long350
It is not visible to the caller why the function returned. It could be ending or timeout.352
assert data_consumer is None or len(ending) == 1353
assert isinstance(timeout, (type(None), int, float))354
assert isinstance(timeout_overall, (type(None), int, float))357
begin_overall_s = begin_char_s = time.monotonic()359
if data.endswith(ending):361
elif self.serial.inWaiting() > 0:362
new_data = self.serial.read(1)364
data_consumer(new_data)367
data = data + new_data368
begin_char_s = time.monotonic()370
if timeout is not None and time.monotonic() >= begin_char_s + timeout:373
timeout_overall is not None374
and time.monotonic() >= begin_overall_s + timeout_overall380
def enter_raw_repl(self, soft_reset=True, timeout_overall=10):382
self._enter_raw_repl_unprotected(soft_reset, timeout_overall)383
except OSError as er:384
raise PyboardError("could not enter raw repl: {}".format(er))386
def _enter_raw_repl_unprotected(self, soft_reset, timeout_overall):387
self.serial.write(b"\r\x03") # ctrl-C: interrupt any running program389
# flush input (without relying on serial.flushInput())390
n = self.serial.inWaiting()393
n = self.serial.inWaiting()395
self.serial.write(b"\r\x01") # ctrl-A: enter raw REPL398
data = self.read_until(399
1, b"raw REPL; CTRL-B to exit\r\n>", timeout_overall=timeout_overall401
if not data.endswith(b"raw REPL; CTRL-B to exit\r\n>"):403
raise PyboardError("could not enter raw repl")405
self.serial.write(b"\x04") # ctrl-D: soft reset407
# Waiting for "soft reboot" independently to "raw REPL" (done below)408
# allows boot.py to print, which will show up after "soft reboot"409
# and before "raw REPL".410
data = self.read_until(1, b"soft reboot\r\n", timeout_overall=timeout_overall)411
if not data.endswith(b"soft reboot\r\n"):413
raise PyboardError("could not enter raw repl")415
data = self.read_until(1, b"raw REPL; CTRL-B to exit\r\n", timeout_overall=timeout_overall)416
if not data.endswith(b"raw REPL; CTRL-B to exit\r\n"):418
raise PyboardError("could not enter raw repl")420
self.in_raw_repl = True422
def exit_raw_repl(self):423
self.serial.write(b"\r\x02") # ctrl-B: enter friendly REPL424
self.in_raw_repl = False426
def follow(self, timeout, data_consumer=None):427
# wait for normal output428
data = self.read_until(1, b"\x04", timeout=timeout, data_consumer=data_consumer)429
if not data.endswith(b"\x04"):430
raise PyboardError("timeout waiting for first EOF reception")433
# wait for error output434
data_err = self.read_until(1, b"\x04", timeout=timeout)435
if not data_err.endswith(b"\x04"):436
raise PyboardError("timeout waiting for second EOF reception")437
data_err = data_err[:-1]439
# return normal and error output440
return data, data_err442
def raw_paste_write(self, command_bytes):443
# Read initial header, with window size.444
data = self.serial.read(2)445
window_size = struct.unpack("<H", data)[0]446
window_remain = window_size448
# Write out the command_bytes data.450
while i < len(command_bytes):451
while window_remain == 0 or self.serial.inWaiting():452
data = self.serial.read(1)454
# Device indicated that a new window of data can be sent.455
window_remain += window_size456
elif data == b"\x04":457
# Device indicated abrupt end. Acknowledge it and finish.458
self.serial.write(b"\x04")461
# Unexpected data from device.462
raise PyboardError("unexpected read during raw paste: {}".format(data))463
# Send out as much data as possible that fits within the allowed window.464
b = command_bytes[i : min(i + window_remain, len(command_bytes))]466
window_remain -= len(b)469
# Indicate end of data.470
self.serial.write(b"\x04")472
# Wait for device to acknowledge end of data.473
data = self.read_until(1, b"\x04")474
if not data.endswith(b"\x04"):475
raise PyboardError("could not complete raw paste: {}".format(data))477
def exec_raw_no_follow(self, command):478
if isinstance(command, bytes):479
command_bytes = command481
command_bytes = bytes(command, encoding="utf8")483
# check we have a prompt484
data = self.read_until(1, b">")485
if not data.endswith(b">"):486
raise PyboardError("could not enter raw repl")488
if self.use_raw_paste:489
# Try to enter raw-paste mode.490
self.serial.write(b"\x05A\x01")491
data = self.serial.read(2)493
# Device understood raw-paste command but doesn't support it.495
elif data == b"R\x01":496
# Device supports raw-paste mode, write out the command using this mode.497
return self.raw_paste_write(command_bytes)499
# Device doesn't support raw-paste, fall back to normal raw REPL.500
data = self.read_until(1, b"w REPL; CTRL-B to exit\r\n>")501
if not data.endswith(b"w REPL; CTRL-B to exit\r\n>"):503
raise PyboardError("could not enter raw repl")504
# Don't try to use raw-paste mode again for this connection.505
self.use_raw_paste = False507
# Write command using standard raw REPL, 256 bytes every 10ms.508
for i in range(0, len(command_bytes), 256):509
self.serial.write(command_bytes[i : min(i + 256, len(command_bytes))])511
self.serial.write(b"\x04")513
# check if we could exec command514
data = self.serial.read(2)516
raise PyboardError("could not exec command (response: %r)" % data)518
def exec_raw(self, command, timeout=10, data_consumer=None):519
self.exec_raw_no_follow(command)520
return self.follow(timeout, data_consumer)522
def eval(self, expression, parse=False):524
ret = self.exec_("print(repr({}))".format(expression))526
return ast.literal_eval(ret.decode())528
ret = self.exec_("print({})".format(expression))532
# In Python3, call as pyboard.exec(), see the setattr call below.533
def exec_(self, command, data_consumer=None):534
ret, ret_err = self.exec_raw(command, data_consumer=data_consumer)536
raise PyboardError("exception", ret, ret_err)539
def execfile(self, filename):540
with open(filename, "rb") as f:542
return self.exec_(pyfile)545
t = str(self.eval("machine.RTC().datetime()"), encoding="utf8")[1:-1].split(", ")546
return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])548
def fs_exists(self, src):550
self.exec_("import os\nos.stat(%s)" % (("'%s'" % src) if src else ""))555
def fs_ls(self, src):557
"import os\nfor f in os.ilistdir(%s):\n"558
" print('{:12} {}{}'.format(f[3]if len(f)>3 else 0,f[0],'/'if f[1]&0x4000 else ''))"559
% (("'%s'" % src) if src else "")561
self.exec_(cmd, data_consumer=stdout_write_bytes)563
def fs_listdir(self, src=""):566
def repr_consumer(b):567
buf.extend(b.replace(b"\x04", b""))569
cmd = "import os\nfor f in os.ilistdir(%s):\n print(repr(f), end=',')" % (570
("'%s'" % src) if src else ""574
self.exec_(cmd, data_consumer=repr_consumer)576
except PyboardError as e:580
listdir_result(*f) if len(f) == 4 else listdir_result(*(f + (0,)))581
for f in ast.literal_eval(buf.decode())584
def fs_stat(self, src):586
self.exec_("import os")587
return os.stat_result(self.eval("os.stat(%s)" % ("'%s'" % src), parse=True))588
except PyboardError as e:591
def fs_cat(self, src, chunk_size=256):593
"with open('%s') as f:\n while 1:\n"594
" b=f.read(%u)\n if not b:break\n print(b,end='')" % (src, chunk_size)596
self.exec_(cmd, data_consumer=stdout_write_bytes)598
def fs_readfile(self, src, chunk_size=256):601
def repr_consumer(b):602
buf.extend(b.replace(b"\x04", b""))605
"with open('%s', 'rb') as f:\n while 1:\n"606
" b=f.read(%u)\n if not b:break\n print(b,end='')" % (src, chunk_size)609
self.exec_(cmd, data_consumer=repr_consumer)610
except PyboardError as e:612
return ast.literal_eval(buf.decode())614
def fs_writefile(self, dest, data, chunk_size=256):615
self.exec_("f=open('%s','wb')\nw=f.write" % dest)617
chunk = data[:chunk_size]618
self.exec_("w(" + repr(chunk) + ")")619
data = data[len(chunk) :]620
self.exec_("f.close()")622
def fs_cp(self, src, dest, chunk_size=256, progress_callback=None):623
if progress_callback:624
src_size = self.fs_stat(src).st_size626
self.exec_("fr=open('%s','rb')\nr=fr.read\nfw=open('%s','wb')\nw=fw.write" % (src, dest))628
data_len = int(self.exec_("d=r(%u)\nw(d)\nprint(len(d))" % chunk_size))631
if progress_callback:633
progress_callback(written, src_size)634
self.exec_("fr.close()\nfw.close()")636
def fs_get(self, src, dest, chunk_size=256, progress_callback=None):637
if progress_callback:638
src_size = self.fs_stat(src).st_size640
self.exec_("f=open('%s','rb')\nr=f.read" % src)641
with open(dest, "wb") as f:644
self.exec_("print(r(%u))" % chunk_size, data_consumer=lambda d: data.extend(d))645
assert data.endswith(b"\r\n\x04")647
data = ast.literal_eval(str(data[:-3], "ascii"))648
if not isinstance(data, bytes):649
raise ValueError("Not bytes")650
except (UnicodeError, ValueError) as e:651
raise PyboardError("fs_get: Could not interpret received data: %s" % str(e))655
if progress_callback:657
progress_callback(written, src_size)658
self.exec_("f.close()")660
def fs_put(self, src, dest, chunk_size=256, progress_callback=None):661
if progress_callback:662
src_size = os.path.getsize(src)664
self.exec_("f=open('%s','wb')\nw=f.write" % dest)665
with open(src, "rb") as f:667
data = f.read(chunk_size)670
if sys.version_info < (3,):671
self.exec_("w(b" + repr(data) + ")")673
self.exec_("w(" + repr(data) + ")")674
if progress_callback:676
progress_callback(written, src_size)677
self.exec_("f.close()")679
def fs_mkdir(self, dir):680
self.exec_("import os\nos.mkdir('%s')" % dir)682
def fs_rmdir(self, dir):683
self.exec_("import os\nos.rmdir('%s')" % dir)685
def fs_rm(self, src):686
self.exec_("import os\nos.remove('%s')" % src)688
def fs_touch(self, src):689
self.exec_("f=open('%s','a')\nf.close()" % src)692
# in Python2 exec is a keyword so one must use "exec_"693
# but for Python3 we want to provide the nicer version "exec"694
setattr(Pyboard, "exec", Pyboard.exec_)697
def execfile(filename, device="/dev/ttyACM0", baudrate=115200, user="micro", password="python"):698
pyb = Pyboard(device, baudrate, user, password)700
output = pyb.execfile(filename)701
stdout_write_bytes(output)706
def filesystem_command(pyb, args, progress_callback=None, verbose=False):707
def fname_remote(src):708
if src.startswith(":"):710
# Convert all path separators to "/", because that's what a remote device uses.711
return src.replace(os.path.sep, "/")713
def fname_cp_dest(src, dest):714
_, src = os.path.split(src)715
if dest is None or dest == "":719
elif dest.endswith("/"):729
"cp: missing destination file operand after '{}'".format(args[0])733
if dest.startswith(":"):734
op_remote_src = pyb.fs_cp735
op_local_src = pyb.fs_put737
op_remote_src = pyb.fs_get738
op_local_src = lambda src, dest, **_: __import__("shutil").copy(src, dest)741
print("cp %s %s" % (src, dest))742
if src.startswith(":"):746
src2 = fname_remote(src)747
dest2 = fname_cp_dest(src2, fname_remote(dest))748
op(src2, dest2, progress_callback=progress_callback)753
"mkdir": pyb.fs_mkdir,755
"rmdir": pyb.fs_rmdir,756
"touch": pyb.fs_touch,759
raise PyboardError("'{}' is not a filesystem command".format(cmd))760
if cmd == "ls" and not args:763
src = fname_remote(src)765
print("%s :%s" % (cmd, src))767
except PyboardError as er:769
print(str(er.args[2], "ascii"))777
_injected_import_hook_code = """\780
class File(io.IOBase):783
def ioctl(self, request, arg):785
def readinto(self, buf):786
buf[:] = memoryview(_injected_buf)[self.off:self.off + len(buf)]789
mount = umount = chdir = lambda *args: None790
def stat(self, path):791
if path == '_injected.mpy':792
return tuple(0 for _ in range(10))794
raise OSError(-2) # ENOENT795
def open(self, path, mode):799
from _injected import *801
del _injected_buf, _FS808
cmd_parser = argparse.ArgumentParser(description="Run scripts on the pyboard.")809
cmd_parser.add_argument(812
default=os.environ.get("PYBOARD_DEVICE", "/dev/ttyACM0"),813
help="the serial device or the IP address of the pyboard",815
cmd_parser.add_argument(818
default=os.environ.get("PYBOARD_BAUDRATE", "115200"),819
help="the baud rate of the serial device",821
cmd_parser.add_argument("-u", "--user", default="micro", help="the telnet login username")822
cmd_parser.add_argument("-p", "--password", default="python", help="the telnet login password")823
cmd_parser.add_argument("-c", "--command", help="program passed in as string")824
cmd_parser.add_argument(829
help="seconds to wait for USB connected board to become available",831
group = cmd_parser.add_mutually_exclusive_group()836
help="Whether to perform a soft reset when connecting to the board [default]",840
action="store_false",843
group = cmd_parser.add_mutually_exclusive_group()848
help="follow the output after running the scripts [default if no scripts given]",852
action="store_false",855
group = cmd_parser.add_mutually_exclusive_group()860
help="Open the serial device for exclusive access [default]",864
action="store_false",867
cmd_parser.add_argument(871
help="perform a filesystem action: "872
"cp local :device | cp :device local | cat path | ls [path] | rm path | mkdir path | rmdir path",874
cmd_parser.add_argument("files", nargs="*", help="input files")875
args = cmd_parser.parse_args()877
# open the connection to the pyboard880
args.device, args.baudrate, args.user, args.password, args.wait, args.exclusive882
except PyboardError as er:886
# run any command or file(s)887
if args.command is not None or args.filesystem or len(args.files):888
# we must enter raw-REPL mode to execute commands889
# this will do a soft-reset of the board891
pyb.enter_raw_repl(args.soft_reset)892
except PyboardError as er:899
if args.follow is None or args.follow:900
ret, ret_err = pyb.exec_raw(901
buf, timeout=None, data_consumer=stdout_write_bytes904
pyb.exec_raw_no_follow(buf)906
except PyboardError as er:910
except KeyboardInterrupt:915
stdout_write_bytes(ret_err)918
# do filesystem commands, if given920
filesystem_command(pyb, args.files, verbose=True)923
# run the command, if given924
if args.command is not None:925
execbuffer(args.command.encode("utf-8"))928
for filename in args.files:929
with open(filename, "rb") as f:931
if filename.endswith(".mpy") and pyfile[0] == ord("M"):932
pyb.exec_("_injected_buf=" + repr(pyfile))933
pyfile = _injected_import_hook_code936
# exiting raw-REPL just drops to friendly-REPL mode939
# if asked explicitly, or no files given, then follow the output940
if args.follow or (args.command is None and not args.filesystem and len(args.files) == 0):942
ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes)943
except PyboardError as er:946
except KeyboardInterrupt:950
stdout_write_bytes(ret_err)953
# close the connection to the pyboard957
if __name__ == "__main__":