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
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
arbitrary address. Default is False. If true, the runner
must honor the --dt-flash option.
@ -305,6 +308,7 @@ class RunnerCaps:
commands: set[str] = field(default_factory=lambda: set(_RUNNERCAPS_COMMANDS))
dev_id: bool = False
mult_dev_ids: bool = False
flash_addr: bool = False
erase: bool = False
reset: bool = False
@ -316,6 +320,8 @@ class RunnerCaps:
# to allow other commands to use the rtt address
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):
raise ValueError(f'{self.commands=} contains invalid command')
@ -543,7 +549,9 @@ class ZephyrBinaryRunner(abc.ABC):
caps = cls.capabilities()
if caps.dev_id:
action = 'append' if caps.mult_dev_ids else 'store'
parser.add_argument('-i', '--dev-id',
action=action,
dest='dev_id',
help=cls.dev_id_help())
else:
@ -749,10 +757,13 @@ class ZephyrBinaryRunner(abc.ABC):
@classmethod
def dev_id_help(cls) -> str:
''' 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
target when multiple ones are available or
connected.'''
addendum = '''\nThis option can be present multiple times.''' if \
cls.capabilities().mult_dev_ids else ''
return help + addendum
@classmethod
def extload_help(cls) -> str:

View file

@ -101,12 +101,13 @@ class NrfBinaryRunner(ZephyrBinaryRunner):
self.tool_opt += opts
@classmethod
def capabilities(cls):
return RunnerCaps(commands={'flash'}, dev_id=True, erase=True,
reset=True, tool_opt=True)
def _capabilities(cls, mult_dev_ids=False):
return RunnerCaps(commands={'flash'}, dev_id=True,
mult_dev_ids=mult_dev_ids, erase=True, reset=True,
tool_opt=True)
@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
of the device connected over USB. '*' matches one or more
characters/digits'''
@ -146,9 +147,19 @@ class NrfBinaryRunner(ZephyrBinaryRunner):
args.dev_id = previous_runner.dev_id
def ensure_snr(self):
if not self.dev_id or "*" in self.dev_id:
self.dev_id = self.get_board_snr(self.dev_id or "*")
self.dev_id = self.dev_id.lstrip("0")
# dev_id can be None, str or list of str
dev_id = self.dev_id
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
def do_get_boards(self):
@ -528,5 +539,5 @@ class NrfBinaryRunner(ZephyrBinaryRunner):
# All done, now flush any outstanding ops
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.')

View file

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

View file

@ -33,6 +33,15 @@ class NrfUtilBinaryRunner(NrfBinaryRunner):
def name(cls):
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
def tool_opt_help(cls) -> str:
return 'Additional options for nrfutil, e.g. "--log-level"'
@ -107,6 +116,12 @@ class NrfUtilBinaryRunner(NrfBinaryRunner):
self._op_id += 1
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):
_op = op['operation']
op_type = _op['type']
@ -151,7 +166,7 @@ class NrfUtilBinaryRunner(NrfBinaryRunner):
precmd = ['--x-ext-mem-config-file', self.ext_mem_config_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):
self.logger.debug(f'Executing op: {op}')