2023-03-07 11:09:13 +01:00
|
|
|
# Copyright (c) 2023 Nordic Semiconductor ASA.
|
|
|
|
#
|
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
'''Runner for flashing with nrfutil.'''
|
|
|
|
|
|
|
|
import json
|
|
|
|
import subprocess
|
2024-11-20 15:54:37 +01:00
|
|
|
import sys
|
|
|
|
from pathlib import Path
|
2023-03-07 11:09:13 +01:00
|
|
|
|
2024-03-18 11:42:50 +01:00
|
|
|
from runners.core import _DRY_RUN
|
2023-03-07 11:09:13 +01:00
|
|
|
from runners.nrf_common import NrfBinaryRunner
|
|
|
|
|
2024-11-20 15:54:37 +01:00
|
|
|
|
2023-03-07 11:09:13 +01:00
|
|
|
class NrfUtilBinaryRunner(NrfBinaryRunner):
|
|
|
|
'''Runner front-end for nrfutil.'''
|
|
|
|
|
2025-01-30 18:55:00 +01:00
|
|
|
def __init__(self, cfg, family, softreset, pinreset, dev_id, erase=False,
|
2025-04-12 17:48:51 +02:00
|
|
|
erase_mode=None, ext_erase_mode=None, reset=True, tool_opt=None,
|
|
|
|
force=False, recover=False, suit_starter=False,
|
|
|
|
ext_mem_config_file=None):
|
2023-03-07 11:09:13 +01:00
|
|
|
|
2025-03-27 16:10:04 +01:00
|
|
|
super().__init__(cfg, family, softreset, pinreset, dev_id, erase,
|
2025-04-12 17:48:51 +02:00
|
|
|
erase_mode, ext_erase_mode, reset, tool_opt, force,
|
|
|
|
recover)
|
2024-10-23 12:40:10 +02:00
|
|
|
|
|
|
|
self.suit_starter = suit_starter
|
2025-02-03 14:28:00 +01:00
|
|
|
self.ext_mem_config_file = ext_mem_config_file
|
2024-10-23 12:40:10 +02:00
|
|
|
|
2023-03-07 11:09:13 +01:00
|
|
|
self._ops = []
|
|
|
|
self._op_id = 1
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def name(cls):
|
|
|
|
return 'nrfutil'
|
|
|
|
|
2025-02-28 18:34:44 +01:00
|
|
|
@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'''
|
|
|
|
|
2023-03-07 11:09:13 +01:00
|
|
|
@classmethod
|
|
|
|
def tool_opt_help(cls) -> str:
|
|
|
|
return 'Additional options for nrfutil, e.g. "--log-level"'
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def do_create(cls, cfg, args):
|
|
|
|
return NrfUtilBinaryRunner(cfg, args.nrf_family, args.softreset,
|
2025-01-30 18:55:00 +01:00
|
|
|
args.pinreset, args.dev_id, erase=args.erase,
|
2025-04-12 17:48:51 +02:00
|
|
|
erase_mode=args.erase_mode,
|
|
|
|
ext_erase_mode=args.ext_erase_mode,
|
|
|
|
reset=args.reset, tool_opt=args.tool_opt,
|
|
|
|
force=args.force, recover=args.recover,
|
2025-02-03 14:28:00 +01:00
|
|
|
suit_starter=args.suit_manifest_starter,
|
|
|
|
ext_mem_config_file=args.ext_mem_config_file)
|
2024-10-23 12:40:10 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def do_add_parser(cls, parser):
|
|
|
|
super().do_add_parser(parser)
|
|
|
|
parser.add_argument('--suit-manifest-starter', required=False,
|
|
|
|
action='store_true',
|
|
|
|
help='Use the SUIT manifest starter file')
|
2025-02-03 14:28:00 +01:00
|
|
|
parser.add_argument('--ext-mem-config-file', required=False,
|
|
|
|
dest='ext_mem_config_file',
|
|
|
|
help='path to an JSON file with external memory configuration')
|
|
|
|
|
2023-03-07 11:09:13 +01:00
|
|
|
def _exec(self, args):
|
2024-03-18 11:42:50 +01:00
|
|
|
jout_all = []
|
|
|
|
|
|
|
|
cmd = ['nrfutil', '--json', 'device'] + args
|
|
|
|
self._log_cmd(cmd)
|
|
|
|
|
|
|
|
if _DRY_RUN:
|
|
|
|
return {}
|
|
|
|
|
|
|
|
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p:
|
|
|
|
for line in iter(p.stdout.readline, b''):
|
|
|
|
# https://github.com/ndjson/ndjson-spec
|
|
|
|
jout = json.loads(line.decode(sys.getdefaultencoding()))
|
|
|
|
jout_all.append(jout)
|
|
|
|
|
|
|
|
if 'x-execute-batch' in args:
|
|
|
|
if jout['type'] == 'batch_update':
|
|
|
|
pld = jout['data']['data']
|
|
|
|
if (
|
|
|
|
pld['type'] == 'task_progress' and
|
|
|
|
pld['data']['progress']['progressPercentage'] == 0
|
|
|
|
):
|
|
|
|
self.logger.info(pld['data']['progress']['description'])
|
|
|
|
elif jout['type'] == 'batch_end' and jout['data']['error']:
|
|
|
|
raise subprocess.CalledProcessError(
|
|
|
|
jout['data']['error']['code'], cmd
|
|
|
|
)
|
2025-05-09 16:20:07 +02:00
|
|
|
if p.returncode != 0:
|
|
|
|
raise subprocess.CalledProcessError(p.returncode, cmd)
|
2024-03-18 11:42:50 +01:00
|
|
|
|
|
|
|
return jout_all
|
2023-03-07 11:09:13 +01:00
|
|
|
|
|
|
|
def do_get_boards(self):
|
|
|
|
out = self._exec(['list'])
|
|
|
|
devs = []
|
|
|
|
for o in out:
|
|
|
|
if o['type'] == 'task_end':
|
|
|
|
devs = o['data']['data']['devices']
|
2024-05-29 11:15:39 +02:00
|
|
|
snrs = [dev['serialNumber'] for dev in devs if dev['traits']['jlink']]
|
2023-03-07 11:09:13 +01:00
|
|
|
|
|
|
|
self.logger.debug(f'Found boards: {snrs}')
|
|
|
|
return snrs
|
|
|
|
|
|
|
|
def do_require(self):
|
|
|
|
self.require('nrfutil')
|
|
|
|
|
|
|
|
def _insert_op(self, op):
|
|
|
|
op['operationId'] = f'{self._op_id}'
|
|
|
|
self._op_id += 1
|
|
|
|
self._ops.append(op)
|
|
|
|
|
2025-02-28 18:34:44 +01:00
|
|
|
def _format_dev_ids(self):
|
|
|
|
if isinstance(self.dev_id, list):
|
|
|
|
return ','.join(self.dev_id)
|
|
|
|
else:
|
|
|
|
return self.dev_id
|
|
|
|
|
2025-01-12 20:18:56 +01:00
|
|
|
def _append_batch(self, op, json_file):
|
|
|
|
_op = op['operation']
|
|
|
|
op_type = _op['type']
|
|
|
|
|
|
|
|
cmd = [f'{op_type}']
|
|
|
|
|
|
|
|
if op_type == 'program':
|
|
|
|
cmd += ['--firmware', _op['firmware']['file']]
|
|
|
|
opts = _op['options']
|
|
|
|
# populate the options
|
|
|
|
cmd.append('--options')
|
|
|
|
cli_opts = f"chip_erase_mode={opts['chip_erase_mode']}"
|
|
|
|
if opts.get('ext_mem_erase_mode'):
|
|
|
|
cli_opts += f",ext_mem_erase_mode={opts['ext_mem_erase_mode']}"
|
|
|
|
if opts.get('verify'):
|
|
|
|
cli_opts += f",verify={opts['verify']}"
|
|
|
|
cmd.append(cli_opts)
|
|
|
|
elif op_type == 'reset':
|
|
|
|
cmd += ['--reset-kind', _op['kind']]
|
2025-01-30 10:04:39 +01:00
|
|
|
elif op_type == 'erase':
|
2025-01-30 13:23:38 +01:00
|
|
|
cmd.append(f'--{_op["kind"]}')
|
2025-05-29 15:49:29 +02:00
|
|
|
elif op_type == 'x-provision-keys':
|
|
|
|
cmd += ['--key-file', _op['keyfile']]
|
2025-01-12 20:18:56 +01:00
|
|
|
|
|
|
|
cmd += ['--core', op['core']] if op.get('core') else []
|
|
|
|
cmd += ['--x-family', f'{self.family}']
|
|
|
|
cmd += ['--x-append-batch', f'{json_file}']
|
|
|
|
self._exec(cmd)
|
2023-03-07 11:09:13 +01:00
|
|
|
|
2025-01-12 20:18:56 +01:00
|
|
|
def _exec_batch(self):
|
|
|
|
# Use x-append-batch to get the JSON from nrfutil itself
|
|
|
|
json_file = Path(self.hex_).parent / 'generated_nrfutil_batch.json'
|
|
|
|
json_file.unlink(missing_ok=True)
|
|
|
|
for op in self._ops:
|
|
|
|
self._append_batch(op, json_file)
|
2023-03-07 11:09:13 +01:00
|
|
|
|
|
|
|
# reset first in case an exception is thrown
|
|
|
|
self._ops = []
|
|
|
|
self._op_id = 1
|
|
|
|
self.logger.debug(f'Executing batch in: {json_file}')
|
2025-02-03 14:28:00 +01:00
|
|
|
precmd = []
|
|
|
|
if self.ext_mem_config_file:
|
|
|
|
# This needs to be prepended, as it's a global option
|
|
|
|
precmd = ['--x-ext-mem-config-file', self.ext_mem_config_file]
|
|
|
|
|
|
|
|
self._exec(precmd + ['x-execute-batch', '--batch-path', f'{json_file}',
|
2025-02-28 18:34:44 +01:00
|
|
|
'--serial-number', self._format_dev_ids()])
|
2023-03-07 11:09:13 +01:00
|
|
|
|
|
|
|
def do_exec_op(self, op, force=False):
|
|
|
|
self.logger.debug(f'Executing op: {op}')
|
|
|
|
if force:
|
|
|
|
if len(self._ops) != 0:
|
|
|
|
raise RuntimeError(f'Forced exec with {len(self._ops)} ops')
|
|
|
|
self._insert_op(op)
|
|
|
|
self._exec_batch()
|
|
|
|
return True
|
|
|
|
# Defer by default
|
|
|
|
return False
|
|
|
|
|
|
|
|
def flush_ops(self, force=True):
|
|
|
|
if not force:
|
|
|
|
return
|
|
|
|
while self.ops:
|
|
|
|
self._insert_op(self.ops.popleft())
|
|
|
|
self._exec_batch()
|