west: runners: Add support for a common --reset argument

Some of the runners in the tree have been adding their own,
class-specific versions of a switch to instruct the runner to reset or
not the device after flashing.

In order to better support multi-image builds that require more than one
flash operation, introduce a new --reset,--no-reset command-line
parameter that is part of the RunnerCaps so taht this functionality can
be accessed in a standardized manner.

Implementations for the new parameter are provided for the runner
classes that were already configurable in this regard.

Signed-off-by: Carles Cufi <carles.cufi@nordicsemi.no>
Signed-off-by: Jamie McCrae <jamie.mccrae@nordicsemi.no>
diff --git a/scripts/west_commands/runners/core.py b/scripts/west_commands/runners/core.py
index b569625..68ad0ee 100644
--- a/scripts/west_commands/runners/core.py
+++ b/scripts/west_commands/runners/core.py
@@ -229,6 +229,9 @@
       erased by the underlying tool before flashing; UICR on nRF SoCs
       is one example.)
 
+    - reset: whether the runner supports a --reset option, which
+      resets the device after a flash operation is complete.
+
     - tool_opt: whether the runner supports a --tool-opt (-O) option, which
       can be given multiple times and is passed on to the underlying tool
       that the runner wraps.
@@ -240,12 +243,14 @@
                  dev_id: bool = False,
                  flash_addr: bool = False,
                  erase: bool = False,
+                 reset: bool = False,
                  tool_opt: bool = False,
                  file: bool = False):
         self.commands = commands
         self.dev_id = dev_id
         self.flash_addr = bool(flash_addr)
         self.erase = bool(erase)
+        self.reset = bool(reset)
         self.tool_opt = bool(tool_opt)
         self.file = bool(file)
 
@@ -254,6 +259,7 @@
                 f'dev_id={self.dev_id}, '
                 f'flash_addr={self.flash_addr}, '
                 f'erase={self.erase}, '
+                f'reset={self.reset}, '
                 f'tool_opt={self.tool_opt}, '
                 f'file={self.file}'
                 ')')
