Repository: pymeasure/pymeasure

Issue:
Find USB device from hardware information
The COM port of virtual Com ports via USB may change from time to time.
I propose a simple helper method, which retrieves the COM port of a USB virtual port according to hardware information (vendor id, product id, serial number).
This method allows to specify the hardware information and then to connect always to the correct device, independent of the COM port number assigned at that time.

Rough idea:

~~~python
from serial.tools import list_ports

def findPort(vid: int, pid: int, sn: str) -> str:
    """Find the port name ('COM5') of the first device with the given USB information.

    :param int vid: Vendor ID.
    :param int pid: Product ID.
    :param str sn: Serial number.
    :return: Port as a string as `device` attribute returned from `list_ports.comports`.
    """
    for port in sorted(list_ports.comports()):
        if port.vid == vid and port.pid == pid and port.serial_number == str(sn):
            return port.device
    raise AttributeError("No device found for the given data.")
~~~


Discussion:
You'd use this to find the vid/pid/sn _once_, and then use that combination when instantiating instruments? I.e. `instr = TC038(findPort('a', 'b', 'c'))`? 
Mind that pyvisa-py will still not accept (I think) `COM5` as a resource name, so you'd have to find the full resource name, or manually create one from that. _Or_ you make this a bit "smarter" and use `list_resources` output to find the correct full one, and return _that_. 

Sounds like a good idea, could complement `resources.list_resources` nicely. Maybe name it `find_port`, feels like this would fit better into the naming scheme.


> You'd use this to find the vid/pid/sn once, and then use that combination when instantiating instruments? I.e. instr = TC038(findPort('a', 'b', 'c'))?

exactly.

> Mind that pyvisa-py will still not accept (I think) COM5 as a resource name

For a pyvisa-py device, I use `findPort().replace("COM", "ASRL")`.

> Mind that pyvisa-py will still not accept (I think) COM5 as a resource name, so you'd have to find the full resource name, or manually create one from that. Or you make this a bit "smarter" and use list_resources output to find the correct full one, and return that.

good idea

> Sounds like a good idea, could complement resources.list_resources nicely. Maybe name it find_port, feels like this would fit better into the naming scheme.

I agree. I copied the code (legacy naming scheme, inspired by Qt) from my current working setup to show the idea.
A pymeasure implementetion will be reworked.

Resolving patch:
diff --git a/pymeasure/instruments/__init__.py b/pymeasure/instruments/__init__.py
--- a/pymeasure/instruments/__init__.py
+++ b/pymeasure/instruments/__init__.py
@@ -25,7 +25,7 @@
 from ..errors import RangeError, RangeException
 from .channel import Channel
 from .instrument import Instrument
-from .resources import list_resources
+from .resources import find_serial_port, list_resources
 from .validators import discreteTruncate
 
 from . import activetechnologies
diff --git a/pymeasure/instruments/resources.py b/pymeasure/instruments/resources.py
--- a/pymeasure/instruments/resources.py
+++ b/pymeasure/instruments/resources.py
@@ -23,6 +23,7 @@
 #
 
 import pyvisa
+from serial.tools import list_ports
 
 
 def list_resources():
@@ -58,3 +59,28 @@ def list_resources():
             print(e)
     rm.close()
     return instrs
+
+
+def find_serial_port(vendor_id=None, product_id=None, serial_number=None):
+    """Find the VISA port name of the first serial device with the given USB information.
+
+    Use `None` as a value if you do not want to check for that parameter.
+
+    .. code-block:: python
+
+        resource_name = find_serial_port(vendor_id=1256, serial_number="SN12345")
+        dmm = Agilent34410(resource_name)
+
+    :param int vid: Vendor ID.
+    :param int pid: Product ID.
+    :param str sn: Serial number.
+    :return str: Port as a VISA string for a serial device (e.g. "ASRL5" or "ASRL/dev/ttyACM5").
+    """
+    for port in sorted(list_ports.comports()):
+        if ((vendor_id is None or port.vid == vendor_id)
+                and (product_id is None or port.pid == product_id)
+                and (serial_number is None or port.serial_number == str(serial_number))):
+            # remove "COM" from windows serial port names.
+            port_name = port.device.replace("COM", "")
+            return "ASRL" + port_name
+    raise AttributeError("No device found for the given data.")


Test patch: