west: runners: Add support for multiple device IDs

In order to enable the use case where the underlying flash tool supports
bulk-flashing using multiple device IDs, augment the core runner class
with this new runner capability and implement it in the nrfutil runner,
since the nrfutil tool supports it natively.

Signed-off-by: Carles Cufi <carles.cufi@nordicsemi.no>
This commit is contained in:
Carles Cufi 2025-02-28 18:34:44 +01:00 committed by Alberto Escolar
commit c9151be798
4 changed files with 56 additions and 11 deletions

View file

@ -268,6 +268,9 @@ class RunnerCaps:
connected to a single computer, in order to select which one will be used connected to a single computer, in order to select which one will be used
with the command provided. with the command provided.
- mult_dev_ids: whether the runner supports multiple device identifiers
for a single operation, allowing for bulk flashing of devices.
- flash_addr: whether the runner supports flashing to an - flash_addr: whether the runner supports flashing to an
arbitrary address. Default is False. If true, the runner arbitrary address. Default is False. If true, the runner
must honor the --dt-flash option. must honor the --dt-flash option.
@ -305,6 +308,7 @@ class RunnerCaps:
commands: set[str] = field(default_factory=lambda: set(_RUNNERCAPS_COMMANDS)) commands: set[str] = field(default_factory=lambda: set(_RUNNERCAPS_COMMANDS))
dev_id: bool = False dev_id: bool = False
mult_dev_ids: bool = False
flash_addr: bool = False flash_addr: bool = False
erase: bool = False erase: bool = False
reset: bool = False reset: bool = False
@ -316,6 +320,8 @@ class RunnerCaps:
# to allow other commands to use the rtt address # to allow other commands to use the rtt address
def __post_init__(self): def __post_init__(self):
if self.mult_dev_ids and not self.dev_id:
raise RuntimeError('dev_id must be set along mult_dev_ids')
if not self.commands.issubset(_RUNNERCAPS_COMMANDS): if not self.commands.issubset(_RUNNERCAPS_COMMANDS):
raise ValueError(f'{self.commands=} contains invalid command') raise ValueError(f'{self.commands=} contains invalid command')
@ -543,7 +549,9 @@ class ZephyrBinaryRunner(abc.ABC):
caps = cls.capabilities() caps = cls.capabilities()
if caps.dev_id: if caps.dev_id:
action = 'append' if caps.mult_dev_ids else 'store'
parser.add_argument('-i', '--dev-id', parser.add_argument('-i', '--dev-id',
action=action,
dest='dev_id', dest='dev_id',
help=cls.dev_id_help()) help=cls.dev_id_help())
else: else:
@ -749,10 +757,13 @@ class ZephyrBinaryRunner(abc.ABC):
@classmethod @classmethod
def dev_id_help(cls) -> str: def dev_id_help(cls) -> str:
''' Get the ArgParse help text for the --dev-id option.''' ''' Get the ArgParse help text for the --dev-id option.'''
return '''Device identifier. Use it to select help = '''Device identifier. Use it to select
which debugger, device, node or instance to which debugger, device, node or instance to
target when multiple ones are available or target when multiple ones are available or
connected.''' connected.'''
addendum = '''\nThis option can be present multiple times.''' if \
cls.capabilities().mult_dev_ids else ''
return help + addendum
@classmethod @classmethod
def extload_help(cls) -> str: def extload_help(cls) -> str:

View file