@@ -521,9 +527,16 @@
 
         parser.add_argument('--erase', '--no-erase', nargs=0,
                             action=_ToggleAction,
-                            help=("mass erase flash before loading, or don't"
+                            help=("mass erase flash before loading, or don't. "
+                                  "Default action depends on each specific runner."
                                   if caps.erase else argparse.SUPPRESS))
 
+        parser.add_argument('--reset', '--no-reset', nargs=0,
+                            action=_ToggleAction,
+                            help=("reset device after flashing, or don't. "
+                                  "Default action depends on each specific runner."
+                                  if caps.reset else argparse.SUPPRESS))
+
         parser.add_argument('-O', '--tool-opt', dest='tool_opt',
                             default=[], action='append',
                             help=(cls.tool_opt_help() if caps.tool_opt
@@ -552,6 +565,8 @@
             _missing_cap(cls, '--dt-flash')
         if args.erase and not caps.erase:
             _missing_cap(cls, '--erase')
+        if args.reset and not caps.reset:
+            _missing_cap(cls, '--reset')
         if args.tool_opt and not caps.tool_opt:
             _missing_cap(cls, '--tool-opt')
         if args.file and not caps.file:
@@ -564,6 +579,8 @@
         ret = cls.do_create(cfg, args)
         if args.erase:
             ret.logger.info('mass erase requested')
+        if args.reset:
+            ret.logger.info('reset after flashing requested')
         return ret
 
     @classmethod
diff --git a/scripts/west_commands/runners/esp32.py b/scripts/west_commands/runners/esp32.py
index d435014..0ccf0c9 100644
--- a/scripts/west_commands/runners/esp32.py
+++ b/scripts/west_commands/runners/esp32.py
@@ -16,13 +16,15 @@
     '''Runner front-end for espidf.'''
 
     def __init__(self, cfg, device, boot_address, part_table_address,
-                 app_address, erase=False, baud=921600, flash_size='detect',
-                 flash_freq='40m', flash_mode='dio', espidf='espidf',
-                 bootloader_bin=None, partition_table_bin=None, no_stub=False):
+                 app_address, erase=False, reset=False, baud=921600,
+                 flash_size='detect', flash_freq='40m', flash_mode='dio',
+                 espidf='espidf', bootloader_bin=None, partition_table_bin=None,
+                 no_stub=False):
         super().__init__(cfg)
         self.elf = cfg.elf_file
         self.app_bin = cfg.bin_file
         self.erase = bool(erase)
+        self.reset = bool(reset)
         self.device = device
         self.boot_address = boot_address
         self.part_table_address = part_table_address
@@ -42,7 +44,7 @@
 
     @classmethod
     def capabilities(cls):
-        return RunnerCaps(commands={'flash'}, erase=True)
+        return RunnerCaps(commands={'flash'}, erase=True, reset=True)
 
     @classmethod
     def do_add_parser(cls, parser):
@@ -77,6 +79,8 @@
         parser.add_argument('--esp-no-stub', default=False, action='store_true',
                             help='Disable launching the flasher stub, only talk to ROM bootloader')
 
+        parser.set_defaults(reset=True)
+
     @classmethod
     def do_create(cls, cfg, args):
         if args.esp_tool:
@@ -88,7 +92,7 @@
         return Esp32BinaryRunner(
             cfg, args.esp_device, boot_address=args.esp_boot_address,
             part_table_address=args.esp_partition_table_address,
-            app_address=args.esp_app_address, erase=args.erase,
+            app_address=args.esp_app_address, erase=args.erase, reset=args.reset,
             baud=args.esp_baud_rate, flash_size=args.esp_flash_size,
             flash_freq=args.esp_flash_freq, flash_mode=args.esp_flash_mode,
             espidf=espidf, bootloader_bin=args.esp_flash_bootloader,
@@ -111,7 +115,8 @@
             cmd_flash.extend(['--port', self.device])
         cmd_flash.extend(['--baud', self.baud])
         cmd_flash.extend(['--before', 'default_reset'])
-        cmd_flash.extend(['--after', 'hard_reset', 'write_flash', '-u'])
+        if self.reset:
+            cmd_flash.extend(['--after', 'hard_reset', 'write_flash', '-u'])
         cmd_flash.extend(['--flash_mode', self.flash_mode])
         cmd_flash.extend(['--flash_freq', self.flash_freq])
         cmd_flash.extend(['--flash_size', self.flash_size])
diff --git a/scripts/west_commands/runners/ezflashcli.py b/scripts/west_commands/runners/ezflashcli.py
index 4d9d3d3..a3e1aea 100644
--- a/scripts/west_commands/runners/ezflashcli.py
+++ b/scripts/west_commands/runners/ezflashcli.py
@@ -10,13 +10,14 @@
 class EzFlashCliBinaryRunner(ZephyrBinaryRunner):
     '''Runner front-end for ezFlashCLI'''
 
-    def __init__(self, cfg, tool, sn, erase=False):
+    def __init__(self, cfg, tool, sn, erase=False, reset=True):
         super().__init__(cfg)
         self.bin_ = cfg.bin_file
 
         self.tool = tool
         self.sn_arg = ['-j', f'{sn}'] if sn is not None else []
         self.erase = bool(erase)
+        self.reset = bool(reset)
 
     @classmethod
     def name(cls):
@@ -24,7 +25,7 @@
 
     @classmethod
     def capabilities(cls):
-        return RunnerCaps(commands={'flash'}, erase=True)
+        return RunnerCaps(commands={'flash'}, erase=True, reset=True)
 
     @classmethod
     def do_add_parser(cls, parser):
@@ -34,6 +35,8 @@
         parser.add_argument('--sn', default=None, required=False,
                             help='J-Link probe serial number')
 
+        parser.set_defaults(reset=True)
+
     @classmethod
     def do_create(cls, cfg, args):
         return EzFlashCliBinaryRunner(cfg, tool=args.tool, sn=args.sn,
@@ -64,7 +67,7 @@
             load_offset = self.build_conf['CONFIG_FLASH_LOAD_OFFSET']
             self.check_call([self.tool] + self.sn_arg + ["write_flash", f'0x{load_offset:x}', self.bin_])
 
-    def reset(self):
+    def reset_device(self):
         self.logger.info("Resetting...")
         self.check_call([self.tool] + self.sn_arg + ["go"])
 
@@ -72,4 +75,5 @@
         self.require(self.tool)
         self.ensure_output('bin')
         self.program_bin()
-        self.reset()
+        if self.reset:
+            self.reset_device()
diff --git a/scripts/west_commands/runners/jlink.py b/scripts/west_commands/runners/jlink.py
index 80b7de2..4fa71720 100644
--- a/scripts/west_commands/runners/jlink.py
+++ b/scripts/west_commands/runners/jlink.py
@@ -35,7 +35,7 @@
 
     def __init__(self, cfg, device, dev_id=None,
                  commander=DEFAULT_JLINK_EXE,
-                 dt_flash=True, erase=True, reset_after_load=False,
+                 dt_flash=True, erase=True, reset=False,
                  iface='swd', speed='auto',
                  loader=None,
                  gdbserver='JLinkGDBServer',
@@ -54,7 +54,7 @@
         self.commander = commander
         self.dt_flash = dt_flash
         self.erase = erase
-        self.reset_after_load = reset_after_load
+        self.reset = reset
         self.gdbserver = gdbserver
         self.iface = iface
         self.speed = speed
@@ -74,7 +74,7 @@
     @classmethod
     def capabilities(cls):
         return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach'},
-                          dev_id=True, flash_addr=True, erase=True,
+                          dev_id=True, flash_addr=True, erase=True, reset=True,
                           tool_opt=True, file=True)
 
     @classmethod
@@ -114,11 +114,11 @@
                             help=f'''J-Link Commander, default is
                             {DEFAULT_JLINK_EXE}''')
         parser.add_argument('--reset-after-load', '--no-reset-after-load',
-                            dest='reset_after_load', nargs=0,
+                            dest='reset', nargs=0,
                             action=ToggleAction,
-                            help='reset after loading? (default: no)')
+                            help='obsolete synonym for --reset/--no-reset')
 
-        parser.set_defaults(reset_after_load=False)
+        parser.set_defaults(reset=False)
 
     @classmethod
     def do_create(cls, cfg, args):
@@ -127,7 +127,7 @@
                                  commander=args.commander,
                                  dt_flash=args.dt_flash,
                                  erase=args.erase,
-                                 reset_after_load=args.reset_after_load,
+                                 reset=args.reset,
                                  iface=args.iface, speed=args.speed,
                                  gdbserver=args.gdbserver,
                                  loader=args.loader,
@@ -266,7 +266,7 @@
                 client_cmd += ['-ex', 'monitor halt',
                                '-ex', 'monitor reset',
                                '-ex', 'load']
-                if self.reset_after_load:
+                if self.reset:
                     client_cmd += ['-ex', 'monitor reset']
             if not self.gdb_host:
                 self.require(self.gdbserver)
@@ -326,7 +326,7 @@
         # Flash the selected build artifact
         lines.append(flash_cmd)
 
-        if self.reset_after_load:
+        if self.reset:
             lines.append('r') # Reset and halt the target
 
         lines.append('g') # Start the CPU
diff --git a/scripts/west_commands/runners/nrf_common.py b/scripts/west_commands/runners/nrf_common.py
index 1a66ed0..6854066 100644
--- a/scripts/west_commands/runners/nrf_common.py
+++ b/scripts/west_commands/runners/nrf_common.py
@@ -28,7 +28,7 @@
     '''Runner front-end base class for nrf tools.'''
 
     def __init__(self, cfg, family, softreset, dev_id, erase=False,
-                 tool_opt=[], force=False, recover=False):
+                 reset=True, tool_opt=[], force=False, recover=False):
         super().__init__(cfg)
         self.hex_ = cfg.hex_file
         if family and not family.endswith('_FAMILY'):
@@ -37,6 +37,7 @@
         self.softreset = softreset
         self.dev_id = dev_id
         self.erase = bool(erase)
+        self.reset = bool(reset)
         self.force = force
         self.recover = bool(recover)
 
@@ -47,7 +48,7 @@
     @classmethod
     def capabilities(cls):
         return RunnerCaps(commands={'flash'}, dev_id=True, erase=True,
-                          tool_opt=True)
+                          reset=True, tool_opt=True)
 
     @classmethod
     def dev_id_help(cls) -> str:
