zephyr/scripts/west_commands/runners/bossac.py
Benjamin Cabé 4773f42c27 west: bossac: handle stty from coreutils on macOS
There might be situations where people are running the coreutils version
of stty on macOS, hence the need for being smarter at detecting when
that might be the case

Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
2023-10-11 11:05:48 +01:00

284 lines
10 KiB
Python

# Copyright (c) 2017 Linaro Limited.
# Copyright (c) 2020 Gerson Fernando Budke <nandojve@gmail.com>
#
# SPDX-License-Identifier: Apache-2.0
'''bossac-specific runner (flash only) for Atmel SAM microcontrollers.'''
import os
import pathlib
import pickle
import platform
import subprocess
import sys
import time
from runners.core import ZephyrBinaryRunner, RunnerCaps
if platform.system() == 'Darwin':
DEFAULT_BOSSAC_PORT = None
else:
DEFAULT_BOSSAC_PORT = '/dev/ttyACM0'
DEFAULT_BOSSAC_SPEED = '115200'
class BossacBinaryRunner(ZephyrBinaryRunner):
'''Runner front-end for bossac.'''
def __init__(self, cfg, bossac='bossac', port=DEFAULT_BOSSAC_PORT,
speed=DEFAULT_BOSSAC_SPEED, boot_delay=0):
super().__init__(cfg)
self.bossac = bossac
self.port = port
self.speed = speed
self.boot_delay = boot_delay
@classmethod
def name(cls):
return 'bossac'
@classmethod
def capabilities(cls):
return RunnerCaps(commands={'flash'})
@classmethod
def do_add_parser(cls, parser):
parser.add_argument('--bossac', default='bossac',
help='path to bossac, default is bossac')
parser.add_argument('--bossac-port', default=DEFAULT_BOSSAC_PORT,
help='serial port to use, default is ' +
str(DEFAULT_BOSSAC_PORT))
parser.add_argument('--speed', default=DEFAULT_BOSSAC_SPEED,
help='serial port speed to use, default is ' +
DEFAULT_BOSSAC_SPEED)
parser.add_argument('--delay', default=0, type=float,
help='''delay in seconds (may be a floating
point number) to wait between putting the board
into bootloader mode and running bossac;
default is no delay''')
@classmethod
def do_create(cls, cfg, args):
return BossacBinaryRunner(cfg, bossac=args.bossac,
port=args.bossac_port, speed=args.speed,
boot_delay=args.delay)
def read_help(self):
"""Run bossac --help and return the output as a list of lines"""
self.require(self.bossac)
try:
# BOSSA > 1.9.1 returns OK
out = self.check_output([self.bossac, '--help']).decode()
except subprocess.CalledProcessError as ex:
# BOSSA <= 1.9.1 returns an error
out = ex.output.decode()
return out.split('\n')
def supports(self, flag):
"""Check if bossac supports a flag by searching the help"""
for line in self.read_help():
if flag in line:
return True
return False
def is_extended_samba_protocol(self):
ext_samba_versions = ['CONFIG_BOOTLOADER_BOSSA_ARDUINO',
'CONFIG_BOOTLOADER_BOSSA_ADAFRUIT_UF2']
for x in ext_samba_versions:
if self.build_conf.getboolean(x):
return True
return False
def is_partition_enabled(self):
return self.build_conf.getboolean('CONFIG_USE_DT_CODE_PARTITION')
def get_chosen_code_partition_node(self):
# Get the EDT Node corresponding to the zephyr,code-partition
# chosen DT node
# Ensure the build directory has a compiled DTS file
# where we expect it to be.
b = pathlib.Path(self.cfg.build_dir)
edt_pickle = b / 'zephyr' / 'edt.pickle'
if not edt_pickle.is_file():
error_msg = "can't load devicetree; expected to find:" \
+ str(edt_pickle)
raise RuntimeError(error_msg)
# Load the devicetree.
try:
with open(edt_pickle, 'rb') as f:
edt = pickle.load(f)
except ModuleNotFoundError:
error_msg = "could not load devicetree, something may be wrong " \
+ "with the python environment"
raise RuntimeError(error_msg)
return edt.chosen_node('zephyr,code-partition')
def get_board_name(self):
if 'CONFIG_BOARD' not in self.build_conf:
return '<board>'
return self.build_conf['CONFIG_BOARD']
def get_dts_img_offset(self):
if self.build_conf.getboolean('CONFIG_BOOTLOADER_BOSSA_LEGACY'):
return 0
if self.build_conf.getboolean('CONFIG_HAS_FLASH_LOAD_OFFSET'):
return self.build_conf['CONFIG_FLASH_LOAD_OFFSET']
return 0
def get_image_offset(self, supports_offset):
"""Validates and returns the flash offset"""
dts_img_offset = self.get_dts_img_offset()
if int(str(dts_img_offset), 16) > 0:
if not supports_offset:
old_sdk = 'This version of BOSSA does not support the' \
' --offset flag. Please upgrade to a newer Zephyr' \
' SDK version >= 0.12.0.'
raise RuntimeError(old_sdk)
return dts_img_offset
return None
def is_gnu_coreutils_stty(self):
try:
result = subprocess.run(['stty', '--version'], capture_output=True, text=True, check=True)
return 'coreutils' in result.stdout
except subprocess.CalledProcessError:
return False
def set_serial_config(self):
if platform.system() == 'Linux' or platform.system() == 'Darwin':
self.require('stty')
# GNU coreutils uses a capital F flag for 'file'
flag = '-F' if self.is_gnu_coreutils_stty() else '-f'
if self.is_extended_samba_protocol():
self.speed = '1200'
cmd_stty = ['stty', flag, self.port, 'raw', 'ispeed', self.speed,
'ospeed', self.speed, 'cs8', '-cstopb', 'ignpar',
'eol', '255', 'eof', '255']
self.check_call(cmd_stty)
self.magic_delay()
def magic_delay(self):
'''There can be a time lag between the board resetting into
bootloader mode (done via stty above) and the OS enumerating
the USB device again. This function lets users tune a magic
delay for their system to handle this case. By default,
we don't wait.
'''
if self.boot_delay > 0:
time.sleep(self.boot_delay)
def make_bossac_cmd(self):
self.ensure_output('bin')
cmd_flash = [self.bossac, '-p', self.port, '-R', '-e', '-w', '-v',
'-b', self.cfg.bin_file]
dt_chosen_code_partition_nd = self.get_chosen_code_partition_node()
if self.is_partition_enabled():
if dt_chosen_code_partition_nd is None:
error_msg = 'The device tree zephyr,code-partition chosen' \
' node must be defined.'
raise RuntimeError(error_msg)
offset = self.get_image_offset(self.supports('--offset'))
if offset is not None and int(str(offset), 16) > 0:
cmd_flash += ['-o', '%s' % offset]
elif dt_chosen_code_partition_nd is not None:
error_msg = 'There is no CONFIG_USE_DT_CODE_PARTITION Kconfig' \
' defined at ' + self.get_board_name() + \
'_defconfig file.\n This means that' \
' zephyr,code-partition device tree node should not' \
' be defined. Check Zephyr SAM-BA documentation.'
raise RuntimeError(error_msg)
return cmd_flash
def get_darwin_serial_device_list(self):
"""
Get a list of candidate serial ports on Darwin by querying the IOKit
registry.
"""
import plistlib
ioreg_out = self.check_output(['ioreg', '-r', '-c', 'IOSerialBSDClient',
'-k', 'IOCalloutDevice', '-a'])
serial_ports = plistlib.loads(ioreg_out, fmt=plistlib.FMT_XML)
return [port["IOCalloutDevice"] for port in serial_ports]
def get_darwin_user_port_choice(self):
"""
Ask the user to select the serial port from a set of candidate ports
retrieved from IOKit on Darwin.
Modelled on get_board_snr() in the nrfjprog runner.
"""
devices = self.get_darwin_serial_device_list()
if len(devices) == 0:
raise RuntimeError('No candidate serial ports were found!')
elif len(devices) == 1:
print('Using only serial device on the system: ' + devices[0])
return devices[0]
elif not sys.stdin.isatty():
raise RuntimeError('Refusing to guess which serial port to use: '
f'there are {len(devices)} available. '
'(Interactive prompts disabled since standard '
'input is not a terminal - please specify a '
'port using --bossac-port instead)')
print('There are multiple serial ports available on this system:')
for i, device in enumerate(devices, 1):
print(f' {i}. {device}')
p = f'Please select one (1-{len(devices)}, or EOF to exit): '
while True:
try:
value = input(p)
except EOFError:
sys.exit(0)
try:
value = int(value)
except ValueError:
continue
if 1 <= value <= len(devices):
break
return devices[value - 1]
def do_run(self, command, **kwargs):
if platform.system() == 'Linux':
if 'microsoft' in platform.uname().release.lower() or \
os.getenv('WSL_DISTRO_NAME') is not None or \
os.getenv('WSL_INTEROP') is not None:
msg = 'CAUTION: BOSSAC runner not supported on WSL!'
raise RuntimeError(msg)
elif platform.system() == 'Darwin' and self.port is None:
self.port = self.get_darwin_user_port_choice()
self.require(self.bossac)
self.set_serial_config()
self.check_call(self.make_bossac_cmd())