west: runners: uf2: Add new UF2 runner.

Add a new UF2 runner, supporting only the flash capability.

Searches for FAT partitions containing `INFO_UF2.TXT` files,
and can optionally filter on a matching `Board-ID` value in
that file.

Signed-off-by: Peter Johanson <peter@peterjohanson.com>
This commit is contained in:
Peter Johanson 2023-01-30 00:50:57 -05:00 committed by Carles Cufí
parent b2c75f423a
commit 26a04adb8b
7 changed files with 118 additions and 1 deletions

View file

@ -0,0 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
board_set_flasher_ifnset(uf2)
board_finalize_runner_args(uf2) # No default arguments to provide.

View file

@ -48,6 +48,10 @@ function(runners_yaml_append_config)
get_runners_prop(bin_file bin "${KERNEL_BIN_NAME}")
runners_yaml_append(" bin_file: ${bin}")
endif()
if(CONFIG_BUILD_OUTPUT_UF2)
get_runners_prop(uf2_file uf2 "${KERNEL_UF2_NAME}")
runners_yaml_append(" uf2_file: ${uf2}")
endif()
if(CMAKE_GDB OR OPENOCD OR OPENOCD_DEFAULT_PATH)
runners_yaml_append(" # Host tools:")

View file

@ -413,6 +413,7 @@ def get_runner_config(build_dir, yaml_path, runners_yaml, args=None):
output_file('elf'),
output_file('hex'),
output_file('bin'),
output_file('uf2'),
config('file'),
filetype('file_type'),
config('gdb'),

View file

@ -49,6 +49,7 @@ _names = [
'stm32cubeprogrammer',
'stm32flash',
'trace32',
'uf2',
'xtensa',
# Keep this list sorted by runner name; don't add to the end.
]

View file

@ -285,6 +285,7 @@ class RunnerConfig(NamedTuple):
elf_file: Optional[str] # zephyr.elf path, or None
hex_file: Optional[str] # zephyr.hex path, or None
bin_file: Optional[str] # zephyr.bin path, or None
uf2_file: Optional[str] # zephyr.uf2 path, or None
file: Optional[str] # binary file path (provided by the user), or None
file_type: Optional[FileType] = FileType.OTHER # binary file type
gdb: Optional[str] = None # path to a usable gdb
@ -758,7 +759,7 @@ class ZephyrBinaryRunner(abc.ABC):
else:
return
if output_type in ('elf', 'hex', 'bin'):
if output_type in ('elf', 'hex', 'bin', 'uf2'):
err += f' Try enabling CONFIG_BUILD_OUTPUT_{output_type.upper()}.'
# RuntimeError avoids a stack trace saved in run_common.

View file

@ -0,0 +1,105 @@
# Copyright (c) 2023 Peter Johanson <peter@peterjohanson.com>
#
# SPDX-License-Identifier: Apache-2.0
'''UF2 runner (flash only) for UF2 compatible bootloaders.'''
from pathlib import Path
from shutil import copy
from runners.core import ZephyrBinaryRunner, RunnerCaps
try:
import psutil # pylint: disable=unused-import
MISSING_PSUTIL = False
except ImportError:
# This can happen when building the documentation for the
# runners package if psutil is not on sys.path. This is fine
# to ignore in that case.
MISSING_PSUTIL = True
class UF2BinaryRunner(ZephyrBinaryRunner):
'''Runner front-end for copying to UF2 USB-MSC mounts.'''
def __init__(self, cfg, board_id=None):
super().__init__(cfg)
self.board_id = board_id
@classmethod
def name(cls):
return 'uf2'
@classmethod
def capabilities(cls):
return RunnerCaps(commands={'flash'})
@classmethod
def do_add_parser(cls, parser):
parser.add_argument('--board-id', dest='board_id',
help='Board-ID value to match from INFO_UF2.TXT')
@classmethod
def do_create(cls, cfg, args):
return UF2BinaryRunner(cfg, board_id=args.board_id)
@staticmethod
def get_uf2_info_path(part) -> Path:
return Path(part.mountpoint) / "INFO_UF2.TXT"
@staticmethod
def is_uf2_partition(part):
try:
return ((part.fstype in ['vfat', 'FAT']) and
UF2BinaryRunner.get_uf2_info_path(part).is_file())
except PermissionError:
return False
@staticmethod
def get_uf2_info(part):
lines = UF2BinaryRunner.get_uf2_info_path(part).read_text().splitlines()
lines = lines[1:] # Skip the first summary line
def split_uf2_info(line: str):
k, _, val = line.partition(':')
return k.strip(), val.strip()
return {k: v for k, v in (split_uf2_info(line) for line in lines) if k and v}
def match_board_id(self, part):
info = self.get_uf2_info(part)
return info.get('Board-ID') == self.board_id
def get_uf2_partitions(self):
parts = [part for part in psutil.disk_partitions() if self.is_uf2_partition(part)]
if (self.board_id is not None) and parts:
parts = [part for part in parts if self.match_board_id(part)]
if not parts:
self.logger.warning("Discovered UF2 partitions don't match Board-ID '%s'",
self.board_id)
return parts
def copy_uf2_to_partition(self, part):
self.ensure_output('uf2')
copy(self.cfg.uf2_file, part.mountpoint)
def do_run(self, command, **kwargs):
if MISSING_PSUTIL:
raise RuntimeError(
'could not import psutil; something may be wrong with the '
'python environment')
partitions = self.get_uf2_partitions()
if not partitions:
raise RuntimeError('No matching UF2 partitions found')
if len(partitions) > 1:
raise RuntimeError('More than one matching UF2 partitions found')
part = partitions[0]
self.logger.info("Copying UF2 file to '%s'", part.mountpoint)
self.copy_uf2_to_partition(part)

View file

@ -39,5 +39,6 @@ def test_runner_imports():
'stm32cubeprogrammer',
'stm32flash',
'trace32',
'uf2',
'xtensa'))
assert runner_names == expected