Last active
May 24, 2018 12:49
-
-
Save mozurin/585fd488853c87540c08792a7a028c51 to your computer and use it in GitHub Desktop.
Find USB CDC based serial port by its USB serial number
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
serial_by_serial - Find USB CDC based serial port by its USB serial number | |
>>> import serial_by_serial | |
>>> serial_by_serial.find_port_by_serial('5DA0FF...') | |
'COM3' | |
Primary objective of this module is to distinguish multiple Arduino boards | |
connected simultaneously without opening serial connections to them. Opening | |
serial connections is slow and unreliable (protocol is not guaranteed by | |
Arduino spec). | |
This module requires `libserialport` library >= 0.1.1 (can be bulit on | |
Win/Mac/Linux). | |
''' | |
import ctypes | |
import ctypes.util | |
import distutils.version | |
import enum | |
import itertools | |
# Find libserialport.so / libserialport.dll | |
REQUIRED_PACKAGE_VER = distutils.version.StrictVersion('0.1.1') | |
old_package_found = None | |
for name_cand in ('serialport', 'libserialport', 'libserialport-0'): | |
full_name = ctypes.util.find_library(name_cand) | |
if full_name: | |
libserialport = ctypes.CDLL(full_name) | |
# Check library package version | |
getver = libserialport.sp_get_package_version_string | |
getver.restype = ctypes.c_char_p | |
current_package_ver = distutils.version.StrictVersion( | |
getver().decode('ascii') | |
) | |
if current_package_ver >= REQUIRED_PACKAGE_VER: | |
break | |
else: | |
old_package_found = ( | |
max(old_package_found, current_package_ver) | |
if old_package_found is not None else current_package_ver | |
) | |
else: | |
raise ImportError( | |
'Could not find required shared library "libserialport".' | |
if old_package_found is None | |
else ( | |
'Old libserialport version "%s" found, but this module requires ' | |
'"%s" or higher.' | |
) % (old_package_found, REQUIRED_PACKAGE_VER) | |
) | |
# Utility classes | |
class CEnum(enum.IntEnum): | |
''' | |
enum.IntEnum that supports ctypes convention. | |
See: https://stackoverflow.com/questions/27199479/ | |
''' | |
@classmethod | |
def from_param(cls, self): | |
if not isinstance(self, cls): | |
raise TypeError() | |
return self | |
# liberialport definitions | |
class sp_return(CEnum): | |
''' | |
liberialport enum that represents result code of each functions. | |
''' | |
# Operation completed successfully. | |
SP_OK = 0 | |
# Invalid arguments were passed to the function. | |
SP_ERR_ARG = -1 | |
# A system error occurred while executing the operation. | |
SP_ERR_FAIL = -2 | |
# A memory allocation failed while executing the operation. | |
SP_ERR_MEM = -3 | |
# The requested operation is not supported by this system or device. | |
SP_ERR_SUPP = -4 | |
# Port type struct, but internal detail will not be used at all | |
sp_port_p = ctypes.c_void_p | |
# Function: sp_list_ports(port_list_ptr) | |
sp_list_ports = libserialport.sp_list_ports | |
sp_list_ports.argtypes = (ctypes.POINTER(ctypes.POINTER(sp_port_p)), ) | |
sp_list_ports.restype = sp_return | |
# Function: sp_free_port_list(port_list) | |
sp_free_port_list = libserialport.sp_free_port_list | |
sp_free_port_list.argtypes = (ctypes.POINTER(sp_port_p), ) | |
sp_free_port_list.restype = None | |
# Function: sp_port_by_name(portname, port_ptr) | |
sp_get_port_by_name = libserialport.sp_get_port_by_name | |
sp_get_port_by_name.argtypes = (ctypes.c_char_p, ctypes.POINTER(sp_port_p)) | |
sp_get_port_by_name.restype = sp_return | |
# Function: sp_get_port_name(port) | |
sp_get_port_name = libserialport.sp_get_port_name | |
sp_get_port_name.argtypes = (sp_port_p, ) | |
sp_get_port_name.restype = ctypes.c_char_p | |
# Function: sp_get_port_usb_vid_pid(port, vid_ptr, pid_ptr) | |
sp_get_port_usb_vid_pid = libserialport.sp_get_port_usb_vid_pid | |
sp_get_port_usb_vid_pid.argtypes = ( | |
sp_port_p, | |
ctypes.POINTER(ctypes.c_int), | |
ctypes.POINTER(ctypes.c_int), | |
) | |
sp_get_port_usb_vid_pid.restype = sp_return | |
# Function: sp_get_port_usb_serial(port) | |
sp_get_port_usb_serial = libserialport.sp_get_port_usb_serial | |
sp_get_port_usb_serial.argtypes = (sp_port_p, ) | |
sp_get_port_usb_serial.restype = ctypes.c_char_p | |
# Function: sp_get_port_usb_manufacturer(port) | |
sp_get_port_usb_manufacturer = libserialport.sp_get_port_usb_manufacturer | |
sp_get_port_usb_manufacturer.argtypes = (sp_port_p, ) | |
sp_get_port_usb_manufacturer.restype = ctypes.c_char_p | |
# Function: sp_get_port_usb_product(port) | |
sp_get_port_usb_product = libserialport.sp_get_port_usb_product | |
sp_get_port_usb_product.argtypes = (sp_port_p, ) | |
sp_get_port_usb_product.restype = ctypes.c_char_p | |
# Function: sp_free_port(port) | |
sp_free_port = libserialport.sp_free_port | |
sp_free_port.argtypes = (sp_port_p, ) | |
sp_free_port.restype = None | |
# Utility functions | |
def _assert_sp_return(result): | |
''' | |
Check whether returned `sp_return` is `SP_OK` or not. It raises | |
simple `RuntimeError`If not ok. | |
:param result: `sp_return` instance. | |
''' | |
if result != sp_return.SP_OK: | |
raise RuntimeError('Error in libserialport: %s' % result.name) | |
def find_usb_port_by_serial(serial, vid=None, pid=None): | |
''' | |
Find USB CDC based serial port, by its serial number. | |
:param serial: Serial number string. | |
:param vid: VID in int can be specified to filter result. | |
:param pid: PID in int can be specified to filter result. | |
:retval: COM port name in string, or None if not found. | |
''' | |
found = None | |
serial = serial.decode('ascii') if isinstance(serial, bytes) else serial | |
port_list = ctypes.POINTER(sp_port_p)() | |
r = sp_list_ports(ctypes.byref(port_list)) | |
_assert_sp_return(r) | |
try: | |
for n in itertools.count(): | |
# Stop at NULL | |
port_in_list = port_list[n] | |
if port_in_list is None: | |
break | |
# Note: We cannot use `sp_port*` returned by `sp_list_ports()` | |
# directly to get manufacturer / product / serial values on | |
# some environments with Arduino port of libserialport. | |
port = sp_port_p() | |
r = sp_get_port_by_name( | |
sp_get_port_name(port_in_list), | |
ctypes.byref(port) | |
) | |
_assert_sp_return(r) | |
try: | |
# Test device serial number | |
cur_serial = sp_get_port_usb_serial(port) | |
if cur_serial is None: | |
continue | |
cur_serial = cur_serial.decode('ascii', 'ignore') | |
if cur_serial != serial: | |
continue | |
# Test vid / pid if specified | |
if vid is not None or pid is not None: | |
cur_vid = ctypes.c_int(0) | |
cur_pid = ctypes.c_int(0) | |
r = sp_get_port_usb_vid_pid( | |
port, | |
ctypes.byref(cur_vid), | |
ctypes.byref(cur_pid) | |
) | |
_assert_sp_return(r) | |
if ( | |
(vid is not None and vid != cur_vid.value) or | |
(pid is not None and pid != cur_pid.value) | |
): | |
continue | |
# Target found | |
found = sp_get_port_name(port).decode('ascii') | |
break | |
finally: | |
sp_free_port(port) | |
finally: | |
sp_free_port_list(port_list) | |
return found | |
# Dump COM port if executed as main | |
if __name__ == '__main__': | |
print('Found COM ports:') | |
port_list = ctypes.POINTER(sp_port_p)() | |
r = sp_list_ports(ctypes.byref(port_list)) | |
_assert_sp_return(r) | |
try: | |
for n in itertools.count(): | |
# Stop at NULL | |
port_in_list = port_list[n] | |
if port_in_list is None: | |
break | |
# Note: We cannot use `sp_port*` returned by `sp_list_ports()` | |
# directly to get manufacturer / product / serial values on | |
# some environments with Arduino port of libserialport. | |
port = sp_port_p() | |
r = sp_get_port_by_name( | |
sp_get_port_name(port_in_list), | |
ctypes.byref(port) | |
) | |
_assert_sp_return(r) | |
try: | |
print('-' * 50) | |
print( | |
'Port name: %s' % sp_get_port_name(port).decode('ascii') | |
) | |
vid = ctypes.c_int(0) | |
pid = ctypes.c_int(0) | |
r = sp_get_port_usb_vid_pid( | |
port, | |
ctypes.byref(vid), | |
ctypes.byref(pid) | |
) | |
_assert_sp_return(r) | |
print('VID: 0x%04X' % vid.value) | |
print('PID: 0x%04X' % pid.value) | |
manufacturer = sp_get_port_usb_manufacturer(port) | |
product = sp_get_port_usb_product(port) | |
serial = sp_get_port_usb_serial(port) | |
print( | |
'Manufacturer name: %s' % ( | |
manufacturer if manufacturer is None | |
else manufacturer.decode('ascii', 'ignore') | |
) | |
) | |
print( | |
'Product name: %s' % ( | |
product if product is None | |
else product.decode('ascii', 'ignore') | |
) | |
) | |
print( | |
'Serial number: %s' % ( | |
serial if serial is None | |
else serial.decode('ascii', 'ignore') | |
) | |
) | |
finally: | |
sp_free_port(port) | |
finally: | |
sp_free_port_list(port_list) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment