├── enumerator ├── usb_wasm.go ├── usb_freebsd.go ├── usb_openbsd.go ├── doc.go ├── example_getdetailedportlist_test.go ├── enumerator.go ├── usb_windows_test.go ├── usb_linux.go ├── syscall_windows.go ├── usb_darwin.go └── usb_windows.go ├── go.mod ├── serial_specialbaudrate_linux_ppc64le.go ├── enumerator_wasm.go ├── serial_wasm.go ├── serial_bsd.go ├── serial_resetbuf_linux_bsd.go ├── example_getportlist_test.go ├── serial_specialbaudrate_linux.go ├── example_serialport_test.go ├── README.md ├── portlist └── portlist.go ├── .github ├── ISSUE_TEMPLATE │ ├── feature-request.yml │ └── bug-report.yml └── workflows │ └── test.yaml ├── serial_darwin_386.go ├── serial_darwin.go ├── serial_darwin_64.go ├── example_modem_bits_test.go ├── LICENSE ├── example_test.go ├── go.sum ├── unixutils ├── pipe.go └── select.go ├── serial_linux_test.go ├── serial_freebsd.go ├── serial_openbsd.go ├── serial_linux.go ├── doc.go ├── serial.go ├── serial_unix.go └── serial_windows.go /enumerator/usb_wasm.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | func nativeGetDetailedPortsList() ([]*PortDetails, error) { 10 | return nil, &PortEnumerationError{} 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.bug.st/serial 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/creack/goselect v0.1.2 7 | github.com/stretchr/testify v1.8.4 8 | golang.org/x/sys v0.19.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /enumerator/usb_freebsd.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | func nativeGetDetailedPortsList() ([]*PortDetails, error) { 10 | // TODO 11 | return nil, &PortEnumerationError{} 12 | } 13 | -------------------------------------------------------------------------------- /enumerator/usb_openbsd.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | func nativeGetDetailedPortsList() ([]*PortDetails, error) { 10 | // TODO 11 | return nil, &PortEnumerationError{} 12 | } 13 | -------------------------------------------------------------------------------- /serial_specialbaudrate_linux_ppc64le.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | func (port *unixPort) setSpecialBaudrate(speed uint32) error { 10 | // TODO: unimplemented 11 | return &PortError{code: InvalidSpeed} 12 | } 13 | -------------------------------------------------------------------------------- /enumerator_wasm.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import ( 10 | "errors" 11 | ) 12 | 13 | func nativeGetPortsList() ([]string, error) { 14 | return nil, errors.New("nativeGetPortsList is not supported on wasm") 15 | } 16 | -------------------------------------------------------------------------------- /serial_wasm.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import ( 10 | "errors" 11 | ) 12 | 13 | func nativeOpen(portName string, mode *Mode) (Port, error) { 14 | return nil, errors.New("nativeOpen is not supported on wasm") 15 | } 16 | -------------------------------------------------------------------------------- /serial_bsd.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 8 | 9 | package serial 10 | 11 | import "golang.org/x/sys/unix" 12 | 13 | func (port *unixPort) Drain() error { 14 | return unix.IoctlSetInt(port.handle, unix.TIOCDRAIN, 0) 15 | } 16 | -------------------------------------------------------------------------------- /serial_resetbuf_linux_bsd.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | //go:build linux || freebsd || openbsd 8 | 9 | package serial 10 | 11 | import "golang.org/x/sys/unix" 12 | 13 | func (port *unixPort) ResetInputBuffer() error { 14 | return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) 15 | } 16 | 17 | func (port *unixPort) ResetOutputBuffer() error { 18 | return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) 19 | } 20 | -------------------------------------------------------------------------------- /enumerator/doc.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | /* 8 | Package enumerator is a golang cross-platform library for USB serial port discovery. 9 | 10 | This library has been tested on Linux, Windows and Mac and uses specific OS 11 | services to enumerate USB PID/VID, in particular on MacOSX the use of cgo is 12 | required in order to access the IOKit Framework. This means that the library 13 | cannot be easily cross compiled for darwin/* targets. 14 | */ 15 | package enumerator 16 | -------------------------------------------------------------------------------- /example_getportlist_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial_test 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | 13 | "go.bug.st/serial" 14 | ) 15 | 16 | func ExampleGetPortsList() { 17 | ports, err := serial.GetPortsList() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | if len(ports) == 0 { 22 | fmt.Println("No serial ports found!") 23 | } else { 24 | for _, port := range ports { 25 | fmt.Printf("Found port: %v\n", port) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /serial_specialbaudrate_linux.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | //go:build linux && !ppc64le 8 | 9 | package serial 10 | 11 | import "golang.org/x/sys/unix" 12 | 13 | func (port *unixPort) setSpecialBaudrate(speed uint32) error { 14 | settings, err := unix.IoctlGetTermios(port.handle, unix.TCGETS2) 15 | if err != nil { 16 | return err 17 | } 18 | settings.Cflag &^= unix.CBAUD 19 | settings.Cflag |= unix.BOTHER 20 | settings.Ispeed = speed 21 | settings.Ospeed = speed 22 | return unix.IoctlSetTermios(port.handle, unix.TCSETS2, settings) 23 | } 24 | -------------------------------------------------------------------------------- /example_serialport_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial_test 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | 13 | "go.bug.st/serial" 14 | ) 15 | 16 | func ExamplePort_SetMode() { 17 | port, err := serial.Open("/dev/ttyACM0", &serial.Mode{}) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | mode := &serial.Mode{ 22 | BaudRate: 9600, 23 | Parity: serial.NoParity, 24 | DataBits: 8, 25 | StopBits: serial.OneStopBit, 26 | } 27 | if err := port.SetMode(mode); err != nil { 28 | log.Fatal(err) 29 | } 30 | fmt.Println("Port set to 9600 N81") 31 | } 32 | -------------------------------------------------------------------------------- /enumerator/example_getdetailedportlist_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator_test 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | 13 | "go.bug.st/serial/enumerator" 14 | ) 15 | 16 | func ExampleGetDetailedPortsList() { 17 | ports, err := enumerator.GetDetailedPortsList() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | if len(ports) == 0 { 22 | fmt.Println("No serial ports found!") 23 | return 24 | } 25 | for _, port := range ports { 26 | fmt.Printf("Found port: %s\n", port.Name) 27 | if port.IsUSB { 28 | fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID) 29 | fmt.Printf(" USB serial %s\n", port.SerialNumber) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/bugst/go-serial/workflows/test/badge.svg)](https://github.com/bugst/go-serial/actions?workflow=test) 2 | 3 | # go.bug.st/serial 4 | 5 | A cross-platform serial port library for Go. 6 | 7 | ## Documentation and examples 8 | 9 | See the package documentation here: https://pkg.go.dev/go.bug.st/serial 10 | 11 | ## go.mod transition 12 | 13 | This library supports `go.mod` with the import `go.bug.st/serial`. 14 | 15 | If you came from the pre-`go.mod` era please update your import paths from `go.bug.st/serial.v1` to `go.bug.st/serial` to receive updates. The latest `v1` release is still available using the old import path. 16 | 17 | ## Credits 18 | 19 | :sparkles: Thanks to all awesome [contributors]! :sparkles: 20 | 21 | ## License 22 | 23 | This software is released under the [BSD 3-clause license]. 24 | 25 | [contributors]: https://github.com/bugst/go-serial/graphs/contributors 26 | [BSD 3-clause license]: https://github.com/bugst/go-serial/blob/master/LICENSE 27 | 28 | -------------------------------------------------------------------------------- /portlist/portlist.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | // portlist is a tool to list all the available serial ports. 8 | // Just run it and it will produce an output like: 9 | // 10 | // $ go run portlist.go 11 | // Port: /dev/cu.Bluetooth-Incoming-Port 12 | // Port: /dev/cu.usbmodemFD121 13 | // USB ID 2341:8053 14 | // USB serial FB7B6060504B5952302E314AFF08191A 15 | 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "log" 21 | 22 | "go.bug.st/serial/enumerator" 23 | ) 24 | 25 | func main() { 26 | ports, err := enumerator.GetDetailedPortsList() 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | if len(ports) == 0 { 31 | return 32 | } 33 | for _, port := range ports { 34 | fmt.Printf("Port: %s\n", port.Name) 35 | if port.Product != "" { 36 | fmt.Printf(" Product Name: %s\n", port.Product) 37 | } 38 | if port.IsUSB { 39 | fmt.Printf(" USB ID : %s:%s\n", port.VID, port.PID) 40 | fmt.Printf(" USB serial : %s\n", port.SerialNumber) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/arduino/tooling-project-assets/blob/main/issue-templates/forms/platform-dependent/feature-request.yml 2 | # See: https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms 3 | 4 | name: Feature request 5 | description: Suggest an enhancement to this project. 6 | labels: 7 | - "type: enhancement" 8 | body: 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Describe the new feature or change suggestion 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: additional 17 | attributes: 18 | label: Additional context 19 | description: Add any additional information about the feature request here. 20 | - type: checkboxes 21 | id: checklist 22 | attributes: 23 | label: Issue checklist 24 | description: Please double-check that you have done each of the following things before submitting the issue. 25 | options: 26 | - label: I searched for previous requests in [the issue tracker](https://github.com/bugst/go-serial/issues) 27 | required: true 28 | - label: My request contains all necessary details 29 | required: true 30 | -------------------------------------------------------------------------------- /serial_darwin_386.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import "golang.org/x/sys/unix" 10 | 11 | // termios manipulation functions 12 | 13 | var baudrateMap = map[int]uint32{ 14 | 0: unix.B9600, // Default to 9600 15 | 50: unix.B50, 16 | 75: unix.B75, 17 | 110: unix.B110, 18 | 134: unix.B134, 19 | 150: unix.B150, 20 | 200: unix.B200, 21 | 300: unix.B300, 22 | 600: unix.B600, 23 | 1200: unix.B1200, 24 | 1800: unix.B1800, 25 | 2400: unix.B2400, 26 | 4800: unix.B4800, 27 | 9600: unix.B9600, 28 | 19200: unix.B19200, 29 | 38400: unix.B38400, 30 | 57600: unix.B57600, 31 | 115200: unix.B115200, 32 | 230400: unix.B230400, 33 | } 34 | 35 | var databitsMap = map[int]uint32{ 36 | 0: unix.CS8, // Default to 8 bits 37 | 5: unix.CS5, 38 | 6: unix.CS6, 39 | 7: unix.CS7, 40 | 8: unix.CS8, 41 | } 42 | 43 | const tcCMSPAR uint32 = 0 // may be CMSPAR or PAREXT 44 | const tcIUCLC uint32 = 0 45 | 46 | const tcCCTS_OFLOW uint32 = 0x00010000 47 | const tcCRTS_IFLOW uint32 = 0x00020000 48 | 49 | const tcCRTSCTS uint32 = (tcCCTS_OFLOW | tcCRTS_IFLOW) 50 | 51 | func toTermiosSpeedType(speed uint32) uint32 { 52 | return speed 53 | } 54 | -------------------------------------------------------------------------------- /serial_darwin.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import ( 10 | "regexp" 11 | 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | const devFolder = "/dev" 16 | 17 | var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*") 18 | 19 | const ioctlTcgetattr = unix.TIOCGETA 20 | const ioctlTcsetattr = unix.TIOCSETA 21 | const ioctlTcflsh = unix.TIOCFLUSH 22 | const ioctlTioccbrk = unix.TIOCCBRK 23 | const ioctlTiocsbrk = unix.TIOCSBRK 24 | 25 | func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { 26 | baudrate, ok := baudrateMap[speed] 27 | if !ok { 28 | return nil, true 29 | } 30 | settings.Ispeed = toTermiosSpeedType(baudrate) 31 | settings.Ospeed = toTermiosSpeedType(baudrate) 32 | return nil, false 33 | } 34 | 35 | func (port *unixPort) setSpecialBaudrate(speed uint32) error { 36 | const kIOSSIOSPEED = 0x80045402 37 | return unix.IoctlSetPointerInt(port.handle, kIOSSIOSPEED, int(speed)) 38 | } 39 | 40 | func (port *unixPort) ResetInputBuffer() error { 41 | return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) 42 | } 43 | 44 | func (port *unixPort) ResetOutputBuffer() error { 45 | return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) 46 | } 47 | -------------------------------------------------------------------------------- /serial_darwin_64.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | //go:build darwin && (amd64 || arm64) 8 | 9 | package serial 10 | 11 | import "golang.org/x/sys/unix" 12 | 13 | // termios manipulation functions 14 | 15 | var baudrateMap = map[int]uint64{ 16 | 0: unix.B9600, // Default to 9600 17 | 50: unix.B50, 18 | 75: unix.B75, 19 | 110: unix.B110, 20 | 134: unix.B134, 21 | 150: unix.B150, 22 | 200: unix.B200, 23 | 300: unix.B300, 24 | 600: unix.B600, 25 | 1200: unix.B1200, 26 | 1800: unix.B1800, 27 | 2400: unix.B2400, 28 | 4800: unix.B4800, 29 | 9600: unix.B9600, 30 | 19200: unix.B19200, 31 | 38400: unix.B38400, 32 | 57600: unix.B57600, 33 | 115200: unix.B115200, 34 | 230400: unix.B230400, 35 | } 36 | 37 | var databitsMap = map[int]uint64{ 38 | 0: unix.CS8, // Default to 8 bits 39 | 5: unix.CS5, 40 | 6: unix.CS6, 41 | 7: unix.CS7, 42 | 8: unix.CS8, 43 | } 44 | 45 | const tcCMSPAR uint64 = 0 // may be CMSPAR or PAREXT 46 | const tcIUCLC uint64 = 0 47 | 48 | const tcCCTS_OFLOW uint64 = 0x00010000 49 | const tcCRTS_IFLOW uint64 = 0x00020000 50 | 51 | const tcCRTSCTS uint64 = (tcCCTS_OFLOW | tcCRTS_IFLOW) 52 | 53 | func toTermiosSpeedType(speed uint64) uint64 { 54 | return speed 55 | } 56 | -------------------------------------------------------------------------------- /example_modem_bits_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial_test 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "time" 13 | 14 | "go.bug.st/serial" 15 | ) 16 | 17 | func ExamplePort_GetModemStatusBits() { 18 | // Open the first serial port detected at 9600bps N81 19 | mode := &serial.Mode{ 20 | BaudRate: 9600, 21 | Parity: serial.NoParity, 22 | DataBits: 8, 23 | StopBits: serial.OneStopBit, 24 | } 25 | port, err := serial.Open("/dev/ttyACM1", mode) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | defer port.Close() 30 | 31 | count := 0 32 | for count < 25 { 33 | status, err := port.GetModemStatusBits() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Printf("Status: %+v\n", status) 38 | 39 | time.Sleep(time.Second) 40 | count++ 41 | if count == 5 { 42 | err := port.SetDTR(false) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | fmt.Println("Set DTR OFF") 47 | } 48 | if count == 10 { 49 | err := port.SetDTR(true) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | fmt.Println("Set DTR ON") 54 | } 55 | if count == 15 { 56 | err := port.SetRTS(false) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | fmt.Println("Set RTS OFF") 61 | } 62 | if count == 20 { 63 | err := port.SetRTS(true) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | fmt.Println("Set RTS ON") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /enumerator/enumerator.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output syscall_windows.go usb_windows.go 10 | 11 | // PortDetails contains detailed information about USB serial port. 12 | // Use GetDetailedPortsList function to retrieve it. 13 | type PortDetails struct { 14 | Name string 15 | IsUSB bool 16 | VID string 17 | PID string 18 | SerialNumber string 19 | 20 | // Manufacturer string 21 | 22 | // Product is an OS-dependent string that describes the serial port, it may 23 | // be not always available and it may be different across OS. 24 | Product string 25 | } 26 | 27 | // GetDetailedPortsList retrieve ports details like USB VID/PID. 28 | // Please note that this function may not be available on all OS: 29 | // in that case a FunctionNotImplemented error is returned. 30 | func GetDetailedPortsList() ([]*PortDetails, error) { 31 | return nativeGetDetailedPortsList() 32 | } 33 | 34 | // PortEnumerationError is the error type for serial ports enumeration 35 | type PortEnumerationError struct { 36 | causedBy error 37 | } 38 | 39 | // Error returns the complete error code with details on the cause of the error 40 | func (e PortEnumerationError) Error() string { 41 | reason := "Error while enumerating serial ports" 42 | if e.causedBy != nil { 43 | reason += ": " + e.causedBy.Error() 44 | } 45 | return reason 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2014-2024, Cristian Maglie. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in 14 | the documentation and/or other materials provided with the 15 | distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | 34 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial_test 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "strings" 13 | 14 | "go.bug.st/serial" 15 | ) 16 | 17 | // This example prints the list of serial ports and use the first one 18 | // to send a string "10,20,30" and prints the response on the screen. 19 | func Example_sendAndReceive() { 20 | 21 | // Retrieve the port list 22 | ports, err := serial.GetPortsList() 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | if len(ports) == 0 { 27 | log.Fatal("No serial ports found!") 28 | } 29 | 30 | // Print the list of detected ports 31 | for _, port := range ports { 32 | fmt.Printf("Found port: %v\n", port) 33 | } 34 | 35 | // Open the first serial port detected at 9600bps N81 36 | mode := &serial.Mode{ 37 | BaudRate: 9600, 38 | Parity: serial.NoParity, 39 | DataBits: 8, 40 | StopBits: serial.OneStopBit, 41 | } 42 | port, err := serial.Open(ports[0], mode) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | // Send the string "10,20,30\n\r" to the serial port 48 | n, err := port.Write([]byte("10,20,30\n\r")) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | fmt.Printf("Sent %v bytes\n", n) 53 | 54 | // Read and print the response 55 | 56 | buff := make([]byte, 100) 57 | for { 58 | // Reads up to 100 bytes 59 | n, err := port.Read(buff) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | if n == 0 { 64 | fmt.Println("\nEOF") 65 | break 66 | } 67 | 68 | fmt.Printf("%s", string(buff[:n])) 69 | 70 | // If we receive a newline stop reading 71 | if strings.Contains(string(buff[:n]), "\n") { 72 | break 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /enumerator/usb_windows_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func parseAndReturnDeviceID(deviceID string) *PortDetails { 16 | res := &PortDetails{} 17 | parseDeviceID(deviceID, res) 18 | return res 19 | } 20 | 21 | func TestParseDeviceID(t *testing.T) { 22 | r := require.New(t) 23 | test := func(deviceId, vid, pid, serialNo string) { 24 | res := parseAndReturnDeviceID(deviceId) 25 | r.True(res.IsUSB) 26 | r.Equal(vid, res.VID) 27 | r.Equal(pid, res.PID) 28 | r.Equal(serialNo, res.SerialNumber) 29 | } 30 | 31 | test("FTDIBUS\\VID_0403+PID_6001+A6004CCFA\\0000", "0403", "6001", "A6004CCFA") 32 | test("USB\\VID_16C0&PID_0483\\12345", "16C0", "0483", "12345") 33 | test("USB\\VID_2341&PID_0000\\64936333936351400000", "2341", "0000", "64936333936351400000") 34 | test("USB\\VID_2341&PID_0000\\6493234373835191F1F1", "2341", "0000", "6493234373835191F1F1") 35 | test("USB\\VID_2341&PID_804E&MI_00\\6&279A3900&0&0000", "2341", "804E", "") 36 | test("USB\\VID_2341&PID_004E\\5&C3DC240&0&1", "2341", "004E", "") 37 | test("USB\\VID_03EB&PID_2111&MI_01\\6&21F3553F&0&0001", "03EB", "2111", "") // Atmel EDBG 38 | test("USB\\VID_2341&PID_804D&MI_00\\6&1026E213&0&0000", "2341", "804D", "") 39 | test("USB\\VID_2341&PID_004D\\5&C3DC240&0&1", "2341", "004D", "") 40 | test("USB\\VID_067B&PID_2303\\6&2C4CB384&0&3", "067B", "2303", "") // PL2303 41 | } 42 | 43 | func TestParseDeviceIDWithInvalidStrings(t *testing.T) { 44 | r := require.New(t) 45 | res := parseAndReturnDeviceID("ABC") 46 | r.False(res.IsUSB) 47 | res2 := parseAndReturnDeviceID("USB") 48 | r.False(res2.IsUSB) 49 | } 50 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= 2 | github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 11 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 13 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 14 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 15 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 16 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /unixutils/pipe.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | //go:build linux || darwin || freebsd || openbsd 8 | 9 | package unixutils 10 | 11 | import ( 12 | "fmt" 13 | "syscall" 14 | ) 15 | 16 | // Pipe represents a unix-pipe 17 | type Pipe struct { 18 | opened bool 19 | rd int 20 | wr int 21 | } 22 | 23 | // Open creates a new pipe 24 | func (p *Pipe) Open() error { 25 | fds := []int{0, 0} 26 | if err := syscall.Pipe(fds); err != nil { 27 | return err 28 | } 29 | p.rd = fds[0] 30 | p.wr = fds[1] 31 | p.opened = true 32 | return nil 33 | } 34 | 35 | // ReadFD returns the file handle for the read side of the pipe. 36 | func (p *Pipe) ReadFD() int { 37 | if !p.opened { 38 | return -1 39 | } 40 | return p.rd 41 | } 42 | 43 | // WriteFD returns the file handle for the write side of the pipe. 44 | func (p *Pipe) WriteFD() int { 45 | if !p.opened { 46 | return -1 47 | } 48 | return p.wr 49 | } 50 | 51 | // Write to the pipe the content of data. Returns the number of bytes written. 52 | func (p *Pipe) Write(data []byte) (int, error) { 53 | if !p.opened { 54 | return 0, fmt.Errorf("Pipe not opened") 55 | } 56 | return syscall.Write(p.wr, data) 57 | } 58 | 59 | // Read from the pipe into the data array. Returns the number of bytes read. 60 | func (p *Pipe) Read(data []byte) (int, error) { 61 | if !p.opened { 62 | return 0, fmt.Errorf("Pipe not opened") 63 | } 64 | return syscall.Read(p.rd, data) 65 | } 66 | 67 | // Close the pipe 68 | func (p *Pipe) Close() error { 69 | if !p.opened { 70 | return fmt.Errorf("Pipe not opened") 71 | } 72 | err1 := syscall.Close(p.rd) 73 | err2 := syscall.Close(p.wr) 74 | p.opened = false 75 | if err1 != nil { 76 | return err1 77 | } 78 | if err2 != nil { 79 | return err2 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /serial_linux_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | // Testing code idea and fix thanks to @angri 8 | // https://github.com/bugst/go-serial/pull/42 9 | 10 | package serial 11 | 12 | import ( 13 | "context" 14 | "os/exec" 15 | "testing" 16 | "time" 17 | 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func startSocatAndWaitForPort(t *testing.T, ctx context.Context) *exec.Cmd { 22 | cmd := exec.CommandContext(ctx, "socat", "-D", "STDIO", "pty,link=/tmp/faketty") 23 | r, err := cmd.StderrPipe() 24 | require.NoError(t, err) 25 | require.NoError(t, cmd.Start()) 26 | // Let our fake serial port node appear. 27 | // socat will write to stderr before starting transfer phase; 28 | // we don't really care what, just that it did, because then it's ready. 29 | buf := make([]byte, 1024) 30 | _, err = r.Read(buf) 31 | require.NoError(t, err) 32 | return cmd 33 | } 34 | 35 | func TestSerialReadAndCloseConcurrency(t *testing.T) { 36 | 37 | // Run this test with race detector to actually test that 38 | // the correct multitasking behaviour is happening. 39 | 40 | ctx, cancel := context.WithCancel(context.Background()) 41 | defer cancel() 42 | cmd := startSocatAndWaitForPort(t, ctx) 43 | go cmd.Wait() 44 | 45 | port, err := Open("/tmp/faketty", &Mode{}) 46 | require.NoError(t, err) 47 | buf := make([]byte, 100) 48 | go port.Read(buf) 49 | // let port.Read to start 50 | time.Sleep(time.Millisecond * 1) 51 | port.Close() 52 | } 53 | 54 | func TestDoubleCloseIsNoop(t *testing.T) { 55 | ctx, cancel := context.WithCancel(context.Background()) 56 | defer cancel() 57 | cmd := startSocatAndWaitForPort(t, ctx) 58 | go cmd.Wait() 59 | 60 | port, err := Open("/tmp/faketty", &Mode{}) 61 | require.NoError(t, err) 62 | require.NoError(t, port.Close()) 63 | require.NoError(t, port.Close()) 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | native-os-build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macOS-latest] 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: "1.22" 22 | - name: Setup CGO Environment 23 | run: | 24 | if [ ${{ matrix.os }} == 'macOS-latest' ] ; then 25 | echo "CGO_ENABLED=1" >> "$GITHUB_ENV" 26 | fi 27 | shell: bash 28 | - name: Build AMD64 29 | run: GOARCH=amd64 go build -v ./... 30 | shell: bash 31 | - name: Build ARM64 32 | run: GOARCH=arm64 go build -v ./... 33 | shell: bash 34 | - name: Install socat 35 | if: matrix.os == 'ubuntu-latest' 36 | run: sudo apt-get install socat 37 | shell: bash 38 | - name: Run unit tests 39 | run: go test -v -race ./... 40 | shell: bash 41 | - name: Cross-build for 386 42 | if: matrix.os != 'macOS-latest' 43 | run: GOARCH=386 go build -v ./... 44 | shell: bash 45 | - name: Cross-build for arm 46 | if: matrix.os != 'macOS-latest' 47 | run: GOARCH=arm go build -v ./... 48 | shell: bash 49 | 50 | cross-os-build: 51 | strategy: 52 | matrix: 53 | go-os-pairs: 54 | - "freebsd amd64" 55 | - "openbsd amd64" 56 | - "openbsd 386" 57 | - "openbsd arm" 58 | - "linux ppc64le" 59 | 60 | runs-on: "ubuntu-latest" 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | - uses: actions/setup-go@v5 65 | with: 66 | go-version: "1.22" 67 | - name: Cross-build 68 | run: | 69 | set ${{ matrix.go-os-pairs }} 70 | GOOS=$1 GOARCH=$2 go build -v ./... 71 | shell: bash 72 | -------------------------------------------------------------------------------- /serial_freebsd.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import ( 10 | "regexp" 11 | 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | const devFolder = "/dev" 16 | 17 | var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*") 18 | 19 | // termios manipulation functions 20 | 21 | var baudrateMap = map[int]uint32{ 22 | 0: unix.B9600, // Default to 9600 23 | 50: unix.B50, 24 | 75: unix.B75, 25 | 110: unix.B110, 26 | 134: unix.B134, 27 | 150: unix.B150, 28 | 200: unix.B200, 29 | 300: unix.B300, 30 | 600: unix.B600, 31 | 1200: unix.B1200, 32 | 1800: unix.B1800, 33 | 2400: unix.B2400, 34 | 4800: unix.B4800, 35 | 9600: unix.B9600, 36 | 19200: unix.B19200, 37 | 38400: unix.B38400, 38 | 57600: unix.B57600, 39 | 115200: unix.B115200, 40 | 230400: unix.B230400, 41 | 460800: unix.B460800, 42 | 921600: unix.B921600, 43 | } 44 | 45 | var databitsMap = map[int]uint32{ 46 | 0: unix.CS8, // Default to 8 bits 47 | 5: unix.CS5, 48 | 6: unix.CS6, 49 | 7: unix.CS7, 50 | 8: unix.CS8, 51 | } 52 | 53 | const tcCMSPAR uint32 = 0 // may be CMSPAR or PAREXT 54 | const tcIUCLC uint32 = 0 55 | 56 | const tcCCTS_OFLOW uint32 = 0x00010000 57 | const tcCRTS_IFLOW uint32 = 0x00020000 58 | 59 | const tcCRTSCTS uint32 = tcCCTS_OFLOW 60 | 61 | const ioctlTcgetattr = unix.TIOCGETA 62 | const ioctlTcsetattr = unix.TIOCSETA 63 | const ioctlTcflsh = unix.TIOCFLUSH 64 | const ioctlTioccbrk = unix.TIOCCBRK 65 | const ioctlTiocsbrk = unix.TIOCSBRK 66 | 67 | func toTermiosSpeedType(speed uint32) uint32 { 68 | return speed 69 | } 70 | 71 | func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { 72 | baudrate, ok := baudrateMap[speed] 73 | if !ok { 74 | return nil, true 75 | } 76 | // XXX: Is Cflag really needed 77 | // revert old baudrate 78 | for _, rate := range baudrateMap { 79 | settings.Cflag &^= rate 80 | } 81 | // set new baudrate 82 | settings.Cflag |= baudrate 83 | 84 | settings.Ispeed = toTermiosSpeedType(baudrate) 85 | settings.Ospeed = toTermiosSpeedType(baudrate) 86 | return nil, false 87 | } 88 | 89 | func (port *unixPort) setSpecialBaudrate(speed uint32) error { 90 | // TODO: unimplemented 91 | return &PortError{code: InvalidSpeed} 92 | } 93 | -------------------------------------------------------------------------------- /serial_openbsd.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import ( 10 | "regexp" 11 | 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | const devFolder = "/dev" 16 | 17 | var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*") 18 | 19 | // termios manipulation functions 20 | 21 | var baudrateMap = map[int]uint32{ 22 | 0: unix.B9600, // Default to 9600 23 | 50: unix.B50, 24 | 75: unix.B75, 25 | 110: unix.B110, 26 | 134: unix.B134, 27 | 150: unix.B150, 28 | 200: unix.B200, 29 | 300: unix.B300, 30 | 600: unix.B600, 31 | 1200: unix.B1200, 32 | 1800: unix.B1800, 33 | 2400: unix.B2400, 34 | 4800: unix.B4800, 35 | 9600: unix.B9600, 36 | 19200: unix.B19200, 37 | 38400: unix.B38400, 38 | 57600: unix.B57600, 39 | 115200: unix.B115200, 40 | 230400: unix.B230400, 41 | //460800: unix.B460800, 42 | //921600: unix.B921600, 43 | } 44 | 45 | var databitsMap = map[int]uint32{ 46 | 0: unix.CS8, // Default to 8 bits 47 | 5: unix.CS5, 48 | 6: unix.CS6, 49 | 7: unix.CS7, 50 | 8: unix.CS8, 51 | } 52 | 53 | const tcCMSPAR uint32 = 0 // may be CMSPAR or PAREXT 54 | const tcIUCLC uint32 = 0 55 | 56 | const tcCCTS_OFLOW uint32 = 0x00010000 57 | const tcCRTS_IFLOW uint32 = 0x00020000 58 | 59 | const tcCRTSCTS uint32 = tcCCTS_OFLOW 60 | 61 | const ioctlTcgetattr = unix.TIOCGETA 62 | const ioctlTcsetattr = unix.TIOCSETA 63 | const ioctlTcflsh = unix.TIOCFLUSH 64 | const ioctlTioccbrk = unix.TIOCCBRK 65 | const ioctlTiocsbrk = unix.TIOCSBRK 66 | 67 | func toTermiosSpeedType(speed uint32) int32 { 68 | return int32(speed) 69 | } 70 | 71 | func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { 72 | baudrate, ok := baudrateMap[speed] 73 | if !ok { 74 | return nil, true 75 | } 76 | // XXX: Is Cflag really needed 77 | // revert old baudrate 78 | for _, rate := range baudrateMap { 79 | settings.Cflag &^= rate 80 | } 81 | // set new baudrate 82 | settings.Cflag |= baudrate 83 | 84 | settings.Ispeed = toTermiosSpeedType(baudrate) 85 | settings.Ospeed = toTermiosSpeedType(baudrate) 86 | return nil, false 87 | } 88 | 89 | func (port *unixPort) setSpecialBaudrate(speed uint32) error { 90 | // TODO: unimplemented 91 | return &PortError{code: InvalidSpeed} 92 | } 93 | -------------------------------------------------------------------------------- /serial_linux.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import ( 10 | "regexp" 11 | 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | const devFolder = "/dev" 16 | 17 | var osPortFilter = regexp.MustCompile("(ttyS|ttyHS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO|ttymxc)[0-9]{1,3}") 18 | 19 | // termios manipulation functions 20 | 21 | var baudrateMap = map[int]uint32{ 22 | 0: unix.B9600, // Default to 9600 23 | 50: unix.B50, 24 | 75: unix.B75, 25 | 110: unix.B110, 26 | 134: unix.B134, 27 | 150: unix.B150, 28 | 200: unix.B200, 29 | 300: unix.B300, 30 | 600: unix.B600, 31 | 1200: unix.B1200, 32 | 1800: unix.B1800, 33 | 2400: unix.B2400, 34 | 4800: unix.B4800, 35 | 9600: unix.B9600, 36 | 19200: unix.B19200, 37 | 38400: unix.B38400, 38 | 57600: unix.B57600, 39 | 115200: unix.B115200, 40 | 230400: unix.B230400, 41 | 460800: unix.B460800, 42 | 500000: unix.B500000, 43 | 576000: unix.B576000, 44 | 921600: unix.B921600, 45 | 1000000: unix.B1000000, 46 | 1152000: unix.B1152000, 47 | 1500000: unix.B1500000, 48 | 2000000: unix.B2000000, 49 | 2500000: unix.B2500000, 50 | 3000000: unix.B3000000, 51 | 3500000: unix.B3500000, 52 | 4000000: unix.B4000000, 53 | } 54 | 55 | var databitsMap = map[int]uint32{ 56 | 0: unix.CS8, // Default to 8 bits 57 | 5: unix.CS5, 58 | 6: unix.CS6, 59 | 7: unix.CS7, 60 | 8: unix.CS8, 61 | } 62 | 63 | const tcCMSPAR = unix.CMSPAR 64 | const tcIUCLC = unix.IUCLC 65 | 66 | const tcCRTSCTS uint32 = unix.CRTSCTS 67 | 68 | const ioctlTcgetattr = unix.TCGETS 69 | const ioctlTcsetattr = unix.TCSETS 70 | const ioctlTcflsh = unix.TCFLSH 71 | const ioctlTioccbrk = unix.TIOCCBRK 72 | const ioctlTiocsbrk = unix.TIOCSBRK 73 | 74 | func toTermiosSpeedType(speed uint32) uint32 { 75 | return speed 76 | } 77 | 78 | func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { 79 | baudrate, ok := baudrateMap[speed] 80 | if !ok { 81 | return nil, true 82 | } 83 | // revert old baudrate 84 | for _, rate := range baudrateMap { 85 | settings.Cflag &^= rate 86 | } 87 | // set new baudrate 88 | settings.Cflag |= baudrate 89 | settings.Ispeed = toTermiosSpeedType(baudrate) 90 | settings.Ospeed = toTermiosSpeedType(baudrate) 91 | return nil, false 92 | } 93 | 94 | func (port *unixPort) Drain() error { 95 | // It's not super well documented, but this is the same as calling tcdrain: 96 | // - https://git.musl-libc.org/cgit/musl/tree/src/termios/tcdrain.c 97 | // - https://elixir.bootlin.com/linux/v6.2.8/source/drivers/tty/tty_io.c#L2673 98 | return unix.IoctlSetInt(port.handle, unix.TCSBRK, 1) 99 | } 100 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/arduino/tooling-project-assets/blob/main/issue-templates/forms/platform-dependent/bug-report.yml 2 | # See: https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms 3 | 4 | name: Bug report 5 | description: Report a problem with the code or documentation in this repository. 6 | labels: 7 | - bug 8 | body: 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Describe the problem 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: reproduce 17 | attributes: 18 | label: To reproduce 19 | description: | 20 | Provide the specific set of steps we can follow to reproduce the 21 | problem in particular the exact golang source code you used. 22 | validations: 23 | required: true 24 | - type: checkboxes 25 | id: checklist-reproduce 26 | attributes: 27 | label: | 28 | Please double-check that you have reported each of the following 29 | before submitting the issue. 30 | options: 31 | - label: I've provided the FULL source code that causes the problem 32 | required: true 33 | - label: I've provided all the actions required to reproduce the problem 34 | required: true 35 | - type: textarea 36 | id: expected 37 | attributes: 38 | label: Expected behavior 39 | description: | 40 | What would you expect to happen after following those instructions? 41 | validations: 42 | required: true 43 | - type: input 44 | id: os 45 | attributes: 46 | label: Operating system and version 47 | description: | 48 | Which operating system(s) version are you using on your computer? 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: boards 53 | attributes: 54 | label: Please describe your hardware setup 55 | description: | 56 | Arduino boards, USB dongles, hubs or embedded devices you are using and how they 57 | are connected together. 58 | - type: textarea 59 | id: additional 60 | attributes: 61 | label: Additional context 62 | description: | 63 | Add here any additional information that you think might be relevant to 64 | the problem. 65 | validations: 66 | required: false 67 | - type: checkboxes 68 | id: checklist 69 | attributes: 70 | label: Issue checklist 71 | description: | 72 | Please double-check that you have done each of the following things before 73 | submitting the issue. 74 | options: 75 | - label: I searched for previous requests in [the issue tracker](https://github.com/bugst/go-serial/issues) 76 | required: true 77 | - label: My request contains all necessary details 78 | required: true 79 | -------------------------------------------------------------------------------- /unixutils/select.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | //go:build linux || darwin || freebsd || openbsd 8 | 9 | package unixutils 10 | 11 | import ( 12 | "time" 13 | 14 | "github.com/creack/goselect" 15 | ) 16 | 17 | // FDSet is a set of file descriptors suitable for a select call 18 | type FDSet struct { 19 | set goselect.FDSet 20 | max uintptr 21 | } 22 | 23 | // NewFDSet creates a set of file descriptors suitable for a Select call. 24 | func NewFDSet(fds ...int) *FDSet { 25 | s := &FDSet{} 26 | s.Add(fds...) 27 | return s 28 | } 29 | 30 | // Add adds the file descriptors passed as parameter to the FDSet. 31 | func (s *FDSet) Add(fds ...int) { 32 | for _, fd := range fds { 33 | f := uintptr(fd) 34 | s.set.Set(f) 35 | if f > s.max { 36 | s.max = f 37 | } 38 | } 39 | } 40 | 41 | // FDResultSets contains the result of a Select operation. 42 | type FDResultSets struct { 43 | readable *goselect.FDSet 44 | writeable *goselect.FDSet 45 | errors *goselect.FDSet 46 | } 47 | 48 | // IsReadable test if a file descriptor is ready to be read. 49 | func (r *FDResultSets) IsReadable(fd int) bool { 50 | return r.readable.IsSet(uintptr(fd)) 51 | } 52 | 53 | // IsWritable test if a file descriptor is ready to be written. 54 | func (r *FDResultSets) IsWritable(fd int) bool { 55 | return r.writeable.IsSet(uintptr(fd)) 56 | } 57 | 58 | // IsError test if a file descriptor is in error state. 59 | func (r *FDResultSets) IsError(fd int) bool { 60 | return r.errors.IsSet(uintptr(fd)) 61 | } 62 | 63 | // Select performs a select system call, 64 | // file descriptors in the rd set are tested for read-events, 65 | // file descriptors in the wd set are tested for write-events and 66 | // file descriptors in the er set are tested for error-events. 67 | // The function will block until an event happens or the timeout expires. 68 | // The function return an FDResultSets that contains all the file descriptor 69 | // that have a pending read/write/error event. 70 | func Select(rd, wr, er *FDSet, timeout time.Duration) (*FDResultSets, error) { 71 | max := uintptr(0) 72 | res := &FDResultSets{} 73 | if rd != nil { 74 | // fdsets are copied so the parameters are left untouched 75 | copyOfRd := rd.set 76 | res.readable = ©OfRd 77 | // Determine max fd. 78 | max = rd.max 79 | } 80 | if wr != nil { 81 | // fdsets are copied so the parameters are left untouched 82 | copyOfWr := wr.set 83 | res.writeable = ©OfWr 84 | // Determine max fd. 85 | if wr.max > max { 86 | max = wr.max 87 | } 88 | } 89 | if er != nil { 90 | // fdsets are copied so the parameters are left untouched 91 | copyOfEr := er.set 92 | res.errors = ©OfEr 93 | // Determine max fd. 94 | if er.max > max { 95 | max = er.max 96 | } 97 | } 98 | 99 | err := goselect.Select(int(max+1), res.readable, res.writeable, res.errors, timeout) 100 | return res, err 101 | } 102 | -------------------------------------------------------------------------------- /enumerator/usb_linux.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | import ( 10 | "bufio" 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | 15 | "go.bug.st/serial" 16 | ) 17 | 18 | func nativeGetDetailedPortsList() ([]*PortDetails, error) { 19 | // Retrieve the port list 20 | ports, err := serial.GetPortsList() 21 | if err != nil { 22 | return nil, &PortEnumerationError{causedBy: err} 23 | } 24 | 25 | var res []*PortDetails 26 | for _, port := range ports { 27 | details, err := nativeGetPortDetails(port) 28 | if err != nil { 29 | return nil, &PortEnumerationError{causedBy: err} 30 | } 31 | res = append(res, details) 32 | } 33 | return res, nil 34 | } 35 | 36 | func nativeGetPortDetails(portPath string) (*PortDetails, error) { 37 | portName := filepath.Base(portPath) 38 | devicePath := fmt.Sprintf("/sys/class/tty/%s/device", portName) 39 | if _, err := os.Stat(devicePath); err != nil { 40 | return &PortDetails{}, nil 41 | } 42 | realDevicePath, err := filepath.EvalSymlinks(devicePath) 43 | if err != nil { 44 | return nil, fmt.Errorf("Can't determine real path of %s: %s", devicePath, err.Error()) 45 | } 46 | subSystemPath, err := filepath.EvalSymlinks(filepath.Join(realDevicePath, "subsystem")) 47 | if err != nil { 48 | return nil, fmt.Errorf("Can't determine real path of %s: %s", filepath.Join(realDevicePath, "subsystem"), err.Error()) 49 | } 50 | subSystem := filepath.Base(subSystemPath) 51 | 52 | result := &PortDetails{Name: portPath} 53 | switch subSystem { 54 | case "usb-serial": 55 | err := parseUSBSysFS(filepath.Dir(filepath.Dir(realDevicePath)), result) 56 | return result, err 57 | case "usb": 58 | err := parseUSBSysFS(filepath.Dir(realDevicePath), result) 59 | return result, err 60 | // TODO: other cases? 61 | default: 62 | return result, nil 63 | } 64 | } 65 | 66 | func parseUSBSysFS(usbDevicePath string, details *PortDetails) error { 67 | vid, err := readLine(filepath.Join(usbDevicePath, "idVendor")) 68 | if err != nil { 69 | return err 70 | } 71 | pid, err := readLine(filepath.Join(usbDevicePath, "idProduct")) 72 | if err != nil { 73 | return err 74 | } 75 | serial, err := readLine(filepath.Join(usbDevicePath, "serial")) 76 | if err != nil { 77 | return err 78 | } 79 | //manufacturer, err := readLine(filepath.Join(usbDevicePath, "manufacturer")) 80 | //if err != nil { 81 | // return err 82 | //} 83 | //product, err := readLine(filepath.Join(usbDevicePath, "product")) 84 | //if err != nil { 85 | // return err 86 | //} 87 | 88 | details.IsUSB = true 89 | details.VID = vid 90 | details.PID = pid 91 | details.SerialNumber = serial 92 | //details.Manufacturer = manufacturer 93 | //details.Product = product 94 | return nil 95 | } 96 | 97 | func readLine(filename string) (string, error) { 98 | file, err := os.Open(filename) 99 | if os.IsNotExist(err) { 100 | return "", nil 101 | } 102 | if err != nil { 103 | return "", err 104 | } 105 | defer file.Close() 106 | reader := bufio.NewReader(file) 107 | line, _, err := reader.ReadLine() 108 | return string(line), err 109 | } 110 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | /* 8 | Package serial is a cross-platform serial library for the go language. 9 | 10 | The canonical import for this library is go.bug.st/serial so the import line 11 | is the following: 12 | 13 | import "go.bug.st/serial" 14 | 15 | It is possible to get the list of available serial ports with the 16 | GetPortsList function: 17 | 18 | ports, err := serial.GetPortsList() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | if len(ports) == 0 { 23 | log.Fatal("No serial ports found!") 24 | } 25 | for _, port := range ports { 26 | fmt.Printf("Found port: %v\n", port) 27 | } 28 | 29 | The serial port can be opened with the Open function: 30 | 31 | mode := &serial.Mode{ 32 | BaudRate: 115200, 33 | } 34 | port, err := serial.Open("/dev/ttyUSB0", mode) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | The Open function needs a "mode" parameter that specifies the configuration 40 | options for the serial port. If not specified the default options are 9600_N81, 41 | in the example above only the speed is changed so the port is opened using 115200_N81. 42 | The following snippets shows how to declare a configuration for 57600_E71: 43 | 44 | mode := &serial.Mode{ 45 | BaudRate: 57600, 46 | Parity: serial.EvenParity, 47 | DataBits: 7, 48 | StopBits: serial.OneStopBit, 49 | } 50 | 51 | The configuration can be changed at any time with the SetMode function: 52 | 53 | err := port.SetMode(mode) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | The port object implements the io.ReadWriteCloser interface, so we can use 59 | the usual Read, Write and Close functions to send and receive data from the 60 | serial port: 61 | 62 | n, err := port.Write([]byte("10,20,30\n\r")) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | fmt.Printf("Sent %v bytes\n", n) 67 | 68 | buff := make([]byte, 100) 69 | for { 70 | n, err := port.Read(buff) 71 | if err != nil { 72 | log.Fatal(err) 73 | break 74 | } 75 | if n == 0 { 76 | fmt.Println("\nEOF") 77 | break 78 | } 79 | fmt.Printf("%v", string(buff[:n])) 80 | } 81 | 82 | If a port is a virtual USB-CDC serial port (for example an USB-to-RS232 83 | cable or a microcontroller development board) is possible to retrieve 84 | the USB metadata, like VID/PID or USB Serial Number, with the 85 | GetDetailedPortsList function in the enumerator package: 86 | 87 | import "go.bug.st/serial/enumerator" 88 | 89 | ports, err := enumerator.GetDetailedPortsList() 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | if len(ports) == 0 { 94 | fmt.Println("No serial ports found!") 95 | return 96 | } 97 | for _, port := range ports { 98 | fmt.Printf("Found port: %s\n", port.Name) 99 | if port.IsUSB { 100 | fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID) 101 | fmt.Printf(" USB serial %s\n", port.SerialNumber) 102 | } 103 | } 104 | 105 | for details on USB port enumeration see the documentation of the specific package. 106 | 107 | This library tries to avoid the use of the "C" package (and consequently the need 108 | of cgo) to simplify cross compiling. 109 | Unfortunately the USB enumeration package for darwin (MacOSX) requires cgo 110 | to access the IOKit framework. This means that if you need USB enumeration 111 | on darwin you're forced to use cgo. 112 | */ 113 | package serial 114 | -------------------------------------------------------------------------------- /enumerator/syscall_windows.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'go generate'; DO NOT EDIT. 2 | 3 | package enumerator 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | var _ unsafe.Pointer 13 | 14 | // Do the interface allocations only once for common 15 | // Errno values. 16 | const ( 17 | errnoERROR_IO_PENDING = 997 18 | ) 19 | 20 | var ( 21 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 22 | ) 23 | 24 | // errnoErr returns common boxed Errno values, to prevent 25 | // allocations at runtime. 26 | func errnoErr(e syscall.Errno) error { 27 | switch e { 28 | case 0: 29 | return nil 30 | case errnoERROR_IO_PENDING: 31 | return errERROR_IO_PENDING 32 | } 33 | // TODO: add more here, after collecting data on the common 34 | // error values see on Windows. (perhaps when running 35 | // all.bat?) 36 | return e 37 | } 38 | 39 | var ( 40 | modsetupapi = windows.NewLazySystemDLL("setupapi.dll") 41 | modcfgmgr32 = windows.NewLazySystemDLL("cfgmgr32.dll") 42 | 43 | procSetupDiClassGuidsFromNameW = modsetupapi.NewProc("SetupDiClassGuidsFromNameW") 44 | procSetupDiGetClassDevsW = modsetupapi.NewProc("SetupDiGetClassDevsW") 45 | procSetupDiDestroyDeviceInfoList = modsetupapi.NewProc("SetupDiDestroyDeviceInfoList") 46 | procSetupDiEnumDeviceInfo = modsetupapi.NewProc("SetupDiEnumDeviceInfo") 47 | procSetupDiGetDeviceInstanceIdW = modsetupapi.NewProc("SetupDiGetDeviceInstanceIdW") 48 | procSetupDiOpenDevRegKey = modsetupapi.NewProc("SetupDiOpenDevRegKey") 49 | procSetupDiGetDeviceRegistryPropertyW = modsetupapi.NewProc("SetupDiGetDeviceRegistryPropertyW") 50 | procCM_Get_Parent = modcfgmgr32.NewProc("CM_Get_Parent") 51 | procCM_Get_Device_ID_Size = modcfgmgr32.NewProc("CM_Get_Device_ID_Size") 52 | procCM_Get_Device_IDW = modcfgmgr32.NewProc("CM_Get_Device_IDW") 53 | procCM_MapCrToWin32Err = modcfgmgr32.NewProc("CM_MapCrToWin32Err") 54 | ) 55 | 56 | func setupDiClassGuidsFromNameInternal(class string, guid *guid, guidSize uint32, requiredSize *uint32) (err error) { 57 | var _p0 *uint16 58 | _p0, err = syscall.UTF16PtrFromString(class) 59 | if err != nil { 60 | return 61 | } 62 | return _setupDiClassGuidsFromNameInternal(_p0, guid, guidSize, requiredSize) 63 | } 64 | 65 | func _setupDiClassGuidsFromNameInternal(class *uint16, guid *guid, guidSize uint32, requiredSize *uint32) (err error) { 66 | r1, _, e1 := syscall.Syscall6(procSetupDiClassGuidsFromNameW.Addr(), 4, uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(guid)), uintptr(guidSize), uintptr(unsafe.Pointer(requiredSize)), 0, 0) 67 | if r1 == 0 { 68 | if e1 != 0 { 69 | err = errnoErr(e1) 70 | } else { 71 | err = syscall.EINVAL 72 | } 73 | } 74 | return 75 | } 76 | 77 | func setupDiGetClassDevs(guid *guid, enumerator *string, hwndParent uintptr, flags uint32) (set devicesSet, err error) { 78 | r0, _, e1 := syscall.Syscall6(procSetupDiGetClassDevsW.Addr(), 4, uintptr(unsafe.Pointer(guid)), uintptr(unsafe.Pointer(enumerator)), uintptr(hwndParent), uintptr(flags), 0, 0) 79 | set = devicesSet(r0) 80 | if set == 0 { 81 | if e1 != 0 { 82 | err = errnoErr(e1) 83 | } else { 84 | err = syscall.EINVAL 85 | } 86 | } 87 | return 88 | } 89 | 90 | func setupDiDestroyDeviceInfoList(set devicesSet) (err error) { 91 | r1, _, e1 := syscall.Syscall(procSetupDiDestroyDeviceInfoList.Addr(), 1, uintptr(set), 0, 0) 92 | if r1 == 0 { 93 | if e1 != 0 { 94 | err = errnoErr(e1) 95 | } else { 96 | err = syscall.EINVAL 97 | } 98 | } 99 | return 100 | } 101 | 102 | func setupDiEnumDeviceInfo(set devicesSet, index uint32, info *devInfoData) (err error) { 103 | r1, _, e1 := syscall.Syscall(procSetupDiEnumDeviceInfo.Addr(), 3, uintptr(set), uintptr(index), uintptr(unsafe.Pointer(info))) 104 | if r1 == 0 { 105 | if e1 != 0 { 106 | err = errnoErr(e1) 107 | } else { 108 | err = syscall.EINVAL 109 | } 110 | } 111 | return 112 | } 113 | 114 | func setupDiGetDeviceInstanceId(set devicesSet, devInfo *devInfoData, devInstanceId unsafe.Pointer, devInstanceIdSize uint32, requiredSize *uint32) (err error) { 115 | r1, _, e1 := syscall.Syscall6(procSetupDiGetDeviceInstanceIdW.Addr(), 5, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(devInstanceId), uintptr(devInstanceIdSize), uintptr(unsafe.Pointer(requiredSize)), 0) 116 | if r1 == 0 { 117 | if e1 != 0 { 118 | err = errnoErr(e1) 119 | } else { 120 | err = syscall.EINVAL 121 | } 122 | } 123 | return 124 | } 125 | 126 | func setupDiOpenDevRegKey(set devicesSet, devInfo *devInfoData, scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (hkey syscall.Handle, err error) { 127 | r0, _, e1 := syscall.Syscall6(procSetupDiOpenDevRegKey.Addr(), 6, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(scope), uintptr(hwProfile), uintptr(keyType), uintptr(samDesired)) 128 | hkey = syscall.Handle(r0) 129 | if hkey == 0 { 130 | if e1 != 0 { 131 | err = errnoErr(e1) 132 | } else { 133 | err = syscall.EINVAL 134 | } 135 | } 136 | return 137 | } 138 | 139 | func setupDiGetDeviceRegistryProperty(set devicesSet, devInfo *devInfoData, property deviceProperty, propertyType *uint32, outValue *byte, bufSize uint32, reqSize *uint32) (res bool) { 140 | r0, _, _ := syscall.Syscall9(procSetupDiGetDeviceRegistryPropertyW.Addr(), 7, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(property), uintptr(unsafe.Pointer(propertyType)), uintptr(unsafe.Pointer(outValue)), uintptr(bufSize), uintptr(unsafe.Pointer(reqSize)), 0, 0) 141 | res = r0 != 0 142 | return 143 | } 144 | 145 | func cmGetParent(outParentDev *devInstance, dev devInstance, flags uint32) (cmErr cmError) { 146 | r0, _, _ := syscall.Syscall(procCM_Get_Parent.Addr(), 3, uintptr(unsafe.Pointer(outParentDev)), uintptr(dev), uintptr(flags)) 147 | cmErr = cmError(r0) 148 | return 149 | } 150 | 151 | func cmGetDeviceIDSize(outLen *uint32, dev devInstance, flags uint32) (cmErr cmError) { 152 | r0, _, _ := syscall.Syscall(procCM_Get_Device_ID_Size.Addr(), 3, uintptr(unsafe.Pointer(outLen)), uintptr(dev), uintptr(flags)) 153 | cmErr = cmError(r0) 154 | return 155 | } 156 | 157 | func cmGetDeviceID(dev devInstance, buffer unsafe.Pointer, bufferSize uint32, flags uint32) (err cmError) { 158 | r0, _, _ := syscall.Syscall6(procCM_Get_Device_IDW.Addr(), 4, uintptr(dev), uintptr(buffer), uintptr(bufferSize), uintptr(flags), 0, 0) 159 | err = cmError(r0) 160 | return 161 | } 162 | 163 | func cmMapCrToWin32Err(cmErr cmError, defaultErr uint32) (err uint32) { 164 | r0, _, _ := syscall.Syscall(procCM_MapCrToWin32Err.Addr(), 2, uintptr(cmErr), uintptr(defaultErr), 0) 165 | err = uint32(r0) 166 | return 167 | } 168 | -------------------------------------------------------------------------------- /serial.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | import "time" 10 | 11 | //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go 12 | 13 | // Port is the interface for a serial Port 14 | type Port interface { 15 | // SetMode sets all parameters of the serial port 16 | SetMode(mode *Mode) error 17 | 18 | // Stores data received from the serial port into the provided byte array 19 | // buffer. The function returns the number of bytes read. 20 | // 21 | // The Read function blocks until (at least) one byte is received from 22 | // the serial port or an error occurs. 23 | Read(p []byte) (n int, err error) 24 | 25 | // Send the content of the data byte array to the serial port. 26 | // Returns the number of bytes written. 27 | Write(p []byte) (n int, err error) 28 | 29 | // Wait until all data in the buffer are sent 30 | Drain() error 31 | 32 | // ResetInputBuffer Purges port read buffer 33 | ResetInputBuffer() error 34 | 35 | // ResetOutputBuffer Purges port write buffer 36 | ResetOutputBuffer() error 37 | 38 | // SetDTR sets the modem status bit DataTerminalReady 39 | SetDTR(dtr bool) error 40 | 41 | // SetRTS sets the modem status bit RequestToSend 42 | SetRTS(rts bool) error 43 | 44 | // GetModemStatusBits returns a ModemStatusBits structure containing the 45 | // modem status bits for the serial port (CTS, DSR, etc...) 46 | GetModemStatusBits() (*ModemStatusBits, error) 47 | 48 | // SetReadTimeout sets the timeout for the Read operation or use serial.NoTimeout 49 | // to disable read timeout. 50 | SetReadTimeout(t time.Duration) error 51 | 52 | // Close the serial port 53 | Close() error 54 | 55 | // Break sends a break for a determined time 56 | Break(time.Duration) error 57 | } 58 | 59 | // NoTimeout should be used as a parameter to SetReadTimeout to disable timeout. 60 | var NoTimeout time.Duration = -1 61 | 62 | // ModemStatusBits contains all the modem input status bits for a serial port (CTS, DSR, etc...). 63 | // It can be retrieved with the Port.GetModemStatusBits() method. 64 | type ModemStatusBits struct { 65 | CTS bool // ClearToSend status 66 | DSR bool // DataSetReady status 67 | RI bool // RingIndicator status 68 | DCD bool // DataCarrierDetect status 69 | } 70 | 71 | // ModemOutputBits contains all the modem output bits for a serial port. 72 | // This is used in the Mode.InitialStatusBits struct to specify the initial status of the bits. 73 | // Note: Linux and MacOSX (and basically all unix-based systems) can not set the status bits 74 | // before opening the port, even if the initial state of the bit is set to false they will go 75 | // anyway to true for a few milliseconds, resulting in a small pulse. 76 | type ModemOutputBits struct { 77 | RTS bool // ReadyToSend status 78 | DTR bool // DataTerminalReady status 79 | } 80 | 81 | // Open opens the serial port using the specified modes 82 | func Open(portName string, mode *Mode) (Port, error) { 83 | port, err := nativeOpen(portName, mode) 84 | if err != nil { 85 | // Return a nil interface, for which var==nil is true (instead of 86 | // a nil pointer to a struct that satisfies the interface). 87 | return nil, err 88 | } 89 | return port, err 90 | } 91 | 92 | // GetPortsList retrieve the list of available serial ports 93 | func GetPortsList() ([]string, error) { 94 | return nativeGetPortsList() 95 | } 96 | 97 | // Mode describes a serial port configuration. 98 | type Mode struct { 99 | BaudRate int // The serial port bitrate (aka Baudrate) 100 | DataBits int // Size of the character (must be 5, 6, 7 or 8) 101 | Parity Parity // Parity (see Parity type for more info) 102 | StopBits StopBits // Stop bits (see StopBits type for more info) 103 | InitialStatusBits *ModemOutputBits // Initial output modem bits status (if nil defaults to DTR=true and RTS=true) 104 | } 105 | 106 | // Parity describes a serial port parity setting 107 | type Parity int 108 | 109 | const ( 110 | // NoParity disable parity control (default) 111 | NoParity Parity = iota 112 | // OddParity enable odd-parity check 113 | OddParity 114 | // EvenParity enable even-parity check 115 | EvenParity 116 | // MarkParity enable mark-parity (always 1) check 117 | MarkParity 118 | // SpaceParity enable space-parity (always 0) check 119 | SpaceParity 120 | ) 121 | 122 | // StopBits describe a serial port stop bits setting 123 | type StopBits int 124 | 125 | const ( 126 | // OneStopBit sets 1 stop bit (default) 127 | OneStopBit StopBits = iota 128 | // OnePointFiveStopBits sets 1.5 stop bits 129 | OnePointFiveStopBits 130 | // TwoStopBits sets 2 stop bits 131 | TwoStopBits 132 | ) 133 | 134 | // PortError is a platform independent error type for serial ports 135 | type PortError struct { 136 | code PortErrorCode 137 | causedBy error 138 | } 139 | 140 | // PortErrorCode is a code to easily identify the type of error 141 | type PortErrorCode int 142 | 143 | const ( 144 | // PortBusy the serial port is already in used by another process 145 | PortBusy PortErrorCode = iota 146 | // PortNotFound the requested port doesn't exist 147 | PortNotFound 148 | // InvalidSerialPort the requested port is not a serial port 149 | InvalidSerialPort 150 | // PermissionDenied the user doesn't have enough privileges 151 | PermissionDenied 152 | // InvalidSpeed the requested speed is not valid or not supported 153 | InvalidSpeed 154 | // InvalidDataBits the number of data bits is not valid or not supported 155 | InvalidDataBits 156 | // InvalidParity the selected parity is not valid or not supported 157 | InvalidParity 158 | // InvalidStopBits the selected number of stop bits is not valid or not supported 159 | InvalidStopBits 160 | // InvalidTimeoutValue the timeout value is not valid or not supported 161 | InvalidTimeoutValue 162 | // ErrorEnumeratingPorts an error occurred while listing serial port 163 | ErrorEnumeratingPorts 164 | // PortClosed the port has been closed while the operation is in progress 165 | PortClosed 166 | // FunctionNotImplemented the requested function is not implemented 167 | FunctionNotImplemented 168 | ) 169 | 170 | // EncodedErrorString returns a string explaining the error code 171 | func (e PortError) EncodedErrorString() string { 172 | switch e.code { 173 | case PortBusy: 174 | return "Serial port busy" 175 | case PortNotFound: 176 | return "Serial port not found" 177 | case InvalidSerialPort: 178 | return "Invalid serial port" 179 | case PermissionDenied: 180 | return "Permission denied" 181 | case InvalidSpeed: 182 | return "Port speed invalid or not supported" 183 | case InvalidDataBits: 184 | return "Port data bits invalid or not supported" 185 | case InvalidParity: 186 | return "Port parity invalid or not supported" 187 | case InvalidStopBits: 188 | return "Port stop bits invalid or not supported" 189 | case InvalidTimeoutValue: 190 | return "Timeout value invalid or not supported" 191 | case ErrorEnumeratingPorts: 192 | return "Could not enumerate serial ports" 193 | case PortClosed: 194 | return "Port has been closed" 195 | case FunctionNotImplemented: 196 | return "Function not implemented" 197 | default: 198 | return "Other error" 199 | } 200 | } 201 | 202 | // Error returns the complete error code with details on the cause of the error 203 | func (e PortError) Error() string { 204 | if e.causedBy != nil { 205 | return e.EncodedErrorString() + ": " + e.causedBy.Error() 206 | } 207 | return e.EncodedErrorString() 208 | } 209 | 210 | // Code returns an identifier for the kind of error occurred 211 | func (e PortError) Code() PortErrorCode { 212 | return e.code 213 | } 214 | -------------------------------------------------------------------------------- /enumerator/usb_darwin.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | // #cgo LDFLAGS: -framework CoreFoundation -framework IOKit 10 | // #include 11 | // #include 12 | // #include 13 | import "C" 14 | import ( 15 | "errors" 16 | "fmt" 17 | "time" 18 | "unsafe" 19 | ) 20 | 21 | func nativeGetDetailedPortsList() ([]*PortDetails, error) { 22 | var ports []*PortDetails 23 | 24 | services, err := getAllServices("IOSerialBSDClient") 25 | if err != nil { 26 | return nil, &PortEnumerationError{causedBy: err} 27 | } 28 | for _, service := range services { 29 | defer service.Release() 30 | 31 | port, err := extractPortInfo(io_registry_entry_t(service)) 32 | if err != nil { 33 | return nil, &PortEnumerationError{causedBy: err} 34 | } 35 | ports = append(ports, port) 36 | } 37 | return ports, nil 38 | } 39 | 40 | func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { 41 | port := &PortDetails{} 42 | // If called too early the port may still not be ready or fully enumerated 43 | // so we retry 5 times before returning error. 44 | for retries := 5; retries > 0; retries-- { 45 | name, err := service.GetStringProperty("IOCalloutDevice") 46 | if err == nil { 47 | port.Name = name 48 | break 49 | } 50 | if retries == 0 { 51 | return nil, fmt.Errorf("error extracting port info from device: %w", err) 52 | } 53 | time.Sleep(50 * time.Millisecond) 54 | } 55 | port.IsUSB = false 56 | 57 | validUSBDeviceClass := map[string]bool{ 58 | "IOUSBDevice": true, 59 | "IOUSBHostDevice": true, 60 | } 61 | usbDevice := service 62 | var searchErr error 63 | for !validUSBDeviceClass[usbDevice.GetClass()] { 64 | if usbDevice, searchErr = usbDevice.GetParent("IOService"); searchErr != nil { 65 | break 66 | } 67 | } 68 | if searchErr == nil { 69 | // It's an IOUSBDevice 70 | vid, _ := usbDevice.GetIntProperty("idVendor", C.kCFNumberSInt16Type) 71 | pid, _ := usbDevice.GetIntProperty("idProduct", C.kCFNumberSInt16Type) 72 | serialNumber, _ := usbDevice.GetStringProperty("USB Serial Number") 73 | //product, _ := usbDevice.GetStringProperty("USB Product Name") 74 | //manufacturer, _ := usbDevice.GetStringProperty("USB Vendor Name") 75 | //fmt.Println(product + " - " + manufacturer) 76 | 77 | port.IsUSB = true 78 | port.VID = fmt.Sprintf("%04X", vid) 79 | port.PID = fmt.Sprintf("%04X", pid) 80 | port.SerialNumber = serialNumber 81 | } 82 | return port, nil 83 | } 84 | 85 | func getAllServices(serviceType string) ([]io_object_t, error) { 86 | i, err := getMatchingServices(serviceMatching(serviceType)) 87 | if err != nil { 88 | return nil, err 89 | } 90 | defer i.Release() 91 | 92 | var services []io_object_t 93 | tries := 0 94 | for tries < 5 { 95 | // Extract all elements from iterator 96 | if service, ok := i.Next(); ok { 97 | services = append(services, service) 98 | continue 99 | } 100 | // If the list of services is empty or the iterator is still valid return the result 101 | if len(services) == 0 || i.IsValid() { 102 | return services, nil 103 | } 104 | // Otherwise empty the result and retry 105 | for _, s := range services { 106 | s.Release() 107 | } 108 | services = []io_object_t{} 109 | i.Reset() 110 | tries++ 111 | } 112 | // Give up if the iteration continues to fail... 113 | return nil, fmt.Errorf("IOServiceGetMatchingServices failed, data changed while iterating") 114 | } 115 | 116 | // serviceMatching create a matching dictionary that specifies an IOService class match. 117 | func serviceMatching(serviceType string) C.CFMutableDictionaryRef { 118 | t := C.CString(serviceType) 119 | defer C.free(unsafe.Pointer(t)) 120 | return C.IOServiceMatching(t) 121 | } 122 | 123 | // getMatchingServices look up registered IOService objects that match a matching dictionary. 124 | func getMatchingServices(matcher C.CFMutableDictionaryRef) (io_iterator_t, error) { 125 | var i C.io_iterator_t 126 | err := C.IOServiceGetMatchingServices(C.kIOMasterPortDefault, C.CFDictionaryRef(matcher), &i) 127 | if err != C.KERN_SUCCESS { 128 | return 0, fmt.Errorf("IOServiceGetMatchingServices failed (code %d)", err) 129 | } 130 | return io_iterator_t(i), nil 131 | } 132 | 133 | // CFStringRef 134 | 135 | type cfStringRef C.CFStringRef 136 | 137 | func cfStringCreateWithString(s string) cfStringRef { 138 | c := C.CString(s) 139 | defer C.free(unsafe.Pointer(c)) 140 | return cfStringRef(C.CFStringCreateWithCString( 141 | C.kCFAllocatorDefault, c, C.kCFStringEncodingMacRoman)) 142 | } 143 | 144 | func (ref cfStringRef) Release() { 145 | C.CFRelease(C.CFTypeRef(ref)) 146 | } 147 | 148 | // CFTypeRef 149 | 150 | type cfTypeRef C.CFTypeRef 151 | 152 | func (ref cfTypeRef) Release() { 153 | C.CFRelease(C.CFTypeRef(ref)) 154 | } 155 | 156 | // io_registry_entry_t 157 | 158 | type io_registry_entry_t C.io_registry_entry_t 159 | 160 | func (me *io_registry_entry_t) GetParent(plane string) (io_registry_entry_t, error) { 161 | cPlane := C.CString(plane) 162 | defer C.free(unsafe.Pointer(cPlane)) 163 | var parent C.io_registry_entry_t 164 | err := C.IORegistryEntryGetParentEntry(C.io_registry_entry_t(*me), cPlane, &parent) 165 | if err != 0 { 166 | return 0, errors.New("No parent device available") 167 | } 168 | return io_registry_entry_t(parent), nil 169 | } 170 | 171 | func (me *io_registry_entry_t) CreateCFProperty(key string) (cfTypeRef, error) { 172 | k := cfStringCreateWithString(key) 173 | defer k.Release() 174 | property := C.IORegistryEntryCreateCFProperty(C.io_registry_entry_t(*me), C.CFStringRef(k), C.kCFAllocatorDefault, 0) 175 | if property == 0 { 176 | return 0, errors.New("Property not found: " + key) 177 | } 178 | return cfTypeRef(property), nil 179 | } 180 | 181 | func (me *io_registry_entry_t) GetStringProperty(key string) (string, error) { 182 | property, err := me.CreateCFProperty(key) 183 | if err != nil { 184 | return "", err 185 | } 186 | defer property.Release() 187 | 188 | if ptr := C.CFStringGetCStringPtr(C.CFStringRef(property), 0); ptr != nil { 189 | return C.GoString(ptr), nil 190 | } 191 | // in certain circumstances CFStringGetCStringPtr may return NULL 192 | // and we must retrieve the string by copy 193 | buff := make([]C.char, 1024) 194 | if C.CFStringGetCString(C.CFStringRef(property), &buff[0], 1024, 0) != C.true { 195 | return "", fmt.Errorf("Property '%s' can't be converted", key) 196 | } 197 | return C.GoString(&buff[0]), nil 198 | } 199 | 200 | func (me *io_registry_entry_t) GetIntProperty(key string, intType C.CFNumberType) (int, error) { 201 | property, err := me.CreateCFProperty(key) 202 | if err != nil { 203 | return 0, err 204 | } 205 | defer property.Release() 206 | var res int 207 | if C.CFNumberGetValue((C.CFNumberRef)(property), intType, unsafe.Pointer(&res)) != C.true { 208 | return res, fmt.Errorf("Property '%s' can't be converted or has been truncated", key) 209 | } 210 | return res, nil 211 | } 212 | 213 | func (me *io_registry_entry_t) Release() { 214 | C.IOObjectRelease(C.io_object_t(*me)) 215 | } 216 | 217 | func (me *io_registry_entry_t) GetClass() string { 218 | class := make([]C.char, 1024) 219 | C.IOObjectGetClass(C.io_object_t(*me), &class[0]) 220 | return C.GoString(&class[0]) 221 | } 222 | 223 | // io_iterator_t 224 | 225 | type io_iterator_t C.io_iterator_t 226 | 227 | // IsValid checks if an iterator is still valid. 228 | // Some iterators will be made invalid if changes are made to the 229 | // structure they are iterating over. This function checks the iterator 230 | // is still valid and should be called when Next returns zero. 231 | // An invalid iterator can be Reset and the iteration restarted. 232 | func (me *io_iterator_t) IsValid() bool { 233 | return C.IOIteratorIsValid(C.io_iterator_t(*me)) == C.true 234 | } 235 | 236 | func (me *io_iterator_t) Reset() { 237 | C.IOIteratorReset(C.io_iterator_t(*me)) 238 | } 239 | 240 | func (me *io_iterator_t) Next() (io_object_t, bool) { 241 | res := C.IOIteratorNext(C.io_iterator_t(*me)) 242 | return io_object_t(res), res != 0 243 | } 244 | 245 | func (me *io_iterator_t) Release() { 246 | C.IOObjectRelease(C.io_object_t(*me)) 247 | } 248 | 249 | // io_object_t 250 | 251 | type io_object_t C.io_object_t 252 | 253 | func (me *io_object_t) Release() { 254 | C.IOObjectRelease(C.io_object_t(*me)) 255 | } 256 | 257 | func (me *io_object_t) GetClass() string { 258 | class := make([]C.char, 1024) 259 | C.IOObjectGetClass(C.io_object_t(*me), &class[0]) 260 | return C.GoString(&class[0]) 261 | } 262 | -------------------------------------------------------------------------------- /serial_unix.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | //go:build linux || darwin || freebsd || openbsd 8 | 9 | package serial 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | "strings" 15 | "sync" 16 | "sync/atomic" 17 | "time" 18 | 19 | "go.bug.st/serial/unixutils" 20 | "golang.org/x/sys/unix" 21 | ) 22 | 23 | type unixPort struct { 24 | handle int 25 | 26 | readTimeout time.Duration 27 | closeLock sync.RWMutex 28 | closeSignal *unixutils.Pipe 29 | opened uint32 30 | } 31 | 32 | func (port *unixPort) Close() error { 33 | if !atomic.CompareAndSwapUint32(&port.opened, 1, 0) { 34 | return nil 35 | } 36 | 37 | // Close port 38 | port.releaseExclusiveAccess() 39 | if err := unix.Close(port.handle); err != nil { 40 | return err 41 | } 42 | 43 | if port.closeSignal != nil { 44 | // Send close signal to all pending reads (if any) 45 | port.closeSignal.Write([]byte{0}) 46 | 47 | // Wait for all readers to complete 48 | port.closeLock.Lock() 49 | defer port.closeLock.Unlock() 50 | 51 | // Close signaling pipe 52 | if err := port.closeSignal.Close(); err != nil { 53 | return err 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func (port *unixPort) Read(p []byte) (int, error) { 60 | port.closeLock.RLock() 61 | defer port.closeLock.RUnlock() 62 | if atomic.LoadUint32(&port.opened) != 1 { 63 | return 0, &PortError{code: PortClosed} 64 | } 65 | 66 | var deadline time.Time 67 | if port.readTimeout != NoTimeout { 68 | deadline = time.Now().Add(port.readTimeout) 69 | } 70 | 71 | fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD()) 72 | for { 73 | timeout := time.Duration(-1) 74 | if port.readTimeout != NoTimeout { 75 | timeout = time.Until(deadline) 76 | if timeout < 0 { 77 | // a negative timeout means "no-timeout" in Select(...) 78 | timeout = 0 79 | } 80 | } 81 | res, err := unixutils.Select(fds, nil, fds, timeout) 82 | if err == unix.EINTR { 83 | continue 84 | } 85 | if err != nil { 86 | return 0, err 87 | } 88 | if res.IsReadable(port.closeSignal.ReadFD()) { 89 | return 0, &PortError{code: PortClosed} 90 | } 91 | if !res.IsReadable(port.handle) { 92 | // Timeout happened 93 | return 0, nil 94 | } 95 | n, err := unix.Read(port.handle, p) 96 | if err == unix.EINTR { 97 | continue 98 | } 99 | // Linux: when the port is disconnected during a read operation 100 | // the port is left in a "readable with zero-length-data" state. 101 | // https://stackoverflow.com/a/34945814/1655275 102 | if n == 0 && err == nil { 103 | return 0, &PortError{code: PortClosed} 104 | } 105 | if n < 0 { // Do not return -1 unix errors 106 | n = 0 107 | } 108 | return n, err 109 | } 110 | } 111 | 112 | func (port *unixPort) Write(p []byte) (n int, err error) { 113 | n, err = unix.Write(port.handle, p) 114 | if n < 0 { // Do not return -1 unix errors 115 | n = 0 116 | } 117 | return 118 | } 119 | 120 | func (port *unixPort) Break(t time.Duration) error { 121 | if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil { 122 | return err 123 | } 124 | 125 | time.Sleep(t) 126 | 127 | if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil { 128 | return err 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func (port *unixPort) SetMode(mode *Mode) error { 135 | settings, err := port.getTermSettings() 136 | if err != nil { 137 | return err 138 | } 139 | if err := setTermSettingsParity(mode.Parity, settings); err != nil { 140 | return err 141 | } 142 | if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil { 143 | return err 144 | } 145 | if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil { 146 | return err 147 | } 148 | requireSpecialBaudrate := false 149 | if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil { 150 | return err 151 | } else if special { 152 | requireSpecialBaudrate = true 153 | } 154 | if err := port.setTermSettings(settings); err != nil { 155 | return err 156 | } 157 | if requireSpecialBaudrate { 158 | // MacOSX require this one to be the last operation otherwise an 159 | // 'Invalid serial port' error is produced. 160 | if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil { 161 | return err 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | func (port *unixPort) SetDTR(dtr bool) error { 168 | status, err := port.getModemBitsStatus() 169 | if err != nil { 170 | return err 171 | } 172 | if dtr { 173 | status |= unix.TIOCM_DTR 174 | } else { 175 | status &^= unix.TIOCM_DTR 176 | } 177 | return port.setModemBitsStatus(status) 178 | } 179 | 180 | func (port *unixPort) SetRTS(rts bool) error { 181 | status, err := port.getModemBitsStatus() 182 | if err != nil { 183 | return err 184 | } 185 | if rts { 186 | status |= unix.TIOCM_RTS 187 | } else { 188 | status &^= unix.TIOCM_RTS 189 | } 190 | return port.setModemBitsStatus(status) 191 | } 192 | 193 | func (port *unixPort) SetReadTimeout(timeout time.Duration) error { 194 | if timeout < 0 && timeout != NoTimeout { 195 | return &PortError{code: InvalidTimeoutValue} 196 | } 197 | port.readTimeout = timeout 198 | return nil 199 | } 200 | 201 | func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) { 202 | status, err := port.getModemBitsStatus() 203 | if err != nil { 204 | return nil, err 205 | } 206 | return &ModemStatusBits{ 207 | CTS: (status & unix.TIOCM_CTS) != 0, 208 | DCD: (status & unix.TIOCM_CD) != 0, 209 | DSR: (status & unix.TIOCM_DSR) != 0, 210 | RI: (status & unix.TIOCM_RI) != 0, 211 | }, nil 212 | } 213 | 214 | func nativeOpen(portName string, mode *Mode) (*unixPort, error) { 215 | h, err := unix.Open(portName, unix.O_RDWR|unix.O_NOCTTY|unix.O_NDELAY, 0) 216 | if err != nil { 217 | switch err { 218 | case unix.EBUSY: 219 | return nil, &PortError{code: PortBusy} 220 | case unix.EACCES: 221 | return nil, &PortError{code: PermissionDenied} 222 | } 223 | return nil, err 224 | } 225 | port := &unixPort{ 226 | handle: h, 227 | opened: 1, 228 | readTimeout: NoTimeout, 229 | } 230 | 231 | // Setup serial port 232 | settings, err := port.getTermSettings() 233 | if err != nil { 234 | port.Close() 235 | return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting term settings: %w", err)} 236 | } 237 | 238 | // Set raw mode 239 | setRawMode(settings) 240 | 241 | // Explicitly disable RTS/CTS flow control 242 | setTermSettingsCtsRts(false, settings) 243 | 244 | if err = port.setTermSettings(settings); err != nil { 245 | port.Close() 246 | return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting term settings: %w", err)} 247 | } 248 | 249 | if mode.InitialStatusBits != nil { 250 | status, err := port.getModemBitsStatus() 251 | if err != nil { 252 | port.Close() 253 | return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting modem bits status: %w", err)} 254 | } 255 | if mode.InitialStatusBits.DTR { 256 | status |= unix.TIOCM_DTR 257 | } else { 258 | status &^= unix.TIOCM_DTR 259 | } 260 | if mode.InitialStatusBits.RTS { 261 | status |= unix.TIOCM_RTS 262 | } else { 263 | status &^= unix.TIOCM_RTS 264 | } 265 | if err := port.setModemBitsStatus(status); err != nil { 266 | port.Close() 267 | return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting modem bits status: %w", err)} 268 | } 269 | } 270 | 271 | // MacOSX require that this operation is the last one otherwise an 272 | // 'Invalid serial port' error is returned... don't know why... 273 | if err := port.SetMode(mode); err != nil { 274 | port.Close() 275 | return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error configuring port: %w", err)} 276 | } 277 | 278 | unix.SetNonblock(h, false) 279 | 280 | port.acquireExclusiveAccess() 281 | 282 | // This pipe is used as a signal to cancel blocking Read 283 | pipe := &unixutils.Pipe{} 284 | if err := pipe.Open(); err != nil { 285 | port.Close() 286 | return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error opening signaling pipe: %w", err)} 287 | } 288 | port.closeSignal = pipe 289 | 290 | return port, nil 291 | } 292 | 293 | func nativeGetPortsList() ([]string, error) { 294 | files, err := os.ReadDir(devFolder) 295 | if err != nil { 296 | return nil, err 297 | } 298 | 299 | ports := make([]string, 0, len(files)) 300 | for _, f := range files { 301 | // Skip folders 302 | if f.IsDir() { 303 | continue 304 | } 305 | 306 | // Keep only devices with the correct name 307 | if !osPortFilter.MatchString(f.Name()) { 308 | continue 309 | } 310 | 311 | portName := devFolder + "/" + f.Name() 312 | 313 | // Check if serial port is real or is a placeholder serial port "ttySxx" or "ttyHSxx" 314 | if strings.HasPrefix(f.Name(), "ttyS") || strings.HasPrefix(f.Name(), "ttyHS") { 315 | port, err := nativeOpen(portName, &Mode{}) 316 | if err != nil { 317 | continue 318 | } else { 319 | port.Close() 320 | } 321 | } 322 | 323 | // Save serial port in the resulting list 324 | ports = append(ports, portName) 325 | } 326 | 327 | return ports, nil 328 | } 329 | 330 | // termios manipulation functions 331 | 332 | func setTermSettingsParity(parity Parity, settings *unix.Termios) error { 333 | switch parity { 334 | case NoParity: 335 | settings.Cflag &^= unix.PARENB 336 | settings.Cflag &^= unix.PARODD 337 | settings.Cflag &^= tcCMSPAR 338 | settings.Iflag &^= unix.INPCK 339 | case OddParity: 340 | settings.Cflag |= unix.PARENB 341 | settings.Cflag |= unix.PARODD 342 | settings.Cflag &^= tcCMSPAR 343 | settings.Iflag |= unix.INPCK 344 | case EvenParity: 345 | settings.Cflag |= unix.PARENB 346 | settings.Cflag &^= unix.PARODD 347 | settings.Cflag &^= tcCMSPAR 348 | settings.Iflag |= unix.INPCK 349 | case MarkParity: 350 | if tcCMSPAR == 0 { 351 | return &PortError{code: InvalidParity} 352 | } 353 | settings.Cflag |= unix.PARENB 354 | settings.Cflag |= unix.PARODD 355 | settings.Cflag |= tcCMSPAR 356 | settings.Iflag |= unix.INPCK 357 | case SpaceParity: 358 | if tcCMSPAR == 0 { 359 | return &PortError{code: InvalidParity} 360 | } 361 | settings.Cflag |= unix.PARENB 362 | settings.Cflag &^= unix.PARODD 363 | settings.Cflag |= tcCMSPAR 364 | settings.Iflag |= unix.INPCK 365 | default: 366 | return &PortError{code: InvalidParity} 367 | } 368 | return nil 369 | } 370 | 371 | func setTermSettingsDataBits(bits int, settings *unix.Termios) error { 372 | databits, ok := databitsMap[bits] 373 | if !ok { 374 | return &PortError{code: InvalidDataBits} 375 | } 376 | // Remove previous databits setting 377 | settings.Cflag &^= unix.CSIZE 378 | // Set requested databits 379 | settings.Cflag |= databits 380 | return nil 381 | } 382 | 383 | func setTermSettingsStopBits(bits StopBits, settings *unix.Termios) error { 384 | switch bits { 385 | case OneStopBit: 386 | settings.Cflag &^= unix.CSTOPB 387 | case OnePointFiveStopBits: 388 | return &PortError{code: InvalidStopBits} 389 | case TwoStopBits: 390 | settings.Cflag |= unix.CSTOPB 391 | default: 392 | return &PortError{code: InvalidStopBits} 393 | } 394 | return nil 395 | } 396 | 397 | func setTermSettingsCtsRts(enable bool, settings *unix.Termios) { 398 | if enable { 399 | settings.Cflag |= tcCRTSCTS 400 | } else { 401 | settings.Cflag &^= tcCRTSCTS 402 | } 403 | } 404 | 405 | func setRawMode(settings *unix.Termios) { 406 | // Set local mode 407 | settings.Cflag |= unix.CREAD 408 | settings.Cflag |= unix.CLOCAL 409 | 410 | // Set raw mode 411 | settings.Lflag &^= unix.ICANON 412 | settings.Lflag &^= unix.ECHO 413 | settings.Lflag &^= unix.ECHOE 414 | settings.Lflag &^= unix.ECHOK 415 | settings.Lflag &^= unix.ECHONL 416 | settings.Lflag &^= unix.ECHOCTL 417 | settings.Lflag &^= unix.ECHOPRT 418 | settings.Lflag &^= unix.ECHOKE 419 | settings.Lflag &^= unix.ISIG 420 | settings.Lflag &^= unix.IEXTEN 421 | 422 | settings.Iflag &^= unix.IXON 423 | settings.Iflag &^= unix.IXOFF 424 | settings.Iflag &^= unix.IXANY 425 | settings.Iflag &^= unix.INPCK 426 | settings.Iflag &^= unix.IGNPAR 427 | settings.Iflag &^= unix.PARMRK 428 | settings.Iflag &^= unix.ISTRIP 429 | settings.Iflag &^= unix.IGNBRK 430 | settings.Iflag &^= unix.BRKINT 431 | settings.Iflag &^= unix.INLCR 432 | settings.Iflag &^= unix.IGNCR 433 | settings.Iflag &^= unix.ICRNL 434 | settings.Iflag &^= tcIUCLC 435 | 436 | settings.Oflag &^= unix.OPOST 437 | 438 | // Block reads until at least one char is available (no timeout) 439 | settings.Cc[unix.VMIN] = 1 440 | settings.Cc[unix.VTIME] = 0 441 | } 442 | 443 | // native syscall wrapper functions 444 | 445 | func (port *unixPort) getTermSettings() (*unix.Termios, error) { 446 | return unix.IoctlGetTermios(port.handle, ioctlTcgetattr) 447 | } 448 | 449 | func (port *unixPort) setTermSettings(settings *unix.Termios) error { 450 | return unix.IoctlSetTermios(port.handle, ioctlTcsetattr, settings) 451 | } 452 | 453 | func (port *unixPort) getModemBitsStatus() (int, error) { 454 | return unix.IoctlGetInt(port.handle, unix.TIOCMGET) 455 | } 456 | 457 | func (port *unixPort) setModemBitsStatus(status int) error { 458 | return unix.IoctlSetPointerInt(port.handle, unix.TIOCMSET, status) 459 | } 460 | 461 | func (port *unixPort) acquireExclusiveAccess() error { 462 | return unix.IoctlSetInt(port.handle, unix.TIOCEXCL, 0) 463 | } 464 | 465 | func (port *unixPort) releaseExclusiveAccess() error { 466 | return unix.IoctlSetInt(port.handle, unix.TIOCNXCL, 0) 467 | } 468 | -------------------------------------------------------------------------------- /serial_windows.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package serial 8 | 9 | /* 10 | 11 | // MSDN article on Serial Communications: 12 | // http://msdn.microsoft.com/en-us/library/ff802693.aspx 13 | // (alternative link) https://msdn.microsoft.com/en-us/library/ms810467.aspx 14 | 15 | // Arduino Playground article on serial communication with Windows API: 16 | // http://playground.arduino.cc/Interfacing/CPPWindows 17 | 18 | */ 19 | 20 | import ( 21 | "errors" 22 | "strings" 23 | "sync" 24 | "syscall" 25 | "time" 26 | 27 | "golang.org/x/sys/windows" 28 | "golang.org/x/sys/windows/registry" 29 | ) 30 | 31 | type windowsPort struct { 32 | mu sync.Mutex 33 | handle windows.Handle 34 | hasTimeout bool 35 | } 36 | 37 | func nativeGetPortsList() ([]string, error) { 38 | key, err := registry.OpenKey(windows.HKEY_LOCAL_MACHINE, `HARDWARE\DEVICEMAP\SERIALCOMM\`, windows.KEY_READ) 39 | switch { 40 | case errors.Is(err, syscall.ERROR_FILE_NOT_FOUND): 41 | // On machines with no serial ports the registry key does not exist. 42 | // Return this as no serial ports instead of an error. 43 | return nil, nil 44 | case err != nil: 45 | return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err} 46 | } 47 | defer key.Close() 48 | 49 | names, err := key.ReadValueNames(0) 50 | if err != nil { 51 | return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err} 52 | } 53 | 54 | var values []string 55 | for _, n := range names { 56 | v, _, err := key.GetStringValue(n) 57 | if err != nil || v == "" { 58 | continue 59 | } 60 | 61 | values = append(values, v) 62 | } 63 | 64 | return values, nil 65 | } 66 | 67 | func (port *windowsPort) Close() error { 68 | port.mu.Lock() 69 | defer func() { 70 | port.handle = 0 71 | port.mu.Unlock() 72 | }() 73 | if port.handle == 0 { 74 | return nil 75 | } 76 | return windows.CloseHandle(port.handle) 77 | } 78 | 79 | func (port *windowsPort) Read(p []byte) (int, error) { 80 | var readed uint32 81 | ev, err := createOverlappedEvent() 82 | if err != nil { 83 | return 0, err 84 | } 85 | defer windows.CloseHandle(ev.HEvent) 86 | 87 | for { 88 | err = windows.ReadFile(port.handle, p, &readed, ev) 89 | if err == windows.ERROR_IO_PENDING { 90 | err = windows.GetOverlappedResult(port.handle, ev, &readed, true) 91 | } 92 | switch err { 93 | case nil: 94 | // operation completed successfully 95 | case windows.ERROR_OPERATION_ABORTED: 96 | // port may have been closed 97 | return int(readed), &PortError{code: PortClosed, causedBy: err} 98 | default: 99 | // error happened 100 | return int(readed), err 101 | } 102 | if readed > 0 { 103 | return int(readed), nil 104 | } 105 | 106 | // Timeout 107 | port.mu.Lock() 108 | hasTimeout := port.hasTimeout 109 | port.mu.Unlock() 110 | if hasTimeout { 111 | return 0, nil 112 | } 113 | } 114 | } 115 | 116 | func (port *windowsPort) Write(p []byte) (int, error) { 117 | var writed uint32 118 | ev, err := createOverlappedEvent() 119 | if err != nil { 120 | return 0, err 121 | } 122 | defer windows.CloseHandle(ev.HEvent) 123 | err = windows.WriteFile(port.handle, p, &writed, ev) 124 | if err == windows.ERROR_IO_PENDING { 125 | // wait for write to complete 126 | err = windows.GetOverlappedResult(port.handle, ev, &writed, true) 127 | } 128 | return int(writed), err 129 | } 130 | 131 | func (port *windowsPort) Drain() (err error) { 132 | return windows.FlushFileBuffers(port.handle) 133 | } 134 | 135 | func (port *windowsPort) ResetInputBuffer() error { 136 | return windows.PurgeComm(port.handle, windows.PURGE_RXCLEAR|windows.PURGE_RXABORT) 137 | } 138 | 139 | func (port *windowsPort) ResetOutputBuffer() error { 140 | return windows.PurgeComm(port.handle, windows.PURGE_TXCLEAR|windows.PURGE_TXABORT) 141 | } 142 | 143 | const ( 144 | dcbBinary uint32 = 0x00000001 145 | dcbParity = 0x00000002 146 | dcbOutXCTSFlow = 0x00000004 147 | dcbOutXDSRFlow = 0x00000008 148 | dcbDTRControlDisableMask = ^uint32(0x00000030) 149 | dcbDTRControlEnable = 0x00000010 150 | dcbDTRControlHandshake = 0x00000020 151 | dcbDSRSensitivity = 0x00000040 152 | dcbTXContinueOnXOFF = 0x00000080 153 | dcbOutX = 0x00000100 154 | dcbInX = 0x00000200 155 | dcbErrorChar = 0x00000400 156 | dcbNull = 0x00000800 157 | dcbRTSControlDisableMask = ^uint32(0x00003000) 158 | dcbRTSControlEnable = 0x00001000 159 | dcbRTSControlHandshake = 0x00002000 160 | dcbRTSControlToggle = 0x00003000 161 | dcbAbortOnError = 0x00004000 162 | ) 163 | 164 | var parityMap = map[Parity]byte{ 165 | NoParity: windows.NOPARITY, 166 | OddParity: windows.ODDPARITY, 167 | EvenParity: windows.EVENPARITY, 168 | MarkParity: windows.MARKPARITY, 169 | SpaceParity: windows.SPACEPARITY, 170 | } 171 | 172 | var stopBitsMap = map[StopBits]byte{ 173 | OneStopBit: windows.ONESTOPBIT, 174 | OnePointFiveStopBits: windows.ONE5STOPBITS, 175 | TwoStopBits: windows.TWOSTOPBITS, 176 | } 177 | 178 | func (port *windowsPort) SetMode(mode *Mode) error { 179 | params := windows.DCB{} 180 | if windows.GetCommState(port.handle, ¶ms) != nil { 181 | port.Close() 182 | return &PortError{code: InvalidSerialPort} 183 | } 184 | port.setModeParams(mode, ¶ms) 185 | if windows.SetCommState(port.handle, ¶ms) != nil { 186 | port.Close() 187 | return &PortError{code: InvalidSerialPort} 188 | } 189 | return nil 190 | } 191 | 192 | func (port *windowsPort) setModeParams(mode *Mode, params *windows.DCB) { 193 | if mode.BaudRate == 0 { 194 | params.BaudRate = windows.CBR_9600 // Default to 9600 195 | } else { 196 | params.BaudRate = uint32(mode.BaudRate) 197 | } 198 | if mode.DataBits == 0 { 199 | params.ByteSize = 8 // Default to 8 bits 200 | } else { 201 | params.ByteSize = byte(mode.DataBits) 202 | } 203 | params.StopBits = stopBitsMap[mode.StopBits] 204 | params.Parity = parityMap[mode.Parity] 205 | } 206 | 207 | func (port *windowsPort) SetDTR(dtr bool) error { 208 | // Like for RTS there are problems with the windows.EscapeCommFunction 209 | // observed behaviour was that DTR is set from false -> true 210 | // when setting RTS from true -> false 211 | // 1) Connect -> RTS = true (low) DTR = true (low) OKAY 212 | // 2) SetDTR(false) -> RTS = true (low) DTR = false (high) OKAY 213 | // 3) SetRTS(false) -> RTS = false (high) DTR = true (low) ERROR: DTR toggled 214 | // 215 | // In addition this way the CommState Flags are not updated 216 | /* 217 | var err error 218 | if dtr { 219 | err = windows.EscapeCommFunction(port.handle, windows.SETDTR) 220 | } else { 221 | err = windows.EscapeCommFunction(port.handle, windows.CLTDTR) 222 | } 223 | if err != nil { 224 | return &PortError{} 225 | } 226 | return nil 227 | */ 228 | 229 | // The following seems a more reliable way to do it 230 | 231 | params := &windows.DCB{} 232 | if err := windows.GetCommState(port.handle, params); err != nil { 233 | return &PortError{causedBy: err} 234 | } 235 | params.Flags &= dcbDTRControlDisableMask 236 | if dtr { 237 | params.Flags |= windows.DTR_CONTROL_ENABLE 238 | } 239 | if err := windows.SetCommState(port.handle, params); err != nil { 240 | return &PortError{causedBy: err} 241 | } 242 | 243 | return nil 244 | } 245 | 246 | func (port *windowsPort) SetRTS(rts bool) error { 247 | // It seems that there is a bug in the Windows VCP driver: 248 | // it doesn't send USB control message when the RTS bit is 249 | // changed, so the following code not always works with 250 | // USB-to-serial adapters. 251 | // 252 | // In addition this way the CommState Flags are not updated 253 | 254 | /* 255 | var err error 256 | if rts { 257 | err = windows.EscapeCommFunction(port.handle, windows.SETRTS) 258 | } else { 259 | err = windows.EscapeCommFunction(port.handle, windows.CLRRTS) 260 | } 261 | if err != nil { 262 | return &PortError{} 263 | } 264 | return nil 265 | */ 266 | 267 | // The following seems a more reliable way to do it 268 | 269 | params := &windows.DCB{} 270 | if err := windows.GetCommState(port.handle, params); err != nil { 271 | return &PortError{causedBy: err} 272 | } 273 | params.Flags &= dcbRTSControlDisableMask 274 | if rts { 275 | params.Flags |= windows.RTS_CONTROL_ENABLE 276 | } 277 | if err := windows.SetCommState(port.handle, params); err != nil { 278 | return &PortError{causedBy: err} 279 | } 280 | return nil 281 | } 282 | 283 | func (port *windowsPort) GetModemStatusBits() (*ModemStatusBits, error) { 284 | // GetCommModemStatus constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcommmodemstatus. 285 | const ( 286 | MS_CTS_ON = 0x0010 287 | MS_DSR_ON = 0x0020 288 | MS_RING_ON = 0x0040 289 | MS_RLSD_ON = 0x0080 290 | ) 291 | var bits uint32 292 | if err := windows.GetCommModemStatus(port.handle, &bits); err != nil { 293 | return nil, &PortError{} 294 | } 295 | return &ModemStatusBits{ 296 | CTS: (bits & MS_CTS_ON) != 0, 297 | DCD: (bits & MS_RLSD_ON) != 0, 298 | DSR: (bits & MS_DSR_ON) != 0, 299 | RI: (bits & MS_RING_ON) != 0, 300 | }, nil 301 | } 302 | 303 | func (port *windowsPort) SetReadTimeout(timeout time.Duration) error { 304 | // This is a brutal hack to make the CH340 chipset work properly. 305 | // Normally this value should be 0xFFFFFFFE but, after a lot of 306 | // tinkering, I discovered that any value with the highest 307 | // bit set will make the CH340 driver behave like the timeout is 0, 308 | // in the best cases leading to a spinning loop... 309 | // (could this be a wrong signed vs unsigned conversion in the driver?) 310 | // https://github.com/arduino/serial-monitor/issues/112 311 | const MaxReadTotalTimeoutConstant = 0x7FFFFFFE 312 | 313 | commTimeouts := &windows.CommTimeouts{ 314 | ReadIntervalTimeout: 0xFFFFFFFF, 315 | ReadTotalTimeoutMultiplier: 0xFFFFFFFF, 316 | ReadTotalTimeoutConstant: MaxReadTotalTimeoutConstant, 317 | WriteTotalTimeoutConstant: 0, 318 | WriteTotalTimeoutMultiplier: 0, 319 | } 320 | if timeout != NoTimeout { 321 | ms := timeout.Milliseconds() 322 | if ms > 0xFFFFFFFE || ms < 0 { 323 | return &PortError{code: InvalidTimeoutValue} 324 | } 325 | 326 | if ms > MaxReadTotalTimeoutConstant { 327 | ms = MaxReadTotalTimeoutConstant 328 | } 329 | 330 | commTimeouts.ReadTotalTimeoutConstant = uint32(ms) 331 | } 332 | 333 | port.mu.Lock() 334 | defer port.mu.Unlock() 335 | if err := windows.SetCommTimeouts(port.handle, commTimeouts); err != nil { 336 | return &PortError{code: InvalidTimeoutValue, causedBy: err} 337 | } 338 | port.hasTimeout = (timeout != NoTimeout) 339 | 340 | return nil 341 | } 342 | 343 | func (port *windowsPort) Break(d time.Duration) error { 344 | if err := windows.SetCommBreak(port.handle); err != nil { 345 | return &PortError{causedBy: err} 346 | } 347 | 348 | time.Sleep(d) 349 | 350 | if err := windows.ClearCommBreak(port.handle); err != nil { 351 | return &PortError{causedBy: err} 352 | } 353 | 354 | return nil 355 | } 356 | 357 | func createOverlappedEvent() (*windows.Overlapped, error) { 358 | h, err := windows.CreateEvent(nil, 1, 0, nil) 359 | return &windows.Overlapped{HEvent: h}, err 360 | } 361 | 362 | func nativeOpen(portName string, mode *Mode) (*windowsPort, error) { 363 | if !strings.HasPrefix(portName, `\\.\`) { 364 | portName = `\\.\` + portName 365 | } 366 | path, err := windows.UTF16PtrFromString(portName) 367 | if err != nil { 368 | return nil, err 369 | } 370 | handle, err := windows.CreateFile( 371 | path, 372 | windows.GENERIC_READ|windows.GENERIC_WRITE, 373 | 0, nil, 374 | windows.OPEN_EXISTING, 375 | windows.FILE_FLAG_OVERLAPPED, 376 | 0, 377 | ) 378 | if err != nil { 379 | switch err { 380 | case windows.ERROR_ACCESS_DENIED: 381 | return nil, &PortError{code: PortBusy} 382 | case windows.ERROR_FILE_NOT_FOUND: 383 | return nil, &PortError{code: PortNotFound} 384 | } 385 | return nil, err 386 | } 387 | // Create the serial port 388 | port := &windowsPort{ 389 | handle: handle, 390 | } 391 | 392 | // Set port parameters 393 | params := &windows.DCB{} 394 | if windows.GetCommState(port.handle, params) != nil { 395 | port.Close() 396 | return nil, &PortError{code: InvalidSerialPort} 397 | } 398 | port.setModeParams(mode, params) 399 | params.Flags &= dcbDTRControlDisableMask 400 | params.Flags &= dcbRTSControlDisableMask 401 | if mode.InitialStatusBits == nil { 402 | params.Flags |= windows.DTR_CONTROL_ENABLE 403 | params.Flags |= windows.RTS_CONTROL_ENABLE 404 | } else { 405 | if mode.InitialStatusBits.DTR { 406 | params.Flags |= windows.DTR_CONTROL_ENABLE 407 | } 408 | if mode.InitialStatusBits.RTS { 409 | params.Flags |= windows.RTS_CONTROL_ENABLE 410 | } 411 | } 412 | params.Flags &^= dcbOutXCTSFlow 413 | params.Flags &^= dcbOutXDSRFlow 414 | params.Flags &^= dcbDSRSensitivity 415 | params.Flags |= dcbTXContinueOnXOFF 416 | params.Flags &^= dcbInX 417 | params.Flags &^= dcbOutX 418 | params.Flags &^= dcbErrorChar 419 | params.Flags &^= dcbNull 420 | params.Flags &^= dcbAbortOnError 421 | params.XonLim = 2048 422 | params.XoffLim = 512 423 | params.XonChar = 17 // DC1 424 | params.XoffChar = 19 // C3 425 | if windows.SetCommState(port.handle, params) != nil { 426 | port.Close() 427 | return nil, &PortError{code: InvalidSerialPort} 428 | } 429 | 430 | if port.SetReadTimeout(NoTimeout) != nil { 431 | port.Close() 432 | return nil, &PortError{code: InvalidSerialPort} 433 | } 434 | return port, nil 435 | } 436 | -------------------------------------------------------------------------------- /enumerator/usb_windows.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2014-2024 Cristian Maglie. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package enumerator 8 | 9 | import ( 10 | "fmt" 11 | "regexp" 12 | "syscall" 13 | "unsafe" 14 | 15 | "golang.org/x/sys/windows" 16 | ) 17 | 18 | func parseDeviceID(deviceID string, details *PortDetails) { 19 | // Windows stock USB-CDC driver 20 | if len(deviceID) >= 3 && deviceID[:3] == "USB" { 21 | re := regexp.MustCompile("VID_(....)&PID_(....)(\\\\(\\w+)$)?").FindAllStringSubmatch(deviceID, -1) 22 | if re == nil || len(re[0]) < 2 { 23 | // Silently ignore unparsable strings 24 | return 25 | } 26 | details.IsUSB = true 27 | details.VID = re[0][1] 28 | details.PID = re[0][2] 29 | if len(re[0]) >= 4 { 30 | details.SerialNumber = re[0][4] 31 | } 32 | return 33 | } 34 | 35 | // FTDI driver 36 | if len(deviceID) >= 7 && deviceID[:7] == "FTDIBUS" { 37 | re := regexp.MustCompile("VID_(....)\\+PID_(....)(\\+(\\w+))?").FindAllStringSubmatch(deviceID, -1) 38 | if re == nil || len(re[0]) < 2 { 39 | // Silently ignore unparsable strings 40 | return 41 | } 42 | details.IsUSB = true 43 | details.VID = re[0][1] 44 | details.PID = re[0][2] 45 | if len(re[0]) >= 4 { 46 | details.SerialNumber = re[0][4] 47 | } 48 | return 49 | } 50 | 51 | // Other unidentified device type 52 | } 53 | 54 | // setupapi based 55 | // -------------- 56 | 57 | //sys setupDiClassGuidsFromNameInternal(class string, guid *guid, guidSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiClassGuidsFromNameW 58 | //sys setupDiGetClassDevs(guid *guid, enumerator *string, hwndParent uintptr, flags uint32) (set devicesSet, err error) = setupapi.SetupDiGetClassDevsW 59 | //sys setupDiDestroyDeviceInfoList(set devicesSet) (err error) = setupapi.SetupDiDestroyDeviceInfoList 60 | //sys setupDiEnumDeviceInfo(set devicesSet, index uint32, info *devInfoData) (err error) = setupapi.SetupDiEnumDeviceInfo 61 | //sys setupDiGetDeviceInstanceId(set devicesSet, devInfo *devInfoData, devInstanceId unsafe.Pointer, devInstanceIdSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiGetDeviceInstanceIdW 62 | //sys setupDiOpenDevRegKey(set devicesSet, devInfo *devInfoData, scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (hkey syscall.Handle, err error) = setupapi.SetupDiOpenDevRegKey 63 | //sys setupDiGetDeviceRegistryProperty(set devicesSet, devInfo *devInfoData, property deviceProperty, propertyType *uint32, outValue *byte, bufSize uint32, reqSize *uint32) (res bool) = setupapi.SetupDiGetDeviceRegistryPropertyW 64 | 65 | //sys cmGetParent(outParentDev *devInstance, dev devInstance, flags uint32) (cmErr cmError) = cfgmgr32.CM_Get_Parent 66 | //sys cmGetDeviceIDSize(outLen *uint32, dev devInstance, flags uint32) (cmErr cmError) = cfgmgr32.CM_Get_Device_ID_Size 67 | //sys cmGetDeviceID(dev devInstance, buffer unsafe.Pointer, bufferSize uint32, flags uint32) (err cmError) = cfgmgr32.CM_Get_Device_IDW 68 | //sys cmMapCrToWin32Err(cmErr cmError, defaultErr uint32) (err uint32) = cfgmgr32.CM_MapCrToWin32Err 69 | 70 | // Device registry property codes 71 | // (Codes marked as read-only (R) may only be used for 72 | // SetupDiGetDeviceRegistryProperty) 73 | // 74 | // These values should cover the same set of registry properties 75 | // as defined by the CM_DRP codes in cfgmgr32.h. 76 | // 77 | // Note that SPDRP codes are zero based while CM_DRP codes are one based! 78 | type deviceProperty uint32 79 | 80 | const ( 81 | spdrpDeviceDesc deviceProperty = 0x00000000 // DeviceDesc = R/W 82 | spdrpHardwareID = 0x00000001 // HardwareID = R/W 83 | spdrpCompatibleIDS = 0x00000002 // CompatibleIDs = R/W 84 | spdrpUnused0 = 0x00000003 // Unused 85 | spdrpService = 0x00000004 // Service = R/W 86 | spdrpUnused1 = 0x00000005 // Unused 87 | spdrpUnused2 = 0x00000006 // Unused 88 | spdrpClass = 0x00000007 // Class = R--tied to ClassGUID 89 | spdrpClassGUID = 0x00000008 // ClassGUID = R/W 90 | spdrpDriver = 0x00000009 // Driver = R/W 91 | spdrpConfigFlags = 0x0000000A // ConfigFlags = R/W 92 | spdrpMFG = 0x0000000B // Mfg = R/W 93 | spdrpFriendlyName = 0x0000000C // FriendlyName = R/W 94 | spdrpLocationIinformation = 0x0000000D // LocationInformation = R/W 95 | spdrpPhysicalDeviceObjectName = 0x0000000E // PhysicalDeviceObjectName = R 96 | spdrpCapabilities = 0x0000000F // Capabilities = R 97 | spdrpUINumber = 0x00000010 // UiNumber = R 98 | spdrpUpperFilters = 0x00000011 // UpperFilters = R/W 99 | spdrpLowerFilters = 0x00000012 // LowerFilters = R/W 100 | spdrpBusTypeGUID = 0x00000013 // BusTypeGUID = R 101 | spdrpLegacyBusType = 0x00000014 // LegacyBusType = R 102 | spdrpBusNumber = 0x00000015 // BusNumber = R 103 | spdrpEnumeratorName = 0x00000016 // Enumerator Name = R 104 | spdrpSecurity = 0x00000017 // Security = R/W, binary form 105 | spdrpSecuritySDS = 0x00000018 // Security = W, SDS form 106 | spdrpDevType = 0x00000019 // Device Type = R/W 107 | spdrpExclusive = 0x0000001A // Device is exclusive-access = R/W 108 | spdrpCharacteristics = 0x0000001B // Device Characteristics = R/W 109 | spdrpAddress = 0x0000001C // Device Address = R 110 | spdrpUINumberDescFormat = 0x0000001D // UiNumberDescFormat = R/W 111 | spdrpDevicePowerData = 0x0000001E // Device Power Data = R 112 | spdrpRemovalPolicy = 0x0000001F // Removal Policy = R 113 | spdrpRemovalPolicyHWDefault = 0x00000020 // Hardware Removal Policy = R 114 | spdrpRemovalPolicyOverride = 0x00000021 // Removal Policy Override = RW 115 | spdrpInstallState = 0x00000022 // Device Install State = R 116 | spdrpLocationPaths = 0x00000023 // Device Location Paths = R 117 | spdrpBaseContainerID = 0x00000024 // Base ContainerID = R 118 | 119 | spdrpMaximumProperty = 0x00000025 // Upper bound on ordinals 120 | ) 121 | 122 | // Values specifying the scope of a device property change 123 | type dicsScope uint32 124 | 125 | const ( 126 | dicsFlagGlobal dicsScope = 0x00000001 // make change in all hardware profiles 127 | dicsFlagConfigSspecific = 0x00000002 // make change in specified profile only 128 | dicsFlagConfigGeneral = 0x00000004 // 1 or more hardware profile-specific 129 | ) 130 | 131 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724878(v=vs.85).aspx 132 | type regsam uint32 133 | 134 | const ( 135 | keyAllAccess regsam = 0xF003F 136 | keyCreateLink = 0x00020 137 | keyCreateSubKey = 0x00004 138 | keyEnumerateSubKeys = 0x00008 139 | keyExecute = 0x20019 140 | keyNotify = 0x00010 141 | keyQueryValue = 0x00001 142 | keyRead = 0x20019 143 | keySetValue = 0x00002 144 | keyWOW64_32key = 0x00200 145 | keyWOW64_64key = 0x00100 146 | keyWrite = 0x20006 147 | ) 148 | 149 | // KeyType values for SetupDiCreateDevRegKey, SetupDiOpenDevRegKey, and 150 | // SetupDiDeleteDevRegKey. 151 | const ( 152 | diregDev = 0x00000001 // Open/Create/Delete device key 153 | diregDrv = 0x00000002 // Open/Create/Delete driver key 154 | diregBoth = 0x00000004 // Delete both driver and Device key 155 | ) 156 | 157 | // https://msdn.microsoft.com/it-it/library/windows/desktop/aa373931(v=vs.85).aspx 158 | type guid struct { 159 | data1 uint32 160 | data2 uint16 161 | data3 uint16 162 | data4 [8]byte 163 | } 164 | 165 | func (g guid) String() string { 166 | return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", 167 | g.data1, g.data2, g.data3, 168 | g.data4[0], g.data4[1], g.data4[2], g.data4[3], 169 | g.data4[4], g.data4[5], g.data4[6], g.data4[7]) 170 | } 171 | 172 | func classGuidsFromName(className string) ([]guid, error) { 173 | // Determine the number of GUIDs for className 174 | n := uint32(0) 175 | if err := setupDiClassGuidsFromNameInternal(className, nil, 0, &n); err != nil { 176 | // ignore error: UIDs array size too small 177 | } 178 | 179 | res := make([]guid, n) 180 | err := setupDiClassGuidsFromNameInternal(className, &res[0], n, &n) 181 | return res, err 182 | } 183 | 184 | const ( 185 | digcfDefault = 0x00000001 // only valid with digcfDeviceInterface 186 | digcfPresent = 0x00000002 187 | digcfAllClasses = 0x00000004 188 | digcfProfile = 0x00000008 189 | digcfDeviceInterface = 0x00000010 190 | ) 191 | 192 | type devicesSet syscall.Handle 193 | 194 | func (g *guid) getDevicesSet() (devicesSet, error) { 195 | return setupDiGetClassDevs(g, nil, 0, digcfPresent) 196 | } 197 | 198 | func (set devicesSet) destroy() { 199 | setupDiDestroyDeviceInfoList(set) 200 | } 201 | 202 | type cmError uint32 203 | 204 | // https://msdn.microsoft.com/en-us/library/windows/hardware/ff552344(v=vs.85).aspx 205 | type devInfoData struct { 206 | size uint32 207 | guid guid 208 | devInst devInstance 209 | reserved uintptr 210 | } 211 | 212 | type devInstance uint32 213 | 214 | func cmConvertError(cmErr cmError) error { 215 | if cmErr == 0 { 216 | return nil 217 | } 218 | winErr := cmMapCrToWin32Err(cmErr, 0) 219 | return fmt.Errorf("error %d", winErr) 220 | } 221 | 222 | func (dev devInstance) getParent() (devInstance, error) { 223 | var res devInstance 224 | errN := cmGetParent(&res, dev, 0) 225 | return res, cmConvertError(errN) 226 | } 227 | 228 | func (dev devInstance) GetDeviceID() (string, error) { 229 | var size uint32 230 | cmErr := cmGetDeviceIDSize(&size, dev, 0) 231 | if err := cmConvertError(cmErr); err != nil { 232 | return "", err 233 | } 234 | buff := make([]uint16, size) 235 | cmErr = cmGetDeviceID(dev, unsafe.Pointer(&buff[0]), uint32(len(buff)), 0) 236 | if err := cmConvertError(cmErr); err != nil { 237 | return "", err 238 | } 239 | return windows.UTF16ToString(buff[:]), nil 240 | } 241 | 242 | type deviceInfo struct { 243 | set devicesSet 244 | data devInfoData 245 | } 246 | 247 | func (set devicesSet) getDeviceInfo(index int) (*deviceInfo, error) { 248 | result := &deviceInfo{set: set} 249 | 250 | result.data.size = uint32(unsafe.Sizeof(result.data)) 251 | err := setupDiEnumDeviceInfo(set, uint32(index), &result.data) 252 | return result, err 253 | } 254 | 255 | func (dev *deviceInfo) getInstanceID() (string, error) { 256 | n := uint32(0) 257 | setupDiGetDeviceInstanceId(dev.set, &dev.data, nil, 0, &n) 258 | buff := make([]uint16, n) 259 | if err := setupDiGetDeviceInstanceId(dev.set, &dev.data, unsafe.Pointer(&buff[0]), uint32(len(buff)), &n); err != nil { 260 | return "", err 261 | } 262 | return windows.UTF16ToString(buff[:]), nil 263 | } 264 | 265 | func (dev *deviceInfo) openDevRegKey(scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (syscall.Handle, error) { 266 | return setupDiOpenDevRegKey(dev.set, &dev.data, scope, hwProfile, keyType, samDesired) 267 | } 268 | 269 | func nativeGetDetailedPortsList() ([]*PortDetails, error) { 270 | guids, err := classGuidsFromName("Ports") 271 | if err != nil { 272 | return nil, &PortEnumerationError{causedBy: err} 273 | } 274 | 275 | var res []*PortDetails 276 | for _, g := range guids { 277 | devsSet, err := g.getDevicesSet() 278 | if err != nil { 279 | return nil, &PortEnumerationError{causedBy: err} 280 | } 281 | defer devsSet.destroy() 282 | 283 | for i := 0; ; i++ { 284 | device, err := devsSet.getDeviceInfo(i) 285 | if err != nil { 286 | break 287 | } 288 | details := &PortDetails{} 289 | portName, err := retrievePortNameFromDevInfo(device) 290 | if err != nil { 291 | continue 292 | } 293 | if len(portName) < 3 || portName[0:3] != "COM" { 294 | // Accept only COM ports 295 | continue 296 | } 297 | details.Name = portName 298 | 299 | if err := retrievePortDetailsFromDevInfo(device, details); err != nil { 300 | return nil, &PortEnumerationError{causedBy: err} 301 | } 302 | res = append(res, details) 303 | } 304 | } 305 | return res, nil 306 | } 307 | 308 | func retrievePortNameFromDevInfo(device *deviceInfo) (string, error) { 309 | h, err := device.openDevRegKey(dicsFlagGlobal, 0, diregDev, keyRead) 310 | if err != nil { 311 | return "", err 312 | } 313 | defer syscall.RegCloseKey(h) 314 | 315 | var name [1024]uint16 316 | nameP := (*byte)(unsafe.Pointer(&name[0])) 317 | nameSize := uint32(len(name) * 2) 318 | if err := syscall.RegQueryValueEx(h, syscall.StringToUTF16Ptr("PortName"), nil, nil, nameP, &nameSize); err != nil { 319 | return "", err 320 | } 321 | return syscall.UTF16ToString(name[:]), nil 322 | } 323 | 324 | func retrievePortDetailsFromDevInfo(device *deviceInfo, details *PortDetails) error { 325 | deviceID, err := device.getInstanceID() 326 | if err != nil { 327 | return err 328 | } 329 | parseDeviceID(deviceID, details) 330 | 331 | // On composite USB devices the serial number is usually reported on the parent 332 | // device, so let's navigate up one level and see if we can get this information 333 | if details.IsUSB && details.SerialNumber == "" { 334 | if parentInfo, err := device.data.devInst.getParent(); err == nil { 335 | if parentDeviceID, err := parentInfo.GetDeviceID(); err == nil { 336 | d := &PortDetails{} 337 | parseDeviceID(parentDeviceID, d) 338 | if details.VID == d.VID && details.PID == d.PID { 339 | details.SerialNumber = d.SerialNumber 340 | } 341 | } 342 | } 343 | } 344 | 345 | /* spdrpDeviceDesc returns a generic name, e.g.: "CDC-ACM", which will be the same for 2 identical devices attached 346 | while spdrpFriendlyName returns a specific name, e.g.: "CDC-ACM (COM44)", 347 | the result of spdrpFriendlyName is therefore unique and suitable as an alternative string to for a port choice */ 348 | n := uint32(0) 349 | setupDiGetDeviceRegistryProperty(device.set, &device.data, spdrpFriendlyName /* spdrpDeviceDesc */, nil, nil, 0, &n) 350 | if n > 0 { 351 | buff := make([]uint16, n*2) 352 | buffP := (*byte)(unsafe.Pointer(&buff[0])) 353 | if setupDiGetDeviceRegistryProperty(device.set, &device.data, spdrpFriendlyName /* spdrpDeviceDesc */, nil, buffP, n, &n) { 354 | details.Product = syscall.UTF16ToString(buff[:]) 355 | } 356 | } 357 | 358 | return nil 359 | } 360 | --------------------------------------------------------------------------------