Merge branch 'usb' into v1

This commit is contained in:
Cristian Maglie
2017-02-04 00:56:12 +01:00
13 changed files with 1043 additions and 3 deletions

View File

@@ -23,8 +23,9 @@ matrix:
before_install:
script:
- GOARM=5 GO386=387 GOOS=$TEST_OS GOARCH=$TEST_ARCH go get github.com/stretchr/testify/require
- GOARM=5 GO386=387 GOOS=$TEST_OS GOARCH=$TEST_ARCH go get golang.org/x/sys/windows
- GOARM=5 GO386=387 GOOS=$TEST_OS GOARCH=$TEST_ARCH go build -v ./...
- GOARM=5 GO386=387 GOOS=$TEST_OS GOARCH=$TEST_ARCH go build -v go.bug.st/serial.v1
- GOARM=5 GO386=387 GOOS=$TEST_OS GOARCH=$TEST_ARCH go test -c -v go.bug.st/serial.v1
notifications:

32
doc.go
View File

@@ -79,7 +79,35 @@ serial port:
fmt.Printf("%v", string(buff[:n]))
}
This library doesn't make use of cgo and "C" package, so it's a pure go library
that can be easily cross compiled.
If a port is a virtual USB-CDC serial port (for example an USB-to-RS232
cable or a microcontroller development board) is possible to retrieve
the USB metadata, like VID/PID or USB Serial Number, with the
GetDetailedPortsList function in the enumerator package:
import "go.bug.st/serial.v1/enumerator"
ports, err := enumerator.GetDetailedPortsList()
if err != nil {
log.Fatal(err)
}
if len(ports) == 0 {
fmt.Println("No serial ports found!")
return
}
for _, port := range ports {
fmt.Printf("Found port: %s\n", port.Name)
if port.IsUSB {
fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID)
fmt.Printf(" USB serial %s\n", port.SerialNumber)
}
}
for details on USB port enumeration see the documentation of the specific package.
This library tries to avoid the use of the "C" package (and consequently the need
of cgo) to simplify cross compiling.
Unfortunately the USB enumeration package for darwin (MacOSX) requires cgo
to access the IOKit framework. This means that if you need USB enumeration
on darwin you're forced to use cgo.
*/
package serial // import "go.bug.st/serial.v1"

21
enumerator/doc.go Normal file
View File

@@ -0,0 +1,21 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
/*
Package enumerator is a golang cross-platform library for USB serial port discovery.
WARNING: this library is still beta-testing code! please consider the library
and the API as *unstable*. Beware that, even if at this point it's unlike to
happen, the API may be subject to change until this notice is removed from
the documentation.
This library has been tested on Linux, Windows and Mac and uses specific OS
services to enumerate USB PID/VID, in particular on MacOSX the use of cgo is
required in order to access the IOKit Framework. This means that the library
cannot be easily cross compiled for GOOS=darwing targets.
*/
package enumerator // import "go.bug.st/serial.v1/enumerator"

43
enumerator/enumerator.go Normal file
View File

@@ -0,0 +1,43 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
package enumerator // import "go.bug.st/serial.v1/enumerator"
//go:generate go run ../extras/mksyscall_windows.go -output syscall_windows.go usb_windows.go
// PortDetails contains detailed information about USB serial port.
// Use GetDetailedPortsList function to retrieve it.
type PortDetails struct {
Name string
IsUSB bool
VID string
PID string
SerialNumber string
// Manufacturer string
// Product string
}
// GetDetailedPortsList retrieve ports details like USB VID/PID.
// Please note that this function may not be available on all OS:
// in that case a FunctionNotImplemented error is returned.
func GetDetailedPortsList() ([]*PortDetails, error) {
return nativeGetDetailedPortsList()
}
// PortEnumerationError is the error type for serial ports enumeration
type PortEnumerationError struct {
causedBy error
}
// Error returns the complete error code with details on the cause of the error
func (e PortEnumerationError) Error() string {
reason := "Error while enumerating serial ports"
if e.causedBy != nil {
reason += ": " + e.causedBy.Error()
}
return reason
}

View File

@@ -0,0 +1,29 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
package enumerator_test
import "fmt"
import "log"
import "go.bug.st/serial.v1/enumerator"
func ExampleGetDetailedPortsList() {
ports, err := enumerator.GetDetailedPortsList()
if err != nil {
log.Fatal(err)
}
if len(ports) == 0 {
fmt.Println("No serial ports found!")
return
}
for _, port := range ports {
fmt.Printf("Found port: %s\n", port.Name)
if port.IsUSB {
fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID)
fmt.Printf(" USB serial %s\n", port.SerialNumber)
}
}
}