@ -101,12 +101,13 @@ class NrfBinaryRunner(ZephyrBinaryRunner):
self.tool_opt += opts self.tool_opt += opts
@classmethod @classmethod
def capabilities(cls): def _capabilities(cls, mult_dev_ids=False):
return RunnerCaps(commands={'flash'}, dev_id=True, erase=True, return RunnerCaps(commands={'flash'}, dev_id=True,
reset=True, tool_opt=True) mult_dev_ids=mult_dev_ids, erase=True, reset=True,
tool_opt=True)
@classmethod @classmethod
def dev_id_help(cls) -> str: def _dev_id_help(cls) -> str:
return '''Device identifier. Use it to select the J-Link Serial Number return '''Device identifier. Use it to select the J-Link Serial Number
of the device connected over USB. '*' matches one or more of the device connected over USB. '*' matches one or more
characters/digits''' characters/digits'''
@ -146,9 +147,19 @@ class NrfBinaryRunner(ZephyrBinaryRunner):
args.dev_id = previous_runner.dev_id args.dev_id = previous_runner.dev_id
def ensure_snr(self): def ensure_snr(self):
if not self.dev_id or "*" in self.dev_id: # dev_id can be None, str or list of str
self.dev_id = self.get_board_snr(self.dev_id or "*") dev_id = self.dev_id
self.dev_id = self.dev_id.lstrip("0") if isinstance(dev_id, list):
if len(dev_id) == 0:
dev_id = None
elif len(dev_id) == 1:
dev_id = dev_id[0]
else:
self.dev_id = [d.lstrip("0") for d in dev_id]
return
if not dev_id or "*" in dev_id:
dev_id = self.get_board_snr(dev_id or "*")
self.dev_id = dev_id.lstrip("0")
@abc.abstractmethod @abc.abstractmethod
def do_get_boards(self): def do_get_boards(self):
@ -528,5 +539,5 @@ class NrfBinaryRunner(ZephyrBinaryRunner):
# All done, now flush any outstanding ops # All done, now flush any outstanding ops
self.flush(force=True) self.flush(force=True)
self.logger.info(f'Board with serial number {self.dev_id} ' self.logger.info(f'Board(s) with serial number(s) {self.dev_id} '
'flashed successfully.') 'flashed successfully.')

View file

@ -30,6 +30,14 @@ class NrfJprogBinaryRunner(NrfBinaryRunner):
def name(cls): def name(cls):
return 'nrfjprog' return 'nrfjprog'
@classmethod
def capabilities(cls):
return NrfBinaryRunner._capabilities()
@classmethod
def dev_id_help(cls) -> str:
return NrfBinaryRunner._dev_id_help()
@classmethod @classmethod
def tool_opt_help(cls) -> str: def tool_opt_help(cls) -> str:
return 'Additional options for nrfjprog, e.g. "--clockspeed"' return 'Additional options for nrfjprog, e.g. "--clockspeed"'

View file

@ -33,6 +33,15 @@ class NrfUtilBinaryRunner(NrfBinaryRunner):
def name(cls): def name(cls):
return 'nrfutil' return 'nrfutil'
@classmethod
def capabilities(cls):
return NrfBinaryRunner._capabilities(mult_dev_ids=True)
@classmethod
def dev_id_help(cls) -> str:
return NrfBinaryRunner._dev_id_help() + \
'''.\n This option can be specified multiple times'''
@classmethod @classmethod
def tool_opt_help(cls) -> str: def tool_opt_help(cls) -> str:
return 'Additional options for nrfutil, e.g. "--log-level"' return 'Additional options for nrfutil, e.g. "--log-level"'
@ -107,6 +116,12 @@ class NrfUtilBinaryRunner(NrfBinaryRunner):
self._op_id += 1 self._op_id += 1
self._ops.append(op) self._ops.append(op)
def _format_dev_ids(self):
if isinstance(self.dev_id, list):
return ','.join(self.dev_id)
else:
return self.dev_id
def _append_batch(self, op, json_file): def _append_batch(self, op, json_file):
_op = op['operation'] _op = op['operation']
op_type = _op['type'] op_type = _op['type']
@ -151,7 +166,7 @@ class NrfUtilBinaryRunner(NrfBinaryRunner):
precmd = ['--x-ext-mem-config-file', self.ext_mem_config_file] precmd = ['--x-ext-mem-config-file', self.ext_mem_config_file]
self._exec(precmd + ['x-execute-batch', '--batch-path', f'{json_file}', self._exec(precmd + ['x-execute-batch', '--batch-path', f'{json_file}',
'--serial-number', f'{self.dev_id}']) '--serial-number', self._format_dev_ids()])
def do_exec_op(self, op, force=False): def do_exec_op(self, op, force=False):
self.logger.debug(f'Executing op: {op}') self.logger.debug(f'Executing op: {op}')