Repository: pyocd/pyocd
Issue: [0.26.1] ProbeError: J-Link is already open

pyocd sometimes occured jlink related exceptions. Looks like it try to open connected jlink probes. Actually we do not want it. It might cause segger jlink error if it is in parallel.
Is possible to add an option to control such behaviour? just list specified probes by user.

```
  File "C:\\Python27\\lib\\site-packages\\pyocd\\core\\helpers.py", line 236, in session_with_chosen_probe
    unique_id=unique_id,
  File "C:\\Python27\\lib\\site-packages\\pyocd\\core\\helpers.py", line 137, in choose_probe
    allProbes = ConnectHelper.get_all_connected_probes(blocking=blocking, unique_id=unique_id)
  File "C:\\Python27\\lib\\site-packages\\pyocd\\core\\helpers.py", line 82, in get_all_connected_probes
    allProbes = DebugProbeAggregator.get_all_connected_probes(unique_id=unique_id)
  File "C:\\Python27\\lib\\site-packages\\pyocd\\probe\\aggregator.py", line 34, in get_all_connected_probes
    probes += cls.get_all_connected_probes()
  File "C:\\Python27\\lib\\site-packages\\pyocd\\probe\\jlink_probe.py", line 62, in get_all_connected_probes
    return [cls(str(info.SerialNumber)) for info in jlink.connected_emulators()]
  File "C:\\Python27\\lib\\site-packages\\pyocd\\probe\\jlink_probe.py", line 96, in __init__
    self.open()
  File "C:\\Python27\\lib\\site-packages\\pyocd\\probe\\jlink_probe.py", line 155, in open
    six.raise_from(self._convert_exception(exc), exc)
  File "C:\\Python27\\lib\\site-packages\\six.py", line 718, in raise_from
    raise value
ProbeError: J-Link is already open.
```

Changed files:
pyocd/commands/commander.py, pyocd/probe/debug_probe.py, pyocd/probe/aggregator.py, pyocd/probe/jlink_probe.py

diff --git a/pyocd/commands/commander.py b/pyocd/commands/commander.py
index dda3def7..16e1ede0 100755
--- a/pyocd/commands/commander.py
+++ b/pyocd/commands/commander.py
@@ -35,6 +35,13 @@ class PyOCDCommander(object):
     
     Responsible for connecting the execution context, REPL, and commands, and handles connection.
     
+    Exit codes:
+    - 0 = no errors
+    - 1 = command error
+    - 2 = transfer error
+    - 3 = failed to create session (probe might not exist)
+    - 4 = failed to open probe
+    
     @todo Replace use of args from argparse with something cleaner.
     """
     
@@ -169,7 +176,9 @@ class PyOCDCommander(object):
             self.exit_code = 3
             return False
         
-        self._post_connect()
+        if not self._post_connect():
+            self.exit_code = 4
+            return False
         
         result = self.context.attach_session(self.session)
         if not result:
diff --git a/pyocd/probe/aggregator.py b/pyocd/probe/aggregator.py
index ff370ef3..e00a786a 100644
--- a/pyocd/probe/aggregator.py
+++ b/pyocd/probe/aggregator.py
@@ -51,6 +51,15 @@ class DebugProbeAggregator(object):
         klasses, unique_id, is_explicit = DebugProbeAggregator._get_probe_classes(unique_id)
         
         probes = []
+        
+        # First look for a match against the full ID, as this can be more efficient for certain probes.
+        if unique_id is not None:
+            for cls in klasses:
+                probe = cls.get_probe_with_id(unique_id, is_explicit)
+                if probe is not None:
+                    return [probe]
+        
+        # No full match, so ask probe classes for probes.
         for cls in klasses:
             probes += cls.get_all_connected_probes(unique_id, is_explicit)
         
diff --git a/pyocd/probe/debug_probe.py b/pyocd/probe/debug_probe.py
index cc7e8af6..25ae5c14 100644
--- a/pyocd/probe/debug_probe.py
+++ b/pyocd/probe/debug_probe.py
@@ -63,10 +63,14 @@ class DebugProbe(object):
     def get_all_connected_probes(cls, unique_id=None, is_explicit=False):
         """! @brief Returns a list of DebugProbe instances.
         
+        To filter the list of returned probes, the `unique_id` parameter may be set to a string with a full or
+        partial unique ID (canonically the serial number). Alternatively, the probe class may simply return all
+        available probes and let the caller handle filtering.
+        
         @param cls The class instance.
-        @param unique_id Optional unique ID value on which probes are being filtered. May be
-            used by the probe to optimize retreiving the probe list.
-        @param is_explicit Whether the probe type was explicitly specified in the unique ID. This
+        @param unique_id String. Optional partial unique ID value used to filter available probes. May be used by the
+            probe to optimize retreiving the probe list; there is no requirement to filter the results.
+        @param is_explicit Boolean. Whether the probe type was explicitly specified in the unique ID. This
             can be used, for instance, to specially interpret the unique ID as an IP address or
             domain name when the probe class was specifically requested but not for general lists
             of available probes.
