Skip to content

Instantly share code, notes, and snippets.

@mozurin
Last active May 24, 2018 12:49
Show Gist options
  • Save mozurin/585fd488853c87540c08792a7a028c51 to your computer and use it in GitHub Desktop.
Save mozurin/585fd488853c87540c08792a7a028c51 to your computer and use it in GitHub Desktop.
Find USB CDC based serial port by its USB serial number
'''
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