diff --git a/posix_all.go b/posix_all.go new file mode 100644 index 0000000..b72d66e --- /dev/null +++ b/posix_all.go @@ -0,0 +1,119 @@ +// +build !windows + +package serial + +import ( + "io/ioutil" + "regexp" + "strings" + "time" +) + +const ( + devFolder = "/dev" +) + + +// Converts the timeout values for Linux / POSIX systems +// Moved to this new source module from serial.go +func posixTimeoutValues(readTimeout time.Duration) (vmin uint8, vtime uint8) { + const MAXUINT8 = 1<<8 - 1 // 255 + // set blocking / non-blocking read + var minBytesToRead uint8 = 1 + var readTimeoutInDeci int64 + if readTimeout > 0 { + // EOF on zero read + minBytesToRead = 0 + // convert timeout to deciseconds as expected by VTIME + readTimeoutInDeci = (readTimeout.Nanoseconds() / 1e6 / 100) + // capping the timeout + if readTimeoutInDeci < 1 { + // min possible timeout 1 Deciseconds (0.1s) + readTimeoutInDeci = 1 + } else if readTimeoutInDeci > MAXUINT8 { + // max possible timeout is 255 deciseconds (25.5s) + readTimeoutInDeci = MAXUINT8 + } + } + return minBytesToRead, uint8(readTimeoutInDeci) +} + +/* +This function was taken with minor modifications from the go.bug.st/serial package (https://github.com/bugst/go-serial), and is subject to the conditions of its license (reproduced below): + +Copyright (c) 2014, Cristian Maglie. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +func listPorts() ([]string, error) { + files, err := ioutil.ReadDir(devFolder) + if err != nil { + return nil, err + } + + ports := make([]string, 0, len(files)) + for _, f := range files { + // Skip folders + if f.IsDir() { + continue + } + + // Keep only devices with name matching the port name filter defined for this platform + // (discarding placeholder entries for non-existent onboard COM ports) + match, err := regexp.MatchString(portNameFilter, f.Name()) + if err != nil { + return nil, err + } + if !match || isALegacyPlaceholder(f.Name()) { + continue + } + + // Save serial port in the resulting list + ports = append(ports, devFolder + "/" + f.Name()) + } + + return ports, nil +} + +// Checks whether port entry is just a placeholder -- e.g. reserved for a legacy ISA COM +// port that doesn't exist +func isALegacyPlaceholder(portName string) (bool){ + const legacyComPortPrefix = "ttyS" + if strings.HasPrefix(portName, legacyComPortPrefix) { + port, err := openPort(devFolder + "/" + portName, 9600, 100) + if err != nil { + return true; + } else { + port.Close() + } + } + return false; +} diff --git a/posix_darwin.go b/posix_darwin.go new file mode 100644 index 0000000..ab292d4 --- /dev/null +++ b/posix_darwin.go @@ -0,0 +1,15 @@ +//+build darwin + +package serial + +// +// Apparently OSX creates a CU (Call-Up) and a tty device entry for +// each attached serial device, prefixing them with 'cu.' and 'tty.' +// respectively +// Should maybe restrict filter to cu devices? +// (see http://pbxbook.com/other/mac-tty.html) +// Although linux has dispensed with / deprecated this distinction: +// http://tldp.org/HOWTO/Modem-HOWTO-9.html#ss9.8 +const ( + portNameFilter = `^(cu|tty)\..*` +) diff --git a/posix_linux.go b/posix_linux.go new file mode 100644 index 0000000..23b2190 --- /dev/null +++ b/posix_linux.go @@ -0,0 +1,15 @@ +//+build linux + +package serial + +/* +This heuristic regex filter should catch most serial devices under linux. +The prefixes represent devices of the following types +ttyS: onboard uarts +ttyUSB: USB<->uart bridges +ttyACM: Abstract Control Model devices (e.g. modems -- see https://www.rfc1149.net/blog/2013/03/05/what-is-the-difference-between-devttyusbx-and-devttyacmx/) +ttyAMA: Don't know what AMA stands for, but seems to be used for Raspberry PI onboard ports at least +*/ +const ( + portNameFilter = `^(ttyS|ttyUSB|ttyACM|ttyAMA)\d+` +) diff --git a/posix_other.go b/posix_other.go new file mode 100644 index 0000000..d16b9eb --- /dev/null +++ b/posix_other.go @@ -0,0 +1,10 @@ +// +build !linux,!darwin + +package serial + +/* +This is a catchall for posix platforms other than linux and OS X +*/ +const ( + portNameFilter = `^tty.*` +) diff --git a/serial.go b/serial.go index 4717738..c5bfcb0 100644 --- a/serial.go +++ b/serial.go @@ -90,32 +90,18 @@ type Config struct { // CRLFTranslate bool } -// OpenPort opens a serial port with the specified configuration func OpenPort(c *Config) (*Port, error) { return openPort(c.Name, c.Baud, c.ReadTimeout) } -// Converts the timeout values for Linux / POSIX systems -func posixTimeoutValues(readTimeout time.Duration) (vmin uint8, vtime uint8) { - const MAXUINT8 = 1<<8 - 1 // 255 - // set blocking / non-blocking read - var minBytesToRead uint8 = 1 - var readTimeoutInDeci int64 - if readTimeout > 0 { - // EOF on zero read - minBytesToRead = 0 - // convert timeout to deciseconds as expected by VTIME - readTimeoutInDeci = (readTimeout.Nanoseconds() / 1e6 / 100) - // capping the timeout - if readTimeoutInDeci < 1 { - // min possible timeout 1 Deciseconds (0.1s) - readTimeoutInDeci = 1 - } else if readTimeoutInDeci > MAXUINT8 { - // max possible timeout is 255 deciseconds (25.5s) - readTimeoutInDeci = MAXUINT8 - } - } - return minBytesToRead, uint8(readTimeoutInDeci) +// ListPorts returns a list of serial ports identified on the system. +// The windows implementation queries the registry and should be accurate. +// The implementation for posix platforms just uses an heuristic approach, applying a regex +// filter to the contents of /dev. Sensible defaults for linux and OS X have been defined in +// 'posix_linux.go' and 'posix_darwin.go', respectively. On any other posix platform, +// all devices matching the overly-inclusive pattern '^tty.*' will be returned. +func ListPorts() ([]string, error) { + return listPorts() } // func SendBreak() diff --git a/serial_posix.go b/serial_posix.go index 95a592b..6339f98 100644 --- a/serial_posix.go +++ b/serial_posix.go @@ -14,7 +14,6 @@ import ( "os" "syscall" "time" - //"unsafe" ) func openPort(name string, baud int, readTimeout time.Duration) (p *Port, err error) { @@ -55,7 +54,6 @@ func openPort(name string, baud int, readTimeout time.Duration) (p *Port, err er f.Close() return nil, fmt.Errorf("Unknown baud rate %v", baud) } - _, err = C.cfsetispeed(&st, speed) if err != nil { f.Close() diff --git a/serial_windows.go b/serial_windows.go index c158dfa..cbede0f 100644 --- a/serial_windows.go +++ b/serial_windows.go @@ -3,6 +3,7 @@ package serial import ( + "errors" "fmt" "os" "sync" @@ -130,7 +131,83 @@ func (p *Port) Read(buf []byte) (int, error) { // Discards data written to the port but not transmitted, // or data received but not read func (p *Port) Flush() error { - return purgeComm(p.fd) + err := purgeComm(p.fd) + clearCommError(p.fd) + return err +} + +/* +This function was taken with minor modifications from the go.bug.st/serial package (https://github.com/bugst/go-serial), and is subject to the conditions of its license (reproduced below): + +Copyright (c) 2014, Cristian Maglie. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +func listPorts() ([]string, error) { + subKey, err := syscall.UTF16PtrFromString("HARDWARE\\DEVICEMAP\\SERIALCOMM\\") + if err != nil { + return nil, errors.New("Error enumerating ports") + } + + var h syscall.Handle + if syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, subKey, 0, syscall.KEY_READ, &h) != nil { + return nil, errors.New("Error enumerating ports") + } + defer syscall.RegCloseKey(h) + + var valuesCount uint32 + if syscall.RegQueryInfoKey(h, nil, nil, nil, nil, nil, nil, &valuesCount, nil, nil, nil, nil) != nil { + return nil, errors.New("Error enumerating ports") + } + + list := make([]string, valuesCount) + for i := range list { + var data [1024]uint16 + dataSize := uint32(len(data)) + var name [1024]uint16 + nameSize := uint32(len(name)) + if regEnumValue(h, uint32(i), &name[0], &nameSize, nil, nil, &data[0], &dataSize) != nil { + return nil, errors.New("Error enumerating ports") + } + list[i] = syscall.UTF16ToString(data[:]) + } + return list, nil +} + +func regEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, class *uint16, value *uint16, valueLen *uint32) (regerrno error) { + r0, _, _ := syscall.Syscall9(nRegEnumValueW, 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(value)), uintptr(unsafe.Pointer(valueLen)), 0) + if r0 != 0 { + return syscall.Errno(r0) + } + return nil } var ( @@ -142,7 +219,9 @@ var ( nCreateEvent, nResetEvent, nPurgeComm, - nFlushFileBuffers uintptr + nFlushFileBuffers, + nClearCommError, + nRegEnumValueW uintptr ) func init() { @@ -151,6 +230,11 @@ func init() { panic("LoadLibrary " + err.Error()) } defer syscall.FreeLibrary(k32) + api32, err := syscall.LoadLibrary("advapi32.dll") + if err != nil { + panic("LoadLibrary " + err.Error()) + } + defer syscall.FreeLibrary(api32) nSetCommState = getProcAddr(k32, "SetCommState") nSetCommTimeouts = getProcAddr(k32, "SetCommTimeouts") @@ -161,6 +245,12 @@ func init() { nResetEvent = getProcAddr(k32, "ResetEvent") nPurgeComm = getProcAddr(k32, "PurgeComm") nFlushFileBuffers = getProcAddr(k32, "FlushFileBuffers") + nClearCommError = getProcAddr(k32, "ClearCommError") + nRegEnumValueW = getProcAddr(api32, "RegEnumValueW") +} + +func clearCommError(h syscall.Handle) error { + return processSyscall(nClearCommError, 1, uintptr(h), 0, 0) } func getProcAddr(lib syscall.Handle, name string) uintptr { @@ -171,6 +261,14 @@ func getProcAddr(lib syscall.Handle, name string) uintptr { return addr } +func processSyscall(systemMethod, nargs, a1, a2, a3 uintptr) error { + result, _, err := syscall.Syscall(systemMethod, nargs, a1, a2, a3) + if result == 0 { + return err + } + return nil +} + func setCommState(h syscall.Handle, baud int) error { var params structDCB params.DCBlength = uint32(unsafe.Sizeof(params)) @@ -178,6 +276,9 @@ func setCommState(h syscall.Handle, baud int) error { params.flags[0] = 0x01 // fBinary params.flags[0] |= 0x10 // Assert DSR + //ADDITION: To help with this problem: http://zachsaw.blogspot.ie/2010/07/net-serialport-woes.html + params.flags[1] &= ^byte(0x40) + params.BaudRate = uint32(baud) params.ByteSize = 8