@@ -78,12 +82,11 @@ class DebugProbe(object):
     def get_probe_with_id(cls, unique_id, is_explicit=False):
         """! @brief Returns a DebugProbe instance for a probe with the given unique ID.
         
-        If no probe is connected with a matching unique ID, then None will be returned.
+        If no probe is connected with a fully matching unique ID, then None will be returned.
         
         @param cls The class instance.
-        @param unique_id Optional unique ID value on which probes are being filtered. May be
-            used by the probe to optimize retreiving the probe list.
-        @param is_explicit Whether the probe type was explicitly specified in the unique ID.
+        @param unique_id Unique ID string to match against probes' full unique ID. No partial matches are allowed.
+        @param is_explicit Boolean. Whether the probe type was explicitly specified in the unique ID.
         @return DebugProbe instance, or None
         """
         raise NotImplementedError()
diff --git a/pyocd/probe/jlink_probe.py b/pyocd/probe/jlink_probe.py
index dcd6da73..62e07886 100644
--- a/pyocd/probe/jlink_probe.py
+++ b/pyocd/probe/jlink_probe.py
@@ -54,6 +54,10 @@ class JLinkProbe(DebugProbe):
                     )
         except TypeError:
             return None
+
+    @classmethod
+    def _format_serial_number(cls, serial_number):
+        return "{:d}".format(serial_number)
     
     @classmethod
     def get_all_connected_probes(cls, unique_id=None, is_explicit=False):
@@ -61,7 +65,7 @@ class JLinkProbe(DebugProbe):
             jlink = cls._get_jlink()
             if jlink is None:
                 return []
-            return [cls(str(info.SerialNumber)) for info in jlink.connected_emulators()]
+            return [cls(cls._format_serial_number(info.SerialNumber)) for info in jlink.connected_emulators()]
         except JLinkException as exc:
             six.raise_from(cls._convert_exception(exc), exc)
     
@@ -72,35 +76,52 @@ class JLinkProbe(DebugProbe):
             if jlink is None:
                 return None
             for info in jlink.connected_emulators():
-                if str(info.SerialNumber) == unique_id:
-                    return cls(str(info.SerialNumber))
+                sn = cls._format_serial_number(info.SerialNumber)
+                if sn == unique_id:
+                    return cls(sn)
+            else:
+                return None
+        except JLinkException as exc:
+            six.raise_from(cls._convert_exception(exc), exc)
+
+    @classmethod
+    def _get_probe_info(cls, serial_number, jlink):
+        """! @brief Look up and return a JLinkConnectInfo for the probe with matching serial number.
+        @param cls The class object.
+        @param serial_number String serial number. Must be the full serial number.
+        @return JLinkConnectInfo object or None if there was no match.
+        """
+        try:
+            for info in jlink.connected_emulators():
+                if cls._format_serial_number(info.SerialNumber) == serial_number:
+                    return info
             else:
                 return None
         except JLinkException as exc:
             six.raise_from(cls._convert_exception(exc), exc)
 
     def __init__(self, serial_number):
+        """! @brief Constructor.
+        @param self The object.
+        @param serial_number String. The J-Link's serial number.
+        """
         super(JLinkProbe, self).__init__()
         self._link = self._get_jlink()
         if self._link is None:
             raise exceptions.ProbeError("unable to open JLink DLL")
 
+        info = self._get_probe_info(serial_number, self._link)
+        if info is None:
+            raise exceptions.ProbeError("could not find JLink probe with serial number '{}'".format(serial_number))
+
         self._serial_number = serial_number
+        self._serial_number_int = int(serial_number, base=10)
         self._supported_protocols = None
         self._protocol = None
         self._default_protocol = None
         self._is_open = False
         self._dp_select = -1
-        self._product_name = None
-        self._oem = None
-        
-        # Get some strings by temporarily opening.
-        try:
-            self.open()
-            self._product_name = self._link.product_name
-            self._oem = self._link.oem
-        finally:
-            self.close()
+        self._product_name = six.ensure_str(info.acProduct)
         
     @property
     def description(self):
@@ -108,7 +129,7 @@ class JLinkProbe(DebugProbe):
     
     @property
     def vendor_name(self):
-        return self._oem or "Segger"
+        return "Segger"
     
     @property
     def product_name(self):
@@ -137,7 +158,7 @@ class JLinkProbe(DebugProbe):
     
     def open(self):
         try:
-            self._link.open(self._serial_number)
+            self._link.open(self._serial_number_int)
             self._is_open = True
         
             # Get available wire protocols.
@@ -147,7 +168,9 @@ class JLinkProbe(DebugProbe):
                 self._supported_protocols.append(DebugProbe.Protocol.JTAG)
             if ifaces & (1 << pylink.enums.JLinkInterfaces.SWD):
                 self._supported_protocols.append(DebugProbe.Protocol.SWD)
-            assert len(self._supported_protocols) > 1
+            if not len(self._supported_protocols) >= 2: # default + 1
+                raise exceptions.ProbeError("J-Link probe {} does not support any known wire protocols".format(
+                        self.unique_id))
             
             # Select default protocol, preferring SWD over JTAG.
             if DebugProbe.Protocol.SWD in self._supported_protocols: