From 8f218a5d3c7583a171c80825f8aa0246fd509fc2 Mon Sep 17 00:00:00 2001 From: Hayden Roche Date: Tue, 1 Aug 2023 16:06:29 -0700 Subject: [PATCH 01/14] Make changes to support HitL testing. --- README.md | 10 +- .../{cpy-example.py => cpy_example.py} | 58 +- .../{i2c-example.py => i2c_example.py} | 0 .../{mpy-example.py => mpy_example.py} | 63 +- .../{rpi-example.py => rpi_example.py} | 0 .../{serial-example.py => serial_example.py} | 0 pytest.ini | 2 + test/hitl/conftest.py | 101 ++ test/hitl/deps/pyboard.py | 922 ++++++++++++++++++ test/hitl/test_basic_comms.py | 23 + 10 files changed, 1108 insertions(+), 71 deletions(-) rename examples/notecard-basics/{cpy-example.py => cpy_example.py} (63%) rename examples/notecard-basics/{i2c-example.py => i2c_example.py} (100%) rename examples/notecard-basics/{mpy-example.py => mpy_example.py} (61%) rename examples/notecard-basics/{rpi-example.py => rpi_example.py} (100%) rename examples/notecard-basics/{serial-example.py => serial_example.py} (100%) create mode 100644 pytest.ini create mode 100644 test/hitl/conftest.py create mode 100644 test/hitl/deps/pyboard.py create mode 100644 test/hitl/test_basic_comms.py diff --git a/README.md b/README.md index babb90c..2d7bc20 100644 --- a/README.md +++ b/README.md @@ -136,11 +136,11 @@ The documentation for this library can be found The [examples](examples/) directory contains examples for using this library with: -- [Serial](examples/notecard-basics/serial-example.py) -- [I2C](examples/notecard-basics/i2c-example.py) -- [RaspberryPi](examples/notecard-basics/rpi-example.py) -- [CircuitPython](examples/notecard-basics/cpy-example.py) -- [MicroPython](examples/notecard-basics/mpy-example.py) +- [Serial](examples/notecard-basics/serial_example.py) +- [I2C](examples/notecard-basics/i2c_example.py) +- [RaspberryPi](examples/notecard-basics/rpi_example.py) +- [CircuitPython](examples/notecard-basics/cpy_example.py) +- [MicroPython](examples/notecard-basics/mpy_example.py) ## Contributing diff --git a/examples/notecard-basics/cpy-example.py b/examples/notecard-basics/cpy_example.py similarity index 63% rename from examples/notecard-basics/cpy-example.py rename to examples/notecard-basics/cpy_example.py index 2ba5ae5..5ab42ec 100644 --- a/examples/notecard-basics/cpy-example.py +++ b/examples/notecard-basics/cpy_example.py @@ -7,12 +7,7 @@ import time import notecard -productUID = "com.your-company.your-project" - -# Choose either UART or I2C for Notecard -use_uart = True - -if sys.implementation.name != 'circuitpython': +if sys.implementation.name != "circuitpython": raise Exception("Please run this example in a CircuitPython environment.") import board # noqa: E402 @@ -30,10 +25,10 @@ def NotecardExceptionInfo(exception): """ name = exception.__class__.__name__ return sys.platform + ": " + name \ - + ": " + ' '.join(map(str, exception.args)) + + ": " + " ".join(map(str, exception.args)) -def configure_notecard(card): +def configure_notecard(card, product_uid): """Submit a simple JSON-based request to the Notecard. Args: @@ -41,7 +36,7 @@ def configure_notecard(card): """ req = {"req": "hub.set"} - req["product"] = productUID + req["product"] = product_uid req["mode"] = "continuous" try: @@ -76,41 +71,38 @@ def get_temp_and_voltage(card): return temp, voltage -def main(): +def run_example(product_uid, use_uart=True): """Connect to Notcard and run a transaction test.""" print("Opening port...") - try: - if use_uart: - port = busio.UART(board.TX, board.RX, baudrate=9600) - else: - port = busio.I2C(board.SCL, board.SDA) - except Exception as exception: - raise Exception("error opening port: " - + NotecardExceptionInfo(exception)) + if use_uart: + port = busio.UART(board.TX, board.RX, baudrate=9600) + else: + port = busio.I2C(board.SCL, board.SDA) print("Opening Notecard...") - try: - if use_uart: - card = notecard.OpenSerial(port, debug=True) - else: - card = notecard.OpenI2C(port, 0, 0, debug=True) - except Exception as exception: - raise Exception("error opening notecard: " - + NotecardExceptionInfo(exception)) + if use_uart: + card = notecard.OpenSerial(port, debug=True) + else: + card = notecard.OpenI2C(port, 0, 0, debug=True) # If success, configure the Notecard and send some data - configure_notecard(card) + configure_notecard(card, product_uid) temp, voltage = get_temp_and_voltage(card) req = {"req": "note.add"} req["sync"] = True req["body"] = {"temp": temp, "voltage": voltage} - try: - card.Transaction(req) - except Exception as exception: - print("Transaction error: " + NotecardExceptionInfo(exception)) - time.sleep(5) + card.Transaction(req) + + # Developer note: do not modify the line below, as we use this as to signify + # that the example ran successfully to completion. We then use that to + # determine pass/fail for certain tests that leverage these examples. + print("Example complete.") -main() +if __name__ == "__main__": + product_uid = "com.your-company.your-project" + # Choose either UART or I2C for Notecard + use_uart = True + run_example(product_uid, use_uart) diff --git a/examples/notecard-basics/i2c-example.py b/examples/notecard-basics/i2c_example.py similarity index 100% rename from examples/notecard-basics/i2c-example.py rename to examples/notecard-basics/i2c_example.py diff --git a/examples/notecard-basics/mpy-example.py b/examples/notecard-basics/mpy_example.py similarity index 61% rename from examples/notecard-basics/mpy-example.py rename to examples/notecard-basics/mpy_example.py index 6bf1b72..2c4b7f5 100644 --- a/examples/notecard-basics/mpy-example.py +++ b/examples/notecard-basics/mpy_example.py @@ -7,16 +7,12 @@ import time import notecard -productUID = "com.your-company.your-project" - -# Choose either UART or I2C for Notecard -use_uart = True - -if sys.implementation.name != 'micropython': +if sys.implementation.name != "micropython": raise Exception("Please run this example in a MicroPython environment.") from machine import UART # noqa: E402 from machine import I2C # noqa: E402 +from machine import Pin def NotecardExceptionInfo(exception): @@ -30,10 +26,10 @@ def NotecardExceptionInfo(exception): """ name = exception.__class__.__name__ return sys.platform + ": " + name + ": " \ - + ' '.join(map(str, exception.args)) + + " ".join(map(str, exception.args)) -def configure_notecard(card): +def configure_notecard(card, product_uid): """Submit a simple JSON-based request to the Notecard. Args: @@ -41,7 +37,7 @@ def configure_notecard(card): """ req = {"req": "hub.set"} - req["product"] = productUID + req["product"] = product_uid req["mode"] = "continuous" try: @@ -76,43 +72,44 @@ def get_temp_and_voltage(card): return temp, voltage -def main(): +def run_example(product_uid, use_uart=True): """Connect to Notcard and run a transaction test.""" print("Opening port...") - try: - if use_uart: - port = UART(2, 9600) - port.init(9600, bits=8, parity=None, stop=1, - timeout=3000, timeout_char=100) + if use_uart: + port = UART(2, 9600) + port.init(9600, bits=8, parity=None, stop=1, + timeout=3000, timeout_char=100) + else: + # If you"re using an ESP32, connect GPIO 22 to SCL and GPIO 21 to SDA. + if "ESP32" in sys.implementation._machine: + port = I2C(1, scl=Pin(22), sda=Pin(21)) else: port = I2C() - except Exception as exception: - raise Exception("error opening port: " - + NotecardExceptionInfo(exception)) print("Opening Notecard...") - try: - if use_uart: - card = notecard.OpenSerial(port, debug=True) - else: - card = notecard.OpenI2C(port, 0, 0, debug=True) - except Exception as exception: - raise Exception("error opening notecard: " - + NotecardExceptionInfo(exception)) + if use_uart: + card = notecard.OpenSerial(port, debug=True) + else: + card = notecard.OpenI2C(port, 0, 0, debug=True) # If success, configure the Notecard and send some data - configure_notecard(card) + configure_notecard(card, product_uid) temp, voltage = get_temp_and_voltage(card) req = {"req": "note.add"} req["sync"] = True req["body"] = {"temp": temp, "voltage": voltage} - try: - card.Transaction(req) - except Exception as exception: - print("Transaction error: " + NotecardExceptionInfo(exception)) - time.sleep(5) + card.Transaction(req) + + # Developer note: do not modify the line below, as we use this as to signify + # that the example ran successfully to completion. We then use that to + # determine pass/fail for certain tests that leverage these examples. + print("Example complete.") -main() +if __name__ == "__main__": + product_uid = "com.your-company.your-project" + # Choose either UART or I2C for Notecard + use_uart = True + run_example(product_uid, use_uart) diff --git a/examples/notecard-basics/rpi-example.py b/examples/notecard-basics/rpi_example.py similarity index 100% rename from examples/notecard-basics/rpi-example.py rename to examples/notecard-basics/rpi_example.py diff --git a/examples/notecard-basics/serial-example.py b/examples/notecard-basics/serial_example.py similarity index 100% rename from examples/notecard-basics/serial-example.py rename to examples/notecard-basics/serial_example.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3ebe93f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --ignore=test/hitl/ diff --git a/test/hitl/conftest.py b/test/hitl/conftest.py new file mode 100644 index 0000000..8f299ac --- /dev/null +++ b/test/hitl/conftest.py @@ -0,0 +1,101 @@ +from pathlib import Path +import shutil +import sys + +# Add the 'deps' folder to the path so we can import the pyboard module from +# it. +deps_path = str(Path(__file__).parent / 'deps') +sys.path.append(deps_path) +import pyboard # noqa: E402 + + +def mkdir_on_host(pyb, dir): + pyb.enter_raw_repl() + try: + pyb.fs_mkdir(dir) + except pyboard.PyboardError as e: + already_exists = ["EEXIST", "File exists"] + if any([keyword in str(e) for keyword in already_exists]): + # If the directory already exists, that's fine. + pass + else: + raise + finally: + pyb.exit_raw_repl() + + +def copy_files_to_host(pyb, files, dest_dir): + pyb.enter_raw_repl() + try: + for f in files: + pyb.fs_put(f, f'{dest_dir}/{f.name}', chunk_size=4096) + finally: + pyb.exit_raw_repl() + + +def copy_file_to_host(pyb, file, dest): + pyb.enter_raw_repl() + try: + pyb.fs_put(file, dest, chunk_size=4096) + finally: + pyb.exit_raw_repl() + + +def setup_host(port, platform): + pyb = pyboard.Pyboard(port, 115200) + # Get the path to the root of the note-python repository. + note_python_root_dir = Path(__file__).parent.parent.parent + notecard_dir = note_python_root_dir / 'notecard' + # Get a list of all the .py files in note-python/notecard/. + notecard_files = list(notecard_dir.glob('*.py')) + + mkdir_on_host(pyb, '/lib') + mkdir_on_host(pyb, '/lib/notecard') + copy_files_to_host(pyb, notecard_files, '/lib/notecard') + + # Copy over mpy_example.py. We'll run this example code on the MicroPython + # host to 1) verify that the host is able to use note-python to communicate + # with the Notecard and 2) verify that the example isn't broken. + if platform == 'circuitpython': + example_file = 'cpy_example.py' + else: + example_file = 'mpy_example.py' + examples_dir = note_python_root_dir / 'examples' + example_file_path = examples_dir / 'notecard-basics' / example_file + copy_file_to_host(pyb, example_file_path, '/example.py') + + pyb.close() + + +def pytest_addoption(parser): + parser.addoption( + '--port', + required=True, + help='The serial port of the MCU host (e.g. /dev/ttyACM0).' + ) + parser.addoption( + '--platform', + required=True, + help='Choose the platform to run the tests on.', + choices=["circuitpython", "micropython"] + ) + parser.addoption( + '--productuid', + required=True, + help='The ProductUID to set on the Notecard.' + ) + parser.addoption( + "--skipsetup", + action="store_true", + help="Skip host setup (copying over note-python, etc.) (default: False)" + ) + + +def pytest_configure(config): + config.port = config.getoption("port") + config.platform = config.getoption("platform") + config.product_uid = config.getoption("productuid") + config.skip_setup = config.getoption("skipsetup") + + if not config.skip_setup: + setup_host(config.port, config.platform) diff --git a/test/hitl/deps/pyboard.py b/test/hitl/deps/pyboard.py new file mode 100644 index 0000000..10c8bfa --- /dev/null +++ b/test/hitl/deps/pyboard.py @@ -0,0 +1,922 @@ +#!/usr/bin/env python3 +# +# This file is part of the MicroPython project, http://micropython.org/ +# +# The MIT License (MIT) +# +# Copyright (c) 2014-2021 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. + +""" +pyboard interface + +This module provides the Pyboard class, used to communicate with and +control a MicroPython device over a communication channel. Both real +boards and emulated devices (e.g. running in QEMU) are supported. +Various communication channels are supported, including a serial +connection, telnet-style network connection, external process +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('import pyb') + 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 + +""" + +# flake8: noqa + +import ast +import errno +import os +import struct +import sys +import time + +from collections import namedtuple + +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(Exception): + def convert(self, info): + if len(self.args) >= 3: + if b"OSError" in self.args[2] and b"ENOENT" in self.args[2]: + return OSError(errno.ENOENT, info) + + return self + + +listdir_result = namedtuple("dir_result", ["name", "st_mode", "st_ino", "st_size"]) + + +class TelnetToSerial: + def __init__(self, ip, user, password, read_timeout=None): + self.tn = 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): + if self.tn: + self.tn.close() + + 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 ProcessToSerial: + "Execute a process and emulate serial connection using its stdin/stdout." + + def __init__(self, cmd): + import subprocess + + self.subp = subprocess.Popen( + cmd, + bufsize=0, + shell=True, + preexec_fn=os.setsid, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + # Initially was implemented with selectors, but that adds Python3 + # dependency. However, there can be race conditions communicating + # with a particular child process (like QEMU), and selectors may + # still work better in that case, so left inplace for now. + # + # import selectors + # self.sel = selectors.DefaultSelector() + # self.sel.register(self.subp.stdout, selectors.EVENT_READ) + + import select + + self.poll = select.poll() + self.poll.register(self.subp.stdout.fileno()) + + def close(self): + import signal + + os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM) + + def read(self, size=1): + data = b"" + while len(data) < size: + data += self.subp.stdout.read(size - len(data)) + return data + + def write(self, data): + self.subp.stdin.write(data) + return len(data) + + def inWaiting(self): + # res = self.sel.select(0) + res = self.poll.poll(0) + if res: + return 1 + return 0 + + +class ProcessPtyToTerminal: + """Execute a process which creates a PTY and prints slave PTY as + first line of its output, and emulate serial connection using + this PTY.""" + + def __init__(self, cmd): + import subprocess + import re + import serial + + self.subp = subprocess.Popen( + cmd.split(), + bufsize=0, + shell=False, + preexec_fn=os.setsid, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + pty_line = self.subp.stderr.readline().decode("utf-8") + m = re.search(r"/dev/pts/[0-9]+", pty_line) + if not m: + print("Error: unable to find PTY device in startup line:", pty_line) + self.close() + sys.exit(1) + pty = m.group() + # rtscts, dsrdtr params are to workaround pyserial bug: + # http://stackoverflow.com/questions/34831131/pyserial-does-not-play-well-with-virtual-port + self.serial = serial.Serial(pty, interCharTimeout=1, rtscts=True, dsrdtr=True) + + def close(self): + import signal + + os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM) + + def read(self, size=1): + return self.serial.read(size) + + def write(self, data): + return self.serial.write(data) + + def inWaiting(self): + return self.serial.inWaiting() + + +class Pyboard: + def __init__( + self, device, baudrate=115200, user="micro", password="python", wait=0, exclusive=True + ): + self.in_raw_repl = False + self.use_raw_paste = True + if device.startswith("exec:"): + self.serial = ProcessToSerial(device[len("exec:") :]) + elif device.startswith("execpty:"): + self.serial = ProcessPtyToTerminal(device[len("qemupty:") :]) + elif 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 + import serial.tools.list_ports + + # Set options, and exclusive if pyserial supports it + serial_kwargs = {"baudrate": baudrate, "interCharTimeout": 1} + if serial.__version__ >= "3.3": + serial_kwargs["exclusive"] = exclusive + + delayed = False + for attempt in range(wait + 1): + try: + if os.name == "nt": + self.serial = serial.Serial(**serial_kwargs) + self.serial.port = device + portinfo = list(serial.tools.list_ports.grep(device)) # type: ignore + if portinfo and portinfo[0].manufacturer != "Microsoft": + # ESP8266/ESP32 boards use RTS/CTS for flashing and boot mode selection. + # DTR False: to avoid using the reset button will hang the MCU in bootloader mode + # RTS False: to prevent pulses on rts on serial.close() that would POWERON_RESET an ESPxx + self.serial.dtr = False # DTR False = gpio0 High = Normal boot + self.serial.rts = False # RTS False = EN High = MCU enabled + self.serial.open() + else: + self.serial = serial.Serial(device, **serial_kwargs) + 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): + # if data_consumer is used then data is not accumulated and the ending must be 1 byte long + assert data_consumer is None or len(ending) == 1 + + 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) + if data_consumer: + data_consumer(new_data) + data = new_data + else: + data = data + 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, soft_reset=True): + 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 + + if soft_reset: + 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 + + # Waiting for "soft reboot" independently to "raw REPL" (done below) + # allows boot.py to print, which will show up after "soft reboot" + # and before "raw REPL". + 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") + + 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.in_raw_repl = True + + def exit_raw_repl(self): + self.serial.write(b"\r\x02") # ctrl-B: enter friendly REPL + self.in_raw_repl = False + + 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 raw_paste_write(self, command_bytes): + # Read initial header, with window size. + data = self.serial.read(2) + window_size = struct.unpack("") + if not data.endswith(b">"): + raise PyboardError("could not enter raw repl") + + if self.use_raw_paste: + # Try to enter raw-paste mode. + self.serial.write(b"\x05A\x01") + data = self.serial.read(2) + if data == b"R\x00": + # Device understood raw-paste command but doesn't support it. + pass + elif data == b"R\x01": + # Device supports raw-paste mode, write out the command using this mode. + return self.raw_paste_write(command_bytes) + else: + # Device doesn't support raw-paste, fall back to normal raw REPL. + data = self.read_until(1, b"w REPL; CTRL-B to exit\r\n>") + if not data.endswith(b"w REPL; CTRL-B to exit\r\n>"): + print(data) + raise PyboardError("could not enter raw repl") + # Don't try to use raw-paste mode again for this connection. + self.use_raw_paste = False + + # Write command using standard raw REPL, 256 bytes every 10ms. + 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 (response: %r)" % data) + + 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, parse=False): + if parse: + ret = self.exec_("print(repr({}))".format(expression)) + ret = ret.strip() + return ast.literal_eval(ret.decode()) + else: + ret = self.exec_("print({})".format(expression)) + ret = ret.strip() + return ret + + # In Python3, call as pyboard.exec(), see the setattr call below. + def exec_(self, command, data_consumer=None): + ret, ret_err = self.exec_raw(command, data_consumer=data_consumer) + 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]) + + def fs_exists(self, src): + try: + self.exec_("import os\nos.stat(%s)" % (("'%s'" % src) if src else "")) + return True + except PyboardError: + return False + + def fs_ls(self, src): + cmd = ( + "import os\nfor f in os.ilistdir(%s):\n" + " print('{:12} {}{}'.format(f[3]if len(f)>3 else 0,f[0],'/'if f[1]&0x4000 else ''))" + % (("'%s'" % src) if src else "") + ) + self.exec_(cmd, data_consumer=stdout_write_bytes) + + def fs_listdir(self, src=""): + buf = bytearray() + + def repr_consumer(b): + buf.extend(b.replace(b"\x04", b"")) + + cmd = "import os\nfor f in os.ilistdir(%s):\n" " print(repr(f), end=',')" % ( + ("'%s'" % src) if src else "" + ) + try: + buf.extend(b"[") + self.exec_(cmd, data_consumer=repr_consumer) + buf.extend(b"]") + except PyboardError as e: + raise e.convert(src) + + return [ + listdir_result(*f) if len(f) == 4 else listdir_result(*(f + (0,))) + for f in ast.literal_eval(buf.decode()) + ] + + def fs_stat(self, src): + try: + self.exec_("import os") + return os.stat_result(self.eval("os.stat(%s)" % (("'%s'" % src)), parse=True)) + except PyboardError as e: + raise e.convert(src) + + def fs_cat(self, src, chunk_size=256): + cmd = ( + "with open('%s') as f:\n while 1:\n" + " b=f.read(%u)\n if not b:break\n print(b,end='')" % (src, chunk_size) + ) + self.exec_(cmd, data_consumer=stdout_write_bytes) + + def fs_readfile(self, src, chunk_size=256): + buf = bytearray() + + def repr_consumer(b): + buf.extend(b.replace(b"\x04", b"")) + + cmd = ( + "with open('%s', 'rb') as f:\n while 1:\n" + " b=f.read(%u)\n if not b:break\n print(b,end='')" % (src, chunk_size) + ) + try: + self.exec_(cmd, data_consumer=repr_consumer) + except PyboardError as e: + raise e.convert(src) + return ast.literal_eval(buf.decode()) + + def fs_writefile(self, dest, data, chunk_size=256): + self.exec_("f=open('%s','wb')\nw=f.write" % dest) + while data: + chunk = data[:chunk_size] + self.exec_("w(" + repr(chunk) + ")") + data = data[len(chunk) :] + self.exec_("f.close()") + + def fs_cp(self, src, dest, chunk_size=256, progress_callback=None): + if progress_callback: + src_size = self.fs_stat(src).st_size + written = 0 + self.exec_("fr=open('%s','rb')\nr=fr.read\nfw=open('%s','wb')\nw=fw.write" % (src, dest)) + while True: + data_len = int(self.exec_("d=r(%u)\nw(d)\nprint(len(d))" % chunk_size)) + if not data_len: + break + if progress_callback: + written += data_len + progress_callback(written, src_size) + self.exec_("fr.close()\nfw.close()") + + def fs_get(self, src, dest, chunk_size=256, progress_callback=None): + if progress_callback: + src_size = self.fs_stat(src).st_size + written = 0 + self.exec_("f=open('%s','rb')\nr=f.read" % src) + with open(dest, "wb") as f: + while True: + data = bytearray() + self.exec_("print(r(%u))" % chunk_size, data_consumer=lambda d: data.extend(d)) + assert data.endswith(b"\r\n\x04") + try: + data = ast.literal_eval(str(data[:-3], "ascii")) + if not isinstance(data, bytes): + raise ValueError("Not bytes") + except (UnicodeError, ValueError) as e: + raise PyboardError("fs_get: Could not interpret received data: %s" % str(e)) + if not data: + break + f.write(data) + if progress_callback: + written += len(data) + progress_callback(written, src_size) + self.exec_("f.close()") + + def fs_put(self, src, dest, chunk_size=256, progress_callback=None): + if progress_callback: + src_size = os.path.getsize(src) + written = 0 + self.exec_("f=open('%s','wb')\nw=f.write" % dest) + with open(src, "rb") as f: + while True: + data = f.read(chunk_size) + if not data: + break + if sys.version_info < (3,): + self.exec_("w(b" + repr(data) + ")") + else: + self.exec_("w(" + repr(data) + ")") + if progress_callback: + written += len(data) + progress_callback(written, src_size) + self.exec_("f.close()") + + def fs_mkdir(self, dir): + self.exec_("import os\nos.mkdir('%s')" % dir) + + def fs_rmdir(self, dir): + self.exec_("import os\nos.rmdir('%s')" % dir) + + def fs_rm(self, src): + self.exec_("import os\nos.remove('%s')" % src) + + def fs_touch(self, src): + self.exec_("f=open('%s','a')\nf.close()" % src) + + +# 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 filesystem_command(pyb, args, progress_callback=None, verbose=False): + def fname_remote(src): + if src.startswith(":"): + src = src[1:] + # Convert all path separators to "/", because that's what a remote device uses. + return src.replace(os.path.sep, "/") + + def fname_cp_dest(src, dest): + _, src = os.path.split(src) + if dest is None or dest == "": + dest = src + elif dest == ".": + dest = "./" + src + elif dest.endswith("/"): + dest += src + return dest + + cmd = args[0] + args = args[1:] + try: + if cmd == "cp": + if len(args) == 1: + raise PyboardError( + "cp: missing destination file operand after '{}'".format(args[0]) + ) + srcs = args[:-1] + dest = args[-1] + if dest.startswith(":"): + op_remote_src = pyb.fs_cp + op_local_src = pyb.fs_put + else: + op_remote_src = pyb.fs_get + op_local_src = lambda src, dest, **_: __import__("shutil").copy(src, dest) + for src in srcs: + if verbose: + print("cp %s %s" % (src, dest)) + if src.startswith(":"): + op = op_remote_src + else: + op = op_local_src + src2 = fname_remote(src) + dest2 = fname_cp_dest(src2, fname_remote(dest)) + op(src2, dest2, progress_callback=progress_callback) + else: + ops = { + "cat": pyb.fs_cat, + "ls": pyb.fs_ls, + "mkdir": pyb.fs_mkdir, + "rm": pyb.fs_rm, + "rmdir": pyb.fs_rmdir, + "touch": pyb.fs_touch, + } + if cmd not in ops: + raise PyboardError("'{}' is not a filesystem command".format(cmd)) + if cmd == "ls" and not args: + args = [""] + for src in args: + src = fname_remote(src) + if verbose: + print("%s :%s" % (cmd, src)) + ops[cmd](src) + except PyboardError as er: + if len(er.args) > 1: + print(str(er.args[2], "ascii")) + else: + print(er) + pyb.exit_raw_repl() + pyb.close() + sys.exit(1) + + +_injected_import_hook_code = """\ +import os, io +class _FS: + class File(io.IOBase): + def __init__(self): + self.off = 0 + def ioctl(self, request, arg): + return 0 + def readinto(self, buf): + buf[:] = memoryview(_injected_buf)[self.off:self.off + len(buf)] + self.off += len(buf) + return len(buf) + mount = umount = chdir = lambda *args: None + def stat(self, path): + if path == '_injected.mpy': + return tuple(0 for _ in range(10)) + else: + raise OSError(-2) # ENOENT + def open(self, path, mode): + return self.File() +os.mount(_FS(), '/_') +os.chdir('/_') +from _injected import * +os.umount('/_') +del _injected_buf, _FS +""" + + +def main(): + import argparse + + cmd_parser = argparse.ArgumentParser(description="Run scripts on the pyboard.") + cmd_parser.add_argument( + "-d", + "--device", + default=os.environ.get("PYBOARD_DEVICE", "/dev/ttyACM0"), + help="the serial device or the IP address of the pyboard", + ) + cmd_parser.add_argument( + "-b", + "--baudrate", + default=os.environ.get("PYBOARD_BAUDRATE", "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", + ) + group = cmd_parser.add_mutually_exclusive_group() + group.add_argument( + "--soft-reset", + default=True, + action="store_true", + help="Whether to perform a soft reset when connecting to the board [default]", + ) + group.add_argument( + "--no-soft-reset", + action="store_false", + dest="soft_reset", + ) + group = cmd_parser.add_mutually_exclusive_group() + group.add_argument( + "--follow", + action="store_true", + default=None, + help="follow the output after running the scripts [default if no scripts given]", + ) + group.add_argument( + "--no-follow", + action="store_false", + dest="follow", + ) + group = cmd_parser.add_mutually_exclusive_group() + group.add_argument( + "--exclusive", + action="store_true", + default=True, + help="Open the serial device for exclusive access [default]", + ) + group.add_argument( + "--no-exclusive", + action="store_false", + dest="exclusive", + ) + cmd_parser.add_argument( + "-f", + "--filesystem", + action="store_true", + help="perform a filesystem action: " + "cp local :device | cp :device local | cat path | ls [path] | rm path | mkdir path | rmdir path", + ) + cmd_parser.add_argument("files", nargs="*", help="input files") + args = cmd_parser.parse_args() + + # open the connection to the pyboard + try: + pyb = Pyboard( + args.device, args.baudrate, args.user, args.password, args.wait, args.exclusive + ) + except PyboardError as er: + print(er) + sys.exit(1) + + # run any command or file(s) + if args.command is not None or args.filesystem or len(args.files): + # we must enter raw-REPL mode to execute commands + # this will do a soft-reset of the board + try: + pyb.enter_raw_repl(args.soft_reset) + except PyboardError as er: + print(er) + pyb.close() + sys.exit(1) + + def execbuffer(buf): + try: + if args.follow is None or args.follow: + ret, ret_err = pyb.exec_raw( + buf, timeout=None, data_consumer=stdout_write_bytes + ) + else: + pyb.exec_raw_no_follow(buf) + ret_err = None + except PyboardError as er: + print(er) + pyb.close() + sys.exit(1) + except KeyboardInterrupt: + sys.exit(1) + if ret_err: + pyb.exit_raw_repl() + pyb.close() + stdout_write_bytes(ret_err) + sys.exit(1) + + # do filesystem commands, if given + if args.filesystem: + filesystem_command(pyb, args.files, verbose=True) + del args.files[:] + + # run the command, if given + if args.command is not None: + execbuffer(args.command.encode("utf-8")) + + # run any files + for filename in args.files: + with open(filename, "rb") as f: + pyfile = f.read() + if filename.endswith(".mpy") and pyfile[0] == ord("M"): + pyb.exec_("_injected_buf=" + repr(pyfile)) + pyfile = _injected_import_hook_code + execbuffer(pyfile) + + # exiting raw-REPL just drops to friendly-REPL mode + pyb.exit_raw_repl() + + # if asked explicitly, or no files given, then follow the output + if args.follow or (args.command is None and not args.filesystem and len(args.files) == 0): + try: + ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes) + except PyboardError as er: + print(er) + sys.exit(1) + except KeyboardInterrupt: + sys.exit(1) + if ret_err: + pyb.close() + stdout_write_bytes(ret_err) + sys.exit(1) + + # close the connection to the pyboard + pyb.close() + + +if __name__ == "__main__": + main() diff --git a/test/hitl/test_basic_comms.py b/test/hitl/test_basic_comms.py new file mode 100644 index 0000000..0637679 --- /dev/null +++ b/test/hitl/test_basic_comms.py @@ -0,0 +1,23 @@ +import pyboard +import pytest + + +def run_example(port, product_uid, use_uart): + pyb = pyboard.Pyboard(port, 115200) + pyb.enter_raw_repl() + try: + cmd = f'from example import run_example; run_example("{product_uid}", {use_uart})' + output = pyb.exec(cmd) + output = output.decode() + print(output) + assert 'Example complete.' in output + finally: + pyb.exit_raw_repl() + + +def test_example_i2c(pytestconfig): + run_example(pytestconfig.port, pytestconfig.product_uid, use_uart=False) + + +def test_example_serial(pytestconfig): + run_example(pytestconfig.port, pytestconfig.product_uid, use_uart=True) From e6d2ade2111cb5ef47f2e5017fd3458f1881f378 Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Thu, 10 Aug 2023 02:40:20 -0700 Subject: [PATCH 02/14] HIL tests for CircuitPython --- .github/workflows/hil-circuitpython.yml | 133 ++++++++++++++++++ test/hitl/requirements.txt | 2 + test/scripts/check_runner_config.sh | 31 ++++ .../usbmount/mount.d/00_create_model_symlink | 40 ++++++ .../usbmount/mount.d/01_create_label_symlink | 26 ++++ .../usbmount/mount.d/02_create_id_symlink | 33 +++++ .../usbmount/umount.d/00_remove_model_symlink | 24 ++++ test/scripts/usbmount/usbmount.conf | 53 +++++++ test/scripts/wait_for_file.sh | 14 ++ 9 files changed, 356 insertions(+) create mode 100644 .github/workflows/hil-circuitpython.yml create mode 100644 test/hitl/requirements.txt create mode 100755 test/scripts/check_runner_config.sh create mode 100755 test/scripts/usbmount/mount.d/00_create_model_symlink create mode 100755 test/scripts/usbmount/mount.d/01_create_label_symlink create mode 100755 test/scripts/usbmount/mount.d/02_create_id_symlink create mode 100755 test/scripts/usbmount/umount.d/00_remove_model_symlink create mode 100644 test/scripts/usbmount/usbmount.conf create mode 100755 test/scripts/wait_for_file.sh diff --git a/.github/workflows/hil-circuitpython.yml b/.github/workflows/hil-circuitpython.yml new file mode 100644 index 0000000..3dd48ef --- /dev/null +++ b/.github/workflows/hil-circuitpython.yml @@ -0,0 +1,133 @@ +# + +name: HIL-circuitpython + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + test: + runs-on: circuitpython + defaults: + run: + shell: bash + strategy: + matrix: + CIRCUITPYTHON_VERSION: [8.2.2] + flash_device: [true] + lock_cpy_filesystem: [true] + env: + # todo - move these to the runner .env file + SWAN_SERIAL: 2047315B5856 + # path to the Swan's SWANBOOT drive + # the unique path the SWANBOOT drive is mouted at + # CPY_FS_UF2: /run/usbmount/usb-Adafruit_UF2_Bootloader_10001500035056584B313220-00_SWANBOOT + # CPY_FS_CIRCUITPY: /run/usbmount/usb-Blues_In_Swan_R5_7B4303088409002000000000-00-part1_CIRCUITPY + USB_MSD_ATTACH_TIME: 10 + CIRCUITPYTHON_UF2: "adafruit-circuitpython-swan_r5-en_US-${{ matrix.CIRCUITPYTHON_VERSION }}.uf2" + CIRCUITPYTHON_VERSION: ${{ matrix.CIRCUITPYTHON_VERSION}} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set Env Vars + run: | + # environment variables set in a step cannot be used until subsequent steps + echo "CIRCUITPYTHON_UF2_URL=https://downloads.circuitpython.org/bin/swan_r5/en_US/${CIRCUITPYTHON_UF2}" >> $GITHUB_ENV + + - name: Check Runner Config + run: test/scripts/check_runner_config.sh + + - name: Download Latest Bootloader + env: + REPO: adafruit/tinyuf2 + ASSET: tinyuf2-swan_r5 + if: ${{ matrix.flash_device }} + run: | + echo "retrieving the latest release from ${REPO}" + wget -q -O latest.json "https://api.github.com/repos/${REPO}/releases/latest" + + echo "extracting asset details for ${ASSET}" + asset_file="${ASSET}_asset.json" + jq -r --arg ASSET "$ASSET" '.assets[] | select(.name | startswith($ASSET))' latest.json > $asset_file + + # extract the name and download url without double quotes + download_name=$(jq -r '.name' $asset_file) + download_url=$(jq -r '.browser_download_url' $asset_file) + echo "Downloading release from $download_url" + wget -q -N $download_url + unzip -o $download_name + binfile=$(basename $download_name .zip).bin + echo "TINYUF2_BIN=$binfile" >> $GITHUB_ENV + + - name: Download CircuitPython v${{ env.CIRCUITPYTHON_VERSION }} + if: ${{ matrix.flash_device }} + run: | + echo "Downloading CircuitPython for Swan from $CIRCUITPYTHON_UF2_URL" + wget -q -N "$CIRCUITPYTHON_UF2_URL" + + - name: Erase device and program bootloader + if: ${{ matrix.flash_device }} + run: | + # cannot use st-flash - every 2nd programing incorrectly puts the device in DFU mode + # st-flash --reset write $binfile 0x8000000 + # Have to use the version of openocd bundled with the STM32 platform in PlatformIO, which (presumably) has the stm32 extensions compiled in + ~/.platformio/packages/tool-openocd/bin/openocd \ + -d2 -s ~/.platformio/packages/tool-openocd/openocd/scripts \ + -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32l4x.cfg \ + -c "init; halt; stm32l4x mass_erase 0" \ + -c "program $TINYUF2_BIN 0x8000000 verify reset; shutdown" + + - name: Program CircuitPython + if: ${{ matrix.flash_device }} + run: | + # wait for the bootloader drive to appear + timeout $USB_MSD_ATTACH_TIME bash test/scripts/wait_for_file.sh "$CPY_FS_UF2" + + # The bootloader reboots quickly once the whole file has been received, + # causing an input/output error to be reported. + # Ignore that, and fail if the CIRCUITPY filesystem doesn't appear + echo "Uploading CircuitPython binary..." + cp "$CIRCUITPYTHON_UF2" "$CPY_FS_UF2" || true + echo Ignore the input/output error above. Waiting for device to boot. + timeout $USB_MSD_ATTACH_TIME bash test/scripts/wait_for_file.sh "$CPY_FS_CIRCUITPY" + echo "CircuitPython binary uploaded and running." + + - name: Make CircuitPython filesystem writeable to pyboard + if: ${{ matrix.lock_cpy_filesystem }} + run: | + timeout $USB_MSD_ATTACH_TIME bash test/scripts/wait_for_file.sh "$CPY_FS_CIRCUITPY" + + cp test/hitl/boot.py "$CPY_FS_CIRCUITPY" + + # reset the device (todo move this blob to a utility script) + ~/.platformio/packages/tool-openocd/bin/openocd \ + -d2 -s ~/.platformio/packages/tool-openocd/openocd/scripts \ + -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32l4x.cfg \ + -c "init; halt; reset; shutdown" + + # wait for the device to come back + timeout $USB_MSD_ATTACH_TIME bash test/scripts/wait_for_file.sh "$CPY_FS_CIRCUITPY" + + - name: Setup Python + run: | + python3 -m venv .venv-runner + . .venv-runner/bin/activate + pip install -r test/hitl/requirements.txt + + - name: Setup 'note-python' on device + if: ${{ ! matrix.lock_cpy_filesystem }} + run: | + mkdir -p ${CPY_FS_CIRCUITPY}/lib/notecard + cp notecard/*.py ${CPY_FS_CIRCUITPY}/lib/notecard/ + cp examples/notecard-basics/cpy_example.py ${CPY_FS_CIRCUITPY}/example.py + + - name: Run CircuitPython Tests + run: | + ${{ ! matrix.lock_cpy_filesystem }} && skipsetup=--skipsetup + pytest $skipsetup "--productuid=$CPY_PRODUCT_UID" "--port=$CPY_SERIAL" --platform=circuitpython test/hitl + \ No newline at end of file diff --git a/test/hitl/requirements.txt b/test/hitl/requirements.txt new file mode 100644 index 0000000..ca50f0f --- /dev/null +++ b/test/hitl/requirements.txt @@ -0,0 +1,2 @@ +pytest +pyserial diff --git a/test/scripts/check_runner_config.sh b/test/scripts/check_runner_config.sh new file mode 100755 index 0000000..46299de --- /dev/null +++ b/test/scripts/check_runner_config.sh @@ -0,0 +1,31 @@ + +function diff_dir() { + src=$1 + dest=$2 + diff -r $src $dest +} + +function env_var_defined() { + [ -v $1 ] || echo "Environment variable '$1' not set." +} + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +function check_all() { + diff_dir $SCRIPT_DIR/usbmount /etc/usbmount + env_var_defined "CPY_SERIAL" + env_var_defined "CPY_FS_UF2" + env_var_defined "CPY_FS_CIRCUITPY" + env_var_defined "CPY_PRODUCT_UID" + env_var_defined "CIRCUITPYTHON_UF2" + env_var_defined "CIRCUITPYTHON_UF2_URL" +} + +errors=$(check_all) +if [ -n "$errors" ]; then + echo "$errors" # quoted to preserve newlines + echo "There are configuration errors. See the log above for details." + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/test/scripts/usbmount/mount.d/00_create_model_symlink b/test/scripts/usbmount/mount.d/00_create_model_symlink new file mode 100755 index 0000000..62707f1 --- /dev/null +++ b/test/scripts/usbmount/mount.d/00_create_model_symlink @@ -0,0 +1,40 @@ +#!/bin/sh +# This script creates the model name symlink in /var/run/usbmount. +# Copyright (C) 2005 Martin Dickopp +# +# This file is free software; the copyright holder gives unlimited +# permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. +# +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. +# +set -e + +# Replace spaces with underscores, remove special characters in vendor +# and model name. +UM_VENDOR=`echo "$UM_VENDOR" | sed 's/ /_/g; s/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-]//g'` +UM_MODEL=`echo "$UM_MODEL" | sed 's/ /_/g; s/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-]//g'` + +# Exit if both vendor and model name are empty. +test -n "$UM_VENDOR" || test -n "$UM_MODEL" || exit 0 + +# Build symlink name. +if test -n "$UM_VENDOR" && test -n "$UM_MODEL"; then + name="${UM_VENDOR}_$UM_MODEL" +else + name="$UM_VENDOR$UM_MODEL" +fi + +# Append partition number, if any, to the symlink name. +partition=`echo "$UM_DEVICE" | sed 's/^.*[^0123456789]\([0123456789]*\)/\1/'` +if test -n "$partition"; then + name="${name}_$partition" +fi + +# If the symlink does not yet exist, create it. +test -e "/var/run/usbmount/$name" || ln -sf "$UM_MOUNTPOINT" "/var/run/usbmount/$name" + +exit 0 diff --git a/test/scripts/usbmount/mount.d/01_create_label_symlink b/test/scripts/usbmount/mount.d/01_create_label_symlink new file mode 100755 index 0000000..8e122de --- /dev/null +++ b/test/scripts/usbmount/mount.d/01_create_label_symlink @@ -0,0 +1,26 @@ +#!/bin/sh +# https://esite.ch/2014/04/mounting-external-usb-drives-automatically-to-its-label/ +# This script creates the volume label symlink in /var/run/usbmount. +# Copyright (C) 2014 Oliver Sauder +# +# This file is free software; the copyright holder gives unlimited +# permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. +# +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. +# +set -e + +# Exit if device or mountpoint is empty. +test -z "$UM_DEVICE" && test -z "$UM_MOUNTPOINT" && exit 0 + +# get volume label name +label=`blkid -s LABEL -o value $UM_DEVICE` +echo $UM_DEVICE +# If the symlink does not yet exist, create it. +test -z $label || test -e "/var/run/usbmount/$label" || ln -sf "$UM_MOUNTPOINT" "/var/run/usbmount/$label" + +exit 0 diff --git a/test/scripts/usbmount/mount.d/02_create_id_symlink b/test/scripts/usbmount/mount.d/02_create_id_symlink new file mode 100755 index 0000000..adadaa6 --- /dev/null +++ b/test/scripts/usbmount/mount.d/02_create_id_symlink @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + + +# Exit if device or mountpoint is empty. +test -z "$UM_DEVICE" && test -z "$UM_MOUNTPOINT" && exit 0 + + +# get volume label name +label=`blkid -s LABEL -o value $UM_DEVICE` + +function find_diskid() { + ls /dev/disk/by-id | while read name; do + device_link="`readlink -f \"/dev/disk/by-id/${name}\" || :`" + if test "${device_link}" = "$UM_DEVICE"; then + echo "$name" + break + fi + done +} + +diskid=`find_diskid` +# remove special characters +name=`echo "${diskid}" | sed 's/ /_/g; s/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-]//g'` +if test -n "$label"; then + name="${name}_${label}" +fi + +# If the symlink does not yet exist, create it. +test -z "${name}" || test -e "/var/run/usbmount/${name}" || ln -sf "$UM_MOUNTPOINT" "/var/run/usbmount/${name}" + +exit 0 diff --git a/test/scripts/usbmount/umount.d/00_remove_model_symlink b/test/scripts/usbmount/umount.d/00_remove_model_symlink new file mode 100755 index 0000000..8e091c4 --- /dev/null +++ b/test/scripts/usbmount/umount.d/00_remove_model_symlink @@ -0,0 +1,24 @@ +#!/bin/sh +# This script removes the model name symlink in /var/run/usbmount. +# Copyright (C) 2005 Martin Dickopp +# +# This file is free software; the copyright holder gives unlimited +# permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. +# +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. +# +set -e + +ls /var/run/usbmount | while read name; do + if test "`readlink \"/var/run/usbmount/$name\" || :`" = "$UM_MOUNTPOINT"; then + rm -f "/var/run/usbmount/$name" + # remove all links + # break + fi +done + +exit 0 diff --git a/test/scripts/usbmount/usbmount.conf b/test/scripts/usbmount/usbmount.conf new file mode 100644 index 0000000..99a6c5e --- /dev/null +++ b/test/scripts/usbmount/usbmount.conf @@ -0,0 +1,53 @@ +# Configuration file for the usbmount package, which mounts removable +# storage devices when they are plugged in and unmounts them when they +# are removed. + +# Change to zero to disable usbmount +ENABLED=1 + +# Mountpoints: These directories are eligible as mointpoints for +# removable storage devices. A newly plugged in device is mounted on +# the first directory in this list that exists and on which nothing is +# mounted yet. +MOUNTPOINTS="/media/usb0 /media/usb1 /media/usb2 /media/usb3 + /media/usb4 /media/usb5 /media/usb6 /media/usb7" + +# Filesystem types: removable storage devices are only mounted if they +# contain a filesystem type which is in this list. +FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus" + +############################################################################# +# WARNING! # +# # +# The "sync" option may not be a good choice to use with flash drives, as # +# it forces a greater amount of writing operating on the drive. This makes # +# the writing speed considerably lower and also leads to a faster wear out # +# of the disk. # +# # +# If you omit it, don't forget to use the command "sync" to synchronize the # +# data on your disk before removing the drive or you may experience data # +# loss. # +# # +# It is highly recommended that you use the pumount command (as a regular # +# user) before unplugging the device. It makes calling the "sync" command # +# and mounting with the sync option unnecessary---this is similar to other # +# operating system's "safely disconnect the device" option. # +############################################################################# +# Mount options: Options passed to the mount command with the -o flag. +# See the warning above regarding removing "sync" from the options. +MOUNTOPTIONS="sync,noexec,nodev,noatime,nodiratime" + +# Filesystem type specific mount options: This variable contains a space +# separated list of strings, each which the form "-fstype=TYPE,OPTIONS". +# +# If a filesystem with a type listed here is mounted, the corresponding +# options are appended to those specificed in the MOUNTOPTIONS variable. +# +# For example, "-fstype=vfat,gid=floppy,dmask=0007,fmask=0117" would add +# the options "gid=floppy,dmask=0007,fmask=0117" when a vfat filesystem +# is mounted. +FS_MOUNTOPTIONS="-fstype=vfat,flush,uid=1000,gid=plugdev,dmask=0007,fmask=0117" + +# If set to "yes", more information will be logged via the syslog +# facility. +VERBOSE=no diff --git a/test/scripts/wait_for_file.sh b/test/scripts/wait_for_file.sh new file mode 100755 index 0000000..faf7189 --- /dev/null +++ b/test/scripts/wait_for_file.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# $1 filename to wait for +set -e + +if [ -z "$1" ]; then + echo "Expected 1 argument: " + exit 1 +else + echo "Waiting for file $1..." +fi + +while ! test -e "$1"; do + sleep 0.5 +done From 127f16185c47836ba2c6dd0e958135cf490c991d Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Thu, 10 Aug 2023 02:53:00 -0700 Subject: [PATCH 03/14] fuller definition of runner labels --- .github/workflows/hil-circuitpython.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/hil-circuitpython.yml b/.github/workflows/hil-circuitpython.yml index 3dd48ef..73e0650 100644 --- a/.github/workflows/hil-circuitpython.yml +++ b/.github/workflows/hil-circuitpython.yml @@ -11,7 +11,7 @@ on: jobs: test: - runs-on: circuitpython + runs-on: [self-hosted, linux, circuitpython, swan-3.0, notecard-serial] defaults: run: shell: bash @@ -21,18 +21,12 @@ jobs: flash_device: [true] lock_cpy_filesystem: [true] env: - # todo - move these to the runner .env file - SWAN_SERIAL: 2047315B5856 - # path to the Swan's SWANBOOT drive - # the unique path the SWANBOOT drive is mouted at - # CPY_FS_UF2: /run/usbmount/usb-Adafruit_UF2_Bootloader_10001500035056584B313220-00_SWANBOOT - # CPY_FS_CIRCUITPY: /run/usbmount/usb-Blues_In_Swan_R5_7B4303088409002000000000-00-part1_CIRCUITPY - USB_MSD_ATTACH_TIME: 10 + USB_MSD_ATTACH_TIME: 15 CIRCUITPYTHON_UF2: "adafruit-circuitpython-swan_r5-en_US-${{ matrix.CIRCUITPYTHON_VERSION }}.uf2" CIRCUITPYTHON_VERSION: ${{ matrix.CIRCUITPYTHON_VERSION}} steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Set Env Vars run: | From a7002e56c08881c7ea341aeada94b88aa638fceb Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Thu, 10 Aug 2023 02:59:06 -0700 Subject: [PATCH 04/14] missing file --- test/hitl/boot.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/hitl/boot.py diff --git a/test/hitl/boot.py b/test/hitl/boot.py new file mode 100644 index 0000000..e9a4e8e --- /dev/null +++ b/test/hitl/boot.py @@ -0,0 +1,3 @@ +import storage + +storage.remount("/", False) From 2f2d353b6bbfe6de71bdce32581d3f015058c356 Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Thu, 10 Aug 2023 03:05:48 -0700 Subject: [PATCH 05/14] fix: python venv doesn't carry over between steps, but does when run locally. --- .github/workflows/hil-circuitpython.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/hil-circuitpython.yml b/.github/workflows/hil-circuitpython.yml index 73e0650..d5da2c7 100644 --- a/.github/workflows/hil-circuitpython.yml +++ b/.github/workflows/hil-circuitpython.yml @@ -122,6 +122,7 @@ jobs: - name: Run CircuitPython Tests run: | + . .venv-runner/bin/activate ${{ ! matrix.lock_cpy_filesystem }} && skipsetup=--skipsetup pytest $skipsetup "--productuid=$CPY_PRODUCT_UID" "--port=$CPY_SERIAL" --platform=circuitpython test/hitl \ No newline at end of file From 1077e03c3fd03c8d3a8163bb18c2fbf5d63070cc Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Thu, 10 Aug 2023 18:04:11 -0700 Subject: [PATCH 06/14] fix: close pyboard after running an example. Without it, the serial port can still remain locked, causing the next test to fail. --- test/hitl/test_basic_comms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hitl/test_basic_comms.py b/test/hitl/test_basic_comms.py index 0637679..1567084 100644 --- a/test/hitl/test_basic_comms.py +++ b/test/hitl/test_basic_comms.py @@ -13,7 +13,7 @@ def run_example(port, product_uid, use_uart): assert 'Example complete.' in output finally: pyb.exit_raw_repl() - + pyb.close() def test_example_i2c(pytestconfig): run_example(pytestconfig.port, pytestconfig.product_uid, use_uart=False) From 73b5998f0aa50202d90c3c946c6d119cfef118f5 Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Thu, 10 Aug 2023 18:05:09 -0700 Subject: [PATCH 07/14] feat: added a simple boards mechanism to avoid hard-coded values in the main example. This allows different boards to be easily targeted. --- examples/notecard-basics/mpy_example.py | 8 +++++--- test/hitl/conftest.py | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/notecard-basics/mpy_example.py b/examples/notecard-basics/mpy_example.py index 2c4b7f5..4d180cf 100644 --- a/examples/notecard-basics/mpy_example.py +++ b/examples/notecard-basics/mpy_example.py @@ -6,6 +6,7 @@ import sys import time import notecard +import board if sys.implementation.name != "micropython": raise Exception("Please run this example in a MicroPython environment.") @@ -76,15 +77,16 @@ def run_example(product_uid, use_uart=True): """Connect to Notcard and run a transaction test.""" print("Opening port...") if use_uart: - port = UART(2, 9600) + port = UART(board.UART, 9600) port.init(9600, bits=8, parity=None, stop=1, timeout=3000, timeout_char=100) else: + port = I2C(board.I2C_ID, scl=Pin(board.SCL), sda=Pin(board.SDA)) # If you"re using an ESP32, connect GPIO 22 to SCL and GPIO 21 to SDA. - if "ESP32" in sys.implementation._machine: + if "ESP32abc" in sys.implementation._machine: port = I2C(1, scl=Pin(22), sda=Pin(21)) else: - port = I2C() + port = I2C(0, scl=Pin(22), sda=Pin(23)) print("Opening Notecard...") if use_uart: diff --git a/test/hitl/conftest.py b/test/hitl/conftest.py index 8f299ac..a837efe 100644 --- a/test/hitl/conftest.py +++ b/test/hitl/conftest.py @@ -41,7 +41,7 @@ def copy_file_to_host(pyb, file, dest): pyb.exit_raw_repl() -def setup_host(port, platform): +def setup_host(port, platform, board): pyb = pyboard.Pyboard(port, 115200) # Get the path to the root of the note-python repository. note_python_root_dir = Path(__file__).parent.parent.parent @@ -60,6 +60,10 @@ def setup_host(port, platform): example_file = 'cpy_example.py' else: example_file = 'mpy_example.py' + boards_dir = note_python_root_dir / 'mpy_board' + board_file_path = boards_dir / f"{board}.py" + copy_file_to_host(pyb, board_file_path, '/board.py') + examples_dir = note_python_root_dir / 'examples' example_file_path = examples_dir / 'notecard-basics' / example_file copy_file_to_host(pyb, example_file_path, '/example.py') @@ -89,6 +93,11 @@ def pytest_addoption(parser): action="store_true", help="Skip host setup (copying over note-python, etc.) (default: False)" ) + parser.addoption( + '--board', + required=True, + help='The board name that is being used.' + ) def pytest_configure(config): @@ -96,6 +105,7 @@ def pytest_configure(config): config.platform = config.getoption("platform") config.product_uid = config.getoption("productuid") config.skip_setup = config.getoption("skipsetup") + config.board = config.getoption("board") if not config.skip_setup: - setup_host(config.port, config.platform) + setup_host(config.port, config.platform, config.board) From 896f97aade4c667fa3039acdd04916f53f1d377a Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Thu, 10 Aug 2023 20:02:55 -0700 Subject: [PATCH 08/14] feat: implements HIL tests for micropython --- .github/workflows/hil-circuitpython.yml | 6 +- .github/workflows/hil-micropython.yml | 63 +++++++++++++++++++ examples/notecard-basics/board.py | 18 ++++++ mpy_board/espressif_esp32.py | 27 ++++++++ mpy_board/huzzah32.py | 26 ++++++++ test/hitl/conftest.py | 19 +++--- test/hitl/requirements-esp32.txt | 1 + ...r_config.sh => check_cpy_runner_config.sh} | 0 test/scripts/check_mpy_runner_config.sh | 22 +++++++ 9 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/hil-micropython.yml create mode 100644 examples/notecard-basics/board.py create mode 100644 mpy_board/espressif_esp32.py create mode 100644 mpy_board/huzzah32.py create mode 100644 test/hitl/requirements-esp32.txt rename test/scripts/{check_runner_config.sh => check_cpy_runner_config.sh} (100%) create mode 100755 test/scripts/check_mpy_runner_config.sh diff --git a/.github/workflows/hil-circuitpython.yml b/.github/workflows/hil-circuitpython.yml index d5da2c7..f93e2ae 100644 --- a/.github/workflows/hil-circuitpython.yml +++ b/.github/workflows/hil-circuitpython.yml @@ -3,8 +3,6 @@ name: HIL-circuitpython on: - push: - branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: @@ -34,8 +32,8 @@ jobs: echo "CIRCUITPYTHON_UF2_URL=https://downloads.circuitpython.org/bin/swan_r5/en_US/${CIRCUITPYTHON_UF2}" >> $GITHUB_ENV - name: Check Runner Config - run: test/scripts/check_runner_config.sh - + run: test/scripts/check_cpy_runner_config.sh + - name: Download Latest Bootloader env: REPO: adafruit/tinyuf2 diff --git a/.github/workflows/hil-micropython.yml b/.github/workflows/hil-micropython.yml new file mode 100644 index 0000000..3d82a17 --- /dev/null +++ b/.github/workflows/hil-micropython.yml @@ -0,0 +1,63 @@ +name: HIL-micropython + +on: + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + huzzah32: + runs-on: [self-hosted, linux, esp32, notecard-serial, micropython] + defaults: + run: + shell: bash + strategy: + matrix: + MICROPYTHON_VERSION: [1.20.0] + MICROPYTHON_DATE: [20230426] + MICROPYTHON_MCU: [esp32] + MPY_BOARD: [huzzah32] # the --mpyboard parameter to the tests + flash_device: [false] + env: + VENV: .venv-runner-mpy + USB_MSD_ATTACH_TIME: 15 + MICROPYTHON_BIN: "${{matrix.MICROPYTHON_MCU}}-${{matrix.MICROPYTHON_DATE}}-v${{matrix.MICROPYTHON_VERSION}}.bin" + MICROPYTHON_VERSION: ${{matrix.MICROPYTHON_VERSION}} + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set Env Vars + run: | + # environment variables set in a step cannot be used until subsequent steps + echo "MICROPYTHON_BIN_URL=https://micropython.org/resources/firmware/${{env.MICROPYTHON_BIN}}" >> $GITHUB_ENV + + - name: Check Runner Config + run: test/scripts/check_mpy_runner_config.sh + + - name: Download MicroPython v${{ env.MICROPYTHON_VERSION }} + if: ${{ matrix.flash_device }} + run: | + echo "Downloading MicroPython for ESP32 from $MICROPYTHON_BIN_URL" + wget -q -N "$MICROPYTHON_BIN_URL" + + - name: Setup Python + run: | + python3 -m venv ${{ env.VENV }} + . ${{ env.VENV }}/bin/activate + pip install -r test/hitl/requirements.txt -r test/hitl/requirements-esp32.txt + + - name: Erase device and Program Micropython + if: ${{ matrix.flash_device }} + run: | + . ${{ env.VENV }}/bin/activate + esptool.py --chip esp32 -p ${MPY_SERIAL} erase_flash + timeout 10 bash test/scripts/wait_for_file.sh "$MPY_SERIAL" + + esptool.py --chip esp32 --port ${MPY_SERIAL} --baud 460800 write_flash -z 0x1000 ${{ env.MICROPYTHON_BIN }} + timeout 10 bash test/scripts/wait_for_file.sh "$MPY_SERIAL" + + - name: Run MicroPython Tests + run: | + . ${{ env.VENV }}/bin/activate + pytest "--productuid=$MPY_PRODUCT_UID" "--port=$MPY_SERIAL" --platform=micropython --mpyboard=${{ matrix.MPY_BOARD }} test/hitl diff --git a/examples/notecard-basics/board.py b/examples/notecard-basics/board.py new file mode 100644 index 0000000..6a936ee --- /dev/null +++ b/examples/notecard-basics/board.py @@ -0,0 +1,18 @@ +""" +This module is used by the mpy_example to set define the appropriate peripherals for +different types of boards. The values here are defaults. +""" + +""" +The UART instance to use that is connected to Notecard. +""" +UART=2 + +""" +The SCL pin of the I2C bus connected to Notecard +""" +I2C_ID=0 +SCL=0 +SDA=0 + + diff --git a/mpy_board/espressif_esp32.py b/mpy_board/espressif_esp32.py new file mode 100644 index 0000000..c37a6ab --- /dev/null +++ b/mpy_board/espressif_esp32.py @@ -0,0 +1,27 @@ + +""" +Peripheral definitions for Adafruit HUZZAH32 +""" + + +""" +The UART instance to use that is connected to Notecard. +""" +UART=2 + + +""" +The I2C peripheral ID to use. +""" +I2C_ID=1 + + +""" +The SCL pin number of the the I2C peripheral +""" +SCL=22 + +""" +The SDA pin number of the I2C peripheral +""" +SDA=21 \ No newline at end of file diff --git a/mpy_board/huzzah32.py b/mpy_board/huzzah32.py new file mode 100644 index 0000000..669930c --- /dev/null +++ b/mpy_board/huzzah32.py @@ -0,0 +1,26 @@ +""" +Peripheral definitions for Adafruit HUZZAH32 +""" + + +""" +The UART instance to use that is connected to Notecard. +""" +UART=2 + + +""" +The I2C peripheral ID to use. +""" +I2C_ID=0 + + +""" +The SCL pin number of the the I2C peripheral +""" +SCL=22 + +""" +The SDA pin number of the I2C peripheral +""" +SDA=23 \ No newline at end of file diff --git a/test/hitl/conftest.py b/test/hitl/conftest.py index a837efe..d75dbab 100644 --- a/test/hitl/conftest.py +++ b/test/hitl/conftest.py @@ -41,7 +41,7 @@ def copy_file_to_host(pyb, file, dest): pyb.exit_raw_repl() -def setup_host(port, platform, board): +def setup_host(port, platform, mpy_board): pyb = pyboard.Pyboard(port, 115200) # Get the path to the root of the note-python repository. note_python_root_dir = Path(__file__).parent.parent.parent @@ -60,9 +60,10 @@ def setup_host(port, platform, board): example_file = 'cpy_example.py' else: example_file = 'mpy_example.py' - boards_dir = note_python_root_dir / 'mpy_board' - board_file_path = boards_dir / f"{board}.py" - copy_file_to_host(pyb, board_file_path, '/board.py') + if mpy_board: + boards_dir = note_python_root_dir / 'mpy_board' + board_file_path = boards_dir / f"{mpy_board}.py" + copy_file_to_host(pyb, board_file_path, '/board.py') examples_dir = note_python_root_dir / 'examples' example_file_path = examples_dir / 'notecard-basics' / example_file @@ -94,9 +95,9 @@ def pytest_addoption(parser): help="Skip host setup (copying over note-python, etc.) (default: False)" ) parser.addoption( - '--board', - required=True, - help='The board name that is being used.' + '--mpyboard', + required=False, + help='The board name that is being used. Required only when running micropython.' ) @@ -105,7 +106,7 @@ def pytest_configure(config): config.platform = config.getoption("platform") config.product_uid = config.getoption("productuid") config.skip_setup = config.getoption("skipsetup") - config.board = config.getoption("board") + config.mpy_board = config.getoption("mpyboard") if not config.skip_setup: - setup_host(config.port, config.platform, config.board) + setup_host(config.port, config.platform, config.mpy_board) diff --git a/test/hitl/requirements-esp32.txt b/test/hitl/requirements-esp32.txt new file mode 100644 index 0000000..d7424bf --- /dev/null +++ b/test/hitl/requirements-esp32.txt @@ -0,0 +1 @@ +esptool \ No newline at end of file diff --git a/test/scripts/check_runner_config.sh b/test/scripts/check_cpy_runner_config.sh similarity index 100% rename from test/scripts/check_runner_config.sh rename to test/scripts/check_cpy_runner_config.sh diff --git a/test/scripts/check_mpy_runner_config.sh b/test/scripts/check_mpy_runner_config.sh new file mode 100755 index 0000000..053bab8 --- /dev/null +++ b/test/scripts/check_mpy_runner_config.sh @@ -0,0 +1,22 @@ +function env_var_defined() { + [ -v $1 ] || echo "Environment variable '$1' not set." +} + +function check_all() { + env_var_defined "MPY_SERIAL" + env_var_defined "MPY_PRODUCT_UID" + # these are defined in the workflow, but no harm sanity checking them + env_var_defined "MICROPYTHON_BIN" + env_var_defined "MICROPYTHON_BIN_URL" + env_var_defined "VENV" + env_var_defined "MPY_BOARD" +} + +errors=$(check_all) +if [ -n "$errors" ]; then + echo "$errors" # quoted to preserve newlines + echo "There are configuration errors. See the log above for details." + exit 1 +fi + +exit 0 \ No newline at end of file From 8ba3ee6f4b724e53ffce2398d6950ecdfbcb043d Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Fri, 11 Aug 2023 09:11:14 -0700 Subject: [PATCH 09/14] chore: misc cleanup [skip ci] --- .github/workflows/hil-micropython.yml | 5 ++++- test/hitl/requirements-esp32.txt | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 test/hitl/requirements-esp32.txt diff --git a/.github/workflows/hil-micropython.yml b/.github/workflows/hil-micropython.yml index 3d82a17..ff07d1e 100644 --- a/.github/workflows/hil-micropython.yml +++ b/.github/workflows/hil-micropython.yml @@ -45,12 +45,15 @@ jobs: run: | python3 -m venv ${{ env.VENV }} . ${{ env.VENV }}/bin/activate - pip install -r test/hitl/requirements.txt -r test/hitl/requirements-esp32.txt + # esptool installed directly because it's only a dependency of this workflow + # while requirements.txt are dependencies of the tests in test/hitl + pip install -r test/hitl/requirements.txt esptool - name: Erase device and Program Micropython if: ${{ matrix.flash_device }} run: | . ${{ env.VENV }}/bin/activate + # esptool requires the flash to be erased first esptool.py --chip esp32 -p ${MPY_SERIAL} erase_flash timeout 10 bash test/scripts/wait_for_file.sh "$MPY_SERIAL" diff --git a/test/hitl/requirements-esp32.txt b/test/hitl/requirements-esp32.txt deleted file mode 100644 index d7424bf..0000000 --- a/test/hitl/requirements-esp32.txt +++ /dev/null @@ -1 +0,0 @@ -esptool \ No newline at end of file From 24cef64553c4bf49abdc75a43c6471dc31ba1aa1 Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Fri, 11 Aug 2023 10:27:55 -0700 Subject: [PATCH 10/14] feat: allow manual runs to optionally not flash firmware. Trigger HIL tests only when key files changed, so they don't run unnecessarily. --- .github/workflows/hil-circuitpython.yml | 34 ++++++++++++++++++------- .github/workflows/hil-micropython.yml | 22 ++++++++++++++-- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/.github/workflows/hil-circuitpython.yml b/.github/workflows/hil-circuitpython.yml index f93e2ae..71984d6 100644 --- a/.github/workflows/hil-circuitpython.yml +++ b/.github/workflows/hil-circuitpython.yml @@ -5,7 +5,21 @@ name: HIL-circuitpython on: pull_request: branches: [ main ] + paths: + # This is quite a big job so run only when files affecting it change. + - .github/workflows/hil-circuitpython.yml + - examples/notecard-basics/cpy_example.py + - test/hitl/** + - test/scripts/usbmount + - test/scripts/check_cpy*.* + - notecard/** + workflow_dispatch: + inputs: + flash_device: + required: false + type: boolean + default: true jobs: test: @@ -16,7 +30,8 @@ jobs: strategy: matrix: CIRCUITPYTHON_VERSION: [8.2.2] - flash_device: [true] + flash_device: # has to be an array - use the input from workflow_dispatch if present, otherwlse true + - ${{ github.event.inputs.flash_device=='' && true || github.event.inputs.flash_device }} lock_cpy_filesystem: [true] env: USB_MSD_ATTACH_TIME: 15 @@ -51,7 +66,7 @@ jobs: download_name=$(jq -r '.name' $asset_file) download_url=$(jq -r '.browser_download_url' $asset_file) echo "Downloading release from $download_url" - wget -q -N $download_url + wget -q -N $download_url unzip -o $download_name binfile=$(basename $download_name .zip).bin echo "TINYUF2_BIN=$binfile" >> $GITHUB_ENV @@ -73,14 +88,14 @@ jobs: -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32l4x.cfg \ -c "init; halt; stm32l4x mass_erase 0" \ -c "program $TINYUF2_BIN 0x8000000 verify reset; shutdown" - + - name: Program CircuitPython if: ${{ matrix.flash_device }} run: | # wait for the bootloader drive to appear timeout $USB_MSD_ATTACH_TIME bash test/scripts/wait_for_file.sh "$CPY_FS_UF2" - # The bootloader reboots quickly once the whole file has been received, + # The bootloader reboots quickly once the whole file has been received, # causing an input/output error to be reported. # Ignore that, and fail if the CIRCUITPY filesystem doesn't appear echo "Uploading CircuitPython binary..." @@ -88,13 +103,15 @@ jobs: echo Ignore the input/output error above. Waiting for device to boot. timeout $USB_MSD_ATTACH_TIME bash test/scripts/wait_for_file.sh "$CPY_FS_CIRCUITPY" echo "CircuitPython binary uploaded and running." - + - name: Make CircuitPython filesystem writeable to pyboard if: ${{ matrix.lock_cpy_filesystem }} run: | timeout $USB_MSD_ATTACH_TIME bash test/scripts/wait_for_file.sh "$CPY_FS_CIRCUITPY" - cp test/hitl/boot.py "$CPY_FS_CIRCUITPY" + # only copy if it's changed or not present. After the device has reset, no further changes can be made + # until the filesystem is erased. This allows the workflow to be rerun flash_device=false + diff test/hitl/boot.py "$CPY_FS_CIRCUITPY/boot.py" || test/hitl/boot.py "$CPY_FS_CIRCUITPY" # reset the device (todo move this blob to a utility script) ~/.platformio/packages/tool-openocd/bin/openocd \ @@ -110,17 +127,16 @@ jobs: python3 -m venv .venv-runner . .venv-runner/bin/activate pip install -r test/hitl/requirements.txt - + - name: Setup 'note-python' on device if: ${{ ! matrix.lock_cpy_filesystem }} run: | mkdir -p ${CPY_FS_CIRCUITPY}/lib/notecard cp notecard/*.py ${CPY_FS_CIRCUITPY}/lib/notecard/ - cp examples/notecard-basics/cpy_example.py ${CPY_FS_CIRCUITPY}/example.py + cp examples/notecard-basics/cpy_example.py ${CPY_FS_CIRCUITPY}/example.py - name: Run CircuitPython Tests run: | . .venv-runner/bin/activate ${{ ! matrix.lock_cpy_filesystem }} && skipsetup=--skipsetup pytest $skipsetup "--productuid=$CPY_PRODUCT_UID" "--port=$CPY_SERIAL" --platform=circuitpython test/hitl - \ No newline at end of file diff --git a/.github/workflows/hil-micropython.yml b/.github/workflows/hil-micropython.yml index ff07d1e..b1c293b 100644 --- a/.github/workflows/hil-micropython.yml +++ b/.github/workflows/hil-micropython.yml @@ -3,11 +3,28 @@ name: HIL-micropython on: pull_request: branches: [ main ] + paths: + - .github/workflows/hil-micropython.yml + - test/hitl/** + - notecard/** + - examples/notecard-basics/mpy_example.py + - test/scripts/check_mpy*.* + workflow_dispatch: + inputs: + flash_device: + required: false + type: boolean + default: true jobs: huzzah32: - runs-on: [self-hosted, linux, esp32, notecard-serial, micropython] + runs-on: + - self-hosted, + - linux, + - ${{ matrix.MPY_BOARD }}, + - notecard-serial, + - micropython defaults: run: shell: bash @@ -17,7 +34,8 @@ jobs: MICROPYTHON_DATE: [20230426] MICROPYTHON_MCU: [esp32] MPY_BOARD: [huzzah32] # the --mpyboard parameter to the tests - flash_device: [false] + flash_device: # has to be an array - use the input from workflow_dispatch if present, otherwlse true + - ${{ github.event.inputs.flash_device=='' && true || github.event.inputs.flash_device }} env: VENV: .venv-runner-mpy USB_MSD_ATTACH_TIME: 15 From fa6c36bcdee65eafba125473e8aa8a6363139d3b Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Fri, 11 Aug 2023 12:32:16 -0700 Subject: [PATCH 11/14] chore: added pre-commit configuration. fixed up formatting. --- .github/workflows/hil-micropython.yml | 8 ++-- .github/workflows/python-ci.yml | 2 +- .pre-commit-config.yaml | 18 +++++++++ Makefile | 54 ++++++++++++++----------- README.md | 15 +++++++ examples/notecard-basics/board.py | 21 +++++----- examples/notecard-basics/mpy_example.py | 5 --- mpy_board/espressif_esp32.py | 29 +++++-------- mpy_board/huzzah32.py | 28 +++++-------- requirements.txt | 1 + test/hitl/test_basic_comms.py | 1 + test/scripts/check_cpy_runner_config.sh | 6 +-- test/scripts/check_mpy_runner_config.sh | 4 +- 13 files changed, 106 insertions(+), 86 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/hil-micropython.yml b/.github/workflows/hil-micropython.yml index b1c293b..4e4d681 100644 --- a/.github/workflows/hil-micropython.yml +++ b/.github/workflows/hil-micropython.yml @@ -20,10 +20,10 @@ on: jobs: huzzah32: runs-on: - - self-hosted, - - linux, - - ${{ matrix.MPY_BOARD }}, - - notecard-serial, + - self-hosted + - linux + - ${{ matrix.MPY_BOARD }} + - notecard-serial - micropython defaults: run: diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index cf9a5d9..f779b6d 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -78,7 +78,7 @@ jobs: DD_SERVICE: note-python DD_ENV: ci run: | - coverage run -m pytest --ddtrace --ddtrace-patch-all + coverage run -m pytest --ddtrace --ddtrace-patch-all --ignore=test/hitl - name: Publish to Coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..45a081b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + +- repo: local + hooks: + - id: Formatting + name: Formatting + entry: make precommit + language: python # This sets up a virtual environment + additional_dependencies: [flake8, pydocstyle] diff --git a/Makefile b/Makefile index 47dcc30..be6237e 100644 --- a/Makefile +++ b/Makefile @@ -1,40 +1,46 @@ # define VENV_NAME to use a specific virtual environment. It defaults to `env`. VENV_NAME?=env -VENV_ACTIVATE=. $(VENV_NAME)/bin/activate +VENV_ACTIVATE=$(VENV_NAME)/bin/activate PYTHON=python -VENV = +# the target to activate the virtual environment. Only defined if it exists. -# check if the VENV file exists -ifneq ("$(wildcard $(PVENV_ACTIVATE))","") - VENV = venv - PYTHON = ${VENV_NAME}/bin/python3 +# check if the VENV file exists, if it does assume that's been made active +ifneq ("$(wildcard ${VENV_ACTIVATE})","") + RUN_VENV_ACTIVATE=. ${VENV_ACTIVATE} + PYTHON = ${VENV_NAME}/bin/python3 endif -default: docstyle flake8 test +default: precommit -venv: $(VENV_NAME)/bin/activate +precommit: docstyle flake8 -test: $(VENV) - ${PYTHON} -m pytest test --cov=notecard +test: + ${RUN_VENV_ACTIVATE} + ${PYTHON} -m pytest test --cov=notecard --ignore=test/hitl -docstyle: $(VENV) - ${PYTHON} -m pydocstyle notecard/ examples/ +docstyle: + ${RUN_VENV_ACTIVATE} + ${PYTHON} -m pydocstyle notecard/ examples/ mpy_board/ -flake8: $(VENV) - # E722 Do not use bare except, specify exception instead https://www.flake8rules.com/rules/E722.html - # F401 Module imported but unused https://www.flake8rules.com/rules/F401.html - # F403 'from module import *' used; unable to detect undefined names https://www.flake8rules.com/rules/F403.html - # W503 Line break occurred before a binary operator https://www.flake8rules.com/rules/W503.html - # E501 Line too long (>79 characters) https://www.flake8rules.com/rules/E501.html - ${PYTHON} -m flake8 test/ notecard/ examples/ --count --ignore=E722,F401,F403,W503,E501 --show-source --statistics +flake8: + ${RUN_VENV_ACTIVATE} + # E722 Do not use bare except, specify exception instead https://www.flake8rules.com/rules/E722.html + # F401 Module imported but unused https://www.flake8rules.com/rules/F401.html + # F403 'from module import *' used; unable to detect undefined names https://www.flake8rules.com/rules/F403.html + # W503 Line break occurred before a binary operator https://www.flake8rules.com/rules/W503.html + # E501 Line too long (>79 characters) https://www.flake8rules.com/rules/E501.html + ${PYTHON} -m flake8 test/ notecard/ examples/ mpy_board/ --count --ignore=E722,F401,F403,W503,E501 --show-source --statistics -coverage: $(VENV) - ${PYTHON} -m pytest test --doctest-modules --junitxml=junit/test-results.xml --cov=notecard --cov-report=xml --cov-report=html +coverage: + ${RUN_VENV_ACTIVATE} + ${PYTHON} -m pytest test --ignore=test/hitl --doctest-modules --junitxml=junit/test-results.xml --cov=notecard --cov-report=xml --cov-report=html -run_build: $(VENV) +run_build: + ${RUN_VENV_ACTIVATE} ${PYTHON} -m setup sdist bdist_wheel -deploy: $(VENV) +deploy: + ${RUN_VENV_ACTIVATE} ${PYTHON} -m twine upload -r "pypi" --config-file .pypirc 'dist/*' -.PHONY: venv test coverage run_build deploy +.PHONY: precommit venv test coverage run_build deploy diff --git a/README.md b/README.md index 2d7bc20..5684a1d 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,21 @@ Alternatively, you can inspect the contents of the [Makefile](Makefile) and run `Makefile` run against every pull request, so your best bet is to ensure these tests are successful before submitting your PR. +## Installing the `pre-commit` Hook + +Please run + +`pre-commit install` + +Before committing to this repo. It will catch a lot of common errors that you can fix locally. + +You may also run the pre-commit checks before committing with + +`pre-commit run` + +Note that `pre-commit run` only considers staged changes, so be sure all +changes are staged before running this. + ## More Information For additional Notecard SDKs and Libraries, see: diff --git a/examples/notecard-basics/board.py b/examples/notecard-basics/board.py index 6a936ee..aeb8ea1 100644 --- a/examples/notecard-basics/board.py +++ b/examples/notecard-basics/board.py @@ -1,18 +1,21 @@ """ -This module is used by the mpy_example to set define the appropriate peripherals for -different types of boards. The values here are defaults. +Define peripherals for different types of boards. + +This module, or it's variants are used by the mpy_example to use the appropriate +UART or I2C configuration for the particular board being used. +The values here are defaults. The definitions for real boards are located in ./mpy_board/* +at the root of the repo. """ + """ The UART instance to use that is connected to Notecard. """ -UART=2 +UART = 2 """ -The SCL pin of the I2C bus connected to Notecard +The I2C ID and SDL and SDA pins of the I2C bus connected to Notecard """ -I2C_ID=0 -SCL=0 -SDA=0 - - +I2C_ID = 0 +SCL = 0 +SDA = 0 diff --git a/examples/notecard-basics/mpy_example.py b/examples/notecard-basics/mpy_example.py index 4d180cf..ba0c68e 100644 --- a/examples/notecard-basics/mpy_example.py +++ b/examples/notecard-basics/mpy_example.py @@ -82,11 +82,6 @@ def run_example(product_uid, use_uart=True): timeout=3000, timeout_char=100) else: port = I2C(board.I2C_ID, scl=Pin(board.SCL), sda=Pin(board.SDA)) - # If you"re using an ESP32, connect GPIO 22 to SCL and GPIO 21 to SDA. - if "ESP32abc" in sys.implementation._machine: - port = I2C(1, scl=Pin(22), sda=Pin(21)) - else: - port = I2C(0, scl=Pin(22), sda=Pin(23)) print("Opening Notecard...") if use_uart: diff --git a/mpy_board/espressif_esp32.py b/mpy_board/espressif_esp32.py index c37a6ab..db40d46 100644 --- a/mpy_board/espressif_esp32.py +++ b/mpy_board/espressif_esp32.py @@ -1,27 +1,16 @@ -""" -Peripheral definitions for Adafruit HUZZAH32 -""" +"""Peripheral definitions for Espressif ESP32 board.""" -""" -The UART instance to use that is connected to Notecard. -""" -UART=2 +"""The UART instance to use that is connected to Notecard.""" +UART = 2 -""" -The I2C peripheral ID to use. -""" -I2C_ID=1 +"""The I2C peripheral ID to use.""" +I2C_ID = 1 +"""The SCL pin number of the the I2C peripheral.""" +SCL = 22 -""" -The SCL pin number of the the I2C peripheral -""" -SCL=22 - -""" -The SDA pin number of the I2C peripheral -""" -SDA=21 \ No newline at end of file +"""The SDA pin number of the I2C peripheral.""" +SDA = 21 diff --git a/mpy_board/huzzah32.py b/mpy_board/huzzah32.py index 669930c..8c97325 100644 --- a/mpy_board/huzzah32.py +++ b/mpy_board/huzzah32.py @@ -1,26 +1,16 @@ -""" -Peripheral definitions for Adafruit HUZZAH32 -""" +"""Peripheral definitions for Adafruit HUZZAH32 board.""" -""" -The UART instance to use that is connected to Notecard. -""" -UART=2 +"""The UART instance to use that is connected to Notecard.""" +UART = 2 -""" -The I2C peripheral ID to use. -""" -I2C_ID=0 +"""The I2C peripheral ID to use.""" +I2C_ID = 0 -""" -The SCL pin number of the the I2C peripheral -""" -SCL=22 +"""The SCL pin number of the the I2C peripheral.""" +SCL = 22 -""" -The SDA pin number of the I2C peripheral -""" -SDA=23 \ No newline at end of file +"""The SDA pin number of the I2C peripheral.""" +SDA = 23 diff --git a/requirements.txt b/requirements.txt index 65df04e..398a0c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ pytest-cov==2.8.1 filelock==3.0.12 pydocstyle==5.0.2 packaging>=20.4 +pre-commit diff --git a/test/hitl/test_basic_comms.py b/test/hitl/test_basic_comms.py index 1567084..854be15 100644 --- a/test/hitl/test_basic_comms.py +++ b/test/hitl/test_basic_comms.py @@ -15,6 +15,7 @@ def run_example(port, product_uid, use_uart): pyb.exit_raw_repl() pyb.close() + def test_example_i2c(pytestconfig): run_example(pytestconfig.port, pytestconfig.product_uid, use_uart=False) diff --git a/test/scripts/check_cpy_runner_config.sh b/test/scripts/check_cpy_runner_config.sh index 46299de..7fa4488 100755 --- a/test/scripts/check_cpy_runner_config.sh +++ b/test/scripts/check_cpy_runner_config.sh @@ -1,7 +1,7 @@ - +#!/bin/bash function diff_dir() { src=$1 - dest=$2 + dest=$2 diff -r $src $dest } @@ -28,4 +28,4 @@ if [ -n "$errors" ]; then exit 1 fi -exit 0 \ No newline at end of file +exit 0 diff --git a/test/scripts/check_mpy_runner_config.sh b/test/scripts/check_mpy_runner_config.sh index 053bab8..d3e570e 100755 --- a/test/scripts/check_mpy_runner_config.sh +++ b/test/scripts/check_mpy_runner_config.sh @@ -1,3 +1,5 @@ +#!/bin/bash + function env_var_defined() { [ -v $1 ] || echo "Environment variable '$1' not set." } @@ -19,4 +21,4 @@ if [ -n "$errors" ]; then exit 1 fi -exit 0 \ No newline at end of file +exit 0 From 219b3a64ddbe7f38cb9cf15380238159358faab5 Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Fri, 11 Aug 2023 12:41:26 -0700 Subject: [PATCH 12/14] chore: rename job [skip ci] --- .github/workflows/hil-micropython.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hil-micropython.yml b/.github/workflows/hil-micropython.yml index 4e4d681..1b60b56 100644 --- a/.github/workflows/hil-micropython.yml +++ b/.github/workflows/hil-micropython.yml @@ -18,7 +18,7 @@ on: default: true jobs: - huzzah32: + test: runs-on: - self-hosted - linux From d2890b436718ed3e178edf6606ee890cb5ab5f7a Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Fri, 11 Aug 2023 12:58:22 -0700 Subject: [PATCH 13/14] fix: wait for uPy to complete initial setup. ``` b'Performing initial setup\r\nMicroPython v1.20.0 on 2023-04-26; ESP32 module with ESP32\r\nType "help()" for more information.\r\n>>> ' INTERNALERROR> pyb.enter_raw_repl() INTERNALERROR> File "/home/mat/runners/notecard-nbgl-carr_f-huzzah32-serial/_work/note-python/note-python/test/hitl/deps/pyboard.py", line 367, in enter_raw_repl INTERNALERROR> raise PyboardError("could not enter raw repl") INTERNALERROR> pyboard.PyboardError: could not enter raw repl ``` --- .github/workflows/hil-micropython.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/hil-micropython.yml b/.github/workflows/hil-micropython.yml index 1b60b56..1579403 100644 --- a/.github/workflows/hil-micropython.yml +++ b/.github/workflows/hil-micropython.yml @@ -78,6 +78,10 @@ jobs: esptool.py --chip esp32 --port ${MPY_SERIAL} --baud 460800 write_flash -z 0x1000 ${{ env.MICROPYTHON_BIN }} timeout 10 bash test/scripts/wait_for_file.sh "$MPY_SERIAL" + # wait for MicroPython to complete initial setup + echo "help()" >> "$MPY_SERIAL" + sleep 10 + - name: Run MicroPython Tests run: | . ${{ env.VENV }}/bin/activate From 7587d814ec63f0e1b8a89425214b71290e7a8479 Mon Sep 17 00:00:00 2001 From: Matthew McGowan Date: Fri, 11 Aug 2023 13:09:47 -0700 Subject: [PATCH 14/14] fix: missing `cp` --- .github/workflows/hil-circuitpython.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hil-circuitpython.yml b/.github/workflows/hil-circuitpython.yml index 71984d6..7d7e490 100644 --- a/.github/workflows/hil-circuitpython.yml +++ b/.github/workflows/hil-circuitpython.yml @@ -111,7 +111,7 @@ jobs: # only copy if it's changed or not present. After the device has reset, no further changes can be made # until the filesystem is erased. This allows the workflow to be rerun flash_device=false - diff test/hitl/boot.py "$CPY_FS_CIRCUITPY/boot.py" || test/hitl/boot.py "$CPY_FS_CIRCUITPY" + diff test/hitl/boot.py "$CPY_FS_CIRCUITPY/boot.py" || cp test/hitl/boot.py "$CPY_FS_CIRCUITPY" # reset the device (todo move this blob to a utility script) ~/.platformio/packages/tool-openocd/bin/openocd \