twister: add flash-before option

Add option to flash board before attach serial.

Current implementation performs the following sequence:

1. Open serial port to listen to board log output
2. Flash device

In case of ESP32 where it uses the same serial port
for both operations, flashing needs to come first.

This PR adds a twister option named --flash-before
which enables the process above, allowing tests to be
performed properly.

Signed-off-by: Lucas Tamborrino <lucas.tamborrino@espressif.com>
diff --git a/scripts/pylib/twister/twisterlib/environment.py b/scripts/pylib/twister/twisterlib/environment.py
index daa691c..86770eb 100644
--- a/scripts/pylib/twister/twisterlib/environment.py
+++ b/scripts/pylib/twister/twisterlib/environment.py
@@ -178,6 +178,12 @@
                         when flash operation also executes test case on the platform.
                         """)
 
+    parser.add_argument("--flash-before", action="store_true", default=False,
+                        help="""Flash device before attaching to serial port.
+                        This is useful for devices that share the same port for programming
+                        and serial console, where flash must come first.
+                        """)
+
     test_or_build.add_argument(
         "-b", "--build-only", action="store_true", default="--prep-artifacts-for-testing" in sys.argv,
         help="Only build the code, do not attempt to run the code on targets.")
@@ -796,6 +802,14 @@
         logger.error("--device-flash-with-test requires --device_testing")
         sys.exit(1)
 
+    if options.flash_before and options.device_flash_with_test:
+        logger.error("--device-flash-with-test does not apply when --flash-before is used")
+        sys.exit(1)
+
+    if options.flash_before and options.device_serial_pty:
+        logger.error("--device-serial-pty cannot be used when --flash-before is set (for now)")
+        sys.exit(1)
+
     if options.shuffle_tests and options.subset is None:
         logger.error("--shuffle-tests requires --subset")
         sys.exit(1)
diff --git a/scripts/pylib/twister/twisterlib/handlers.py b/scripts/pylib/twister/twisterlib/handlers.py
index 8a732f2..ed0ee90 100755
--- a/scripts/pylib/twister/twisterlib/handlers.py
+++ b/scripts/pylib/twister/twisterlib/handlers.py
@@ -378,6 +378,10 @@
             # from the test.
             harness.capture_coverage = True
 
+        # Wait for serial connection
+        while not ser.isOpen():
+            time.sleep(0.1)
+
         # Clear serial leftover.
         ser.reset_input_buffer()
 
@@ -647,9 +651,13 @@
         if hardware.flash_with_test:
             flash_timeout += self.get_test_timeout()
 
+        serial_port = None
+        if hardware.flash_before is False:
+            serial_port = serial_device
+
         try:
             ser = self._create_serial_connection(
-                serial_device,
+                serial_port,
                 hardware.baud,
                 flash_timeout,
                 serial_pty,
@@ -703,6 +711,15 @@
         if post_flash_script:
             self.run_custom_script(post_flash_script, 30)
 
+        # Connect to device after flashing it
+        if hardware.flash_before:
+            try:
+                logger.debug(f"Attach serial device {serial_device} @ {hardware.baud} baud")
+                ser.port = serial_device
+                ser.open()
+            except serial.SerialException:
+                return
+
         if not flash_error:
             # Always wait at most the test timeout here after flashing.
             t.join(self.get_test_timeout())
diff --git a/scripts/pylib/twister/twisterlib/hardwaremap.py b/scripts/pylib/twister/twisterlib/hardwaremap.py
index 27bca00..2ef780c 100644
--- a/scripts/pylib/twister/twisterlib/hardwaremap.py
+++ b/scripts/pylib/twister/twisterlib/hardwaremap.py
@@ -49,7 +49,8 @@
                  post_flash_script=None,
                  runner=None,
                  flash_timeout=60,
-                 flash_with_test=False):
+                 flash_with_test=False,
+                 flash_before=False):
 
         self.serial = serial
         self.baud = serial_baud or 115200
@@ -63,6 +64,7 @@
         self.product = product
         self.runner = runner
         self.runner_params = runner_params
+        self.flash_before = flash_before
         self.fixtures = []
         self.post_flash_script = post_flash_script
         self.post_script = post_script
@@ -177,7 +179,8 @@
                                 False,
                                 baud=self.options.device_serial_baud,
                                 flash_timeout=self.options.device_flash_timeout,
-                                flash_with_test=self.options.device_flash_with_test
+                                flash_with_test=self.options.device_flash_with_test,
+                                flash_before=self.options.flash_before,
                                 )
 
             elif self.options.device_serial_pty:
@@ -186,7 +189,8 @@
                                 self.options.pre_script,
                                 True,
                                 flash_timeout=self.options.device_flash_timeout,
-                                flash_with_test=self.options.device_flash_with_test
+                                flash_with_test=self.options.device_flash_with_test,
+                                flash_before=False,
                                 )
 
             # the fixtures given by twister command explicitly should be assigned to each DUT
@@ -207,9 +211,9 @@
         print(tabulate(table, headers=header, tablefmt="github"))
 
 
-    def add_device(self, serial, platform, pre_script, is_pty, baud=None, flash_timeout=60, flash_with_test=False):
+    def add_device(self, serial, platform, pre_script, is_pty, baud=None, flash_timeout=60, flash_with_test=False, flash_before=False):
         device = DUT(platform=platform, connected=True, pre_script=pre_script, serial_baud=baud,
-                     flash_timeout=flash_timeout, flash_with_test=flash_with_test
+                     flash_timeout=flash_timeout, flash_with_test=flash_with_test, flash_before=flash_before
                     )
         if is_pty:
             device.serial_pty = serial
@@ -229,6 +233,9 @@
             flash_with_test = dut.get('flash_with_test')
             if flash_with_test is None:
                 flash_with_test = self.options.device_flash_with_test
+            flash_before = dut.get('flash_before')
+            if flash_before is None:
+                flash_before = self.options.flash_before and (not (flash_with_test or serial_pty))
             platform  = dut.get('platform')
             id = dut.get('id')
             runner = dut.get('runner')
@@ -251,6 +258,7 @@
                           serial_baud=baud,
                           connected=connected,
                           pre_script=pre_script,
+                          flash_before=flash_before,
                           post_script=post_script,
                           post_flash_script=post_flash_script,
                           flash_timeout=flash_timeout,
diff --git a/scripts/schemas/twister/hwmap-schema.yaml b/scripts/schemas/twister/hwmap-schema.yaml
index 3ecb064..1865462 100644
--- a/scripts/schemas/twister/hwmap-schema.yaml
+++ b/scripts/schemas/twister/hwmap-schema.yaml
@@ -61,3 +61,6 @@
       "flash_with_test":
         type: bool
         required: false
+      "flash_before":
+        type: bool
+        required: false