View File

@@ -0,0 +1,113 @@
// MACHINE GENERATED BY 'go generate' COMMAND; DO NOT EDIT
package enumerator
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
var (
modsetupapi = windows.NewLazySystemDLL("setupapi.dll")
procSetupDiClassGuidsFromNameW = modsetupapi.NewProc("SetupDiClassGuidsFromNameW")
procSetupDiGetClassDevsW = modsetupapi.NewProc("SetupDiGetClassDevsW")
procSetupDiDestroyDeviceInfoList = modsetupapi.NewProc("SetupDiDestroyDeviceInfoList")
procSetupDiEnumDeviceInfo = modsetupapi.NewProc("SetupDiEnumDeviceInfo")
procSetupDiGetDeviceInstanceIdW = modsetupapi.NewProc("SetupDiGetDeviceInstanceIdW")
procSetupDiOpenDevRegKey = modsetupapi.NewProc("SetupDiOpenDevRegKey")
procSetupDiGetDeviceRegistryPropertyW = modsetupapi.NewProc("SetupDiGetDeviceRegistryPropertyW")
)
func setupDiClassGuidsFromNameInternal(class string, guid *guid, guidSize uint32, requiredSize *uint32) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(class)
if err != nil {
return
}
return _setupDiClassGuidsFromNameInternal(_p0, guid, guidSize, requiredSize)
}
func _setupDiClassGuidsFromNameInternal(class *uint16, guid *guid, guidSize uint32, requiredSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procSetupDiClassGuidsFromNameW.Addr(), 4, uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(guid)), uintptr(guidSize), uintptr(unsafe.Pointer(requiredSize)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setupDiGetClassDevs(guid *guid, enumerator *string, hwndParent uintptr, flags uint32) (set devicesSet, err error) {
r0, _, e1 := syscall.Syscall6(procSetupDiGetClassDevsW.Addr(), 4, uintptr(unsafe.Pointer(guid)), uintptr(unsafe.Pointer(enumerator)), uintptr(hwndParent), uintptr(flags), 0, 0)
set = devicesSet(r0)
if set == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setupDiDestroyDeviceInfoList(set devicesSet) (err error) {
r1, _, e1 := syscall.Syscall(procSetupDiDestroyDeviceInfoList.Addr(), 1, uintptr(set), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setupDiEnumDeviceInfo(set devicesSet, index uint32, info *devInfoData) (err error) {
r1, _, e1 := syscall.Syscall(procSetupDiEnumDeviceInfo.Addr(), 3, uintptr(set), uintptr(index), uintptr(unsafe.Pointer(info)))
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setupDiGetDeviceInstanceId(set devicesSet, devInfo *devInfoData, devInstanceId unsafe.Pointer, devInstanceIdSize uint32, requiredSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procSetupDiGetDeviceInstanceIdW.Addr(), 5, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(devInstanceId), uintptr(devInstanceIdSize), uintptr(unsafe.Pointer(requiredSize)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setupDiOpenDevRegKey(set devicesSet, devInfo *devInfoData, scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (hkey syscall.Handle, err error) {
r0, _, e1 := syscall.Syscall6(procSetupDiOpenDevRegKey.Addr(), 6, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(scope), uintptr(hwProfile), uintptr(keyType), uintptr(samDesired))
hkey = syscall.Handle(r0)
if hkey == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setupDiGetDeviceRegistryProperty(set devicesSet, devInfo *devInfoData, property deviceProperty, propertyType *uint32, outValue *byte, outSize *uint32, reqSize *uint32) (res bool) {
r0, _, _ := syscall.Syscall9(procSetupDiGetDeviceRegistryPropertyW.Addr(), 7, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(property), uintptr(unsafe.Pointer(propertyType)), uintptr(unsafe.Pointer(outValue)), uintptr(unsafe.Pointer(outSize)), uintptr(unsafe.Pointer(reqSize)), 0, 0)
res = r0 != 0
return
}

216
enumerator/usb_darwin.go Normal file
View File

@@ -0,0 +1,216 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
package enumerator // import "go.bug.st/serial.v1/enumerator"
// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit -fconstant-cfstrings
// #include <IOKit/IOKitLib.h>
// #include <CoreFoundation/CoreFoundation.h>
// #include <stdlib.h>
import "C"
import (
"errors"
"fmt"
"unsafe"
)
func nativeGetDetailedPortsList() ([]*PortDetails, error) {
var ports []*PortDetails
services, err := getAllServices("IOSerialBSDClient")
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
for _, service := range services {
defer service.Release()
port, err := extractPortInfo(C.io_registry_entry_t(service))
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
ports = append(ports, port)
}
return ports, nil
}
func extractPortInfo(service C.io_registry_entry_t) (*PortDetails, error) {
name, err := service.GetStringProperty("IOCalloutDevice")
if err != nil {
return nil, fmt.Errorf("Error extracting port info from device: %s", err.Error())
}
port := &PortDetails{}
port.Name = name
port.IsUSB = false
usbDevice := service
for usbDevice.GetClass() != "IOUSBDevice" {
if usbDevice, err = usbDevice.GetParent("IOService"); err != nil {
break
}
}
if err == nil {
// It's an IOUSBDevice
vid, _ := usbDevice.GetIntProperty("idVendor", C.kCFNumberSInt16Type)
pid, _ := usbDevice.GetIntProperty("idProduct", C.kCFNumberSInt16Type)
serialNumber, _ := usbDevice.GetStringProperty("USB Serial Number")
//product, _ := usbDevice.GetStringProperty("USB Product Name")
//manufacturer, _ := usbDevice.GetStringProperty("USB Vendor Name")
//fmt.Println(product + " - " + manufacturer)
port.IsUSB = true
port.VID = fmt.Sprintf("%04X", vid)
port.PID = fmt.Sprintf("%04X", pid)
port.SerialNumber = serialNumber
}
return port, nil
}
func getAllServices(serviceType string) ([]C.io_object_t, error) {
i, err := getMatchingServices(serviceMatching(serviceType))
if err != nil {
return nil, err
}
defer i.Release()
var services []C.io_object_t
tries := 0
for tries < 5 {
// Extract all elements from iterator
if service, ok := i.Next(); ok {
services = append(services, service)
continue
}
// If iterator is still valid return the result
if i.IsValid() {
return services, nil
}
// Otherwise empty the result and retry
for _, s := range services {
s.Release()
}
services = []C.io_object_t{}
i.Reset()
tries++
}
// Give up if the iteration continues to fail...
return nil, fmt.Errorf("IOServiceGetMatchingServices failed, data changed while iterating")
}
// serviceMatching create a matching dictionary that specifies an IOService class match.
func serviceMatching(serviceType string) C.CFMutableDictionaryRef {
t := C.CString(serviceType)
defer C.free(unsafe.Pointer(t))
return C.IOServiceMatching(t)
}
// getMatchingServices look up registered IOService objects that match a matching dictionary.
func getMatchingServices(matcher C.CFMutableDictionaryRef) (C.io_iterator_t, error) {
var i C.io_iterator_t
err := C.IOServiceGetMatchingServices(C.kIOMasterPortDefault, matcher, &i)
if err != C.KERN_SUCCESS {
return 0, fmt.Errorf("IOServiceGetMatchingServices failed (code %d)", err)
}
return i, nil
}
// CFStringRef
func cfStringCreateWithString(s string) C.CFStringRef {
c := C.CString(s)
defer C.free(unsafe.Pointer(c))
return C.CFStringCreateWithCString(
C.kCFAllocatorDefault, c, C.kCFStringEncodingMacRoman)
}
// io_registry_entry_t
func (me *C.io_registry_entry_t) GetParent(plane string) (C.io_registry_entry_t, error) {
cPlane := C.CString(plane)
defer C.free(unsafe.Pointer(cPlane))
var parent C.io_registry_entry_t
err := C.IORegistryEntryGetParentEntry(*me, cPlane, &parent)
if err != 0 {
return 0, errors.New("No parent device available")
}
return parent, nil
}
func (me *C.io_registry_entry_t) GetClass() string {
obj := (*C.io_object_t)(me)
return obj.GetClass()
}
func (me *C.io_registry_entry_t) GetStringProperty(key string) (string, error) {
k := cfStringCreateWithString(key)
defer C.CFRelease(C.CFTypeRef(k))
property := C.IORegistryEntryCreateCFProperty(*me, k, C.kCFAllocatorDefault, 0)
if property == nil {
return "", errors.New("Property not found: " + key)
}
defer C.CFRelease(property)
if ptr := C.CFStringGetCStringPtr(property, 0); ptr != nil {
return C.GoString(ptr), nil
}
// in certain circumstances CFStringGetCStringPtr may return NULL
// and we must retrieve the string by copy
buff := make([]C.char, 1024)
if C.CFStringGetCString(property, &buff[0], 1024, 0) != C.true {
return "", fmt.Errorf("Property '%s' can't be converted", key)
}
return C.GoString(&buff[0]), nil
}
func (me *C.io_registry_entry_t) GetIntProperty(key string, intType C.CFNumberType) (int, error) {
k := cfStringCreateWithString(key)
defer C.CFRelease(C.CFTypeRef(k))
property := C.IORegistryEntryCreateCFProperty(*me, k, C.kCFAllocatorDefault, 0)
if property == nil {
return 0, errors.New("Property not found: " + key)
}
defer C.CFRelease(property)
var res int
if C.CFNumberGetValue(property, intType, unsafe.Pointer(&res)) != C.true {
return res, fmt.Errorf("Property '%s' can't be converted or has been truncated", key)
}
return res, nil
}
// io_iterator_t
// IsValid checks if an iterator is still valid.
// Some iterators will be made invalid if changes are made to the
// structure they are iterating over. This function checks the iterator
// is still valid and should be called when Next returns zero.
// An invalid iterator can be Reset and the iteration restarted.
func (me *C.io_iterator_t) IsValid() bool {
return C.IOIteratorIsValid(*me) == C.true
}
func (me *C.io_iterator_t) Reset() {
C.IOIteratorReset(*me)
}
func (me *C.io_iterator_t) Next() (C.io_object_t, bool) {
res := C.IOIteratorNext(*me)
return res, res != 0
}
func (me *C.io_iterator_t) Release() {
C.IOObjectRelease(C.io_object_t(*me))
}
// io_object_t
func (me *C.io_object_t) Release() {
C.IOObjectRelease(*me)
}
func (me *C.io_object_t) GetClass() string {
class := make([]C.char, 1024)
C.IOObjectGetClass(*me, &class[0])
return C.GoString(&class[0])
}

12
enumerator/usb_freebsd.go Normal file
View File

@@ -0,0 +1,12 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
package enumerator // import "go.bug.st/serial.v1/enumerator"
func nativeGetDetailedPortsList() ([]*PortDetails, error) {
// TODO
return nil, &PortEnumerationError{}
}

109
enumerator/usb_linux.go Normal file
View File

@@ -0,0 +1,109 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
package enumerator // import "go.bug.st/serial.v1/enumerator"
import (
"bufio"
"fmt"
"os"
"path/filepath"
"go.bug.st/serial.v1"
)
func nativeGetDetailedPortsList() ([]*PortDetails, error) {
// Retrieve the port list
ports, err := serial.GetPortsList()
if err != nil {
return nil, &PortEnumerationError{causedBy: err}
}
var res []*PortDetails
for _, port := range ports {
details, err := nativeGetPortDetails(port)
if err != nil {
return nil, &PortEnumerationError{causedBy: err}
}
res = append(res, details)
}
return res, nil
}
func nativeGetPortDetails(portPath string) (*PortDetails, error) {
portName := filepath.Base(portPath)
devicePath := fmt.Sprintf("/sys/class/tty/%s/device", portName)
if _, err := os.Stat(devicePath); err != nil {
return &PortDetails{}, nil
}
realDevicePath, err := filepath.EvalSymlinks(devicePath)
if err != nil {
return nil, fmt.Errorf("Can't determine real path of %s: %s", devicePath, err.Error())
}
subSystemPath, err := filepath.EvalSymlinks(filepath.Join(realDevicePath, "subsystem"))
if err != nil {
return nil, fmt.Errorf("Can't determine real path of %s: %s", filepath.Join(realDevicePath, "subsystem"), err.Error())
}
subSystem := filepath.Base(subSystemPath)
result := &PortDetails{Name: portPath}
switch subSystem {
case "usb-serial":
err := parseUSBSysFS(filepath.Dir(filepath.Dir(realDevicePath)), result)
return result, err
case "usb":
err := parseUSBSysFS(filepath.Dir(realDevicePath), result)
return result, err
// TODO: other cases?
default:
return result, nil
}
}
func parseUSBSysFS(usbDevicePath string, details *PortDetails) error {
vid, err := readLine(filepath.Join(usbDevicePath, "idVendor"))
if err != nil {
return err
}
pid, err := readLine(filepath.Join(usbDevicePath, "idProduct"))
if err != nil {
return err
}
serial, err := readLine(filepath.Join(usbDevicePath, "serial"))
if err != nil {
return err
}
//manufacturer, err := readLine(filepath.Join(usbDevicePath, "manufacturer"))
//if err != nil {
// return err
//}
//product, err := readLine(filepath.Join(usbDevicePath, "product"))
//if err != nil {
// return err
//}
details.IsUSB = true
details.VID = vid
details.PID = pid
details.SerialNumber = serial
//details.Manufacturer = manufacturer
//details.Product = product
return nil
}
func readLine(filename string) (string, error) {
file, err := os.Open(filename)
if os.IsNotExist(err) {
return "", nil
}
if err != nil {
return "", err
}
defer file.Close()
reader := bufio.NewReader(file)
line, _, err := reader.ReadLine()
return string(line), err
}

View File

@@ -0,0 +1,113 @@
//
// Copyright 2014-2017 Lars Knudsen, Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// +build ignore
package enumerator
import (
"log"
"regexp"
ole "github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
)
func init() {
err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
if err != nil {
log.Fatal("Init error: ", err)
}
}
func nativeGetDetailedPortsList() ([]*PortDetails, error) {
unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator")
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
defer unknown.Release()
wmi, err := unknown.QueryInterface(ole.IID_IDispatch)
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
defer wmi.Release()
serviceRaw, err := wmi.CallMethod("ConnectServer")
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
service := serviceRaw.ToIDispatch()
defer service.Release()
query := "SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0 and Name like '%(COM%'"
queryResult, err := oleutil.CallMethod(service, "ExecQuery", query)
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
result := queryResult.ToIDispatch()
defer result.Release()
countVar, err := result.GetProperty("Count")
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
count := int(countVar.Val)
res := []*PortDetails{}
// Retrieve all items
for i := 0; i < count; i++ {
itemRaw, err := result.CallMethod("ItemIndex", i)
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
item := itemRaw.ToIDispatch()
defer item.Release()
detail := &PortDetails{}
if err := getPortDetails(item, detail); err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
// SerialPort{Path: path, VendorId: VID, ProductId: PID, DisplayName: displayName.ToString()}
res = append(res, detail)
}
return res, nil
}
func getPortDetails(item *ole.IDispatch, res *PortDetails) error {
// Find port name
itemName, err := item.GetProperty("Name")
if err != nil {
return err
}
re := regexp.MustCompile("\\((COM[0-9]+)\\)").FindAllStringSubmatch(itemName.ToString(), 1)
if re == nil || len(re[0]) < 2 {
// Discard items that are not serial ports
return nil
}
res.Name = re[0][1]
//itemPnPDeviceID, err := item.GetProperty("PnPDeviceID")
//if err != nil {
// return err
//}
//PnPDeviceID := itemPnPDeviceID.ToString()
itemDeviceID, err := item.GetProperty("DeviceID")
if err != nil {
return err
}
parseDeviceID(itemDeviceID.ToString(), res)
itemManufacturer, err := item.GetProperty("Product")
if err != nil {
return err
}
res.Manufacturer = itemManufacturer.ToString()
return nil
}

302
enumerator/usb_windows.go Normal file
View File

@@ -0,0 +1,302 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
package enumerator // import "go.bug.st/serial.v1/enumerator"
import (
"fmt"
"regexp"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
func parseDeviceID(deviceID string, details *PortDetails) {
// Windows stock USB-CDC driver
if len(deviceID) >= 3 && deviceID[:3] == "USB" {
re := regexp.MustCompile("VID_(....)&PID_(....)(\\\\(\\w+)$)?").FindAllStringSubmatch(deviceID, -1)
if re == nil || len(re[0]) < 2 {
// Silently ignore unparsable strings
return
}
details.IsUSB = true
details.VID = re[0][1]
details.PID = re[0][2]
if len(re[0]) >= 4 {
details.SerialNumber = re[0][4]
}
return
}
// FTDI driver
if len(deviceID) >= 7 && deviceID[:7] == "FTDIBUS" {
re := regexp.MustCompile("VID_(....)\\+PID_(....)(\\+(\\w+))?").FindAllStringSubmatch(deviceID, -1)
if re == nil || len(re[0]) < 2 {
// Silently ignore unparsable strings
return
}
details.IsUSB = true
details.VID = re[0][1]
details.PID = re[0][2]
if len(re[0]) >= 4 {
details.SerialNumber = re[0][4]
}
return
}
// Other unidentified device type
}
// setupapi based
// --------------
//sys setupDiClassGuidsFromNameInternal(class string, guid *guid, guidSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiClassGuidsFromNameW
//sys setupDiGetClassDevs(guid *guid, enumerator *string, hwndParent uintptr, flags uint32) (set devicesSet, err error) = setupapi.SetupDiGetClassDevsW
//sys setupDiDestroyDeviceInfoList(set devicesSet) (err error) = setupapi.SetupDiDestroyDeviceInfoList
//sys setupDiEnumDeviceInfo(set devicesSet, index uint32, info *devInfoData) (err error) = setupapi.SetupDiEnumDeviceInfo
//sys setupDiGetDeviceInstanceId(set devicesSet, devInfo *devInfoData, devInstanceId unsafe.Pointer, devInstanceIdSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiGetDeviceInstanceIdW
//sys setupDiOpenDevRegKey(set devicesSet, devInfo *devInfoData, scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (hkey syscall.Handle, err error) = setupapi.SetupDiOpenDevRegKey
//sys setupDiGetDeviceRegistryProperty(set devicesSet, devInfo *devInfoData, property deviceProperty, propertyType *uint32, outValue *byte, outSize *uint32, reqSize *uint32) (res bool) = setupapi.SetupDiGetDeviceRegistryPropertyW
// Device registry property codes
// (Codes marked as read-only (R) may only be used for
// SetupDiGetDeviceRegistryProperty)
//
// These values should cover the same set of registry properties
// as defined by the CM_DRP codes in cfgmgr32.h.
//
// Note that SPDRP codes are zero based while CM_DRP codes are one based!
type deviceProperty uint32
const (
spdrpDeviceDesc deviceProperty = 0x00000000 // DeviceDesc = R/W
spdrpHardwareID = 0x00000001 // HardwareID = R/W
spdrpCompatibleIDS = 0x00000002 // CompatibleIDs = R/W
spdrpUnused0 = 0x00000003 // Unused
spdrpService = 0x00000004 // Service = R/W
spdrpUnused1 = 0x00000005 // Unused
spdrpUnused2 = 0x00000006 // Unused
spdrpClass = 0x00000007 // Class = R--tied to ClassGUID
spdrpClassGUID = 0x00000008 // ClassGUID = R/W
spdrpDriver = 0x00000009 // Driver = R/W
spdrpConfigFlags = 0x0000000A // ConfigFlags = R/W
spdrpMFG = 0x0000000B // Mfg = R/W
spdrpFriendlyName = 0x0000000C // FriendlyName = R/W
spdrpLocationIinformation = 0x0000000D // LocationInformation = R/W
spdrpPhysicalDeviceObjectName = 0x0000000E // PhysicalDeviceObjectName = R
spdrpCapabilities = 0x0000000F // Capabilities = R
spdrpUINumber = 0x00000010 // UiNumber = R
spdrpUpperFilters = 0x00000011 // UpperFilters = R/W
spdrpLowerFilters = 0x00000012 // LowerFilters = R/W
spdrpBusTypeGUID = 0x00000013 // BusTypeGUID = R
spdrpLegactBusType = 0x00000014 // LegacyBusType = R
spdrpBusNumber = 0x00000015 // BusNumber = R
spdrpEnumeratorName = 0x00000016 // Enumerator Name = R
spdrpSecurity = 0x00000017 // Security = R/W, binary form
spdrpSecuritySDS = 0x00000018 // Security = W, SDS form
spdrpDevType = 0x00000019 // Device Type = R/W
spdrpExclusive = 0x0000001A // Device is exclusive-access = R/W
spdrpCharacteristics = 0x0000001B // Device Characteristics = R/W
spdrpAddress = 0x0000001C // Device Address = R
spdrpUINumberDescFormat = 0X0000001D // UiNumberDescFormat = R/W
spdrpDevicePowerData = 0x0000001E // Device Power Data = R
spdrpRemovalPolicy = 0x0000001F // Removal Policy = R
spdrpRemovalPolicyHWDefault = 0x00000020 // Hardware Removal Policy = R
spdrpRemovalPolicyOverride = 0x00000021 // Removal Policy Override = RW
spdrpInstallState = 0x00000022 // Device Install State = R
spdrpLocationPaths = 0x00000023 // Device Location Paths = R
spdrpBaseContainerID = 0x00000024 // Base ContainerID = R
spdrpMaximumProperty = 0x00000025 // Upper bound on ordinals
)
// Values specifying the scope of a device property change
type dicsScope uint32
const (
dicsFlagGlobal dicsScope = 0x00000001 // make change in all hardware profiles
dicsFlagConfigSspecific = 0x00000002 // make change in specified profile only
dicsFlagConfigGeneral = 0x00000004 // 1 or more hardware profile-specific
)
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724878(v=vs.85).aspx
type regsam uint32
const (
keyAllAccess regsam = 0xF003F
keyCreateLink = 0x00020
keyCreateSubKey = 0x00004
keyEnumerateSubKeys = 0x00008
keyExecute = 0x20019
keyNotify = 0x00010
keyQueryValue = 0x00001
keyRead = 0x20019
keySetValue = 0x00002
keyWOW64_32key = 0x00200
keyWOW64_64key = 0x00100
keyWrite = 0x20006
)
// KeyType values for SetupDiCreateDevRegKey, SetupDiOpenDevRegKey, and
// SetupDiDeleteDevRegKey.
const (
diregDev = 0x00000001 // Open/Create/Delete device key
diregDrv = 0x00000002 // Open/Create/Delete driver key
diregBoth = 0x00000004 // Delete both driver and Device key
)
// https://msdn.microsoft.com/it-it/library/windows/desktop/aa373931(v=vs.85).aspx
type guid struct {
data1 uint32
data2 uint16
data3 uint16
data4 [8]byte
}
func (g guid) String() string {
return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
g.data1, g.data2, g.data3,
g.data4[0], g.data4[1], g.data4[2], g.data4[3],
g.data4[4], g.data4[5], g.data4[6], g.data4[7])
}
func classGuidsFromName(className string) ([]guid, error) {
// Determine the number of GUIDs for className
n := uint32(0)
if err := setupDiClassGuidsFromNameInternal(className, nil, 0, &n); err != nil {
// ignore error: UIDs array size too small
}
res := make([]guid, n)
err := setupDiClassGuidsFromNameInternal(className, &res[0], n, &n)
return res, err
}
const (
digcfDefault = 0x00000001 // only valid with digcfDeviceInterface
digcfPresent = 0x00000002
digcfAllClasses = 0x00000004
digcfProfile = 0x00000008
digcfDeviceInterface = 0x00000010
)
type devicesSet syscall.Handle
func (g *guid) getDevicesSet() (devicesSet, error) {
return setupDiGetClassDevs(g, nil, 0, digcfPresent)
}
func (set devicesSet) destroy() {
setupDiDestroyDeviceInfoList(set)
}
// https://msdn.microsoft.com/en-us/library/windows/hardware/ff552344(v=vs.85).aspx
type devInfoData struct {
size uint32
guid guid
devInst uint32
reserved uintptr
}
type deviceInfo struct {
set devicesSet
data devInfoData
}
func (set devicesSet) getDeviceInfo(index int) (*deviceInfo, error) {
result := &deviceInfo{set: set}
result.data.size = uint32(unsafe.Sizeof(result.data))
err := setupDiEnumDeviceInfo(set, uint32(index), &result.data)
return result, err
}
func (dev *deviceInfo) getInstanceID() (string, error) {
n := uint32(0)
setupDiGetDeviceInstanceId(dev.set, &dev.data, nil, 0, &n)
buff := make([]uint16, n)
if err := setupDiGetDeviceInstanceId(dev.set, &dev.data, unsafe.Pointer(&buff[0]), uint32(len(buff)), &n); err != nil {
return "", err
}
return windows.UTF16ToString(buff[:]), nil
}
func (dev *deviceInfo) openDevRegKey(scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (syscall.Handle, error) {
return setupDiOpenDevRegKey(dev.set, &dev.data, scope, hwProfile, keyType, samDesired)
}
func nativeGetDetailedPortsList() ([]*PortDetails, error) {
guids, err := classGuidsFromName("Ports")
if err != nil {
return nil, &PortEnumerationError{causedBy: err}
}
var res []*PortDetails
for _, g := range guids {
devsSet, err := g.getDevicesSet()
if err != nil {
return nil, &PortEnumerationError{causedBy: err}
}
defer devsSet.destroy()
for i := 0; ; i++ {
device, err := devsSet.getDeviceInfo(i)
if err != nil {
break
}
details := &PortDetails{}
portName, err := retrievePortNameFromDevInfo(device)
if err != nil {
continue
}
if len(portName) < 3 || portName[0:3] != "COM" {
// Accept only COM ports
continue
}
details.Name = portName
if err := retrievePortDetailsFromDevInfo(device, details); err != nil {
return nil, &PortEnumerationError{causedBy: err}
}
res = append(res, details)
}
}
return res, nil
}
func retrievePortNameFromDevInfo(device *deviceInfo) (string, error) {
h, err := device.openDevRegKey(dicsFlagGlobal, 0, diregDev, keyRead)
if err != nil {
return "", err
}
defer syscall.RegCloseKey(h)
var name [1024]uint16
nameP := (*byte)(unsafe.Pointer(&name[0]))
nameSize := uint32(len(name) * 2)
if err := syscall.RegQueryValueEx(h, syscall.StringToUTF16Ptr("PortName"), nil, nil, nameP, &nameSize); err != nil {
return "", err
}
return syscall.UTF16ToString(name[:]), nil
}
func retrievePortDetailsFromDevInfo(device *deviceInfo, details *PortDetails) error {
deviceID, err := device.getInstanceID()
if err != nil {
return err
}
parseDeviceID(deviceID, details)
var friendlyName [1024]uint16
friendlyNameP := (*byte)(unsafe.Pointer(&friendlyName[0]))
friendlyNameSize := uint32(len(friendlyName) * 2)
if setupDiGetDeviceRegistryProperty(device.set, &device.data, spdrpDeviceDesc /* spdrpFriendlyName */, nil, friendlyNameP, &friendlyNameSize, nil) {
//details.Product = syscall.UTF16ToString(friendlyName[:])
}
return nil
}

View File

@@ -0,0 +1,49 @@
//
// Copyright 2014-2017 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
package enumerator // import "go.bug.st/serial.v1/enumerator"
import (
"testing"
"github.com/stretchr/testify/require"
)
func parseAndReturnDeviceID(deviceID string) *PortDetails {
res := &PortDetails{}
parseDeviceID(deviceID, res)
return res
}
func TestParseDeviceID(t *testing.T) {
r := require.New(t)
test := func(deviceId, vid, pid, serialNo string) {
res := parseAndReturnDeviceID(deviceId)
r.True(res.IsUSB)
r.Equal(vid, res.VID)
r.Equal(pid, res.PID)
r.Equal(serialNo, res.SerialNumber)
}
test("FTDIBUS\\VID_0403+PID_6001+A6004CCFA\\0000", "0403", "6001", "A6004CCFA")
test("USB\\VID_16C0&PID_0483\\12345", "16C0", "0483", "12345")
test("USB\\VID_2341&PID_0000\\64936333936351400000", "2341", "0000", "64936333936351400000")
test("USB\\VID_2341&PID_0000\\6493234373835191F1F1", "2341", "0000", "6493234373835191F1F1")
test("USB\\VID_2341&PID_804E&MI_00\\6&279A3900&0&0000", "2341", "804E", "")
test("USB\\VID_2341&PID_004E\\5&C3DC240&0&1", "2341", "004E", "")
test("USB\\VID_03EB&PID_2111&MI_01\\6&21F3553F&0&0001", "03EB", "2111", "") // Atmel EDBG
test("USB\\VID_2341&PID_804D&MI_00\\6&1026E213&0&0000", "2341", "804D", "")
test("USB\\VID_2341&PID_004D\\5&C3DC240&0&1", "2341", "004D", "")
test("USB\\VID_067B&PID_2303\\6&2C4CB384&0&3", "067B", "2303", "") // PL2303
}
func TestParseDeviceIDWithInvalidStrings(t *testing.T) {
r := require.New(t)
res := parseAndReturnDeviceID("ABC")
r.False(res.IsUSB)
res2 := parseAndReturnDeviceID("USB")
r.False(res2.IsUSB)
}

View File

@@ -123,6 +123,8 @@ const (
ErrorEnumeratingPorts
// PortClosed the port has been closed while the operation is in progress
PortClosed
// FunctionNotImplemented the requested function is not implemented
FunctionNotImplemented
)
// EncodedErrorString returns a string explaining the error code
@@ -148,6 +150,8 @@ func (e PortError) EncodedErrorString() string {
return "Could not enumerate serial ports"
case PortClosed:
return "Port has been closed"
case FunctionNotImplemented:
return "Function not implemented"
default:
return "Other error"
}