diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..b544840 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,78 @@ +# Source: https://github.com/arduino/tooling-project-assets/blob/main/issue-templates/forms/platform-dependent/bug-report.yml +# See: https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms + +name: Bug report +description: Report a problem with the code or documentation in this repository. +labels: + - bug +body: + - type: textarea + id: description + attributes: + label: Describe the problem + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: To reproduce + description: | + Provide the specific set of steps we can follow to reproduce the + problem in particular the exact golang source code you used. + validations: + required: true + - type: checkboxes + id: checklist-reproduce + attributes: + label: | + Please double-check that you have reported each of the following + before submitting the issue. + options: + - label: I've provided the FULL source code that causes the problem + required: true + - label: I've provided all the actions required to reproduce the problem + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: | + What would you expect to happen after following those instructions? + validations: + required: true + - type: input + id: os + attributes: + label: Operating system and version + description: | + Which operating system(s) version are you using on your computer? + validations: + required: true + - type: textarea + id: boards + attributes: + label: Please describe your hardware setup + description: | + Arduino boards, USB dongles, hubs or embedded devices you are using and how they + are connected together. + - type: textarea + id: additional + attributes: + label: Additional context + description: | + Add here any additional information that you think might be relevant to + the problem. + validations: + required: false + - type: checkboxes + id: checklist + attributes: + label: Issue checklist + description: | + Please double-check that you have done each of the following things before + submitting the issue. + options: + - label: I searched for previous requests in [the issue tracker](https://github.com/bugst/go-serial/issues) + required: true + - label: My request contains all necessary details + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..d56393d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,29 @@ +# Source: https://github.com/arduino/tooling-project-assets/blob/main/issue-templates/forms/platform-dependent/feature-request.yml +# See: https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms + +name: Feature request +description: Suggest an enhancement to this project. +labels: + - "type: enhancement" +body: + - type: textarea + id: description + attributes: + label: Describe the new feature or change suggestion + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any additional information about the feature request here. + - type: checkboxes + id: checklist + attributes: + label: Issue checklist + description: Please double-check that you have done each of the following things before submitting the issue. + options: + - label: I searched for previous requests in [the issue tracker](https://github.com/bugst/go-serial/issues) + required: true + - label: My request contains all necessary details + required: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..bfacab5 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,71 @@ +name: test + +on: + push: + branches: + - master + pull_request: + +jobs: + native-os-build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + - name: Setup CGO Environment + run: | + if [ ${{ matrix.os }} == 'macOS-latest' ] ; then + echo "CGO_ENABLED=1" >> "$GITHUB_ENV" + fi + shell: bash + - name: Build AMD64 + run: GOARCH=amd64 go build -v ./... + shell: bash + - name: Build ARM64 + run: GOARCH=arm64 go build -v ./... + shell: bash + - name: Install socat + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install socat + shell: bash + - name: Run unit tests + run: go test -v -race ./... + shell: bash + - name: Cross-build for 386 + if: matrix.os != 'macOS-latest' + run: GOARCH=386 go build -v ./... + shell: bash + - name: Cross-build for arm + if: matrix.os != 'macOS-latest' + run: GOARCH=arm go build -v ./... + shell: bash + + cross-os-build: + strategy: + matrix: + go-os-pairs: + - "freebsd amd64" + - "openbsd amd64" + - "openbsd 386" + - "openbsd arm" + - "linux ppc64le" + + runs-on: "ubuntu-latest" + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + - name: Cross-build + run: | + set ${{ matrix.go-os-pairs }} + GOOS=$1 GOARCH=$2 go build -v ./... + shell: bash diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 67c8b50..0000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: go - -go: - - 1.4.2 - -go_import_path: go.bug.st/serial - -env: - - TEST_OS=linux TEST_ARCH=386 - - TEST_OS=linux TEST_ARCH=amd64 - - TEST_OS=linux TEST_ARCH=arm - - TEST_OS=windows TEST_ARCH=386 - - TEST_OS=windows TEST_ARCH=amd64 - - TEST_OS=darwin TEST_ARCH=386 - - TEST_OS=darwin TEST_ARCH=amd64 - - TEST_OS=freebsd TEST_ARCH=amd64 - - TEST_OS=dragonfly TEST_ARCH=amd64 - -matrix: - allow_failures: - - env: TEST_OS=dragonfly TEST_ARCH=amd64 - -before_install: -# bootstrap go tools for the specific OS/Arch - - ( cd $GOROOT/src; GOOS=$TEST_OS GOARCH=$TEST_ARCH ./make.bash ) - -script: - - GOARM=5 GO386=387 GOOS=$TEST_OS GOARCH=$TEST_ARCH go build -v ./... - - GOARM=5 GO386=387 GOOS=$TEST_OS GOARCH=$TEST_ARCH go test -c -v ./... - -notifications: - email: - recipients: - - c.maglie@bug.st - on_success: change - on_failure: always - diff --git a/LICENSE b/LICENSE index 8596fab..5aca55c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) 2014-2016, Cristian Maglie. +Copyright (c) 2014-2024, Cristian Maglie. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index f2b1950..c32fa18 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ -[![Build Status](https://travis-ci.org/bugst/go-serial.svg?branch=master)](https://travis-ci.org/bugst/go-serial) +[![Build Status](https://github.com/bugst/go-serial/workflows/test/badge.svg)](https://github.com/bugst/go-serial/actions?workflow=test) -A cross-platform serial library for go-lang. +# go.bug.st/serial -Documentation and examples: https://godoc.org/go.bug.st/serial +A cross-platform serial port library for Go. -License: https://github.com/bugst/go-serial/blob/master/LICENSE +## Documentation and examples + +See the package documentation here: https://pkg.go.dev/go.bug.st/serial + +## go.mod transition + +This library supports `go.mod` with the import `go.bug.st/serial`. + +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. + +## Credits + +:sparkles: Thanks to all awesome [contributors]! :sparkles: + +## License + +This software is released under the [BSD 3-clause license]. + +[contributors]: https://github.com/bugst/go-serial/graphs/contributors +[BSD 3-clause license]: https://github.com/bugst/go-serial/blob/master/LICENSE diff --git a/doc.go b/doc.go index fff8acf..360f0cc 100644 --- a/doc.go +++ b/doc.go @@ -1,18 +1,18 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // /* -A cross-platform serial library for the go language. +Package serial is a cross-platform serial library for the go language. The canonical import for this library is go.bug.st/serial so the import line is the following: import "go.bug.st/serial" -It is possibile to get the list of available serial ports with the +It is possible to get the list of available serial ports with the GetPortsList function: ports, err := serial.GetPortsList() @@ -26,26 +26,26 @@ GetPortsList function: fmt.Printf("Found port: %v\n", port) } -The serial port can be opened with the OpenPort function: +The serial port can be opened with the Open function: mode := &serial.Mode{ BaudRate: 115200, } - port, err := serial.OpenPort("/dev/ttyUSB0", mode) + port, err := serial.Open("/dev/ttyUSB0", mode) if err != nil { log.Fatal(err) } -The OpenPort command needs a "mode" parameter that specifies the configuration +The Open function needs a "mode" parameter that specifies the configuration options for the serial port. If not specified the default options are 9600_N81, in the example above only the speed is changed so the port is opened using 115200_N81. The following snippets shows how to declare a configuration for 57600_E71: mode := &serial.Mode{ BaudRate: 57600, - Parity: serial.PARITY_EVEN, + Parity: serial.EvenParity, DataBits: 7, - StopBits: serial.STOPBITS_ONE, + StopBits: serial.OneStopBit, } The configuration can be changed at any time with the SetMode function: @@ -79,7 +79,35 @@ serial port: fmt.Printf("%v", string(buff[:n])) } -This library doesn't make use of cgo and "C" package, so it's a pure go library -that can be easily cross compiled. +If a port is a virtual USB-CDC serial port (for example an USB-to-RS232 +cable or a microcontroller development board) is possible to retrieve +the USB metadata, like VID/PID or USB Serial Number, with the +GetDetailedPortsList function in the enumerator package: + + import "go.bug.st/serial/enumerator" + + ports, err := enumerator.GetDetailedPortsList() + if err != nil { + log.Fatal(err) + } + if len(ports) == 0 { + fmt.Println("No serial ports found!") + return + } + for _, port := range ports { + fmt.Printf("Found port: %s\n", port.Name) + if port.IsUSB { + fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID) + fmt.Printf(" USB serial %s\n", port.SerialNumber) + } + } + +for details on USB port enumeration see the documentation of the specific package. + +This library tries to avoid the use of the "C" package (and consequently the need +of cgo) to simplify cross compiling. +Unfortunately the USB enumeration package for darwin (MacOSX) requires cgo +to access the IOKit framework. This means that if you need USB enumeration +on darwin you're forced to use cgo. */ -package serial // import "go.bug.st/serial" +package serial diff --git a/enumerator/doc.go b/enumerator/doc.go new file mode 100644 index 0000000..5d27f00 --- /dev/null +++ b/enumerator/doc.go @@ -0,0 +1,15 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +/* +Package enumerator is a golang cross-platform library for USB serial port discovery. + +This library has been tested on Linux, Windows and Mac and uses specific OS +services to enumerate USB PID/VID, in particular on MacOSX the use of cgo is +required in order to access the IOKit Framework. This means that the library +cannot be easily cross compiled for darwin/* targets. +*/ +package enumerator diff --git a/enumerator/enumerator.go b/enumerator/enumerator.go new file mode 100644 index 0000000..2bd5043 --- /dev/null +++ b/enumerator/enumerator.go @@ -0,0 +1,46 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output syscall_windows.go usb_windows.go + +// PortDetails contains detailed information about USB serial port. +// Use GetDetailedPortsList function to retrieve it. +type PortDetails struct { + Name string + IsUSB bool + VID string + PID string + SerialNumber string + + // Manufacturer string + + // Product is an OS-dependent string that describes the serial port, it may + // be not always available and it may be different across OS. + Product string +} + +// GetDetailedPortsList retrieve ports details like USB VID/PID. +// Please note that this function may not be available on all OS: +// in that case a FunctionNotImplemented error is returned. +func GetDetailedPortsList() ([]*PortDetails, error) { + return nativeGetDetailedPortsList() +} + +// PortEnumerationError is the error type for serial ports enumeration +type PortEnumerationError struct { + causedBy error +} + +// Error returns the complete error code with details on the cause of the error +func (e PortEnumerationError) Error() string { + reason := "Error while enumerating serial ports" + if e.causedBy != nil { + reason += ": " + e.causedBy.Error() + } + return reason +} diff --git a/enumerator/example_getdetailedportlist_test.go b/enumerator/example_getdetailedportlist_test.go new file mode 100644 index 0000000..6d3c2db --- /dev/null +++ b/enumerator/example_getdetailedportlist_test.go @@ -0,0 +1,32 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator_test + +import ( + "fmt" + "log" + + "go.bug.st/serial/enumerator" +) + +func ExampleGetDetailedPortsList() { + ports, err := enumerator.GetDetailedPortsList() + if err != nil { + log.Fatal(err) + } + if len(ports) == 0 { + fmt.Println("No serial ports found!") + return + } + for _, port := range ports { + fmt.Printf("Found port: %s\n", port.Name) + if port.IsUSB { + fmt.Printf(" USB ID %s:%s\n", port.VID, port.PID) + fmt.Printf(" USB serial %s\n", port.SerialNumber) + } + } +} diff --git a/enumerator/syscall_windows.go b/enumerator/syscall_windows.go new file mode 100644 index 0000000..2c6b379 --- /dev/null +++ b/enumerator/syscall_windows.go @@ -0,0 +1,167 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package enumerator + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return nil + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modsetupapi = windows.NewLazySystemDLL("setupapi.dll") + modcfgmgr32 = windows.NewLazySystemDLL("cfgmgr32.dll") + + procSetupDiClassGuidsFromNameW = modsetupapi.NewProc("SetupDiClassGuidsFromNameW") + procSetupDiGetClassDevsW = modsetupapi.NewProc("SetupDiGetClassDevsW") + procSetupDiDestroyDeviceInfoList = modsetupapi.NewProc("SetupDiDestroyDeviceInfoList") + procSetupDiEnumDeviceInfo = modsetupapi.NewProc("SetupDiEnumDeviceInfo") + procSetupDiGetDeviceInstanceIdW = modsetupapi.NewProc("SetupDiGetDeviceInstanceIdW") + procSetupDiOpenDevRegKey = modsetupapi.NewProc("SetupDiOpenDevRegKey") + procSetupDiGetDeviceRegistryPropertyW = modsetupapi.NewProc("SetupDiGetDeviceRegistryPropertyW") + procCM_Get_Parent = modcfgmgr32.NewProc("CM_Get_Parent") + procCM_Get_Device_ID_Size = modcfgmgr32.NewProc("CM_Get_Device_ID_Size") + procCM_Get_Device_IDW = modcfgmgr32.NewProc("CM_Get_Device_IDW") + procCM_MapCrToWin32Err = modcfgmgr32.NewProc("CM_MapCrToWin32Err") +) + +func setupDiClassGuidsFromNameInternal(class string, guid *guid, guidSize uint32, requiredSize *uint32) (err error) { + var _p0 *uint16 + _p0, err = syscall.UTF16PtrFromString(class) + if err != nil { + return + } + return _setupDiClassGuidsFromNameInternal(_p0, guid, guidSize, requiredSize) +} + +func _setupDiClassGuidsFromNameInternal(class *uint16, guid *guid, guidSize uint32, requiredSize *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiClassGuidsFromNameW.Addr(), 4, uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(guid)), uintptr(guidSize), uintptr(unsafe.Pointer(requiredSize)), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetClassDevs(guid *guid, enumerator *string, hwndParent uintptr, flags uint32) (set devicesSet, err error) { + r0, _, e1 := syscall.Syscall6(procSetupDiGetClassDevsW.Addr(), 4, uintptr(unsafe.Pointer(guid)), uintptr(unsafe.Pointer(enumerator)), uintptr(hwndParent), uintptr(flags), 0, 0) + set = devicesSet(r0) + if set == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiDestroyDeviceInfoList(set devicesSet) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiDestroyDeviceInfoList.Addr(), 1, uintptr(set), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiEnumDeviceInfo(set devicesSet, index uint32, info *devInfoData) (err error) { + r1, _, e1 := syscall.Syscall(procSetupDiEnumDeviceInfo.Addr(), 3, uintptr(set), uintptr(index), uintptr(unsafe.Pointer(info))) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetDeviceInstanceId(set devicesSet, devInfo *devInfoData, devInstanceId unsafe.Pointer, devInstanceIdSize uint32, requiredSize *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procSetupDiGetDeviceInstanceIdW.Addr(), 5, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(devInstanceId), uintptr(devInstanceIdSize), uintptr(unsafe.Pointer(requiredSize)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiOpenDevRegKey(set devicesSet, devInfo *devInfoData, scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (hkey syscall.Handle, err error) { + r0, _, e1 := syscall.Syscall6(procSetupDiOpenDevRegKey.Addr(), 6, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(scope), uintptr(hwProfile), uintptr(keyType), uintptr(samDesired)) + hkey = syscall.Handle(r0) + if hkey == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func setupDiGetDeviceRegistryProperty(set devicesSet, devInfo *devInfoData, property deviceProperty, propertyType *uint32, outValue *byte, bufSize uint32, reqSize *uint32) (res bool) { + r0, _, _ := syscall.Syscall9(procSetupDiGetDeviceRegistryPropertyW.Addr(), 7, uintptr(set), uintptr(unsafe.Pointer(devInfo)), uintptr(property), uintptr(unsafe.Pointer(propertyType)), uintptr(unsafe.Pointer(outValue)), uintptr(bufSize), uintptr(unsafe.Pointer(reqSize)), 0, 0) + res = r0 != 0 + return +} + +func cmGetParent(outParentDev *devInstance, dev devInstance, flags uint32) (cmErr cmError) { + r0, _, _ := syscall.Syscall(procCM_Get_Parent.Addr(), 3, uintptr(unsafe.Pointer(outParentDev)), uintptr(dev), uintptr(flags)) + cmErr = cmError(r0) + return +} + +func cmGetDeviceIDSize(outLen *uint32, dev devInstance, flags uint32) (cmErr cmError) { + r0, _, _ := syscall.Syscall(procCM_Get_Device_ID_Size.Addr(), 3, uintptr(unsafe.Pointer(outLen)), uintptr(dev), uintptr(flags)) + cmErr = cmError(r0) + return +} + +func cmGetDeviceID(dev devInstance, buffer unsafe.Pointer, bufferSize uint32, flags uint32) (err cmError) { + r0, _, _ := syscall.Syscall6(procCM_Get_Device_IDW.Addr(), 4, uintptr(dev), uintptr(buffer), uintptr(bufferSize), uintptr(flags), 0, 0) + err = cmError(r0) + return +} + +func cmMapCrToWin32Err(cmErr cmError, defaultErr uint32) (err uint32) { + r0, _, _ := syscall.Syscall(procCM_MapCrToWin32Err.Addr(), 2, uintptr(cmErr), uintptr(defaultErr), 0) + err = uint32(r0) + return +} diff --git a/enumerator/usb_darwin.go b/enumerator/usb_darwin.go new file mode 100644 index 0000000..1f51c47 --- /dev/null +++ b/enumerator/usb_darwin.go @@ -0,0 +1,261 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit +// #include +// #include +// #include +import "C" +import ( + "errors" + "fmt" + "time" + "unsafe" +) + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + var ports []*PortDetails + + services, err := getAllServices("IOSerialBSDClient") + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + for _, service := range services { + defer service.Release() + + port, err := extractPortInfo(io_registry_entry_t(service)) + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + ports = append(ports, port) + } + return ports, nil +} + +func extractPortInfo(service io_registry_entry_t) (*PortDetails, error) { + port := &PortDetails{} + // If called too early the port may still not be ready or fully enumerated + // so we retry 5 times before returning error. + for retries := 5; retries > 0; retries-- { + name, err := service.GetStringProperty("IOCalloutDevice") + if err == nil { + port.Name = name + break + } + if retries == 0 { + return nil, fmt.Errorf("error extracting port info from device: %w", err) + } + time.Sleep(50 * time.Millisecond) + } + port.IsUSB = false + + validUSBDeviceClass := map[string]bool{ + "IOUSBDevice": true, + "IOUSBHostDevice": true, + } + usbDevice := service + var searchErr error + for !validUSBDeviceClass[usbDevice.GetClass()] { + if usbDevice, searchErr = usbDevice.GetParent("IOService"); searchErr != nil { + break + } + } + if searchErr == nil { + // It's an IOUSBDevice + vid, _ := usbDevice.GetIntProperty("idVendor", C.kCFNumberSInt16Type) + pid, _ := usbDevice.GetIntProperty("idProduct", C.kCFNumberSInt16Type) + serialNumber, _ := usbDevice.GetStringProperty("USB Serial Number") + //product, _ := usbDevice.GetStringProperty("USB Product Name") + //manufacturer, _ := usbDevice.GetStringProperty("USB Vendor Name") + //fmt.Println(product + " - " + manufacturer) + + port.IsUSB = true + port.VID = fmt.Sprintf("%04X", vid) + port.PID = fmt.Sprintf("%04X", pid) + port.SerialNumber = serialNumber + } + return port, nil +} + +func getAllServices(serviceType string) ([]io_object_t, error) { + i, err := getMatchingServices(serviceMatching(serviceType)) + if err != nil { + return nil, err + } + defer i.Release() + + var services []io_object_t + tries := 0 + for tries < 5 { + // Extract all elements from iterator + if service, ok := i.Next(); ok { + services = append(services, service) + continue + } + // If the list of services is empty or the iterator is still valid return the result + if len(services) == 0 || i.IsValid() { + return services, nil + } + // Otherwise empty the result and retry + for _, s := range services { + s.Release() + } + services = []io_object_t{} + i.Reset() + tries++ + } + // Give up if the iteration continues to fail... + return nil, fmt.Errorf("IOServiceGetMatchingServices failed, data changed while iterating") +} + +// serviceMatching create a matching dictionary that specifies an IOService class match. +func serviceMatching(serviceType string) C.CFMutableDictionaryRef { + t := C.CString(serviceType) + defer C.free(unsafe.Pointer(t)) + return C.IOServiceMatching(t) +} + +// getMatchingServices look up registered IOService objects that match a matching dictionary. +func getMatchingServices(matcher C.CFMutableDictionaryRef) (io_iterator_t, error) { + var i C.io_iterator_t + err := C.IOServiceGetMatchingServices(C.kIOMasterPortDefault, C.CFDictionaryRef(matcher), &i) + if err != C.KERN_SUCCESS { + return 0, fmt.Errorf("IOServiceGetMatchingServices failed (code %d)", err) + } + return io_iterator_t(i), nil +} + +// CFStringRef + +type cfStringRef C.CFStringRef + +func cfStringCreateWithString(s string) cfStringRef { + c := C.CString(s) + defer C.free(unsafe.Pointer(c)) + return cfStringRef(C.CFStringCreateWithCString( + C.kCFAllocatorDefault, c, C.kCFStringEncodingMacRoman)) +} + +func (ref cfStringRef) Release() { + C.CFRelease(C.CFTypeRef(ref)) +} + +// CFTypeRef + +type cfTypeRef C.CFTypeRef + +func (ref cfTypeRef) Release() { + C.CFRelease(C.CFTypeRef(ref)) +} + +// io_registry_entry_t + +type io_registry_entry_t C.io_registry_entry_t + +func (me *io_registry_entry_t) GetParent(plane string) (io_registry_entry_t, error) { + cPlane := C.CString(plane) + defer C.free(unsafe.Pointer(cPlane)) + var parent C.io_registry_entry_t + err := C.IORegistryEntryGetParentEntry(C.io_registry_entry_t(*me), cPlane, &parent) + if err != 0 { + return 0, errors.New("No parent device available") + } + return io_registry_entry_t(parent), nil +} + +func (me *io_registry_entry_t) CreateCFProperty(key string) (cfTypeRef, error) { + k := cfStringCreateWithString(key) + defer k.Release() + property := C.IORegistryEntryCreateCFProperty(C.io_registry_entry_t(*me), C.CFStringRef(k), C.kCFAllocatorDefault, 0) + if property == 0 { + return 0, errors.New("Property not found: " + key) + } + return cfTypeRef(property), nil +} + +func (me *io_registry_entry_t) GetStringProperty(key string) (string, error) { + property, err := me.CreateCFProperty(key) + if err != nil { + return "", err + } + defer property.Release() + + if ptr := C.CFStringGetCStringPtr(C.CFStringRef(property), 0); ptr != nil { + return C.GoString(ptr), nil + } + // in certain circumstances CFStringGetCStringPtr may return NULL + // and we must retrieve the string by copy + buff := make([]C.char, 1024) + if C.CFStringGetCString(C.CFStringRef(property), &buff[0], 1024, 0) != C.true { + return "", fmt.Errorf("Property '%s' can't be converted", key) + } + return C.GoString(&buff[0]), nil +} + +func (me *io_registry_entry_t) GetIntProperty(key string, intType C.CFNumberType) (int, error) { + property, err := me.CreateCFProperty(key) + if err != nil { + return 0, err + } + defer property.Release() + var res int + if C.CFNumberGetValue((C.CFNumberRef)(property), intType, unsafe.Pointer(&res)) != C.true { + return res, fmt.Errorf("Property '%s' can't be converted or has been truncated", key) + } + return res, nil +} + +func (me *io_registry_entry_t) Release() { + C.IOObjectRelease(C.io_object_t(*me)) +} + +func (me *io_registry_entry_t) GetClass() string { + class := make([]C.char, 1024) + C.IOObjectGetClass(C.io_object_t(*me), &class[0]) + return C.GoString(&class[0]) +} + +// io_iterator_t + +type io_iterator_t C.io_iterator_t + +// IsValid checks if an iterator is still valid. +// Some iterators will be made invalid if changes are made to the +// structure they are iterating over. This function checks the iterator +// is still valid and should be called when Next returns zero. +// An invalid iterator can be Reset and the iteration restarted. +func (me *io_iterator_t) IsValid() bool { + return C.IOIteratorIsValid(C.io_iterator_t(*me)) == C.true +} + +func (me *io_iterator_t) Reset() { + C.IOIteratorReset(C.io_iterator_t(*me)) +} + +func (me *io_iterator_t) Next() (io_object_t, bool) { + res := C.IOIteratorNext(C.io_iterator_t(*me)) + return io_object_t(res), res != 0 +} + +func (me *io_iterator_t) Release() { + C.IOObjectRelease(C.io_object_t(*me)) +} + +// io_object_t + +type io_object_t C.io_object_t + +func (me *io_object_t) Release() { + C.IOObjectRelease(C.io_object_t(*me)) +} + +func (me *io_object_t) GetClass() string { + class := make([]C.char, 1024) + C.IOObjectGetClass(C.io_object_t(*me), &class[0]) + return C.GoString(&class[0]) +} diff --git a/enumerator/usb_freebsd.go b/enumerator/usb_freebsd.go new file mode 100644 index 0000000..553c5a0 --- /dev/null +++ b/enumerator/usb_freebsd.go @@ -0,0 +1,12 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + // TODO + return nil, &PortEnumerationError{} +} diff --git a/enumerator/usb_linux.go b/enumerator/usb_linux.go new file mode 100644 index 0000000..2e3737e --- /dev/null +++ b/enumerator/usb_linux.go @@ -0,0 +1,109 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + + "go.bug.st/serial" +) + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + // Retrieve the port list + ports, err := serial.GetPortsList() + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + + var res []*PortDetails + for _, port := range ports { + details, err := nativeGetPortDetails(port) + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + res = append(res, details) + } + return res, nil +} + +func nativeGetPortDetails(portPath string) (*PortDetails, error) { + portName := filepath.Base(portPath) + devicePath := fmt.Sprintf("/sys/class/tty/%s/device", portName) + if _, err := os.Stat(devicePath); err != nil { + return &PortDetails{}, nil + } + realDevicePath, err := filepath.EvalSymlinks(devicePath) + if err != nil { + return nil, fmt.Errorf("Can't determine real path of %s: %s", devicePath, err.Error()) + } + subSystemPath, err := filepath.EvalSymlinks(filepath.Join(realDevicePath, "subsystem")) + if err != nil { + return nil, fmt.Errorf("Can't determine real path of %s: %s", filepath.Join(realDevicePath, "subsystem"), err.Error()) + } + subSystem := filepath.Base(subSystemPath) + + result := &PortDetails{Name: portPath} + switch subSystem { + case "usb-serial": + err := parseUSBSysFS(filepath.Dir(filepath.Dir(realDevicePath)), result) + return result, err + case "usb": + err := parseUSBSysFS(filepath.Dir(realDevicePath), result) + return result, err + // TODO: other cases? + default: + return result, nil + } +} + +func parseUSBSysFS(usbDevicePath string, details *PortDetails) error { + vid, err := readLine(filepath.Join(usbDevicePath, "idVendor")) + if err != nil { + return err + } + pid, err := readLine(filepath.Join(usbDevicePath, "idProduct")) + if err != nil { + return err + } + serial, err := readLine(filepath.Join(usbDevicePath, "serial")) + if err != nil { + return err + } + //manufacturer, err := readLine(filepath.Join(usbDevicePath, "manufacturer")) + //if err != nil { + // return err + //} + //product, err := readLine(filepath.Join(usbDevicePath, "product")) + //if err != nil { + // return err + //} + + details.IsUSB = true + details.VID = vid + details.PID = pid + details.SerialNumber = serial + //details.Manufacturer = manufacturer + //details.Product = product + return nil +} + +func readLine(filename string) (string, error) { + file, err := os.Open(filename) + if os.IsNotExist(err) { + return "", nil + } + if err != nil { + return "", err + } + defer file.Close() + reader := bufio.NewReader(file) + line, _, err := reader.ReadLine() + return string(line), err +} diff --git a/enumerator/usb_openbsd.go b/enumerator/usb_openbsd.go new file mode 100644 index 0000000..553c5a0 --- /dev/null +++ b/enumerator/usb_openbsd.go @@ -0,0 +1,12 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + // TODO + return nil, &PortEnumerationError{} +} diff --git a/enumerator/usb_wasm.go b/enumerator/usb_wasm.go new file mode 100644 index 0000000..3f3b22f --- /dev/null +++ b/enumerator/usb_wasm.go @@ -0,0 +1,11 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + return nil, &PortEnumerationError{} +} diff --git a/enumerator/usb_windows.go b/enumerator/usb_windows.go new file mode 100644 index 0000000..2d3793b --- /dev/null +++ b/enumerator/usb_windows.go @@ -0,0 +1,359 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +import ( + "fmt" + "regexp" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +func parseDeviceID(deviceID string, details *PortDetails) { + // Windows stock USB-CDC driver + if len(deviceID) >= 3 && deviceID[:3] == "USB" { + re := regexp.MustCompile("VID_(....)&PID_(....)(\\\\(\\w+)$)?").FindAllStringSubmatch(deviceID, -1) + if re == nil || len(re[0]) < 2 { + // Silently ignore unparsable strings + return + } + details.IsUSB = true + details.VID = re[0][1] + details.PID = re[0][2] + if len(re[0]) >= 4 { + details.SerialNumber = re[0][4] + } + return + } + + // FTDI driver + if len(deviceID) >= 7 && deviceID[:7] == "FTDIBUS" { + re := regexp.MustCompile("VID_(....)\\+PID_(....)(\\+(\\w+))?").FindAllStringSubmatch(deviceID, -1) + if re == nil || len(re[0]) < 2 { + // Silently ignore unparsable strings + return + } + details.IsUSB = true + details.VID = re[0][1] + details.PID = re[0][2] + if len(re[0]) >= 4 { + details.SerialNumber = re[0][4] + } + return + } + + // Other unidentified device type +} + +// setupapi based +// -------------- + +//sys setupDiClassGuidsFromNameInternal(class string, guid *guid, guidSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiClassGuidsFromNameW +//sys setupDiGetClassDevs(guid *guid, enumerator *string, hwndParent uintptr, flags uint32) (set devicesSet, err error) = setupapi.SetupDiGetClassDevsW +//sys setupDiDestroyDeviceInfoList(set devicesSet) (err error) = setupapi.SetupDiDestroyDeviceInfoList +//sys setupDiEnumDeviceInfo(set devicesSet, index uint32, info *devInfoData) (err error) = setupapi.SetupDiEnumDeviceInfo +//sys setupDiGetDeviceInstanceId(set devicesSet, devInfo *devInfoData, devInstanceId unsafe.Pointer, devInstanceIdSize uint32, requiredSize *uint32) (err error) = setupapi.SetupDiGetDeviceInstanceIdW +//sys setupDiOpenDevRegKey(set devicesSet, devInfo *devInfoData, scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (hkey syscall.Handle, err error) = setupapi.SetupDiOpenDevRegKey +//sys setupDiGetDeviceRegistryProperty(set devicesSet, devInfo *devInfoData, property deviceProperty, propertyType *uint32, outValue *byte, bufSize uint32, reqSize *uint32) (res bool) = setupapi.SetupDiGetDeviceRegistryPropertyW + +//sys cmGetParent(outParentDev *devInstance, dev devInstance, flags uint32) (cmErr cmError) = cfgmgr32.CM_Get_Parent +//sys cmGetDeviceIDSize(outLen *uint32, dev devInstance, flags uint32) (cmErr cmError) = cfgmgr32.CM_Get_Device_ID_Size +//sys cmGetDeviceID(dev devInstance, buffer unsafe.Pointer, bufferSize uint32, flags uint32) (err cmError) = cfgmgr32.CM_Get_Device_IDW +//sys cmMapCrToWin32Err(cmErr cmError, defaultErr uint32) (err uint32) = cfgmgr32.CM_MapCrToWin32Err + +// Device registry property codes +// (Codes marked as read-only (R) may only be used for +// SetupDiGetDeviceRegistryProperty) +// +// These values should cover the same set of registry properties +// as defined by the CM_DRP codes in cfgmgr32.h. +// +// Note that SPDRP codes are zero based while CM_DRP codes are one based! +type deviceProperty uint32 + +const ( + spdrpDeviceDesc deviceProperty = 0x00000000 // DeviceDesc = R/W + spdrpHardwareID = 0x00000001 // HardwareID = R/W + spdrpCompatibleIDS = 0x00000002 // CompatibleIDs = R/W + spdrpUnused0 = 0x00000003 // Unused + spdrpService = 0x00000004 // Service = R/W + spdrpUnused1 = 0x00000005 // Unused + spdrpUnused2 = 0x00000006 // Unused + spdrpClass = 0x00000007 // Class = R--tied to ClassGUID + spdrpClassGUID = 0x00000008 // ClassGUID = R/W + spdrpDriver = 0x00000009 // Driver = R/W + spdrpConfigFlags = 0x0000000A // ConfigFlags = R/W + spdrpMFG = 0x0000000B // Mfg = R/W + spdrpFriendlyName = 0x0000000C // FriendlyName = R/W + spdrpLocationIinformation = 0x0000000D // LocationInformation = R/W + spdrpPhysicalDeviceObjectName = 0x0000000E // PhysicalDeviceObjectName = R + spdrpCapabilities = 0x0000000F // Capabilities = R + spdrpUINumber = 0x00000010 // UiNumber = R + spdrpUpperFilters = 0x00000011 // UpperFilters = R/W + spdrpLowerFilters = 0x00000012 // LowerFilters = R/W + spdrpBusTypeGUID = 0x00000013 // BusTypeGUID = R + spdrpLegacyBusType = 0x00000014 // LegacyBusType = R + spdrpBusNumber = 0x00000015 // BusNumber = R + spdrpEnumeratorName = 0x00000016 // Enumerator Name = R + spdrpSecurity = 0x00000017 // Security = R/W, binary form + spdrpSecuritySDS = 0x00000018 // Security = W, SDS form + spdrpDevType = 0x00000019 // Device Type = R/W + spdrpExclusive = 0x0000001A // Device is exclusive-access = R/W + spdrpCharacteristics = 0x0000001B // Device Characteristics = R/W + spdrpAddress = 0x0000001C // Device Address = R + spdrpUINumberDescFormat = 0x0000001D // UiNumberDescFormat = R/W + spdrpDevicePowerData = 0x0000001E // Device Power Data = R + spdrpRemovalPolicy = 0x0000001F // Removal Policy = R + spdrpRemovalPolicyHWDefault = 0x00000020 // Hardware Removal Policy = R + spdrpRemovalPolicyOverride = 0x00000021 // Removal Policy Override = RW + spdrpInstallState = 0x00000022 // Device Install State = R + spdrpLocationPaths = 0x00000023 // Device Location Paths = R + spdrpBaseContainerID = 0x00000024 // Base ContainerID = R + + spdrpMaximumProperty = 0x00000025 // Upper bound on ordinals +) + +// Values specifying the scope of a device property change +type dicsScope uint32 + +const ( + dicsFlagGlobal dicsScope = 0x00000001 // make change in all hardware profiles + dicsFlagConfigSspecific = 0x00000002 // make change in specified profile only + dicsFlagConfigGeneral = 0x00000004 // 1 or more hardware profile-specific +) + +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724878(v=vs.85).aspx +type regsam uint32 + +const ( + keyAllAccess regsam = 0xF003F + keyCreateLink = 0x00020 + keyCreateSubKey = 0x00004 + keyEnumerateSubKeys = 0x00008 + keyExecute = 0x20019 + keyNotify = 0x00010 + keyQueryValue = 0x00001 + keyRead = 0x20019 + keySetValue = 0x00002 + keyWOW64_32key = 0x00200 + keyWOW64_64key = 0x00100 + keyWrite = 0x20006 +) + +// KeyType values for SetupDiCreateDevRegKey, SetupDiOpenDevRegKey, and +// SetupDiDeleteDevRegKey. +const ( + diregDev = 0x00000001 // Open/Create/Delete device key + diregDrv = 0x00000002 // Open/Create/Delete driver key + diregBoth = 0x00000004 // Delete both driver and Device key +) + +// https://msdn.microsoft.com/it-it/library/windows/desktop/aa373931(v=vs.85).aspx +type guid struct { + data1 uint32 + data2 uint16 + data3 uint16 + data4 [8]byte +} + +func (g guid) String() string { + return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", + g.data1, g.data2, g.data3, + g.data4[0], g.data4[1], g.data4[2], g.data4[3], + g.data4[4], g.data4[5], g.data4[6], g.data4[7]) +} + +func classGuidsFromName(className string) ([]guid, error) { + // Determine the number of GUIDs for className + n := uint32(0) + if err := setupDiClassGuidsFromNameInternal(className, nil, 0, &n); err != nil { + // ignore error: UIDs array size too small + } + + res := make([]guid, n) + err := setupDiClassGuidsFromNameInternal(className, &res[0], n, &n) + return res, err +} + +const ( + digcfDefault = 0x00000001 // only valid with digcfDeviceInterface + digcfPresent = 0x00000002 + digcfAllClasses = 0x00000004 + digcfProfile = 0x00000008 + digcfDeviceInterface = 0x00000010 +) + +type devicesSet syscall.Handle + +func (g *guid) getDevicesSet() (devicesSet, error) { + return setupDiGetClassDevs(g, nil, 0, digcfPresent) +} + +func (set devicesSet) destroy() { + setupDiDestroyDeviceInfoList(set) +} + +type cmError uint32 + +// https://msdn.microsoft.com/en-us/library/windows/hardware/ff552344(v=vs.85).aspx +type devInfoData struct { + size uint32 + guid guid + devInst devInstance + reserved uintptr +} + +type devInstance uint32 + +func cmConvertError(cmErr cmError) error { + if cmErr == 0 { + return nil + } + winErr := cmMapCrToWin32Err(cmErr, 0) + return fmt.Errorf("error %d", winErr) +} + +func (dev devInstance) getParent() (devInstance, error) { + var res devInstance + errN := cmGetParent(&res, dev, 0) + return res, cmConvertError(errN) +} + +func (dev devInstance) GetDeviceID() (string, error) { + var size uint32 + cmErr := cmGetDeviceIDSize(&size, dev, 0) + if err := cmConvertError(cmErr); err != nil { + return "", err + } + buff := make([]uint16, size) + cmErr = cmGetDeviceID(dev, unsafe.Pointer(&buff[0]), uint32(len(buff)), 0) + if err := cmConvertError(cmErr); err != nil { + return "", err + } + return windows.UTF16ToString(buff[:]), nil +} + +type deviceInfo struct { + set devicesSet + data devInfoData +} + +func (set devicesSet) getDeviceInfo(index int) (*deviceInfo, error) { + result := &deviceInfo{set: set} + + result.data.size = uint32(unsafe.Sizeof(result.data)) + err := setupDiEnumDeviceInfo(set, uint32(index), &result.data) + return result, err +} + +func (dev *deviceInfo) getInstanceID() (string, error) { + n := uint32(0) + setupDiGetDeviceInstanceId(dev.set, &dev.data, nil, 0, &n) + buff := make([]uint16, n) + if err := setupDiGetDeviceInstanceId(dev.set, &dev.data, unsafe.Pointer(&buff[0]), uint32(len(buff)), &n); err != nil { + return "", err + } + return windows.UTF16ToString(buff[:]), nil +} + +func (dev *deviceInfo) openDevRegKey(scope dicsScope, hwProfile uint32, keyType uint32, samDesired regsam) (syscall.Handle, error) { + return setupDiOpenDevRegKey(dev.set, &dev.data, scope, hwProfile, keyType, samDesired) +} + +func nativeGetDetailedPortsList() ([]*PortDetails, error) { + guids, err := classGuidsFromName("Ports") + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + + var res []*PortDetails + for _, g := range guids { + devsSet, err := g.getDevicesSet() + if err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + defer devsSet.destroy() + + for i := 0; ; i++ { + device, err := devsSet.getDeviceInfo(i) + if err != nil { + break + } + details := &PortDetails{} + portName, err := retrievePortNameFromDevInfo(device) + if err != nil { + continue + } + if len(portName) < 3 || portName[0:3] != "COM" { + // Accept only COM ports + continue + } + details.Name = portName + + if err := retrievePortDetailsFromDevInfo(device, details); err != nil { + return nil, &PortEnumerationError{causedBy: err} + } + res = append(res, details) + } + } + return res, nil +} + +func retrievePortNameFromDevInfo(device *deviceInfo) (string, error) { + h, err := device.openDevRegKey(dicsFlagGlobal, 0, diregDev, keyRead) + if err != nil { + return "", err + } + defer syscall.RegCloseKey(h) + + var name [1024]uint16 + nameP := (*byte)(unsafe.Pointer(&name[0])) + nameSize := uint32(len(name) * 2) + if err := syscall.RegQueryValueEx(h, syscall.StringToUTF16Ptr("PortName"), nil, nil, nameP, &nameSize); err != nil { + return "", err + } + return syscall.UTF16ToString(name[:]), nil +} + +func retrievePortDetailsFromDevInfo(device *deviceInfo, details *PortDetails) error { + deviceID, err := device.getInstanceID() + if err != nil { + return err + } + parseDeviceID(deviceID, details) + + // On composite USB devices the serial number is usually reported on the parent + // device, so let's navigate up one level and see if we can get this information + if details.IsUSB && details.SerialNumber == "" { + if parentInfo, err := device.data.devInst.getParent(); err == nil { + if parentDeviceID, err := parentInfo.GetDeviceID(); err == nil { + d := &PortDetails{} + parseDeviceID(parentDeviceID, d) + if details.VID == d.VID && details.PID == d.PID { + details.SerialNumber = d.SerialNumber + } + } + } + } + + /* spdrpDeviceDesc returns a generic name, e.g.: "CDC-ACM", which will be the same for 2 identical devices attached + while spdrpFriendlyName returns a specific name, e.g.: "CDC-ACM (COM44)", + the result of spdrpFriendlyName is therefore unique and suitable as an alternative string to for a port choice */ + n := uint32(0) + setupDiGetDeviceRegistryProperty(device.set, &device.data, spdrpFriendlyName /* spdrpDeviceDesc */, nil, nil, 0, &n) + if n > 0 { + buff := make([]uint16, n*2) + buffP := (*byte)(unsafe.Pointer(&buff[0])) + if setupDiGetDeviceRegistryProperty(device.set, &device.data, spdrpFriendlyName /* spdrpDeviceDesc */, nil, buffP, n, &n) { + details.Product = syscall.UTF16ToString(buff[:]) + } + } + + return nil +} diff --git a/enumerator/usb_windows_test.go b/enumerator/usb_windows_test.go new file mode 100644 index 0000000..3d39e0a --- /dev/null +++ b/enumerator/usb_windows_test.go @@ -0,0 +1,49 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package enumerator + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func parseAndReturnDeviceID(deviceID string) *PortDetails { + res := &PortDetails{} + parseDeviceID(deviceID, res) + return res +} + +func TestParseDeviceID(t *testing.T) { + r := require.New(t) + test := func(deviceId, vid, pid, serialNo string) { + res := parseAndReturnDeviceID(deviceId) + r.True(res.IsUSB) + r.Equal(vid, res.VID) + r.Equal(pid, res.PID) + r.Equal(serialNo, res.SerialNumber) + } + + test("FTDIBUS\\VID_0403+PID_6001+A6004CCFA\\0000", "0403", "6001", "A6004CCFA") + test("USB\\VID_16C0&PID_0483\\12345", "16C0", "0483", "12345") + test("USB\\VID_2341&PID_0000\\64936333936351400000", "2341", "0000", "64936333936351400000") + test("USB\\VID_2341&PID_0000\\6493234373835191F1F1", "2341", "0000", "6493234373835191F1F1") + test("USB\\VID_2341&PID_804E&MI_00\\6&279A3900&0&0000", "2341", "804E", "") + test("USB\\VID_2341&PID_004E\\5&C3DC240&0&1", "2341", "004E", "") + test("USB\\VID_03EB&PID_2111&MI_01\\6&21F3553F&0&0001", "03EB", "2111", "") // Atmel EDBG + test("USB\\VID_2341&PID_804D&MI_00\\6&1026E213&0&0000", "2341", "804D", "") + test("USB\\VID_2341&PID_004D\\5&C3DC240&0&1", "2341", "004D", "") + test("USB\\VID_067B&PID_2303\\6&2C4CB384&0&3", "067B", "2303", "") // PL2303 +} + +func TestParseDeviceIDWithInvalidStrings(t *testing.T) { + r := require.New(t) + res := parseAndReturnDeviceID("ABC") + r.False(res.IsUSB) + res2 := parseAndReturnDeviceID("USB") + r.False(res2.IsUSB) +} diff --git a/enumerator_wasm.go b/enumerator_wasm.go new file mode 100644 index 0000000..349d272 --- /dev/null +++ b/enumerator_wasm.go @@ -0,0 +1,15 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package serial + +import ( + "errors" +) + +func nativeGetPortsList() ([]string, error) { + return nil, errors.New("nativeGetPortsList is not supported on wasm") +} diff --git a/example_getportlist_test.go b/example_getportlist_test.go index be34e81..a1756ac 100644 --- a/example_getportlist_test.go +++ b/example_getportlist_test.go @@ -1,14 +1,17 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package serial_test -import "fmt" -import "log" -import "go.bug.st/serial" +import ( + "fmt" + "log" + + "go.bug.st/serial" +) func ExampleGetPortsList() { ports, err := serial.GetPortsList() @@ -23,4 +26,3 @@ func ExampleGetPortsList() { } } } - diff --git a/example_modem_bits_test.go b/example_modem_bits_test.go new file mode 100644 index 0000000..eaad39a --- /dev/null +++ b/example_modem_bits_test.go @@ -0,0 +1,70 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package serial_test + +import ( + "fmt" + "log" + "time" + + "go.bug.st/serial" +) + +func ExamplePort_GetModemStatusBits() { + // Open the first serial port detected at 9600bps N81 + mode := &serial.Mode{ + BaudRate: 9600, + Parity: serial.NoParity, + DataBits: 8, + StopBits: serial.OneStopBit, + } + port, err := serial.Open("/dev/ttyACM1", mode) + if err != nil { + log.Fatal(err) + } + defer port.Close() + + count := 0 + for count < 25 { + status, err := port.GetModemStatusBits() + if err != nil { + log.Fatal(err) + } + fmt.Printf("Status: %+v\n", status) + + time.Sleep(time.Second) + count++ + if count == 5 { + err := port.SetDTR(false) + if err != nil { + log.Fatal(err) + } + fmt.Println("Set DTR OFF") + } + if count == 10 { + err := port.SetDTR(true) + if err != nil { + log.Fatal(err) + } + fmt.Println("Set DTR ON") + } + if count == 15 { + err := port.SetRTS(false) + if err != nil { + log.Fatal(err) + } + fmt.Println("Set RTS OFF") + } + if count == 20 { + err := port.SetRTS(true) + if err != nil { + log.Fatal(err) + } + fmt.Println("Set RTS ON") + } + } +} diff --git a/example_serialport_test.go b/example_serialport_test.go index 2ee936c..937b8b9 100644 --- a/example_serialport_test.go +++ b/example_serialport_test.go @@ -1,29 +1,31 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package serial_test -import "fmt" -import "log" -import "go.bug.st/serial" +import ( + "fmt" + "log" -func ExampleSerialPort_SetMode() { - port, err := serial.OpenPort("/dev/ttyACM0", &serial.Mode{}) + "go.bug.st/serial" +) + +func ExamplePort_SetMode() { + port, err := serial.Open("/dev/ttyACM0", &serial.Mode{}) if err != nil { log.Fatal(err) } mode := &serial.Mode{ BaudRate: 9600, - Parity: serial.PARITY_NONE, + Parity: serial.NoParity, DataBits: 8, - StopBits: serial.STOPBITS_ONE, + StopBits: serial.OneStopBit, } if err := port.SetMode(mode); err != nil { log.Fatal(err) } fmt.Println("Port set to 9600 N81") } - diff --git a/example_test.go b/example_test.go index c087746..7ad5eef 100644 --- a/example_test.go +++ b/example_test.go @@ -1,14 +1,18 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package serial_test -import "fmt" -import "log" -import "go.bug.st/serial" +import ( + "fmt" + "log" + "strings" + + "go.bug.st/serial" +) // This example prints the list of serial ports and use the first one // to send a string "10,20,30" and prints the response on the screen. @@ -31,11 +35,11 @@ func Example_sendAndReceive() { // Open the first serial port detected at 9600bps N81 mode := &serial.Mode{ BaudRate: 9600, - Parity: serial.PARITY_NONE, + Parity: serial.NoParity, DataBits: 8, - StopBits: serial.STOPBITS_ONE, + StopBits: serial.OneStopBit, } - port, err := serial.OpenPort(ports[0], mode) + port, err := serial.Open(ports[0], mode) if err != nil { log.Fatal(err) } @@ -48,19 +52,24 @@ func Example_sendAndReceive() { fmt.Printf("Sent %v bytes\n", n) // Read and print the response + buff := make([]byte, 100) for { // Reads up to 100 bytes n, err := port.Read(buff) if err != nil { log.Fatal(err) - break } if n == 0 { fmt.Println("\nEOF") break } - fmt.Printf("%v", string(buff[:n])) + + fmt.Printf("%s", string(buff[:n])) + + // If we receive a newline stop reading + if strings.Contains(string(buff[:n]), "\n") { + break + } } } - diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e89755e --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module go.bug.st/serial + +go 1.17 + +require ( + github.com/creack/goselect v0.1.2 + github.com/stretchr/testify v1.8.4 + golang.org/x/sys v0.19.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4cef3dc --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/portlist/portlist.go b/portlist/portlist.go new file mode 100644 index 0000000..5d58409 --- /dev/null +++ b/portlist/portlist.go @@ -0,0 +1,43 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +// portlist is a tool to list all the available serial ports. +// Just run it and it will produce an output like: +// +// $ go run portlist.go +// Port: /dev/cu.Bluetooth-Incoming-Port +// Port: /dev/cu.usbmodemFD121 +// USB ID 2341:8053 +// USB serial FB7B6060504B5952302E314AFF08191A + +package main + +import ( + "fmt" + "log" + + "go.bug.st/serial/enumerator" +) + +func main() { + ports, err := enumerator.GetDetailedPortsList() + if err != nil { + log.Fatal(err) + } + if len(ports) == 0 { + return + } + for _, port := range ports { + fmt.Printf("Port: %s\n", port.Name) + if port.Product != "" { + fmt.Printf(" Product Name: %s\n", port.Product) + } + if port.IsUSB { + fmt.Printf(" USB ID : %s:%s\n", port.VID, port.PID) + fmt.Printf(" USB serial : %s\n", port.SerialNumber) + } + } +} diff --git a/serial.go b/serial.go index 9cb1052..a2f7333 100644 --- a/serial.go +++ b/serial.go @@ -1,75 +1,213 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -package serial // import "go.bug.st/serial" +package serial -// This structure describes a serial port configuration. +import "time" + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go + +// Port is the interface for a serial Port +type Port interface { + // SetMode sets all parameters of the serial port + SetMode(mode *Mode) error + + // Stores data received from the serial port into the provided byte array + // buffer. The function returns the number of bytes read. + // + // The Read function blocks until (at least) one byte is received from + // the serial port or an error occurs. + Read(p []byte) (n int, err error) + + // Send the content of the data byte array to the serial port. + // Returns the number of bytes written. + Write(p []byte) (n int, err error) + + // Wait until all data in the buffer are sent + Drain() error + + // ResetInputBuffer Purges port read buffer + ResetInputBuffer() error + + // ResetOutputBuffer Purges port write buffer + ResetOutputBuffer() error + + // SetDTR sets the modem status bit DataTerminalReady + SetDTR(dtr bool) error + + // SetRTS sets the modem status bit RequestToSend + SetRTS(rts bool) error + + // GetModemStatusBits returns a ModemStatusBits structure containing the + // modem status bits for the serial port (CTS, DSR, etc...) + GetModemStatusBits() (*ModemStatusBits, error) + + // SetReadTimeout sets the timeout for the Read operation or use serial.NoTimeout + // to disable read timeout. + SetReadTimeout(t time.Duration) error + + // Close the serial port + Close() error + + // Break sends a break for a determined time + Break(time.Duration) error +} + +// NoTimeout should be used as a parameter to SetReadTimeout to disable timeout. +var NoTimeout time.Duration = -1 + +// ModemStatusBits contains all the modem input status bits for a serial port (CTS, DSR, etc...). +// It can be retrieved with the Port.GetModemStatusBits() method. +type ModemStatusBits struct { + CTS bool // ClearToSend status + DSR bool // DataSetReady status + RI bool // RingIndicator status + DCD bool // DataCarrierDetect status +} + +// ModemOutputBits contains all the modem output bits for a serial port. +// This is used in the Mode.InitialStatusBits struct to specify the initial status of the bits. +// Note: Linux and MacOSX (and basically all unix-based systems) can not set the status bits +// before opening the port, even if the initial state of the bit is set to false they will go +// anyway to true for a few milliseconds, resulting in a small pulse. +type ModemOutputBits struct { + RTS bool // ReadyToSend status + DTR bool // DataTerminalReady status +} + +// Open opens the serial port using the specified modes +func Open(portName string, mode *Mode) (Port, error) { + port, err := nativeOpen(portName, mode) + if err != nil { + // Return a nil interface, for which var==nil is true (instead of + // a nil pointer to a struct that satisfies the interface). + return nil, err + } + return port, err +} + +// GetPortsList retrieve the list of available serial ports +func GetPortsList() ([]string, error) { + return nativeGetPortsList() +} + +// Mode describes a serial port configuration. type Mode struct { - BaudRate int // The serial port bitrate (aka Baudrate) - DataBits int // Size of the character (must be 5, 6, 7 or 8) - Parity Parity // Parity (see Parity type for more info) - StopBits StopBits // Stop bits (see StopBits type for more info) + BaudRate int // The serial port bitrate (aka Baudrate) + DataBits int // Size of the character (must be 5, 6, 7 or 8) + Parity Parity // Parity (see Parity type for more info) + StopBits StopBits // Stop bits (see StopBits type for more info) + InitialStatusBits *ModemOutputBits // Initial output modem bits status (if nil defaults to DTR=true and RTS=true) } +// Parity describes a serial port parity setting type Parity int const ( - PARITY_NONE Parity = iota // No parity (default) - PARITY_ODD // Odd parity - PARITY_EVEN // Even parity - PARITY_MARK // Mark parity (always 1) - PARITY_SPACE // Space parity (always 0) + // NoParity disable parity control (default) + NoParity Parity = iota + // OddParity enable odd-parity check + OddParity + // EvenParity enable even-parity check + EvenParity + // MarkParity enable mark-parity (always 1) check + MarkParity + // SpaceParity enable space-parity (always 0) check + SpaceParity ) +// StopBits describe a serial port stop bits setting type StopBits int const ( - STOPBITS_ONE StopBits = iota // 1 Stop bit - STOPBITS_ONEPOINTFIVE // 1.5 Stop bits - STOPBITS_TWO // 2 Stop bits + // OneStopBit sets 1 stop bit (default) + OneStopBit StopBits = iota + // OnePointFiveStopBits sets 1.5 stop bits + OnePointFiveStopBits + // TwoStopBits sets 2 stop bits + TwoStopBits ) -// Platform independent error type for serial ports -type SerialPortError struct { - err string - code int +// PortError is a platform independent error type for serial ports +type PortError struct { + code PortErrorCode + causedBy error } +// PortErrorCode is a code to easily identify the type of error +type PortErrorCode int + const ( - ERROR_PORT_BUSY = iota - ERROR_PORT_NOT_FOUND - ERROR_INVALID_SERIAL_PORT - ERROR_PERMISSION_DENIED - ERROR_INVALID_PORT_SPEED - ERROR_INVALID_PORT_DATA_BITS - ERROR_ENUMERATING_PORTS - ERROR_OTHER + // PortBusy the serial port is already in used by another process + PortBusy PortErrorCode = iota + // PortNotFound the requested port doesn't exist + PortNotFound + // InvalidSerialPort the requested port is not a serial port + InvalidSerialPort + // PermissionDenied the user doesn't have enough privileges + PermissionDenied + // InvalidSpeed the requested speed is not valid or not supported + InvalidSpeed + // InvalidDataBits the number of data bits is not valid or not supported + InvalidDataBits + // InvalidParity the selected parity is not valid or not supported + InvalidParity + // InvalidStopBits the selected number of stop bits is not valid or not supported + InvalidStopBits + // InvalidTimeoutValue the timeout value is not valid or not supported + InvalidTimeoutValue + // ErrorEnumeratingPorts an error occurred while listing serial port + ErrorEnumeratingPorts + // PortClosed the port has been closed while the operation is in progress + PortClosed + // FunctionNotImplemented the requested function is not implemented + FunctionNotImplemented ) -func (e SerialPortError) Error() string { +// EncodedErrorString returns a string explaining the error code +func (e PortError) EncodedErrorString() string { switch e.code { - case ERROR_PORT_BUSY: + case PortBusy: return "Serial port busy" - case ERROR_PORT_NOT_FOUND: + case PortNotFound: return "Serial port not found" - case ERROR_INVALID_SERIAL_PORT: + case InvalidSerialPort: return "Invalid serial port" - case ERROR_PERMISSION_DENIED: + case PermissionDenied: return "Permission denied" - case ERROR_INVALID_PORT_SPEED: - return "Invalid port speed" - case ERROR_INVALID_PORT_DATA_BITS: - return "Invalid port data bits" - case ERROR_ENUMERATING_PORTS: + case InvalidSpeed: + return "Port speed invalid or not supported" + case InvalidDataBits: + return "Port data bits invalid or not supported" + case InvalidParity: + return "Port parity invalid or not supported" + case InvalidStopBits: + return "Port stop bits invalid or not supported" + case InvalidTimeoutValue: + return "Timeout value invalid or not supported" + case ErrorEnumeratingPorts: return "Could not enumerate serial ports" + case PortClosed: + return "Port has been closed" + case FunctionNotImplemented: + return "Function not implemented" + default: + return "Other error" } - return e.err } -func (e SerialPortError) Code() int { - return e.code +// Error returns the complete error code with details on the cause of the error +func (e PortError) Error() string { + if e.causedBy != nil { + return e.EncodedErrorString() + ": " + e.causedBy.Error() + } + return e.EncodedErrorString() } +// Code returns an identifier for the kind of error occurred +func (e PortError) Code() PortErrorCode { + return e.code +} diff --git a/serial_bsd.go b/serial_bsd.go new file mode 100644 index 0000000..0368d18 --- /dev/null +++ b/serial_bsd.go @@ -0,0 +1,15 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build darwin || dragonfly || freebsd || netbsd || openbsd + +package serial + +import "golang.org/x/sys/unix" + +func (port *unixPort) Drain() error { + return unix.IoctlSetInt(port.handle, unix.TIOCDRAIN, 0) +} diff --git a/serial_darwin.go b/serial_darwin.go index 52742c0..a87458e 100644 --- a/serial_darwin.go +++ b/serial_darwin.go @@ -1,54 +1,46 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -package serial // import "go.bug.st/serial" +package serial -import "syscall" +import ( + "regexp" + + "golang.org/x/sys/unix" +) const devFolder = "/dev" -const regexFilter = "^(cu|tty)\\..*" - -// termios manipulation functions - -var baudrateMap = map[int]int{ - 0: syscall.B9600, // Default to 9600 - 50: syscall.B50, - 75: syscall.B75, - 110: syscall.B110, - 134: syscall.B134, - 150: syscall.B150, - 200: syscall.B200, - 300: syscall.B300, - 600: syscall.B600, - 1200: syscall.B1200, - 1800: syscall.B1800, - 2400: syscall.B2400, - 4800: syscall.B4800, - 9600: syscall.B9600, - 19200: syscall.B19200, - 38400: syscall.B38400, - 57600: syscall.B57600, - 115200: syscall.B115200, - 230400: syscall.B230400, -} -var databitsMap = map[int]int{ - 0: syscall.CS8, // Default to 8 bits - 5: syscall.CS5, - 6: syscall.CS6, - 7: syscall.CS7, - 8: syscall.CS8, +var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*") + +const ioctlTcgetattr = unix.TIOCGETA +const ioctlTcsetattr = unix.TIOCSETA +const ioctlTcflsh = unix.TIOCFLUSH +const ioctlTioccbrk = unix.TIOCCBRK +const ioctlTiocsbrk = unix.TIOCSBRK + +func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { + baudrate, ok := baudrateMap[speed] + if !ok { + return nil, true + } + settings.Ispeed = toTermiosSpeedType(baudrate) + settings.Ospeed = toTermiosSpeedType(baudrate) + return nil, false } -const tc_CMSPAR int = 0 // may be CMSPAR or PAREXT -const tc_IUCLC int = 0 - -// syscall wrappers +func (port *unixPort) setSpecialBaudrate(speed uint32) error { + const kIOSSIOSPEED = 0x80045402 + return unix.IoctlSetPointerInt(port.handle, kIOSSIOSPEED, int(speed)) +} -//sys ioctl(fd int, req uint64, data uintptr) (err error) +func (port *unixPort) ResetInputBuffer() error { + return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) +} -const ioctl_tcgetattr = syscall.TIOCGETA -const ioctl_tcsetattr = syscall.TIOCSETA +func (port *unixPort) ResetOutputBuffer() error { + return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) +} diff --git a/serial_darwin_386.go b/serial_darwin_386.go index a88e554..f3842b5 100644 --- a/serial_darwin_386.go +++ b/serial_darwin_386.go @@ -1,11 +1,53 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -package serial // import "go.bug.st/serial" +package serial -func termiosMask(data int) uint32 { - return uint32(data) +import "golang.org/x/sys/unix" + +// termios manipulation functions + +var baudrateMap = map[int]uint32{ + 0: unix.B9600, // Default to 9600 + 50: unix.B50, + 75: unix.B75, + 110: unix.B110, + 134: unix.B134, + 150: unix.B150, + 200: unix.B200, + 300: unix.B300, + 600: unix.B600, + 1200: unix.B1200, + 1800: unix.B1800, + 2400: unix.B2400, + 4800: unix.B4800, + 9600: unix.B9600, + 19200: unix.B19200, + 38400: unix.B38400, + 57600: unix.B57600, + 115200: unix.B115200, + 230400: unix.B230400, +} + +var databitsMap = map[int]uint32{ + 0: unix.CS8, // Default to 8 bits + 5: unix.CS5, + 6: unix.CS6, + 7: unix.CS7, + 8: unix.CS8, +} + +const tcCMSPAR uint32 = 0 // may be CMSPAR or PAREXT +const tcIUCLC uint32 = 0 + +const tcCCTS_OFLOW uint32 = 0x00010000 +const tcCRTS_IFLOW uint32 = 0x00020000 + +const tcCRTSCTS uint32 = (tcCCTS_OFLOW | tcCRTS_IFLOW) + +func toTermiosSpeedType(speed uint32) uint32 { + return speed } diff --git a/serial_darwin_64.go b/serial_darwin_64.go new file mode 100644 index 0000000..c0ed76c --- /dev/null +++ b/serial_darwin_64.go @@ -0,0 +1,55 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build darwin && (amd64 || arm64) + +package serial + +import "golang.org/x/sys/unix" + +// termios manipulation functions + +var baudrateMap = map[int]uint64{ + 0: unix.B9600, // Default to 9600 + 50: unix.B50, + 75: unix.B75, + 110: unix.B110, + 134: unix.B134, + 150: unix.B150, + 200: unix.B200, + 300: unix.B300, + 600: unix.B600, + 1200: unix.B1200, + 1800: unix.B1800, + 2400: unix.B2400, + 4800: unix.B4800, + 9600: unix.B9600, + 19200: unix.B19200, + 38400: unix.B38400, + 57600: unix.B57600, + 115200: unix.B115200, + 230400: unix.B230400, +} + +var databitsMap = map[int]uint64{ + 0: unix.CS8, // Default to 8 bits + 5: unix.CS5, + 6: unix.CS6, + 7: unix.CS7, + 8: unix.CS8, +} + +const tcCMSPAR uint64 = 0 // may be CMSPAR or PAREXT +const tcIUCLC uint64 = 0 + +const tcCCTS_OFLOW uint64 = 0x00010000 +const tcCRTS_IFLOW uint64 = 0x00020000 + +const tcCRTSCTS uint64 = (tcCCTS_OFLOW | tcCRTS_IFLOW) + +func toTermiosSpeedType(speed uint64) uint64 { + return speed +} diff --git a/serial_darwin_amd64.go b/serial_darwin_amd64.go deleted file mode 100644 index 377c614..0000000 --- a/serial_darwin_amd64.go +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright 2014-2016 Cristian Maglie. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// - -package serial // import "go.bug.st/serial" - -// termios manipulation functions - -func termiosMask(data int) uint64 { - return uint64(data) -} diff --git a/serial_freebsd.go b/serial_freebsd.go index 47075af..775b7a6 100644 --- a/serial_freebsd.go +++ b/serial_freebsd.go @@ -1,58 +1,92 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -package serial // import "go.bug.st/serial" +package serial -import "syscall" +import ( + "regexp" + + "golang.org/x/sys/unix" +) const devFolder = "/dev" -const regexFilter = "^(cu|tty)\\..*" + +var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*") // termios manipulation functions -var baudrateMap = map[int]int{ - 0: syscall.B9600, // Default to 9600 - 50: syscall.B50, - 75: syscall.B75, - 110: syscall.B110, - 134: syscall.B134, - 150: syscall.B150, - 200: syscall.B200, - 300: syscall.B300, - 600: syscall.B600, - 1200: syscall.B1200, - 1800: syscall.B1800, - 2400: syscall.B2400, - 4800: syscall.B4800, - 9600: syscall.B9600, - 19200: syscall.B19200, - 38400: syscall.B38400, - 57600: syscall.B57600, - 115200: syscall.B115200, - 230400: syscall.B230400, +var baudrateMap = map[int]uint32{ + 0: unix.B9600, // Default to 9600 + 50: unix.B50, + 75: unix.B75, + 110: unix.B110, + 134: unix.B134, + 150: unix.B150, + 200: unix.B200, + 300: unix.B300, + 600: unix.B600, + 1200: unix.B1200, + 1800: unix.B1800, + 2400: unix.B2400, + 4800: unix.B4800, + 9600: unix.B9600, + 19200: unix.B19200, + 38400: unix.B38400, + 57600: unix.B57600, + 115200: unix.B115200, + 230400: unix.B230400, + 460800: unix.B460800, + 921600: unix.B921600, } -var databitsMap = map[int]int{ - 0: syscall.CS8, // Default to 8 bits - 5: syscall.CS5, - 6: syscall.CS6, - 7: syscall.CS7, - 8: syscall.CS8, +var databitsMap = map[int]uint32{ + 0: unix.CS8, // Default to 8 bits + 5: unix.CS5, + 6: unix.CS6, + 7: unix.CS7, + 8: unix.CS8, } -const tc_CMSPAR int = 0 // may be CMSPAR or PAREXT -const tc_IUCLC int = 0 +const tcCMSPAR uint32 = 0 // may be CMSPAR or PAREXT +const tcIUCLC uint32 = 0 -// syscall wrappers +const tcCCTS_OFLOW uint32 = 0x00010000 +const tcCRTS_IFLOW uint32 = 0x00020000 -//sys ioctl(fd int, req uint64, data uintptr) (err error) +const tcCRTSCTS uint32 = tcCCTS_OFLOW -const ioctl_tcgetattr = syscall.TIOCGETA -const ioctl_tcsetattr = syscall.TIOCSETA +const ioctlTcgetattr = unix.TIOCGETA +const ioctlTcsetattr = unix.TIOCSETA +const ioctlTcflsh = unix.TIOCFLUSH +const ioctlTioccbrk = unix.TIOCCBRK +const ioctlTiocsbrk = unix.TIOCSBRK + +func toTermiosSpeedType(speed uint32) uint32 { + return speed +} + +func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { + baudrate, ok := baudrateMap[speed] + if !ok { + return nil, true + } + // XXX: Is Cflag really needed + // revert old baudrate + for _, rate := range baudrateMap { + settings.Cflag &^= rate + } + // set new baudrate + settings.Cflag |= baudrate + + settings.Ispeed = toTermiosSpeedType(baudrate) + settings.Ospeed = toTermiosSpeedType(baudrate) + return nil, false +} -func termiosMask(data int) uint32 { - return uint32(data) +func (port *unixPort) setSpecialBaudrate(speed uint32) error { + // TODO: unimplemented + return &PortError{code: InvalidSpeed} } diff --git a/serial_linux.go b/serial_linux.go index 4376806..a1218bb 100644 --- a/serial_linux.go +++ b/serial_linux.go @@ -1,70 +1,99 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -package serial // import "go.bug.st/serial" +package serial -import "syscall" +import ( + "regexp" + + "golang.org/x/sys/unix" +) const devFolder = "/dev" -const regexFilter = "(ttyS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO)[0-9]{1,3}" + +var osPortFilter = regexp.MustCompile("(ttyS|ttyHS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO|ttymxc)[0-9]{1,3}") // termios manipulation functions -var baudrateMap = map[int]int{ - 0: syscall.B9600, // Default to 9600 - 50: syscall.B50, - 75: syscall.B75, - 110: syscall.B110, - 134: syscall.B134, - 150: syscall.B150, - 200: syscall.B200, - 300: syscall.B300, - 600: syscall.B600, - 1200: syscall.B1200, - 1800: syscall.B1800, - 2400: syscall.B2400, - 4800: syscall.B4800, - 9600: syscall.B9600, - 19200: syscall.B19200, - 38400: syscall.B38400, - 57600: syscall.B57600, - 115200: syscall.B115200, - 230400: syscall.B230400, - 460800: syscall.B460800, - 500000: syscall.B500000, - 576000: syscall.B576000, - 921600: syscall.B921600, - 1000000: syscall.B1000000, - 1152000: syscall.B1152000, - 1500000: syscall.B1500000, - 2000000: syscall.B2000000, - 2500000: syscall.B2500000, - 3000000: syscall.B3000000, - 3500000: syscall.B3500000, - 4000000: syscall.B4000000, +var baudrateMap = map[int]uint32{ + 0: unix.B9600, // Default to 9600 + 50: unix.B50, + 75: unix.B75, + 110: unix.B110, + 134: unix.B134, + 150: unix.B150, + 200: unix.B200, + 300: unix.B300, + 600: unix.B600, + 1200: unix.B1200, + 1800: unix.B1800, + 2400: unix.B2400, + 4800: unix.B4800, + 9600: unix.B9600, + 19200: unix.B19200, + 38400: unix.B38400, + 57600: unix.B57600, + 115200: unix.B115200, + 230400: unix.B230400, + 460800: unix.B460800, + 500000: unix.B500000, + 576000: unix.B576000, + 921600: unix.B921600, + 1000000: unix.B1000000, + 1152000: unix.B1152000, + 1500000: unix.B1500000, + 2000000: unix.B2000000, + 2500000: unix.B2500000, + 3000000: unix.B3000000, + 3500000: unix.B3500000, + 4000000: unix.B4000000, } -var databitsMap = map[int]int{ - 0: syscall.CS8, // Default to 8 bits - 5: syscall.CS5, - 6: syscall.CS6, - 7: syscall.CS7, - 8: syscall.CS8, +var databitsMap = map[int]uint32{ + 0: unix.CS8, // Default to 8 bits + 5: unix.CS5, + 6: unix.CS6, + 7: unix.CS7, + 8: unix.CS8, } -const tc_CMSPAR int = 0 // may be CMSPAR or PAREXT -const tc_IUCLC = syscall.IUCLC +const tcCMSPAR = unix.CMSPAR +const tcIUCLC = unix.IUCLC -func termiosMask(data int) uint32 { - return uint32(data) -} +const tcCRTSCTS uint32 = unix.CRTSCTS -// syscall wrappers +const ioctlTcgetattr = unix.TCGETS +const ioctlTcsetattr = unix.TCSETS +const ioctlTcflsh = unix.TCFLSH +const ioctlTioccbrk = unix.TIOCCBRK +const ioctlTiocsbrk = unix.TIOCSBRK -//sys ioctl(fd int, req uint64, data uintptr) (err error) +func toTermiosSpeedType(speed uint32) uint32 { + return speed +} -const ioctl_tcgetattr = syscall.TCGETS -const ioctl_tcsetattr = syscall.TCSETS +func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { + baudrate, ok := baudrateMap[speed] + if !ok { + return nil, true + } + // revert old baudrate + for _, rate := range baudrateMap { + settings.Cflag &^= rate + } + // set new baudrate + settings.Cflag |= baudrate + settings.Ispeed = toTermiosSpeedType(baudrate) + settings.Ospeed = toTermiosSpeedType(baudrate) + return nil, false +} + +func (port *unixPort) Drain() error { + // It's not super well documented, but this is the same as calling tcdrain: + // - https://git.musl-libc.org/cgit/musl/tree/src/termios/tcdrain.c + // - https://elixir.bootlin.com/linux/v6.2.8/source/drivers/tty/tty_io.c#L2673 + return unix.IoctlSetInt(port.handle, unix.TCSBRK, 1) +} diff --git a/serial_linux_test.go b/serial_linux_test.go new file mode 100644 index 0000000..4a76fbe --- /dev/null +++ b/serial_linux_test.go @@ -0,0 +1,64 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +// Testing code idea and fix thanks to @angri +// https://github.com/bugst/go-serial/pull/42 + +package serial + +import ( + "context" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func startSocatAndWaitForPort(t *testing.T, ctx context.Context) *exec.Cmd { + cmd := exec.CommandContext(ctx, "socat", "-D", "STDIO", "pty,link=/tmp/faketty") + r, err := cmd.StderrPipe() + require.NoError(t, err) + require.NoError(t, cmd.Start()) + // Let our fake serial port node appear. + // socat will write to stderr before starting transfer phase; + // we don't really care what, just that it did, because then it's ready. + buf := make([]byte, 1024) + _, err = r.Read(buf) + require.NoError(t, err) + return cmd +} + +func TestSerialReadAndCloseConcurrency(t *testing.T) { + + // Run this test with race detector to actually test that + // the correct multitasking behaviour is happening. + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cmd := startSocatAndWaitForPort(t, ctx) + go cmd.Wait() + + port, err := Open("/tmp/faketty", &Mode{}) + require.NoError(t, err) + buf := make([]byte, 100) + go port.Read(buf) + // let port.Read to start + time.Sleep(time.Millisecond * 1) + port.Close() +} + +func TestDoubleCloseIsNoop(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cmd := startSocatAndWaitForPort(t, ctx) + go cmd.Wait() + + port, err := Open("/tmp/faketty", &Mode{}) + require.NoError(t, err) + require.NoError(t, port.Close()) + require.NoError(t, port.Close()) +} diff --git a/serial_openbsd.go b/serial_openbsd.go new file mode 100644 index 0000000..ad5efd0 --- /dev/null +++ b/serial_openbsd.go @@ -0,0 +1,92 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package serial + +import ( + "regexp" + + "golang.org/x/sys/unix" +) + +const devFolder = "/dev" + +var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*") + +// termios manipulation functions + +var baudrateMap = map[int]uint32{ + 0: unix.B9600, // Default to 9600 + 50: unix.B50, + 75: unix.B75, + 110: unix.B110, + 134: unix.B134, + 150: unix.B150, + 200: unix.B200, + 300: unix.B300, + 600: unix.B600, + 1200: unix.B1200, + 1800: unix.B1800, + 2400: unix.B2400, + 4800: unix.B4800, + 9600: unix.B9600, + 19200: unix.B19200, + 38400: unix.B38400, + 57600: unix.B57600, + 115200: unix.B115200, + 230400: unix.B230400, + //460800: unix.B460800, + //921600: unix.B921600, +} + +var databitsMap = map[int]uint32{ + 0: unix.CS8, // Default to 8 bits + 5: unix.CS5, + 6: unix.CS6, + 7: unix.CS7, + 8: unix.CS8, +} + +const tcCMSPAR uint32 = 0 // may be CMSPAR or PAREXT +const tcIUCLC uint32 = 0 + +const tcCCTS_OFLOW uint32 = 0x00010000 +const tcCRTS_IFLOW uint32 = 0x00020000 + +const tcCRTSCTS uint32 = tcCCTS_OFLOW + +const ioctlTcgetattr = unix.TIOCGETA +const ioctlTcsetattr = unix.TIOCSETA +const ioctlTcflsh = unix.TIOCFLUSH +const ioctlTioccbrk = unix.TIOCCBRK +const ioctlTiocsbrk = unix.TIOCSBRK + +func toTermiosSpeedType(speed uint32) int32 { + return int32(speed) +} + +func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { + baudrate, ok := baudrateMap[speed] + if !ok { + return nil, true + } + // XXX: Is Cflag really needed + // revert old baudrate + for _, rate := range baudrateMap { + settings.Cflag &^= rate + } + // set new baudrate + settings.Cflag |= baudrate + + settings.Ispeed = toTermiosSpeedType(baudrate) + settings.Ospeed = toTermiosSpeedType(baudrate) + return nil, false +} + +func (port *unixPort) setSpecialBaudrate(speed uint32) error { + // TODO: unimplemented + return &PortError{code: InvalidSpeed} +} diff --git a/serial_resetbuf_linux_bsd.go b/serial_resetbuf_linux_bsd.go new file mode 100644 index 0000000..9e24604 --- /dev/null +++ b/serial_resetbuf_linux_bsd.go @@ -0,0 +1,19 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build linux || freebsd || openbsd + +package serial + +import "golang.org/x/sys/unix" + +func (port *unixPort) ResetInputBuffer() error { + return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) +} + +func (port *unixPort) ResetOutputBuffer() error { + return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) +} diff --git a/serial_specialbaudrate_linux.go b/serial_specialbaudrate_linux.go new file mode 100644 index 0000000..4e55fdb --- /dev/null +++ b/serial_specialbaudrate_linux.go @@ -0,0 +1,23 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build linux && !ppc64le + +package serial + +import "golang.org/x/sys/unix" + +func (port *unixPort) setSpecialBaudrate(speed uint32) error { + settings, err := unix.IoctlGetTermios(port.handle, unix.TCGETS2) + if err != nil { + return err + } + settings.Cflag &^= unix.CBAUD + settings.Cflag |= unix.BOTHER + settings.Ispeed = speed + settings.Ospeed = speed + return unix.IoctlSetTermios(port.handle, unix.TCSETS2, settings) +} diff --git a/serial_specialbaudrate_linux_ppc64le.go b/serial_specialbaudrate_linux_ppc64le.go new file mode 100644 index 0000000..b31d6c5 --- /dev/null +++ b/serial_specialbaudrate_linux_ppc64le.go @@ -0,0 +1,12 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package serial + +func (port *unixPort) setSpecialBaudrate(speed uint32) error { + // TODO: unimplemented + return &PortError{code: InvalidSpeed} +} diff --git a/serial_unix.go b/serial_unix.go index bb7bad2..e6913c2 100644 --- a/serial_unix.go +++ b/serial_unix.go @@ -1,53 +1,139 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -// +build linux darwin freebsd +//go:build linux || darwin || freebsd || openbsd -package serial // import "go.bug.st/serial" +package serial -import "io/ioutil" -import "regexp" -import "strings" -import "syscall" -import "unsafe" +import ( + "fmt" + "os" + "strings" + "sync" + "sync/atomic" + "time" -// Opaque type that implements SerialPort interface for linux -type SerialPort struct { + "go.bug.st/serial/unixutils" + "golang.org/x/sys/unix" +) + +type unixPort struct { handle int + + readTimeout time.Duration + closeLock sync.RWMutex + closeSignal *unixutils.Pipe + opened uint32 } -// Close the serial port -func (port *SerialPort) Close() error { +func (port *unixPort) Close() error { + if !atomic.CompareAndSwapUint32(&port.opened, 1, 0) { + return nil + } + + // Close port port.releaseExclusiveAccess() - return syscall.Close(port.handle) + if err := unix.Close(port.handle); err != nil { + return err + } + + if port.closeSignal != nil { + // Send close signal to all pending reads (if any) + port.closeSignal.Write([]byte{0}) + + // Wait for all readers to complete + port.closeLock.Lock() + defer port.closeLock.Unlock() + + // Close signaling pipe + if err := port.closeSignal.Close(); err != nil { + return err + } + } + return nil } -// Stores data received from the serial port into the provided byte array -// buffer. The function returns the number of bytes read. -// -// The Read function blocks until (at least) one byte is received from -// the serial port or an error occurs. -func (port *SerialPort) Read(p []byte) (n int, err error) { - return syscall.Read(port.handle, p) +func (port *unixPort) Read(p []byte) (int, error) { + port.closeLock.RLock() + defer port.closeLock.RUnlock() + if atomic.LoadUint32(&port.opened) != 1 { + return 0, &PortError{code: PortClosed} + } + + var deadline time.Time + if port.readTimeout != NoTimeout { + deadline = time.Now().Add(port.readTimeout) + } + + fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD()) + for { + timeout := time.Duration(-1) + if port.readTimeout != NoTimeout { + timeout = time.Until(deadline) + if timeout < 0 { + // a negative timeout means "no-timeout" in Select(...) + timeout = 0 + } + } + res, err := unixutils.Select(fds, nil, fds, timeout) + if err == unix.EINTR { + continue + } + if err != nil { + return 0, err + } + if res.IsReadable(port.closeSignal.ReadFD()) { + return 0, &PortError{code: PortClosed} + } + if !res.IsReadable(port.handle) { + // Timeout happened + return 0, nil + } + n, err := unix.Read(port.handle, p) + if err == unix.EINTR { + continue + } + // Linux: when the port is disconnected during a read operation + // the port is left in a "readable with zero-length-data" state. + // https://stackoverflow.com/a/34945814/1655275 + if n == 0 && err == nil { + return 0, &PortError{code: PortClosed} + } + if n < 0 { // Do not return -1 unix errors + n = 0 + } + return n, err + } } -// Send the content of the data byte array to the serial port. -// Returns the number of bytes written. -func (port *SerialPort) Write(p []byte) (n int, err error) { - return syscall.Write(port.handle, p) +func (port *unixPort) Write(p []byte) (n int, err error) { + n, err = unix.Write(port.handle, p) + if n < 0 { // Do not return -1 unix errors + n = 0 + } + return } -// Set all parameters of the serial port. See the Mode structure for more -// info. -func (port *SerialPort) SetMode(mode *Mode) error { - settings, err := port.getTermSettings() - if err != nil { +func (port *unixPort) Break(t time.Duration) error { + if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil { return err } - if err := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil { + + time.Sleep(t) + + if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil { + return err + } + + return nil +} + +func (port *unixPort) SetMode(mode *Mode) error { + settings, err := port.getTermSettings() + if err != nil { return err } if err := setTermSettingsParity(mode.Parity, settings); err != nil { @@ -59,52 +145,153 @@ func (port *SerialPort) SetMode(mode *Mode) error { if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil { return err } - return port.setTermSettings(settings) + requireSpecialBaudrate := false + if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil { + return err + } else if special { + requireSpecialBaudrate = true + } + if err := port.setTermSettings(settings); err != nil { + return err + } + if requireSpecialBaudrate { + // MacOSX require this one to be the last operation otherwise an + // 'Invalid serial port' error is produced. + if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil { + return err + } + } + return nil } -// Open the serial port using the specified modes -func OpenPort(portName string, mode *Mode) (*SerialPort, error) { - h, err := syscall.Open(portName, syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_NDELAY, 0) +func (port *unixPort) SetDTR(dtr bool) error { + status, err := port.getModemBitsStatus() + if err != nil { + return err + } + if dtr { + status |= unix.TIOCM_DTR + } else { + status &^= unix.TIOCM_DTR + } + return port.setModemBitsStatus(status) +} + +func (port *unixPort) SetRTS(rts bool) error { + status, err := port.getModemBitsStatus() + if err != nil { + return err + } + if rts { + status |= unix.TIOCM_RTS + } else { + status &^= unix.TIOCM_RTS + } + return port.setModemBitsStatus(status) +} + +func (port *unixPort) SetReadTimeout(timeout time.Duration) error { + if timeout < 0 && timeout != NoTimeout { + return &PortError{code: InvalidTimeoutValue} + } + port.readTimeout = timeout + return nil +} + +func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) { + status, err := port.getModemBitsStatus() + if err != nil { + return nil, err + } + return &ModemStatusBits{ + CTS: (status & unix.TIOCM_CTS) != 0, + DCD: (status & unix.TIOCM_CD) != 0, + DSR: (status & unix.TIOCM_DSR) != 0, + RI: (status & unix.TIOCM_RI) != 0, + }, nil +} + +func nativeOpen(portName string, mode *Mode) (*unixPort, error) { + h, err := unix.Open(portName, unix.O_RDWR|unix.O_NOCTTY|unix.O_NDELAY, 0) if err != nil { switch err { - case syscall.EBUSY: - return nil, &SerialPortError{code: ERROR_PORT_BUSY} - case syscall.EACCES: - return nil, &SerialPortError{code: ERROR_PERMISSION_DENIED} + case unix.EBUSY: + return nil, &PortError{code: PortBusy} + case unix.EACCES: + return nil, &PortError{code: PermissionDenied} } return nil, err } - port := &SerialPort{ - handle: h, + port := &unixPort{ + handle: h, + opened: 1, + readTimeout: NoTimeout, } // Setup serial port - if port.SetMode(mode) != nil { + settings, err := port.getTermSettings() + if err != nil { port.Close() - return nil, &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting term settings: %w", err)} } // Set raw mode - settings, err := port.getTermSettings() - if err != nil { + setRawMode(settings) + + // Explicitly disable RTS/CTS flow control + setTermSettingsCtsRts(false, settings) + + if err = port.setTermSettings(settings); err != nil { port.Close() - return nil, &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting term settings: %w", err)} } - setRawMode(settings) - if port.setTermSettings(settings) != nil { + + if mode.InitialStatusBits != nil { + status, err := port.getModemBitsStatus() + if err != nil { + port.Close() + return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting modem bits status: %w", err)} + } + if mode.InitialStatusBits.DTR { + status |= unix.TIOCM_DTR + } else { + status &^= unix.TIOCM_DTR + } + if mode.InitialStatusBits.RTS { + status |= unix.TIOCM_RTS + } else { + status &^= unix.TIOCM_RTS + } + if err := port.setModemBitsStatus(status); err != nil { + port.Close() + return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting modem bits status: %w", err)} + } + } + + // MacOSX require that this operation is the last one otherwise an + // 'Invalid serial port' error is returned... don't know why... + if err := port.SetMode(mode); err != nil { port.Close() - return nil, &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error configuring port: %w", err)} } - syscall.SetNonblock(h, false) + unix.SetNonblock(h, false) port.acquireExclusiveAccess() + // This pipe is used as a signal to cancel blocking Read + pipe := &unixutils.Pipe{} + if err := pipe.Open(); err != nil { + port.Close() + return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error opening signaling pipe: %w", err)} + } + port.closeSignal = pipe + return port, nil } -func GetPortsList() ([]string, error) { - files, err := ioutil.ReadDir(devFolder) +func nativeGetPortsList() ([]string, error) { + files, err := os.ReadDir(devFolder) if err != nil { return nil, err } @@ -117,24 +304,17 @@ func GetPortsList() ([]string, error) { } // Keep only devices with the correct name - match, err := regexp.MatchString(regexFilter, f.Name()) - if err != nil { - return nil, err - } - if !match { + if !osPortFilter.MatchString(f.Name()) { continue } portName := devFolder + "/" + f.Name() - // Check if serial port is real or is a placeholder serial port "ttySxx" - if strings.HasPrefix(f.Name(), "ttyS") { - port, err := OpenPort(portName, &Mode{}) + // Check if serial port is real or is a placeholder serial port "ttySxx" or "ttyHSxx" + if strings.HasPrefix(f.Name(), "ttyS") || strings.HasPrefix(f.Name(), "ttyHS") { + port, err := nativeOpen(portName, &Mode{}) if err != nil { - serr, ok := err.(*SerialPortError) - if ok && serr.Code() == ERROR_INVALID_SERIAL_PORT { - continue - } + continue } else { port.Close() } @@ -149,101 +329,139 @@ func GetPortsList() ([]string, error) { // termios manipulation functions -func setTermSettingsBaudrate(speed int, settings *syscall.Termios) error { - baudrate, ok := baudrateMap[speed] - if !ok { - return &SerialPortError{code: ERROR_INVALID_PORT_SPEED} - } - // revert old baudrate - BAUDMASK := 0 - for _, rate := range baudrateMap { - BAUDMASK |= rate - } - settings.Cflag &= ^termiosMask(BAUDMASK) - // set new baudrate - settings.Cflag |= termiosMask(baudrate) - settings.Ispeed = termiosMask(baudrate) - settings.Ospeed = termiosMask(baudrate) - return nil -} - -func setTermSettingsParity(parity Parity, settings *syscall.Termios) error { +func setTermSettingsParity(parity Parity, settings *unix.Termios) error { switch parity { - case PARITY_NONE: - settings.Cflag &= ^termiosMask(syscall.PARENB | syscall.PARODD | tc_CMSPAR) - settings.Iflag &= ^termiosMask(syscall.INPCK) - case PARITY_ODD: - settings.Cflag |= termiosMask(syscall.PARENB | syscall.PARODD) - settings.Cflag &= ^termiosMask(tc_CMSPAR) - settings.Iflag |= termiosMask(syscall.INPCK) - case PARITY_EVEN: - settings.Cflag &= ^termiosMask(syscall.PARODD | tc_CMSPAR) - settings.Cflag |= termiosMask(syscall.PARENB) - settings.Iflag |= termiosMask(syscall.INPCK) - case PARITY_MARK: - settings.Cflag |= termiosMask(syscall.PARENB | syscall.PARODD | tc_CMSPAR) - settings.Iflag |= termiosMask(syscall.INPCK) - case PARITY_SPACE: - settings.Cflag &= ^termiosMask(syscall.PARODD) - settings.Cflag |= termiosMask(syscall.PARENB | tc_CMSPAR) - settings.Iflag |= termiosMask(syscall.INPCK) + case NoParity: + settings.Cflag &^= unix.PARENB + settings.Cflag &^= unix.PARODD + settings.Cflag &^= tcCMSPAR + settings.Iflag &^= unix.INPCK + case OddParity: + settings.Cflag |= unix.PARENB + settings.Cflag |= unix.PARODD + settings.Cflag &^= tcCMSPAR + settings.Iflag |= unix.INPCK + case EvenParity: + settings.Cflag |= unix.PARENB + settings.Cflag &^= unix.PARODD + settings.Cflag &^= tcCMSPAR + settings.Iflag |= unix.INPCK + case MarkParity: + if tcCMSPAR == 0 { + return &PortError{code: InvalidParity} + } + settings.Cflag |= unix.PARENB + settings.Cflag |= unix.PARODD + settings.Cflag |= tcCMSPAR + settings.Iflag |= unix.INPCK + case SpaceParity: + if tcCMSPAR == 0 { + return &PortError{code: InvalidParity} + } + settings.Cflag |= unix.PARENB + settings.Cflag &^= unix.PARODD + settings.Cflag |= tcCMSPAR + settings.Iflag |= unix.INPCK + default: + return &PortError{code: InvalidParity} } return nil } -func setTermSettingsDataBits(bits int, settings *syscall.Termios) error { +func setTermSettingsDataBits(bits int, settings *unix.Termios) error { databits, ok := databitsMap[bits] if !ok { - return &SerialPortError{code: ERROR_INVALID_PORT_DATA_BITS} + return &PortError{code: InvalidDataBits} } - settings.Cflag &= ^termiosMask(syscall.CSIZE) - settings.Cflag |= termiosMask(databits) + // Remove previous databits setting + settings.Cflag &^= unix.CSIZE + // Set requested databits + settings.Cflag |= databits return nil } -func setTermSettingsStopBits(bits StopBits, settings *syscall.Termios) error { +func setTermSettingsStopBits(bits StopBits, settings *unix.Termios) error { switch bits { - case STOPBITS_ONE: - settings.Cflag &= ^termiosMask(syscall.CSTOPB) - case STOPBITS_ONEPOINTFIVE, STOPBITS_TWO: - settings.Cflag |= termiosMask(syscall.CSTOPB) + case OneStopBit: + settings.Cflag &^= unix.CSTOPB + case OnePointFiveStopBits: + return &PortError{code: InvalidStopBits} + case TwoStopBits: + settings.Cflag |= unix.CSTOPB + default: + return &PortError{code: InvalidStopBits} } return nil } -func setRawMode(settings *syscall.Termios) { +func setTermSettingsCtsRts(enable bool, settings *unix.Termios) { + if enable { + settings.Cflag |= tcCRTSCTS + } else { + settings.Cflag &^= tcCRTSCTS + } +} + +func setRawMode(settings *unix.Termios) { // Set local mode - settings.Cflag |= termiosMask(syscall.CREAD | syscall.CLOCAL) + settings.Cflag |= unix.CREAD + settings.Cflag |= unix.CLOCAL // Set raw mode - settings.Lflag &= ^termiosMask(syscall.ICANON | syscall.ECHO | syscall.ECHOE | syscall.ECHOK | - syscall.ECHONL | syscall.ECHOCTL | syscall.ECHOPRT | syscall.ECHOKE | syscall.ISIG | syscall.IEXTEN) - settings.Iflag &= ^termiosMask(syscall.IXON | syscall.IXOFF | syscall.IXANY | syscall.INPCK | - syscall.IGNPAR | syscall.PARMRK | syscall.ISTRIP | syscall.IGNBRK | syscall.BRKINT | syscall.INLCR | - syscall.IGNCR | syscall.ICRNL | tc_IUCLC) - settings.Oflag &= ^termiosMask(syscall.OPOST) + settings.Lflag &^= unix.ICANON + settings.Lflag &^= unix.ECHO + settings.Lflag &^= unix.ECHOE + settings.Lflag &^= unix.ECHOK + settings.Lflag &^= unix.ECHONL + settings.Lflag &^= unix.ECHOCTL + settings.Lflag &^= unix.ECHOPRT + settings.Lflag &^= unix.ECHOKE + settings.Lflag &^= unix.ISIG + settings.Lflag &^= unix.IEXTEN + + settings.Iflag &^= unix.IXON + settings.Iflag &^= unix.IXOFF + settings.Iflag &^= unix.IXANY + settings.Iflag &^= unix.INPCK + settings.Iflag &^= unix.IGNPAR + settings.Iflag &^= unix.PARMRK + settings.Iflag &^= unix.ISTRIP + settings.Iflag &^= unix.IGNBRK + settings.Iflag &^= unix.BRKINT + settings.Iflag &^= unix.INLCR + settings.Iflag &^= unix.IGNCR + settings.Iflag &^= unix.ICRNL + settings.Iflag &^= tcIUCLC + + settings.Oflag &^= unix.OPOST // Block reads until at least one char is available (no timeout) - settings.Cc[syscall.VMIN] = 1 - settings.Cc[syscall.VTIME] = 0 + settings.Cc[unix.VMIN] = 1 + settings.Cc[unix.VTIME] = 0 } // native syscall wrapper functions -func (port *SerialPort) getTermSettings() (*syscall.Termios, error) { - settings := &syscall.Termios{} - err := ioctl(port.handle, ioctl_tcgetattr, uintptr(unsafe.Pointer(settings))) - return settings, err +func (port *unixPort) getTermSettings() (*unix.Termios, error) { + return unix.IoctlGetTermios(port.handle, ioctlTcgetattr) +} + +func (port *unixPort) setTermSettings(settings *unix.Termios) error { + return unix.IoctlSetTermios(port.handle, ioctlTcsetattr, settings) +} + +func (port *unixPort) getModemBitsStatus() (int, error) { + return unix.IoctlGetInt(port.handle, unix.TIOCMGET) } -func (port *SerialPort) setTermSettings(settings *syscall.Termios) error { - return ioctl(port.handle, ioctl_tcsetattr, uintptr(unsafe.Pointer(settings))) +func (port *unixPort) setModemBitsStatus(status int) error { + return unix.IoctlSetPointerInt(port.handle, unix.TIOCMSET, status) } -func (port *SerialPort) acquireExclusiveAccess() error { - return ioctl(port.handle, syscall.TIOCEXCL, 0) +func (port *unixPort) acquireExclusiveAccess() error { + return unix.IoctlSetInt(port.handle, unix.TIOCEXCL, 0) } -func (port *SerialPort) releaseExclusiveAccess() error { - return ioctl(port.handle, syscall.TIOCNXCL, 0) +func (port *unixPort) releaseExclusiveAccess() error { + return unix.IoctlSetInt(port.handle, unix.TIOCNXCL, 0) } diff --git a/serial_wasm.go b/serial_wasm.go new file mode 100644 index 0000000..b2369f1 --- /dev/null +++ b/serial_wasm.go @@ -0,0 +1,15 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package serial + +import ( + "errors" +) + +func nativeOpen(portName string, mode *Mode) (Port, error) { + return nil, errors.New("nativeOpen is not supported on wasm") +} diff --git a/serial_windows.go b/serial_windows.go index 999c309..d2800f8 100644 --- a/serial_windows.go +++ b/serial_windows.go @@ -1,184 +1,197 @@ // -// Copyright 2014-2016 Cristian Maglie. All rights reserved. +// Copyright 2014-2024 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -package serial // import "go.bug.st/serial" +package serial /* // MSDN article on Serial Communications: // http://msdn.microsoft.com/en-us/library/ff802693.aspx +// (alternative link) https://msdn.microsoft.com/en-us/library/ms810467.aspx // Arduino Playground article on serial communication with Windows API: // http://playground.arduino.cc/Interfacing/CPPWindows */ -import "syscall" +import ( + "errors" + "strings" + "sync" + "syscall" + "time" -// opaque type that implements SerialPort interface for Windows -type SerialPort struct { - handle syscall.Handle + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +type windowsPort struct { + mu sync.Mutex + handle windows.Handle + hasTimeout bool } -//sys RegEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, class *uint16, value *uint16, valueLen *uint32) (regerrno error) = advapi32.RegEnumValueW +func nativeGetPortsList() ([]string, error) { + key, err := registry.OpenKey(windows.HKEY_LOCAL_MACHINE, `HARDWARE\DEVICEMAP\SERIALCOMM\`, windows.KEY_READ) + switch { + case errors.Is(err, syscall.ERROR_FILE_NOT_FOUND): + // On machines with no serial ports the registry key does not exist. + // Return this as no serial ports instead of an error. + return nil, nil + case err != nil: + return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err} + } + defer key.Close() -func GetPortsList() ([]string, error) { - subKey, err := syscall.UTF16PtrFromString("HARDWARE\\DEVICEMAP\\SERIALCOMM\\") + names, err := key.ReadValueNames(0) if err != nil { - return nil, &SerialPortError{code: ERROR_ENUMERATING_PORTS} + return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err} } - var h syscall.Handle - if syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, subKey, 0, syscall.KEY_READ, &h) != nil { - return nil, &SerialPortError{code: ERROR_ENUMERATING_PORTS} - } - defer syscall.RegCloseKey(h) + var values []string + for _, n := range names { + v, _, err := key.GetStringValue(n) + if err != nil || v == "" { + continue + } - var valuesCount uint32 - if syscall.RegQueryInfoKey(h, nil, nil, nil, nil, nil, nil, &valuesCount, nil, nil, nil, nil) != nil { - return nil, &SerialPortError{code: ERROR_ENUMERATING_PORTS} + values = append(values, v) } - list := make([]string, valuesCount) - for i := range list { - var data [1024]uint16 - dataSize := uint32(len(data)) - var name [1024]uint16 - nameSize := uint32(len(name)) - if RegEnumValue(h, uint32(i), &name[0], &nameSize, nil, nil, &data[0], &dataSize) != nil { - return nil, &SerialPortError{code: ERROR_ENUMERATING_PORTS} - } - list[i] = syscall.UTF16ToString(data[:]) - } - return list, nil + return values, nil } -func (port *SerialPort) Close() error { - return syscall.CloseHandle(port.handle) +func (port *windowsPort) Close() error { + port.mu.Lock() + defer func() { + port.handle = 0 + port.mu.Unlock() + }() + if port.handle == 0 { + return nil + } + return windows.CloseHandle(port.handle) } -func (port *SerialPort) Read(p []byte) (int, error) { +func (port *windowsPort) Read(p []byte) (int, error) { var readed uint32 - params := &DCB{} + ev, err := createOverlappedEvent() + if err != nil { + return 0, err + } + defer windows.CloseHandle(ev.HEvent) + for { - if err := syscall.ReadFile(port.handle, p, &readed, nil); err != nil { + err = windows.ReadFile(port.handle, p, &readed, ev) + if err == windows.ERROR_IO_PENDING { + err = windows.GetOverlappedResult(port.handle, ev, &readed, true) + } + switch err { + case nil: + // operation completed successfully + case windows.ERROR_OPERATION_ABORTED: + // port may have been closed + return int(readed), &PortError{code: PortClosed, causedBy: err} + default: + // error happened return int(readed), err } if readed > 0 { return int(readed), nil } - // At the moment it seems that the only reliable way to check if - // a serial port is alive in Windows is to check if the SetCommState - // function fails. - - GetCommState(port.handle, params) - if err := SetCommState(port.handle, params); err != nil { - port.Close() - return 0, err + // Timeout + port.mu.Lock() + hasTimeout := port.hasTimeout + port.mu.Unlock() + if hasTimeout { + return 0, nil } } } -func (port *SerialPort) Write(p []byte) (int, error) { +func (port *windowsPort) Write(p []byte) (int, error) { var writed uint32 - err := syscall.WriteFile(port.handle, p, &writed, nil) + ev, err := createOverlappedEvent() + if err != nil { + return 0, err + } + defer windows.CloseHandle(ev.HEvent) + err = windows.WriteFile(port.handle, p, &writed, ev) + if err == windows.ERROR_IO_PENDING { + // wait for write to complete + err = windows.GetOverlappedResult(port.handle, ev, &writed, true) + } return int(writed), err } -const ( - DCB_BINARY = 0x00000001 - DCB_PARITY = 0x00000002 - DCB_OUT_X_CTS_FLOW = 0x00000004 - DCB_OUT_X_DSR_FLOW = 0x00000008 - DCB_DTR_CONTROL_DISABLE_MASK = ^0x00000030 - DCB_DTR_CONTROL_ENABLE = 0x00000010 - DCB_DTR_CONTROL_HANDSHAKE = 0x00000020 - DCB_DSR_SENSITIVITY = 0x00000040 - DCB_TX_CONTINUE_ON_XOFF = 0x00000080 - DCB_OUT_X = 0x00000100 - DCB_IN_X = 0x00000200 - DCB_ERROR_CHAR = 0x00000400 - DCB_NULL = 0x00000800 - DCB_RTS_CONTROL_DISABLE_MASK = ^0x00003000 - DCB_RTS_CONTROL_ENABLE = 0x00001000 - DCB_RTS_CONTROL_HANDSHAKE = 0x00002000 - DCB_RTS_CONTROL_TOGGLE = 0x00003000 - DCB_ABORT_ON_ERROR = 0x00004000 -) - -type DCB struct { - DCBlength uint32 - BaudRate uint32 - - // Flags field is a bitfield - // fBinary :1 - // fParity :1 - // fOutxCtsFlow :1 - // fOutxDsrFlow :1 - // fDtrControl :2 - // fDsrSensitivity :1 - // fTXContinueOnXoff :1 - // fOutX :1 - // fInX :1 - // fErrorChar :1 - // fNull :1 - // fRtsControl :2 - // fAbortOnError :1 - // fDummy2 :17 - Flags uint32 - - wReserved uint16 - XonLim uint16 - XoffLim uint16 - ByteSize byte - Parity byte - StopBits byte - XonChar byte - XoffChar byte - ErrorChar byte - EofChar byte - EvtChar byte - wReserved1 uint16 +func (port *windowsPort) Drain() (err error) { + return windows.FlushFileBuffers(port.handle) } -type COMMTIMEOUTS struct { - ReadIntervalTimeout uint32 - ReadTotalTimeoutMultiplier uint32 - ReadTotalTimeoutConstant uint32 - WriteTotalTimeoutMultiplier uint32 - WriteTotalTimeoutConstant uint32 +func (port *windowsPort) ResetInputBuffer() error { + return windows.PurgeComm(port.handle, windows.PURGE_RXCLEAR|windows.PURGE_RXABORT) } -//sys GetCommState(handle syscall.Handle, dcb *DCB) (err error) -//sys SetCommState(handle syscall.Handle, dcb *DCB) (err error) -//sys SetCommTimeouts(handle syscall.Handle, timeouts *COMMTIMEOUTS) (err error) +func (port *windowsPort) ResetOutputBuffer() error { + return windows.PurgeComm(port.handle, windows.PURGE_TXCLEAR|windows.PURGE_TXABORT) +} const ( - NOPARITY = 0 // Default - ODDPARITY = 1 - EVENPARITY = 2 - MARKPARITY = 3 - SPACEPARITY = 4 + dcbBinary uint32 = 0x00000001 + dcbParity = 0x00000002 + dcbOutXCTSFlow = 0x00000004 + dcbOutXDSRFlow = 0x00000008 + dcbDTRControlDisableMask = ^uint32(0x00000030) + dcbDTRControlEnable = 0x00000010 + dcbDTRControlHandshake = 0x00000020 + dcbDSRSensitivity = 0x00000040 + dcbTXContinueOnXOFF = 0x00000080 + dcbOutX = 0x00000100 + dcbInX = 0x00000200 + dcbErrorChar = 0x00000400 + dcbNull = 0x00000800 + dcbRTSControlDisableMask = ^uint32(0x00003000) + dcbRTSControlEnable = 0x00001000 + dcbRTSControlHandshake = 0x00002000 + dcbRTSControlToggle = 0x00003000 + dcbAbortOnError = 0x00004000 ) -const ( - ONESTOPBIT = 0 // Default - ONE5STOPBITS = 1 - TWOSTOPBITS = 2 -) +var parityMap = map[Parity]byte{ + NoParity: windows.NOPARITY, + OddParity: windows.ODDPARITY, + EvenParity: windows.EVENPARITY, + MarkParity: windows.MARKPARITY, + SpaceParity: windows.SPACEPARITY, +} -func (port *SerialPort) SetMode(mode *Mode) error { - params := DCB{} - if GetCommState(port.handle, ¶ms) != nil { +var stopBitsMap = map[StopBits]byte{ + OneStopBit: windows.ONESTOPBIT, + OnePointFiveStopBits: windows.ONE5STOPBITS, + TwoStopBits: windows.TWOSTOPBITS, +} + +func (port *windowsPort) SetMode(mode *Mode) error { + params := windows.DCB{} + if windows.GetCommState(port.handle, ¶ms) != nil { + port.Close() + return &PortError{code: InvalidSerialPort} + } + port.setModeParams(mode, ¶ms) + if windows.SetCommState(port.handle, ¶ms) != nil { port.Close() - return &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + return &PortError{code: InvalidSerialPort} } + return nil +} + +func (port *windowsPort) setModeParams(mode *Mode, params *windows.DCB) { if mode.BaudRate == 0 { - params.BaudRate = 9600 // Default to 9600 + params.BaudRate = windows.CBR_9600 // Default to 9600 } else { params.BaudRate = uint32(mode.BaudRate) } @@ -187,84 +200,236 @@ func (port *SerialPort) SetMode(mode *Mode) error { } else { params.ByteSize = byte(mode.DataBits) } - params.StopBits = byte(mode.StopBits) - params.Parity = byte(mode.Parity) - if SetCommState(port.handle, ¶ms) != nil { - port.Close() - return &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + params.StopBits = stopBitsMap[mode.StopBits] + params.Parity = parityMap[mode.Parity] +} + +func (port *windowsPort) SetDTR(dtr bool) error { + // Like for RTS there are problems with the windows.EscapeCommFunction + // observed behaviour was that DTR is set from false -> true + // when setting RTS from true -> false + // 1) Connect -> RTS = true (low) DTR = true (low) OKAY + // 2) SetDTR(false) -> RTS = true (low) DTR = false (high) OKAY + // 3) SetRTS(false) -> RTS = false (high) DTR = true (low) ERROR: DTR toggled + // + // In addition this way the CommState Flags are not updated + /* + var err error + if dtr { + err = windows.EscapeCommFunction(port.handle, windows.SETDTR) + } else { + err = windows.EscapeCommFunction(port.handle, windows.CLTDTR) + } + if err != nil { + return &PortError{} + } + return nil + */ + + // The following seems a more reliable way to do it + + params := &windows.DCB{} + if err := windows.GetCommState(port.handle, params); err != nil { + return &PortError{causedBy: err} + } + params.Flags &= dcbDTRControlDisableMask + if dtr { + params.Flags |= windows.DTR_CONTROL_ENABLE + } + if err := windows.SetCommState(port.handle, params); err != nil { + return &PortError{causedBy: err} + } + + return nil +} + +func (port *windowsPort) SetRTS(rts bool) error { + // It seems that there is a bug in the Windows VCP driver: + // it doesn't send USB control message when the RTS bit is + // changed, so the following code not always works with + // USB-to-serial adapters. + // + // In addition this way the CommState Flags are not updated + + /* + var err error + if rts { + err = windows.EscapeCommFunction(port.handle, windows.SETRTS) + } else { + err = windows.EscapeCommFunction(port.handle, windows.CLRRTS) + } + if err != nil { + return &PortError{} + } + return nil + */ + + // The following seems a more reliable way to do it + + params := &windows.DCB{} + if err := windows.GetCommState(port.handle, params); err != nil { + return &PortError{causedBy: err} + } + params.Flags &= dcbRTSControlDisableMask + if rts { + params.Flags |= windows.RTS_CONTROL_ENABLE + } + if err := windows.SetCommState(port.handle, params); err != nil { + return &PortError{causedBy: err} + } + return nil +} + +func (port *windowsPort) GetModemStatusBits() (*ModemStatusBits, error) { + // GetCommModemStatus constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcommmodemstatus. + const ( + MS_CTS_ON = 0x0010 + MS_DSR_ON = 0x0020 + MS_RING_ON = 0x0040 + MS_RLSD_ON = 0x0080 + ) + var bits uint32 + if err := windows.GetCommModemStatus(port.handle, &bits); err != nil { + return nil, &PortError{} + } + return &ModemStatusBits{ + CTS: (bits & MS_CTS_ON) != 0, + DCD: (bits & MS_RLSD_ON) != 0, + DSR: (bits & MS_DSR_ON) != 0, + RI: (bits & MS_RING_ON) != 0, + }, nil +} + +func (port *windowsPort) SetReadTimeout(timeout time.Duration) error { + // This is a brutal hack to make the CH340 chipset work properly. + // Normally this value should be 0xFFFFFFFE but, after a lot of + // tinkering, I discovered that any value with the highest + // bit set will make the CH340 driver behave like the timeout is 0, + // in the best cases leading to a spinning loop... + // (could this be a wrong signed vs unsigned conversion in the driver?) + // https://github.com/arduino/serial-monitor/issues/112 + const MaxReadTotalTimeoutConstant = 0x7FFFFFFE + + commTimeouts := &windows.CommTimeouts{ + ReadIntervalTimeout: 0xFFFFFFFF, + ReadTotalTimeoutMultiplier: 0xFFFFFFFF, + ReadTotalTimeoutConstant: MaxReadTotalTimeoutConstant, + WriteTotalTimeoutConstant: 0, + WriteTotalTimeoutMultiplier: 0, + } + if timeout != NoTimeout { + ms := timeout.Milliseconds() + if ms > 0xFFFFFFFE || ms < 0 { + return &PortError{code: InvalidTimeoutValue} + } + + if ms > MaxReadTotalTimeoutConstant { + ms = MaxReadTotalTimeoutConstant + } + + commTimeouts.ReadTotalTimeoutConstant = uint32(ms) + } + + port.mu.Lock() + defer port.mu.Unlock() + if err := windows.SetCommTimeouts(port.handle, commTimeouts); err != nil { + return &PortError{code: InvalidTimeoutValue, causedBy: err} } + port.hasTimeout = (timeout != NoTimeout) + return nil } -func OpenPort(portName string, mode *Mode) (*SerialPort, error) { - portName = "\\\\.\\" + portName - path, err := syscall.UTF16PtrFromString(portName) +func (port *windowsPort) Break(d time.Duration) error { + if err := windows.SetCommBreak(port.handle); err != nil { + return &PortError{causedBy: err} + } + + time.Sleep(d) + + if err := windows.ClearCommBreak(port.handle); err != nil { + return &PortError{causedBy: err} + } + + return nil +} + +func createOverlappedEvent() (*windows.Overlapped, error) { + h, err := windows.CreateEvent(nil, 1, 0, nil) + return &windows.Overlapped{HEvent: h}, err +} + +func nativeOpen(portName string, mode *Mode) (*windowsPort, error) { + if !strings.HasPrefix(portName, `\\.\`) { + portName = `\\.\` + portName + } + path, err := windows.UTF16PtrFromString(portName) if err != nil { return nil, err } - handle, err := syscall.CreateFile( + handle, err := windows.CreateFile( path, - syscall.GENERIC_READ|syscall.GENERIC_WRITE, + windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, - syscall.OPEN_EXISTING, - 0, //syscall.FILE_FLAG_OVERLAPPED, - 0) + windows.OPEN_EXISTING, + windows.FILE_FLAG_OVERLAPPED, + 0, + ) if err != nil { switch err { - case syscall.ERROR_ACCESS_DENIED: - return nil, &SerialPortError{code: ERROR_PORT_BUSY} - case syscall.ERROR_FILE_NOT_FOUND: - return nil, &SerialPortError{code: ERROR_PORT_NOT_FOUND} + case windows.ERROR_ACCESS_DENIED: + return nil, &PortError{code: PortBusy} + case windows.ERROR_FILE_NOT_FOUND: + return nil, &PortError{code: PortNotFound} } return nil, err } // Create the serial port - port := &SerialPort{ + port := &windowsPort{ handle: handle, } // Set port parameters - if port.SetMode(mode) != nil { + params := &windows.DCB{} + if windows.GetCommState(port.handle, params) != nil { port.Close() - return nil, &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + return nil, &PortError{code: InvalidSerialPort} } - - params := &DCB{} - if GetCommState(port.handle, params) != nil { - port.Close() - return nil, &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} - } - params.Flags |= DCB_RTS_CONTROL_ENABLE | DCB_DTR_CONTROL_ENABLE - params.Flags &= ^uint32(DCB_OUT_X_CTS_FLOW) - params.Flags &= ^uint32(DCB_OUT_X_DSR_FLOW) - params.Flags &= ^uint32(DCB_DSR_SENSITIVITY) - params.Flags |= DCB_TX_CONTINUE_ON_XOFF - params.Flags &= ^uint32(DCB_IN_X | DCB_OUT_X) - params.Flags &= ^uint32(DCB_ERROR_CHAR) - params.Flags &= ^uint32(DCB_NULL) - params.Flags &= ^uint32(DCB_ABORT_ON_ERROR) + port.setModeParams(mode, params) + params.Flags &= dcbDTRControlDisableMask + params.Flags &= dcbRTSControlDisableMask + if mode.InitialStatusBits == nil { + params.Flags |= windows.DTR_CONTROL_ENABLE + params.Flags |= windows.RTS_CONTROL_ENABLE + } else { + if mode.InitialStatusBits.DTR { + params.Flags |= windows.DTR_CONTROL_ENABLE + } + if mode.InitialStatusBits.RTS { + params.Flags |= windows.RTS_CONTROL_ENABLE + } + } + params.Flags &^= dcbOutXCTSFlow + params.Flags &^= dcbOutXDSRFlow + params.Flags &^= dcbDSRSensitivity + params.Flags |= dcbTXContinueOnXOFF + params.Flags &^= dcbInX + params.Flags &^= dcbOutX + params.Flags &^= dcbErrorChar + params.Flags &^= dcbNull + params.Flags &^= dcbAbortOnError params.XonLim = 2048 params.XoffLim = 512 params.XonChar = 17 // DC1 params.XoffChar = 19 // C3 - if SetCommState(port.handle, params) != nil { + if windows.SetCommState(port.handle, params) != nil { port.Close() - return nil, &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + return nil, &PortError{code: InvalidSerialPort} } - // Set timeouts to 1 second - timeouts := &COMMTIMEOUTS{ - ReadIntervalTimeout: 0xFFFFFFFF, - ReadTotalTimeoutMultiplier: 0xFFFFFFFF, - ReadTotalTimeoutConstant: 1000, // 1 sec - WriteTotalTimeoutConstant: 0, - WriteTotalTimeoutMultiplier: 0, - } - if SetCommTimeouts(port.handle, timeouts) != nil { + if port.SetReadTimeout(NoTimeout) != nil { port.Close() - return nil, &SerialPortError{code: ERROR_INVALID_SERIAL_PORT} + return nil, &PortError{code: InvalidSerialPort} } - return port, nil } - diff --git a/syscall_darwin.go b/syscall_darwin.go deleted file mode 100644 index f0b87e2..0000000 --- a/syscall_darwin.go +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright 2014-2016 Cristian Maglie. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// - -// This file is machine generated by the command: -// mksyscall.pl serial_darwin.go -// The generated stub is modified to make it compile under the "serial" package - -package serial // import "go.bug.st/serial" - -import "syscall" - -func ioctl(fd int, req uint64, data uintptr) (err error) { - _, _, e1 := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(data)) - if e1 != 0 { - err = e1 - } - return -} diff --git a/syscall_freebsd.go b/syscall_freebsd.go deleted file mode 100644 index f0b87e2..0000000 --- a/syscall_freebsd.go +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright 2014-2016 Cristian Maglie. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// - -// This file is machine generated by the command: -// mksyscall.pl serial_darwin.go -// The generated stub is modified to make it compile under the "serial" package - -package serial // import "go.bug.st/serial" - -import "syscall" - -func ioctl(fd int, req uint64, data uintptr) (err error) { - _, _, e1 := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(data)) - if e1 != 0 { - err = e1 - } - return -} diff --git a/syscall_linux.go b/syscall_linux.go deleted file mode 100644 index 79ff6d3..0000000 --- a/syscall_linux.go +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright 2014-2016 Cristian Maglie. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// - -// This file is machine generated by the command: -// mksyscall.pl serial_linux.go -// The generated stub is modified to make it compile under the "serial" package - -package serial // import "go.bug.st/serial" - -import "syscall" - -func ioctl(fd int, req uint64, data uintptr) (err error) { - _, _, e1 := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(data)) - if e1 != 0 { - err = e1 - } - return -} diff --git a/syscall_windows.go b/syscall_windows.go deleted file mode 100644 index f868cb8..0000000 --- a/syscall_windows.go +++ /dev/null @@ -1,61 +0,0 @@ -// go build mksyscall_windows.go && ./mksyscall_windows serial_windows.go -// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT - -package serial - -import "unsafe" -import "syscall" - -var ( - modadvapi32 = syscall.NewLazyDLL("advapi32.dll") - modkernel32 = syscall.NewLazyDLL("kernel32.dll") - - procRegEnumValueW = modadvapi32.NewProc("RegEnumValueW") - procGetCommState = modkernel32.NewProc("GetCommState") - procSetCommState = modkernel32.NewProc("SetCommState") - procSetCommTimeouts = modkernel32.NewProc("SetCommTimeouts") -) - -func RegEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, class *uint16, value *uint16, valueLen *uint32) (regerrno error) { - r0, _, _ := syscall.Syscall9(procRegEnumValueW.Addr(), 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(value)), uintptr(unsafe.Pointer(valueLen)), 0) - if r0 != 0 { - regerrno = syscall.Errno(r0) - } - return -} - -func GetCommState(handle syscall.Handle, dcb *DCB) (err error) { - r1, _, e1 := syscall.Syscall(procGetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(dcb)), 0) - if r1 == 0 { - if e1 != 0 { - err = error(e1) - } else { - err = syscall.EINVAL - } - } - return -} - -func SetCommState(handle syscall.Handle, dcb *DCB) (err error) { - r1, _, e1 := syscall.Syscall(procSetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(dcb)), 0) - if r1 == 0 { - if e1 != 0 { - err = error(e1) - } else { - err = syscall.EINVAL - } - } - return -} - -func SetCommTimeouts(handle syscall.Handle, timeouts *COMMTIMEOUTS) (err error) { - r1, _, e1 := syscall.Syscall(procSetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0) - if r1 == 0 { - if e1 != 0 { - err = error(e1) - } else { - err = syscall.EINVAL - } - } - return -} diff --git a/unixutils/pipe.go b/unixutils/pipe.go new file mode 100644 index 0000000..f54c916 --- /dev/null +++ b/unixutils/pipe.go @@ -0,0 +1,82 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build linux || darwin || freebsd || openbsd + +package unixutils + +import ( + "fmt" + "syscall" +) + +// Pipe represents a unix-pipe +type Pipe struct { + opened bool + rd int + wr int +} + +// Open creates a new pipe +func (p *Pipe) Open() error { + fds := []int{0, 0} + if err := syscall.Pipe(fds); err != nil { + return err + } + p.rd = fds[0] + p.wr = fds[1] + p.opened = true + return nil +} + +// ReadFD returns the file handle for the read side of the pipe. +func (p *Pipe) ReadFD() int { + if !p.opened { + return -1 + } + return p.rd +} + +// WriteFD returns the file handle for the write side of the pipe. +func (p *Pipe) WriteFD() int { + if !p.opened { + return -1 + } + return p.wr +} + +// Write to the pipe the content of data. Returns the number of bytes written. +func (p *Pipe) Write(data []byte) (int, error) { + if !p.opened { + return 0, fmt.Errorf("Pipe not opened") + } + return syscall.Write(p.wr, data) +} + +// Read from the pipe into the data array. Returns the number of bytes read. +func (p *Pipe) Read(data []byte) (int, error) { + if !p.opened { + return 0, fmt.Errorf("Pipe not opened") + } + return syscall.Read(p.rd, data) +} + +// Close the pipe +func (p *Pipe) Close() error { + if !p.opened { + return fmt.Errorf("Pipe not opened") + } + err1 := syscall.Close(p.rd) + err2 := syscall.Close(p.wr) + p.opened = false + if err1 != nil { + return err1 + } + if err2 != nil { + return err2 + } + return nil +} diff --git a/unixutils/select.go b/unixutils/select.go new file mode 100644 index 0000000..42cacc3 --- /dev/null +++ b/unixutils/select.go @@ -0,0 +1,101 @@ +// +// Copyright 2014-2024 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build linux || darwin || freebsd || openbsd + +package unixutils + +import ( + "time" + + "github.com/creack/goselect" +) + +// FDSet is a set of file descriptors suitable for a select call +type FDSet struct { + set goselect.FDSet + max uintptr +} + +// NewFDSet creates a set of file descriptors suitable for a Select call. +func NewFDSet(fds ...int) *FDSet { + s := &FDSet{} + s.Add(fds...) + return s +} + +// Add adds the file descriptors passed as parameter to the FDSet. +func (s *FDSet) Add(fds ...int) { + for _, fd := range fds { + f := uintptr(fd) + s.set.Set(f) + if f > s.max { + s.max = f + } + } +} + +// FDResultSets contains the result of a Select operation. +type FDResultSets struct { + readable *goselect.FDSet + writeable *goselect.FDSet + errors *goselect.FDSet +} + +// IsReadable test if a file descriptor is ready to be read. +func (r *FDResultSets) IsReadable(fd int) bool { + return r.readable.IsSet(uintptr(fd)) +} + +// IsWritable test if a file descriptor is ready to be written. +func (r *FDResultSets) IsWritable(fd int) bool { + return r.writeable.IsSet(uintptr(fd)) +} + +// IsError test if a file descriptor is in error state. +func (r *FDResultSets) IsError(fd int) bool { + return r.errors.IsSet(uintptr(fd)) +} + +// Select performs a select system call, +// file descriptors in the rd set are tested for read-events, +// file descriptors in the wd set are tested for write-events and +// file descriptors in the er set are tested for error-events. +// The function will block until an event happens or the timeout expires. +// The function return an FDResultSets that contains all the file descriptor +// that have a pending read/write/error event. +func Select(rd, wr, er *FDSet, timeout time.Duration) (*FDResultSets, error) { + max := uintptr(0) + res := &FDResultSets{} + if rd != nil { + // fdsets are copied so the parameters are left untouched + copyOfRd := rd.set + res.readable = ©OfRd + // Determine max fd. + max = rd.max + } + if wr != nil { + // fdsets are copied so the parameters are left untouched + copyOfWr := wr.set + res.writeable = ©OfWr + // Determine max fd. + if wr.max > max { + max = wr.max + } + } + if er != nil { + // fdsets are copied so the parameters are left untouched + copyOfEr := er.set + res.errors = ©OfEr + // Determine max fd. + if er.max > max { + max = er.max + } + } + + err := goselect.Select(int(max+1), res.readable, res.writeable, res.errors, timeout) + return res, err +}