#!/usr/bin/env python

# portable serial port access with python
#
# This is a module that gathers a list of serial ports including details on OSX
#
# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools
# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston
# and modifications by cliechti
#
# this is distributed under a free software license, see license.txt



# List all of the callout devices in OS/X by querying IOKit.

# See the following for a reference of how to do this:
# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD

# More help from darwin_hid.py

# Also see the 'IORegistryExplorer' for an idea of what we are actually searching

import ctypes
from ctypes import util
import re

iokit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit'))
cf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation'))

kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault")
kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault")

kCFStringEncodingMacRoman = 0

iokit.IOServiceMatching.restype = ctypes.c_void_p

iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
iokit.IOServiceGetMatchingServices.restype = ctypes.c_void_p

iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]

iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32]
iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p

iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
iokit.IORegistryEntryGetPath.restype = ctypes.c_void_p

iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
iokit.IORegistryEntryGetName.restype = ctypes.c_void_p

iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
iokit.IOObjectGetClass.restype = ctypes.c_void_p

iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]


cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32]
cf.CFStringCreateWithCString.restype = ctypes.c_void_p

cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
cf.CFStringGetCStringPtr.restype = ctypes.c_char_p

cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
cf.CFNumberGetValue.restype = ctypes.c_void_p

def get_string_property(device_t, property):
    """ Search the given device for the specified string property

    @param device_t Device to search
    @param property String to search for.
    @return Python string containing the value, or None if not found.
    """
    key = cf.CFStringCreateWithCString(
        kCFAllocatorDefault,
        property.encode("mac_roman"),
        kCFStringEncodingMacRoman
    )

    CFContainer = iokit.IORegistryEntryCreateCFProperty(
        device_t,
        key,
        kCFAllocatorDefault,
        0
    );

    output = None

    if CFContainer:
        output = cf.CFStringGetCStringPtr(CFContainer, 0)

    return output

def get_int_property(device_t, property):
    """ Search the given device for the specified string property

    @param device_t Device to search
    @param property String to search for.
    @return Python string containing the value, or None if not found.
    """
    key = cf.CFStringCreateWithCString(
        kCFAllocatorDefault,
        property.encode("mac_roman"),
        kCFStringEncodingMacRoman
    )

    CFContainer = iokit.IORegistryEntryCreateCFProperty(
        device_t,
        key,
        kCFAllocatorDefault,
        0
    );

    number = ctypes.c_uint16()

    if CFContainer:
        output = cf.CFNumberGetValue(CFContainer, 2, ctypes.byref(number))

    return number.value

def IORegistryEntryGetName(device):
    pathname = ctypes.create_string_buffer(100) # TODO: Is this ok?
    iokit.IOObjectGetClass(
        device,
        ctypes.byref(pathname)
    )

    return pathname.value

def GetParentDeviceByType(device, parent_type):
    """ Find the first parent of a device that implements the parent_type
        @param IOService Service to inspect
        @return Pointer to the parent type, or None if it was not found.
    """
    # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice.
    while IORegistryEntryGetName(device) != parent_type:
        parent = ctypes.c_void_p()
        response = iokit.IORegistryEntryGetParentEntry(
            device,
            "IOService".encode("mac_roman"),
            ctypes.byref(parent)
        )

        # If we weren't able to find a parent for the device, we're done.
        if response != 0:
            return None

        device = parent

    return device

def GetIOServicesByType(service_type):
    """
    """
    serial_port_iterator = ctypes.c_void_p()

    response = iokit.IOServiceGetMatchingServices(
        kIOMasterPortDefault,
        iokit.IOServiceMatching(service_type),
        ctypes.byref(serial_port_iterator)
    )

    services = []
    while iokit.IOIteratorIsValid(serial_port_iterator):
        service = iokit.IOIteratorNext(serial_port_iterator)
        if not service:
            break
        services.append(service)

    iokit.IOObjectRelease(serial_port_iterator)

    return services

def comports():
    # Scan for all iokit serial ports
    services = GetIOServicesByType('IOSerialBSDClient')

    ports = []
    for service in services:
        info = []

        # First, add the callout device file.
        info.append(get_string_property(service, "IOCalloutDevice"))

        # If the serial port is implemented by a
        usb_device = GetParentDeviceByType(service, "IOUSBDevice")
        if usb_device != None:
            info.append(get_string_property(usb_device, "USB Product Name"))

            info.append(
                "USB VID:PID=%x:%x SNR=%s"%(
                get_int_property(usb_device, "idVendor"),
                get_int_property(usb_device, "idProduct"),
                get_string_property(usb_device, "USB Serial Number"))
            )
        else:
           info.append('n/a')
           info.append('n/a')

        ports.append(info)

    return ports

# test
if __name__ == '__main__':
    for port, desc, hwid in sorted(comports()):
        print("%s: %s [%s]" % (port, desc, hwid))

