diff --git a/.travis.yml b/.travis.yml index 5c29618..8f4b14e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,14 @@ language: go +os: + - linux + - osx + go: - 1.7.x - 1.8.x + - 1.9.x + - 1.10.x go_import_path: go.bug.st/serial.v1 @@ -20,6 +26,29 @@ env: - TEST_OS=openbsd TEST_ARCH=arm matrix: + exclude: + - os: linux + env: TEST_OS=darwin TEST_ARCH=386 + - os: linux + env: TEST_OS=darwin TEST_ARCH=amd64 + - os: osx + env: TEST_OS=linux TEST_ARCH=386 + - os: osx + env: TEST_OS=linux TEST_ARCH=amd64 + - os: osx + env: TEST_OS=linux TEST_ARCH=arm + - os: osx + env: TEST_OS=windows TEST_ARCH=386 + - os: osx + env: TEST_OS=windows TEST_ARCH=amd64 + - os: osx + env: TEST_OS=freebsd TEST_ARCH=amd64 + - os: osx + env: TEST_OS=openbsd TEST_ARCH=amd64 + - os: osx + env: TEST_OS=openbsd TEST_ARCH=386 + - os: osx + env: TEST_OS=openbsd TEST_ARCH=arm allow_failures: - env: TEST_OS=openbsd TEST_ARCH=arm diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin.go index 4cb9f61..c72a19a 100644 --- a/enumerator/usb_darwin.go +++ b/enumerator/usb_darwin.go @@ -4,9 +4,11 @@ // license that can be found in the LICENSE file. // +// +build go1.10,darwin + package enumerator // import "go.bug.st/serial.v1/enumerator" -// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit -fconstant-cfstrings +// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit // #include // #include // #include @@ -46,12 +48,13 @@ func extractPortInfo(service C.io_registry_entry_t) (*PortDetails, error) { port.IsUSB = false usbDevice := service + var searchErr error for usbDevice.GetClass() != "IOUSBDevice" { - if usbDevice, err = usbDevice.GetParent("IOService"); err != nil { + if usbDevice, searchErr = usbDevice.GetParent("IOService"); searchErr != nil { break } } - if err == nil { + if searchErr == nil { // It's an IOUSBDevice vid, _ := usbDevice.GetIntProperty("idVendor", C.kCFNumberSInt16Type) pid, _ := usbDevice.GetIntProperty("idProduct", C.kCFNumberSInt16Type) @@ -125,6 +128,16 @@ func cfStringCreateWithString(s string) C.CFStringRef { C.kCFAllocatorDefault, c, C.kCFStringEncodingMacRoman) } +func (ref C.CFStringRef) Release() { + C.CFRelease(C.CFTypeRef(ref)) +} + +// CFTypeRef + +func (ref C.CFTypeRef) Release() { + C.CFRelease(ref) +} + // io_registry_entry_t func (me *C.io_registry_entry_t) GetParent(plane string) (C.io_registry_entry_t, error) { @@ -138,40 +151,41 @@ func (me *C.io_registry_entry_t) GetParent(plane string) (C.io_registry_entry_t, 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) CreateCFProperty(key string) (C.CFTypeRef, error) { + k := cfStringCreateWithString(key) + defer k.Release() + property := C.IORegistryEntryCreateCFProperty(*me, k, C.kCFAllocatorDefault, 0) + if property == 0 { + return 0, errors.New("Property not found: " + key) + } + return property, nil } 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) + property, err := me.CreateCFProperty(key) + if err != nil { + return "", err } - defer C.CFRelease(property) + defer property.Release() - if ptr := C.CFStringGetCStringPtr((C.CFStringRef)(unsafe.Pointer(property)), 0); ptr != nil { + if ptr := C.CFStringGetCStringPtr(C.CFStringRef(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((C.CFStringRef)(property), &buff[0], 1024, 0) != C.true { + if C.CFStringGetCString(C.CFStringRef(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) + property, err := me.CreateCFProperty(key) + if err != nil { + return 0, err } - defer C.CFRelease(property) + defer property.Release() var res int if C.CFNumberGetValue((C.CFNumberRef)(property), intType, unsafe.Pointer(&res)) != C.true { return res, fmt.Errorf("Property '%s' can't be converted or has been truncated", key) @@ -199,10 +213,6 @@ func (me *C.io_iterator_t) Next() (C.io_object_t, bool) { 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() { diff --git a/enumerator/usb_darwin_go1.9.go b/enumerator/usb_darwin_go1.9.go new file mode 100644 index 0000000..442278d --- /dev/null +++ b/enumerator/usb_darwin_go1.9.go @@ -0,0 +1,222 @@ +// +// 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. +// + +// +build !go1.10,darwin + +// This file is here to keep compatibility with the older versions of go +// and is no more maintained or bugfixed, please update your go version +// to at least 1.10 to get the latest updates. + +package enumerator // import "go.bug.st/serial.v1/enumerator" + +// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit +// #include +// #include +// #include +import "C" +import ( + "errors" + "fmt" + "unsafe" +) + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + var ports []*PortDetails + + services, err := getAllServices("IOSerialBSDClient") + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + for _, service := range services { + defer service.Release() + + port, err := extractPortInfo(C.io_registry_entry_t(service)) + if err != nil { + return nil, &PortEnumerationError{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((C.CFStringRef)(unsafe.Pointer(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((C.CFStringRef)(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((C.CFNumberRef)(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]) +} diff --git a/portlist/portlist.go b/portlist/portlist.go new file mode 100644 index 0000000..5d4d127 --- /dev/null +++ b/portlist/portlist.go @@ -0,0 +1,37 @@ +// +// Copyright 2014-2018 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. +// + +// portlist is a tool to list all the available serial ports. +// Just run it and it will produce an output like: +// +// $ go run portlist.go +// Port: /dev/cu.Bluetooth-Incoming-Port +// Port: /dev/cu.usbmodemFD121 +// USB ID 2341:8053 +// USB serial FB7B6060504B5952302E314AFF08191A +// +package main + +import "fmt" +import "log" +import "go.bug.st/serial.v1/enumerator" + +func main() { + ports, err := enumerator.GetDetailedPortsList() + if err != nil { + log.Fatal(err) + } + if len(ports) == 0 { + return + } + for _, port := range ports { + fmt.Printf("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) + } + } +}