@@ -75,6 +76,8 @@
                             memory and disable read back protection before
                             flashing (erases flash for both cores on nRF53)''')
 
+        parser.set_defaults(reset=True)
+
     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 "*")
@@ -398,7 +401,8 @@
         if self.recover:
             self.recover_target()
         self.program_hex()
-        self.reset_target()
+        if self.reset:
+            self.reset_target()
         # All done, now flush any outstanding ops
         self.flush(force=True)
 
diff --git a/scripts/west_commands/runners/nrfjprog.py b/scripts/west_commands/runners/nrfjprog.py
index 9671def..8762ce0 100644
--- a/scripts/west_commands/runners/nrfjprog.py
+++ b/scripts/west_commands/runners/nrfjprog.py
@@ -30,6 +30,7 @@
     def do_create(cls, cfg, args):
         return NrfJprogBinaryRunner(cfg, args.nrf_family, args.softreset,
                                     args.dev_id, erase=args.erase,
+                                    reset=args.reset,
                                     tool_opt=args.tool_opt, force=args.force,
                                     recover=args.recover)
 
diff --git a/scripts/west_commands/runners/nrfutil.py b/scripts/west_commands/runners/nrfutil.py
index 9c92c07..719e432 100644
--- a/scripts/west_commands/runners/nrfutil.py
+++ b/scripts/west_commands/runners/nrfutil.py
@@ -16,9 +16,9 @@
     '''Runner front-end for nrfutil.'''
 
     def __init__(self, cfg, family, softreset, dev_id, erase=False,
-                 tool_opt=[], force=False, recover=False):
+                 reset=True, tool_opt=[], force=False, recover=False):
 
-        super().__init__(cfg, family, softreset, dev_id, erase,
+        super().__init__(cfg, family, softreset, dev_id, erase, reset,
                          tool_opt, force, recover)
         self._ops = []
         self._op_id = 1
@@ -35,6 +35,7 @@
     def do_create(cls, cfg, args):
         return NrfUtilBinaryRunner(cfg, args.nrf_family, args.softreset,
                                    args.dev_id, erase=args.erase,
+                                   reset=args.reset,
                                    tool_opt=args.tool_opt, force=args.force,
                                    recover=args.recover)
 
diff --git a/scripts/west_commands/runners/stm32flash.py b/scripts/west_commands/runners/stm32flash.py
index 052c45d..5aa544d 100644
--- a/scripts/west_commands/runners/stm32flash.py
+++ b/scripts/west_commands/runners/stm32flash.py
@@ -37,7 +37,7 @@
 
     @classmethod
     def capabilities(cls):
-        return RunnerCaps(commands={'flash'})
+        return RunnerCaps(commands={'flash'}, reset=True)
 
     @classmethod
     def do_add_parser(cls, parser):
@@ -72,12 +72,11 @@
         parser.add_argument('--serial-mode', default='8e1', required=False,
                             help='serial port mode, default \'8e1\'')
 
-        parser.add_argument('--reset', default=False, required=False, action='store_true',
-                            help='reset device at exit, default False')
-
         parser.add_argument('--verify', default=False, required=False, action='store_true',
                             help='verify writes, default False')
 
+        parser.set_defaults(reset=False)
+
     @classmethod
     def do_create(cls, cfg, args):
         return Stm32flashBinaryRunner(cfg, device=args.device, action=args.action,