#!/usr/bin/env python3 # # This file is part of the MicroPython project, http://micropython.org/ # # The MIT License (MIT) # # Copyright (c) 2017 Scott Shawcroft for Adafruit Industries # Copyright (c) 2018 Noralf Trønnes # # Parts taken from pyboard.py: # Copyright (c) 2014-2016 Damien P. George # Copyright (c) 2017 Paul Sokolovsky # # 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 os import re import serial import sys import time import serial.tools.list_ports import sh import shutil class CPboardError(BaseException): pass # supervisor/messages/default.h: MSG_NEWLINE = b"\r\n" MSG_SAFE_MODE_CRASH = b"Looks like our core CircuitPython code crashed hard. Whoops!" MSG_SAFE_MODE_BROWN_OUT_LINE_1 = b"The microcontroller's power dipped. Please make sure your power supply provides" MSG_SAFE_MODE_BROWN_OUT_LINE_2 = b"enough power for the whole circuit and press reset (after ejecting CIRCUITPY)." MSG_WAIT_BEFORE_REPL = b"Press any key to enter the REPL. Use CTRL-D to reload." class REPL: CHAR_CTRL_A = b'\x01' CHAR_CTRL_B = b'\x02' CHAR_CTRL_C = b'\x03' CHAR_CTRL_D = b'\x04' def __init__(self, board): self.board = board self.write_chunk_size = 32 self.safe_mode = False self.session = b'' def __enter__(self): self.reset() return self def __exit__(self, exception_type, exception_value, traceback): pass @property def serial(self): return self.board.serial def read(self): if self.serial.inWaiting(): data = self.serial.read(self.serial.inWaiting()) else: data = b'' self.session += data return data def read_until(self, ending, timeout=10): data = b'' timeout_count = 0 while True: if data.endswith(ending): break elif self.serial.inWaiting() > 0: new_data = self.serial.read(1) data += new_data self.session += new_data timeout_count = 0 else: timeout_count += 1 if timeout is not None and timeout_count >= 100 * timeout: raise TimeoutError(110, "timeout waiting for", ending) time.sleep(0.01) return data def write(self, data, chunk_size=None): if chunk_size is None: chunk_size = self.write_chunk_size if not isinstance(data, bytes): data = bytes(data, encoding='utf8') for i in range(0, len(data), chunk_size): chunk = data[i:min(i + chunk_size, len(data))] self.session += chunk self.serial.write(chunk) time.sleep(0.01) def reset(self): # Use read() since serial.reset_input_buffer() fails with termios.error now and then self.read() self.session = b'' self.write(b'\r' + REPL.CHAR_CTRL_C + REPL.CHAR_CTRL_C) # interrupt any running program self.write(b'\r' + REPL.CHAR_CTRL_B) # enter or reset friendly repl data = self.read_until(b'>>> ') def execute(self, code, timeout=10, async=False): self.read() # Throw away self.write(REPL.CHAR_CTRL_A) self.read_until(b'\r\n>') self.write(code) self.write(REPL.CHAR_CTRL_D) if async: return b'', b'' self.read_until(b'OK') output = self.read_until(b'\x04', timeout=timeout) output = output[:-1] error = self.read_until(b'\x04') error = error[:-1] return output, error def run(self): if self.safe_mode: raise RuntimeError("Can't run in safe mode") self.reset() self.write(REPL.CHAR_CTRL_D) data = self.read_until(b' output:\r\n') if b'Running in safe mode' in data: self.safe_mode = True raise RuntimeError("Can't run in safe mode") # TODO: MSG_SAFE_MODE_CRASH # TODO: BROWNOUT marker = MSG_NEWLINE + MSG_WAIT_BEFORE_REPL + MSG_NEWLINE data = self.read_until(marker) data = data.split(marker)[0] # Haven't found out why we have to strip off this... if data.endswith(b'\r\n\r\n'): data = data[:-4] return data class Disk: def __init__(self, dev): self.dev = os.path.realpath(dev) self.mountpoint = None with open('/etc/mtab', 'r') as f: mtab = f.read() mount = [mount.split(' ') for mount in mtab.splitlines() if mount.startswith(self.dev)] if mount: self._path = mount[0][1] else: name = os.path.basename(dev) sh.pmount("-tvfat", dev, name, _timeout=10) self.mountpoint = "/media/" + name self._path = self.mountpoint def __enter__(self): return self def __exit__(self, exception_type, exception_value, traceback): try: self.close() except: pass @property def path(self): return self._path def close(self): if not self.mountpoint: return mountpoint = self.mountpoint self.mountpoint = None start_time = time.monotonic() unmounted = False while not unmounted and start_time - time.monotonic() < 30: try: sh.pumount(mountpoint) unmounted = True except sh.ErrorReturnCode_5: time.sleep(0.1) def sync(self): disk_device = os.path.basename(self.dev)[:-1] os.sync() # Monitor the block device so we know when the sync request is actually finished. with open("/sys/block/" + disk_device + "/stat", "r") as f: disk_inflight = 1 last_wait_time = 0 wait_time = 1 while disk_inflight > 0 or wait_time > last_wait_time: f.seek(0) stats = f.read() block_stats = stats.split() disk_inflight = int(block_stats[8]) last_wait_time = wait_time wait_time = int(block_stats[9]) def copy(self, src, dst=None, sync=True): if dst is None: dst = os.path.basename(src) shutil.copy(src, os.path.join(self.path, dst)) if sync: self.sync() class Firmware: def __init__(self, board): self.board = board @property def disk(self): disks = self.board.get_disks() if len(disks) != 1: raise RuntimeError("Boot disk not found for: " + self.board.device) return Disk(disks[0]) @property def info(self): with self.disk as disk: fname = os.path.join(disk.path, 'INFO_UF2.TXT') with open(fname, 'r') as f: info = f.read() lines = info.splitlines() res = {} res['header'] = lines[0] for line in lines[1:]: k, _, v = line.partition(':') res[k.replace(':', '')] = v.strip() return res def upload(self, fw): with open(fw, 'rb') as f: header = f.read(32) if header[0:4] != b'UF2\n': raise ValueError('Only UF2 files are supported') self.board.close() with self.disk as disk: disk.copy(fw, sync=False) class CPboard: @classmethod def from_try_all(cls, name, **kwargs): try: return CPboard.from_build_name(name, **kwargs) except ValueError: pass vendor, _, product = name.partition(':') if vendor and product: return CPboard.from_usb(**kwargs, idVendor=int(vendor, 16), idProduct=int(product, 16)) return CPboard(name, **kwargs) @classmethod def from_build_name(cls, name, **kwargs): boards = { #'arduino_zero' 'circuitplayground_express' : (0x239a, 0x8019), #'feather_m0_adalogger' : (0x239a, ), #'feather_m0_basic' : (0x239a, ), 'feather_m0_express' : (0x239a, 0x8023), #'feather_m0_rfm69' : (0x239a, ), #'feather_m0_rfm9x' : (0x239a, ), #'feather_m0_supersized' : (0x239a, ), #'feather_m4_express' : (0x239a, ), #'gemma_m0' : (0x239a, ), #'itsybitsy_m0_express' : (0x239a, ), #'itsybitsy_m4_express' : (0x239a, ), 'metro_m0_express' : (0x239a, 0x8014), 'metro_m4_express' : (0x239a, 0x8021), #'metro_m4_express_revb' : (0x239a, ), #'pirkey_m0' : (0x239a, ), #'trinket_m0' : (0x239a, ), #'trinket_m0_haxpress' : (0x239a, ), #'ugame10' } try: vendor, product = boards[name] except KeyError: raise ValueError("Unknown build name: " + name) return CPboard.from_usb(**kwargs, idVendor=vendor, idProduct=product) @classmethod def from_build_name_bootloader(cls, name, **kwargs): boards = { #'arduino_zero' #'circuitplayground_express' : (0x239a, ), #'feather_m0_adalogger' : (0x239a, ), #'feather_m0_basic' : (0x239a, ), 'feather_m0_express' : (0x239a, 0x001b), #'feather_m0_rfm69' : (0x239a, ), #'feather_m0_rfm9x' : (0x239a, ), #'feather_m0_supersized' : (0x239a, ), #'feather_m4_express' : (0x239a, ), #'gemma_m0' : (0x239a, ), #'itsybitsy_m0_express' : (0x239a, ), #'itsybitsy_m4_express' : (0x239a, ), #'metro_m0_express' : (0x239a, 0x8014), 'metro_m4_express' : (0x239a, 0x0021), #'metro_m4_express_revb' : (0x239a, ), #'pirkey_m0' : (0x239a, ), #'trinket_m0' : (0x239a, ), #'trinket_m0_haxpress' : (0x239a, ), #'ugame10' } try: vendor, product = boards[name] except KeyError: raise ValueError("Unknown build name: " + name) board = CPboard.from_usb(**kwargs, idVendor=vendor, idProduct=product) board.bootloader = True return board @classmethod def from_usb(cls, baudrate=115200, wait=0, timeout=10, **kwargs): import usb.core dev = usb.core.find(**kwargs) if not dev: s = "Can't find USB device: " args = [] for x in kwargs.items(): try: args.append('%s=0x%x' % x) except: args.append('%s = %s' % x) raise RuntimeError("Can't find USB device: " + ', '.join(args)) return cls(dev, baudrate=baudrate, wait=wait, timeout=timeout) def __init__(self, device, baudrate=115200, wait=0, timeout=10): self.device = device self.usb_dev = None try: # Is it a usb.core.Device? portstr = ':' + '.'.join(map(str, device.port_numbers)) + ':' except: pass else: serials = [serial for serial in os.listdir("/dev/serial/by-path") if portstr in serial] if len(serials) != 1: raise RuntimeError("Can't find excatly one matching usb serial device") self.device = os.path.realpath("/dev/serial/by-path/" + serials[0]) self.usb_dev = device self.baudrate = baudrate self.wait = wait self.timeout = timeout self.debug = True self.mount = None self.serial = None self.bootloader = False self.repl = REPL(self) def __enter__(self): self.open() return self def __exit__(self, exception_type, exception_value, traceback): self.close() def open(self, baudrate=None, wait=None): if self.serial: return if baudrate is None: baudrate = self.baudrate if wait is None: wait = self.wait delayed = False for attempt in range(wait + 1): try: self.serial = serial.Serial(self.device, baudrate=self.baudrate, timeout=self.timeout, write_timeout=self.timeout, 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 board '.format(wait)) delayed = True time.sleep(1) sys.stdout.write('.') sys.stdout.flush() else: if delayed: print('') raise CPboardError('failed to access ' + self.device) if delayed: print('') def close(self): if self.serial: self.serial.close() self.serial = None def exec(self, command, timeout=10, async=False): with self.repl as repl: try: output, error = repl.execute(command, timeout=timeout, async=async) except OSError as e: if self.debug: print('exec: session: ', self.repl.session) raise CPboardError('timeout', e) if error: raise CPboardError('exception', output, error) return output def eval(self, expression, timeout=10): command = 'print({})'.format(expression) with self.repl as repl: output, error = repl.execute(command, timeout=timeout) if error: raise CPboardError('exception', output, error) try: res = eval(str(output, encoding='utf8')) except: raise CPboardError('failed to eval: %s' % output) return res def _reset(self, mode='NORMAL'): self.exec("import microcontroller;microcontroller.on_next_reset(microcontroller.RunMode.%s)" % mode) try: self.exec("import microcontroller;microcontroller.reset()", async=True) except OSError: pass def reset(self, safe_mode=False, delay=5, wait=10): self._reset('SAFE_MODE' if safe_mode else 'NORMAL') self.close() time.sleep(delay) self.open(wait) time.sleep(delay) def reset_to_bootloader(self, repl=False): if repl: self._reset('BOOTLOADER') self.close() else: self.close() s = serial.Serial(self.device, 1200, write_timeout=4, timeout=4) s.close() def get_port_info(self): portinfo = None for port_iter in serial.tools.list_ports.comports(): if port_iter.device == self.device: portinfo = port_iter break return portinfo @property def serial_number(self): try: # Permissions are needed to read the value return self.usb_dev.serial_number except: pass p = self.get_port_info() return p.serial_number if p else None def get_disks(self): if self.usb_dev: portstr = ':' + '.'.join(map(str, self.usb_dev.port_numbers)) + ':' return ["/dev/disk/by-path/" + disk for disk in os.listdir("/dev/disk/by-path") if portstr in disk] serial = self.serial_number if not serial: raise RuntimeError("Serial number not found for: " + self.device) return ["/dev/disk/by-id/" + disk for disk in os.listdir("/dev/disk/by-id") if serial in disk] @property def disk(self): disks = self.get_disks() part = [part for part in disks if 'part1' in part] if not part: raise RuntimeError("Disk not found for: " + self.device) return Disk(part[0]) @property def firmware(self): return Firmware(self) def execfile_disk(self, filename): with self.disk as disk: disk.copy(filename, 'code.py') with self.repl as repl: try: output = repl.run() except OSError as e: raise CPboardError('timeout', e) except RuntimeError: if self.repl.safe_mode: raise PyboardError("Can't run in safe mode") else: raise return output def execfile(self, filename, timeout=10): if os.environ.get('CPBOARD_EXEC_MODE') == 'disk': return self.execfile_disk(filename) else: with open(filename, 'rb') as f: pyfile = f.read() return self.exec(pyfile, timeout=timeout) # Implement just enough to make tests/run-tests work PyboardError = CPboardError class Pyboard: def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0): self.board = CPboard.from_try_all(device, baudrate=baudrate, wait=wait) with self.board.disk as disk: disk.copy('skip_if.py') def close(self): self.board.close() def enter_raw_repl(self): self.board.open() def execfile(self, filename): return self.board.execfile(filename) def eval_namedtuple(board, command): from collections import namedtuple s = board.exec("print(%s)" % command) s = s.decode().strip() items = [key.split('=') for key in s[1:-1].split(', ')] keys = [item[0] for item in items] vals = [item[1] for item in items] nt = namedtuple('eval', keys) res = nt(*[eval(val) for val in vals]) return res def os_uname(board): return eval_namedtuple(board, "__import__('os').uname()") def print_verbose(cargs, *args, **kwargs): if cargs.verbose: print(*args, flush=True, **kwargs) def upload(args): try: board = CPboard.from_build_name_bootloader(args.board) print_verbose(args, 'Board is already in the bootloader') except (ValueError, RuntimeError): board = CPboard.from_try_all(args.board) print_verbose(args, "Serial number :", board.serial_number) if not (args.quiet or board.bootloader): board.open() print('Current version:', os_uname(board).version, flush=True) if not board.bootloader: print_verbose(args, 'Reset to bootloader...', end='') board.reset_to_bootloader(repl=True) # Feather M0 Express doesn't respond to 1200 baud time.sleep(5) print_verbose(args, 'done') print_verbose(args, 'Bootloader:', board.firmware.info) print_verbose(args, 'Upload firmware...', end='') board.firmware.upload(args.firmware) print_verbose(args, 'done') print_verbose(args, 'Wait for board...', end='') time.sleep(5) print_verbose(args, 'done') if not args.quiet: if board.bootloader: board = CPboard.from_try_all(args.board) board.open(wait=10) print('New version:', os_uname(board).version, flush=True) def print_error_exit(args, e): if args.debug: return False if not args.quiet: print(e, file=sys.stderr) sys.exit(1) def main(): import argparse cmd_parser = argparse.ArgumentParser(description='Circuit Python Board Tool') cmd_parser.add_argument('board', help='build_name, vid:pid or /dev/tty') cmd_parser.add_argument('-f', '--firmware', help='upload UF2 firmware file') cmd_parser.add_argument('-c', '--command', help='program passed in as string') cmd_parser.add_argument('--tty', action='store_true', help='print tty') cmd_parser.add_argument('--verbose', '-v', action='count', default=0, help='be verbose') cmd_parser.add_argument('-q', '--quiet', action='store_true', help='be quiet') cmd_parser.add_argument('--debug', action='store_true', help='raise exceptions') args = cmd_parser.parse_args() if args.quiet: args.verbose = 0 args.debug = False if args.firmware: try: upload(args) except BaseException as e: if not print_error_exit(args, e): raise sys.exit(0) try: board = CPboard.from_try_all(args.board) except BaseException as e: if not print_error_exit(args, e): raise if args.verbose: exec_mode = os.environ.get('CPBOARD_EXEC_MODE') if exec_mode: print('CPBOARD_EXEC_MODE =', exec_mode) # Make sure we can open serial try: with board: pass except BaseException as e: if not print_error_exit(args, e): raise if args.tty: print(board.device) elif args.command: with board as b: print(b.eval(args.command)) else: with board as b: print('Device: ', end='') if b.usb_dev: print('%04x:%04x on ' % (b.usb_dev.idVendor, b.usb_dev.idProduct), end='') print(b.device) print('Serial number:', b.serial_number) uname = os_uname(b) print('os.uname:') print(' sysname:', uname.sysname) print(' nodename:', uname.nodename) print(' release:', uname.release) print(' version:', uname.version) print(' machine:', uname.machine) if __name__ == "__main__": main()