41 Commits

Author SHA1 Message Date
f10848f47f fix: 修复串口 read timeout 未生效的问题
Some checks failed
test / native-os-build (macOS-latest) (push) Has been cancelled
test / native-os-build (ubuntu-latest) (push) Has been cancelled
test / native-os-build (windows-latest) (push) Has been cancelled
test / cross-os-build (freebsd amd64) (push) Has been cancelled
test / cross-os-build (linux ppc64le) (push) Has been cancelled
test / cross-os-build (openbsd 386) (push) Has been cancelled
test / cross-os-build (openbsd amd64) (push) Has been cancelled
test / cross-os-build (openbsd arm) (push) Has been cancelled
- 将串口文件描述符设置为非阻塞模式,确保 read() 不会阻塞
- 修改 VMIN 从 1 改为 0,让 select() 正确控制读取超时
- 添加 Timeout 错误代码,超时时正确返回错误而不是 (0, nil)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:47:17 +08:00
b3711b62ad 添加git提交规范:提交信息必须包含中文说明
Some checks failed
test / native-os-build (macOS-latest) (push) Has been cancelled
test / native-os-build (ubuntu-latest) (push) Has been cancelled
test / native-os-build (windows-latest) (push) Has been cancelled
test / cross-os-build (freebsd amd64) (push) Has been cancelled
test / cross-os-build (linux ppc64le) (push) Has been cancelled
test / cross-os-build (openbsd 386) (push) Has been cancelled
test / cross-os-build (openbsd amd64) (push) Has been cancelled
test / cross-os-build (openbsd arm) (push) Has been cancelled
2026-03-03 10:36:03 +08:00
ff4afcdddb Update .gitignore to include .claude/settings.local.json 2026-03-03 10:33:49 +08:00
27af1097f9 Add test program to verify serial port close deadlock fix 2026-03-03 10:33:32 +08:00
80d2794531 Add .gitignore file for Go project 2026-03-03 10:30:52 +08:00
070b7685d5 Linux系统下串口Close相关实现的深度优化
1. 优化了Close方法的资源释放顺序,先释放独占访问权,再关闭实际的端口句柄
2. 添加了句柄有效性检查,避免对无效句柄进行操作
3. 确保所有资源都能正确清理,包括句柄和信号管道
4. 改进了错误处理机制,确保只返回第一个错误
5. 为所有方法添加了端口状态检查,确保在端口关闭后立即返回PortClosed错误
6. 统一了错误处理格式,使用PortError类型包装系统错误
7. 优化了Read方法的错误处理和超时逻辑
8. 为Write、Break、SetMode、SetDTR、SetRTS和GetModemStatusBits方法添加了端口状态检查
2026-03-03 10:27:03 +08:00
Cristian Maglie
f12391c01f Merge pull request #205 from WarningImHack3r/fix-serial-getters
Some checks failed
test / native-os-build (macOS-latest) (push) Has been cancelled
test / native-os-build (ubuntu-latest) (push) Has been cancelled
test / native-os-build (windows-latest) (push) Has been cancelled
test / cross-os-build (freebsd amd64) (push) Has been cancelled
test / cross-os-build (linux ppc64le) (push) Has been cancelled
test / cross-os-build (openbsd 386) (push) Has been cancelled
test / cross-os-build (openbsd amd64) (push) Has been cancelled
test / cross-os-build (openbsd arm) (push) Has been cancelled
fix: incorrect masks for modem status bits on Windows
2025-05-19 15:36:16 +02:00
Cristian Maglie
8f447ebc07 Make win32 internal constants private. 2025-05-19 15:31:36 +02:00
WarningImHack3r
20a47944cf refactor: rename variables to match docs 2025-05-08 13:17:50 +02:00
WarningImHack3r
9710c814f6 fix: incorrect masks for modem status bits on Windows 2025-05-07 20:44:25 +02:00
Cristian Maglie
f0e4a45720 Merge pull request #202 from ctarsjp/win-port-enum
Fix (non-detailed) port enumeration on Windows
2025-03-28 15:54:19 +01:00
Stepan
bcb0408701 Windows: added a check for \\.\ prefix 2025-03-28 16:42:18 +09:00
Stepan
5069d66aa2 Windows: fixed port enumeration 2025-03-28 16:28:13 +09:00
Cristian Maglie
3449d2e7f6 Merge pull request #200 from cmaglie/CH340_hack
Hack to workaround misbehaving CH340 drivers on Windows
2025-03-17 17:24:34 +01:00
Cristian Maglie
1ff9b6fa9a If the timeout is set to NoTimeout, make Read wait forever.
Otherwise the maximul allowed timeout is 0x7FFFFFFF milliseconds,
equivalent to about 24.85 days.
This patch will make a transparent repeated read in case this long
timeout should ever happen.
2025-03-17 10:57:21 +01:00
Cristian Maglie
7dc6297645 Merge pull request #195 from Dirk007/fix/nilErrorReport
Report the actual error instead of nil...
2025-03-13 01:00:39 +01:00
Cristian Maglie
fb4b111d50 Hack for CH340 support 2025-03-12 20:50:56 +01:00
Dirk007
b7483e31a7 Report the actual error instead of nil 2024-09-07 13:26:01 +02:00
Cristian Maglie
0996f840dd Merge pull request #187 from twpayne/sys-windows
Use Windows serial comm functions from golang.org/x/sys/windows
2024-06-25 23:56:14 +02:00
Cristian Maglie
f5a4685ea0 Fixed examples for docs 2024-06-25 23:52:38 +02:00
Tom Payne
56ac2d4e76 Restore check for no detected serial ports on Windows 2024-06-25 11:02:05 +02:00
Tom Payne
1c72447e64 Fix typos
Co-authored-by: Cristian Maglie <c.maglie@bug.st>
2024-06-25 10:48:18 +02:00
Tom Payne
45e996e1b0 Use Windows serial comm functions from golang.org/x/sys/windows 2024-06-24 18:28:26 +02:00
Andreas Deininger
0b7848559a Fix typos (#183)
Co-authored-by: Cristian Maglie <c.maglie@bug.st>
2024-06-24 17:33:18 +02:00
Cristian Maglie
c18d387887 Merge pull request #184 from deining/gofmt
Go-format source code: fix warning
2024-06-24 17:26:52 +02:00
Cristian Maglie
c768d77847 Merge pull request #188 from twpayne/doc-fixes
Fixed minor documentation typos
2024-06-24 17:25:22 +02:00
Andreas Deininger
1282f62c6e Bump GitHub workflow actions to latest versions (#185)
* Bump GitHub workflow actions to latest versions

* Fixed typo

---------

Co-authored-by: Cristian Maglie <c.maglie@bug.st>
2024-06-24 17:24:35 +02:00
Cristian Maglie
4f7d935be3 Merge pull request #186 from AndreRenaud/enumerator-build
Workflows: Use CGO_ENABLED=1 for MacOS
2024-06-24 17:13:45 +02:00
Andre Renaud
03c961bc8a Workflows: Use CGO_ENABLED=1 for MacOS
Also enable explicit ARM64 builds
2024-05-28 09:27:43 +12:00
Andreas Deininger
671075c6ac Go-format source code 2024-04-01 16:04:53 +02:00
Tom Payne
bac809c5a1 Fixed minor documentation typos 2024-03-18 22:25:55 +01:00
Cristian Maglie
0925f99089 Merge pull request #176 from paralin/fix-wasm
fix: add shims for GOARCH=wasm with GOOS=js and GOOS=wasip1
2024-03-11 19:03:49 +01:00
Christian Stewart
42bc112d18 fix: add shims for GOARCH=wasm with GOOS=js and GOOS=wasip1
Fixes build errors:

GOOS=js GOARCH=wasm go build
GOOS=wasip1 GOARCH=wasm go build

Signed-off-by: Christian Stewart <christian@aperture.us>
2024-03-01 14:26:58 -08:00
Cristian Maglie
259bdeb6c7 Fixed typo 2024-02-20 15:57:58 +01:00
Cristian Maglie
572f392ca9 Updated license year 2024-02-20 15:27:44 +01:00
Cristian Maglie
14e5ea68ce Precompile port-filter regexp 2024-02-20 15:24:37 +01:00
Cristian Maglie
bcd8695df4 Removed deprecated package io/ioutil 2024-02-20 15:13:12 +01:00
Cristian Maglie
92703ecb02 Improved issue template message 2024-02-20 09:55:49 +01:00
Cristian Maglie
674fbae95a Upgraded dependencies 2024-02-20 09:52:27 +01:00
Cristian Maglie
2aa105e32e Improved issue template message 2024-02-20 09:51:06 +01:00
Cristian Maglie
7f490f208a Added issues templates 2024-02-18 19:33:23 +01:00
46 changed files with 850 additions and 515 deletions

78
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -15,13 +15,22 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- uses: actions/setup-go@v1 - uses: actions/setup-go@v5
with: with:
go-version: "1.17" go-version: "1.22"
- name: Build native - 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 ./... run: GOARCH=amd64 go build -v ./...
shell: bash shell: bash
- name: Build ARM64
run: GOARCH=arm64 go build -v ./...
shell: bash
- name: Install socat - name: Install socat
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install socat run: sudo apt-get install socat
@@ -51,10 +60,10 @@ jobs:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- uses: actions/setup-go@v1 - uses: actions/setup-go@v5
with: with:
go-version: "1.17" go-version: "1.22"
- name: Cross-build - name: Cross-build
run: | run: |
set ${{ matrix.go-os-pairs }} set ${{ matrix.go-os-pairs }}

54
.gitignore vendored Normal file
View File

@@ -0,0 +1,54 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# IDE directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Test directory
testprogram/
# Build artifacts
*.tmp
*.temp
build/
dist/
# Log files
*.log
*.err
*.out.log
# Configuration files with sensitive information
*.local
*.secret
*.env
.claude/settings.local.json

169
CLAUDE.md Normal file
View File

@@ -0,0 +1,169 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 开发者约束要求
### 语言与地区
- **主要交互语言**: 中文(简体)
- **开发者所在地**: 中国大陆
- **时区**: GMT+8北京时间
### AI Agent交互规范
- 使用中文进行所有AI Agent交互和代码解释
- 文档和注释可以使用英文(保持与现有代码库风格一致)
- 技术术语遵循行业标准翻译
### Git提交规范
- **提交信息必须包含中文说明**确保每个commit都有清晰的中文描述
- 提交信息格式:`<类型>: <主题>`,主题使用中文
- 提交信息应简洁明了,准确描述变更内容
- 类型可以是feat(新功能)、fix(修复)、docs(文档)、style(样式)、refactor(重构)、test(测试)、chore(构建过程或辅助工具的变动)
### 代码库使用注意事项
- 本项目是一个开源Go库主要用于跨平台串口通信
- 遵守BSD 3-clause开源许可证
- 保持代码的跨平台兼容性和简洁性
- 所有修改需要通过GitHub PR流程进行
### 代码库使用注意事项
- 本项目是一个开源Go库主要用于跨平台串口通信
- 遵守BSD 3-clause开源许可证
- 保持代码的跨平台兼容性和简洁性
- 所有修改需要通过GitHub PR流程进行
## Project Overview
**go-serial** is a cross-platform serial port communication library for Go. It provides a simple and consistent API for accessing serial ports on Windows, macOS, Linux, and other Unix-like systems.
## Key Features
- Cross-platform support (Windows, macOS, Linux, FreeBSD, OpenBSD)
- Simple API for opening and configuring serial ports
- Support for modem control lines (RTS, DTR, CTS, DSR, etc.)
- USB device enumeration with VID/PID and serial number detection
- Read/write timeout support
- Parity, data bits, stop bits configuration
- Break signal support
## Package Structure
```
go-serial/
├── serial.go # Core API definition (Port interface, Mode, errors)
├── serial_*.go # Platform-specific implementations (windows, darwin, linux, bsd, wasm)
├── enumerator/ # USB serial port enumeration with detailed info
│ ├── enumerator.go # Core enumerator API
│ └── usb_*.go # Platform-specific USB enumeration
├── portlist/ # Simple port listing implementation
├── unixutils/ # Unix-specific utility functions
└── *.test.go # Example and test files
```
## Core API
### Opening a Serial Port
```go
import "go.bug.st/serial"
mode := &serial.Mode{
BaudRate: 115200,
Parity: serial.EvenParity,
DataBits: 7,
StopBits: serial.OneStopBit,
}
port, err := serial.Open("/dev/ttyUSB0", mode)
if err != nil {
log.Fatal(err)
}
defer port.Close()
```
### Reading and Writing
```go
// Write data
n, err := port.Write([]byte("Hello"))
if err != nil {
log.Fatal(err)
}
// Read data with timeout
buff := make([]byte, 100)
port.SetReadTimeout(5 * time.Second)
n, err := port.Read(buff)
if err != nil {
log.Fatal(err)
}
```
### Getting Port List
```go
// Simple port list
ports, err := serial.GetPortsList()
// Detailed port list with USB info
import "go.bug.st/serial/enumerator"
ports, err := enumerator.GetDetailedPortsList()
for _, port := range ports {
if port.IsUSB {
fmt.Printf("USB Port: %s (VID:%s PID:%s Serial:%s)\n",
port.Name, port.VID, port.PID, port.SerialNumber)
}
}
```
## Common Commands
### Testing
```bash
go test -v ./... # Run all tests
go test -v ./enumerator # Run enumerator tests specifically
```
### Documentation
```bash
go doc go.bug.st/serial # Show package documentation
go doc go.bug.st/serial.Port # Show Port interface documentation
```
### Generating Windows Syscall Bindings
```bash
cd go-serial
go generate
```
## Platform-Specific Notes
- **Windows**: Uses Windows API for serial communication, supports most USB-to-serial adapters
- **macOS**: Uses IOKit framework (requires cgo) for USB enumeration
- **Linux**: Supports standard serial ports and USB-to-serial devices via `/dev/tty*`
- **Unix-like systems**: Use termios API for configuration
- **Wasm**: Limited support (enumeration not implemented)
## Recent Changes
The repository has recent commits related to:
- Fixing serial port getter methods
- Making Windows internal constants private
- Fixing incorrect masks for modem status bits on Windows
- Renaming variables to match documentation
- Windows port enumeration improvements
- **Linux系统下串口Close方法深度优化**
- 优化了资源释放顺序,先释放独占访问权,再关闭实际的端口句柄
- 添加了句柄有效性检查,避免对无效句柄进行操作
- 确保所有资源都能正确清理,包括句柄和信号管道
- 改进了错误处理机制,确保只返回第一个错误
- 为所有方法添加了端口状态检查确保在端口关闭后立即返回PortClosed错误
- 统一了错误处理格式使用PortError类型包装系统错误
- 优化了Read方法的错误处理和超时逻辑
- 为Write、Break、SetMode、SetDTR、SetRTS和GetModemStatusBits方法添加了端口状态检查
## License
BSD 3-clause license - see LICENSE file for details.

View File

@@ -1,5 +1,5 @@
Copyright (c) 2014-2023, Cristian Maglie. Copyright (c) 2014-2024, Cristian Maglie.
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View File

@@ -2,17 +2,17 @@
# go.bug.st/serial # go.bug.st/serial
A cross-platform serial library for go-lang. A cross-platform serial port library for Go.
## Documentation and examples ## Documentation and examples
See the godoc here: https://godoc.org/go.bug.st/serial See the package documentation here: https://pkg.go.dev/go.bug.st/serial
## go.mod transition ## go.mod transition
This library now support `go.mod` with the import `go.bug.st/serial`. 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 new updates. Anyway, the latest `v1` release should still be avaiable using the old import. 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 ## Credits
@@ -20,7 +20,7 @@ If you came from the pre-`go.mod` era please update your import paths from `go.b
## License ## License
The software is release under a [BSD 3-clause license] This software is released under the [BSD 3-clause license].
[contributors]: https://github.com/bugst/go-serial/graphs/contributors [contributors]: https://github.com/bugst/go-serial/graphs/contributors
[BSD 3-clause license]: https://github.com/bugst/go-serial/blob/master/LICENSE [BSD 3-clause license]: https://github.com/bugst/go-serial/blob/master/LICENSE

2
doc.go
View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

11
enumerator/usb_wasm.go Normal file
View File

@@ -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{}
}

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -98,7 +98,7 @@ const (
spdrpUpperFilters = 0x00000011 // UpperFilters = R/W spdrpUpperFilters = 0x00000011 // UpperFilters = R/W
spdrpLowerFilters = 0x00000012 // LowerFilters = R/W spdrpLowerFilters = 0x00000012 // LowerFilters = R/W
spdrpBusTypeGUID = 0x00000013 // BusTypeGUID = R spdrpBusTypeGUID = 0x00000013 // BusTypeGUID = R
spdrpLegactBusType = 0x00000014 // LegacyBusType = R spdrpLegacyBusType = 0x00000014 // LegacyBusType = R
spdrpBusNumber = 0x00000015 // BusNumber = R spdrpBusNumber = 0x00000015 // BusNumber = R
spdrpEnumeratorName = 0x00000016 // Enumerator Name = R spdrpEnumeratorName = 0x00000016 // Enumerator Name = R
spdrpSecurity = 0x00000017 // Security = R/W, binary form spdrpSecurity = 0x00000017 // Security = R/W, binary form

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

15
enumerator_wasm.go Normal file
View File

@@ -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")
}

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -14,7 +14,7 @@ import (
"go.bug.st/serial" "go.bug.st/serial"
) )
func ExampleGetSetModemBits() { func ExamplePort_GetModemStatusBits() {
// Open the first serial port detected at 9600bps N81 // Open the first serial port detected at 9600bps N81
mode := &serial.Mode{ mode := &serial.Mode{
BaudRate: 9600, BaudRate: 9600,

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -13,7 +13,7 @@ import (
"go.bug.st/serial" "go.bug.st/serial"
) )
func ExampleSerialPort_SetMode() { func ExamplePort_SetMode() {
port, err := serial.Open("/dev/ttyACM0", &serial.Mode{}) port, err := serial.Open("/dev/ttyACM0", &serial.Mode{})
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

8
go.mod
View File

@@ -4,12 +4,12 @@ go 1.17
require ( require (
github.com/creack/goselect v0.1.2 github.com/creack/goselect v0.1.2
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.8.4
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 golang.org/x/sys v0.19.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

20
go.sum
View File

@@ -1,17 +1,21 @@
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -12,7 +12,7 @@
// Port: /dev/cu.usbmodemFD121 // Port: /dev/cu.usbmodemFD121
// USB ID 2341:8053 // USB ID 2341:8053
// USB serial FB7B6060504B5952302E314AFF08191A // USB serial FB7B6060504B5952302E314AFF08191A
//
package main package main
import ( import (

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -147,7 +147,7 @@ const (
PortNotFound PortNotFound
// InvalidSerialPort the requested port is not a serial port // InvalidSerialPort the requested port is not a serial port
InvalidSerialPort InvalidSerialPort
// PermissionDenied the user doesn't have enough priviledges // PermissionDenied the user doesn't have enough privileges
PermissionDenied PermissionDenied
// InvalidSpeed the requested speed is not valid or not supported // InvalidSpeed the requested speed is not valid or not supported
InvalidSpeed InvalidSpeed
@@ -163,6 +163,8 @@ const (
ErrorEnumeratingPorts ErrorEnumeratingPorts
// PortClosed the port has been closed while the operation is in progress // PortClosed the port has been closed while the operation is in progress
PortClosed PortClosed
// Timeout the read operation timed out
Timeout
// FunctionNotImplemented the requested function is not implemented // FunctionNotImplemented the requested function is not implemented
FunctionNotImplemented FunctionNotImplemented
) )
@@ -192,6 +194,8 @@ func (e PortError) EncodedErrorString() string {
return "Could not enumerate serial ports" return "Could not enumerate serial ports"
case PortClosed: case PortClosed:
return "Port has been closed" return "Port has been closed"
case Timeout:
return "Read timeout"
case FunctionNotImplemented: case FunctionNotImplemented:
return "Function not implemented" return "Function not implemented"
default: default:

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,15 +1,20 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
package serial package serial
import "golang.org/x/sys/unix" import (
"regexp"
"golang.org/x/sys/unix"
)
const devFolder = "/dev" const devFolder = "/dev"
const regexFilter = "^(cu|tty)\\..*"
var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*")
const ioctlTcgetattr = unix.TIOCGETA const ioctlTcgetattr = unix.TIOCGETA
const ioctlTcsetattr = unix.TIOCSETA const ioctlTcsetattr = unix.TIOCSETA

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,15 +1,20 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
package serial package serial
import "golang.org/x/sys/unix" import (
"regexp"
"golang.org/x/sys/unix"
)
const devFolder = "/dev" const devFolder = "/dev"
const regexFilter = "^(cu|tty)\\..*"
var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*")
// termios manipulation functions // termios manipulation functions

View File

@@ -1,15 +1,20 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
package serial package serial
import "golang.org/x/sys/unix" import (
"regexp"
"golang.org/x/sys/unix"
)
const devFolder = "/dev" const devFolder = "/dev"
const regexFilter = "(ttyS|ttyHS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO|ttymxc)[0-9]{1,3}"
var osPortFilter = regexp.MustCompile("(ttyS|ttyHS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO|ttymxc)[0-9]{1,3}")
// termios manipulation functions // termios manipulation functions

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,15 +1,20 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
package serial package serial
import "golang.org/x/sys/unix" import (
"regexp"
"golang.org/x/sys/unix"
)
const devFolder = "/dev" const devFolder = "/dev"
const regexFilter = "^(cu|tty)\\..*"
var osPortFilter = regexp.MustCompile("^(cu|tty)\\..*")
// termios manipulation functions // termios manipulation functions

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -10,8 +10,7 @@ package serial
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"regexp"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -31,35 +30,48 @@ type unixPort struct {
} }
func (port *unixPort) Close() error { func (port *unixPort) Close() error {
// 原子操作检查并设置端口关闭状态
if !atomic.CompareAndSwapUint32(&port.opened, 1, 0) { if !atomic.CompareAndSwapUint32(&port.opened, 1, 0) {
return nil return nil // 端口已经关闭,直接返回
} }
// Close port var firstErr error
port.releaseExclusiveAccess()
if err := unix.Close(port.handle); err != nil {
return err
}
// 发送关闭信号以取消所有待处理的读操作
if port.closeSignal != nil { if port.closeSignal != nil {
// Send close signal to all pending reads (if any) if _, err := port.closeSignal.Write([]byte{0}); err != nil && firstErr == nil {
port.closeSignal.Write([]byte{0}) firstErr = err
// 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
// 检查句柄是否有效,然后执行关闭操作
if port.handle != -1 {
// 释放独占访问权 - 应在关闭handle之前完成
if err := port.releaseExclusiveAccess(); err != nil && firstErr == nil {
firstErr = err
}
// 关闭实际的端口句柄 - 这将解除任何挂起的读写操作
if err := unix.Close(port.handle); err != nil && firstErr == nil {
firstErr = err
}
port.handle = -1 // 标记句柄无效
}
// 关闭信号管道
if port.closeSignal != nil {
if err := port.closeSignal.Close(); err != nil && firstErr == nil {
firstErr = err
}
port.closeSignal = nil // 清除指针
}
return firstErr
} }
func (port *unixPort) Read(p []byte) (int, error) { func (port *unixPort) Read(p []byte) (int, error) {
port.closeLock.RLock() // 首先检查端口是否已经关闭
defer port.closeLock.RUnlock()
if atomic.LoadUint32(&port.opened) != 1 { if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed} return 0, &PortError{code: PortClosed}
} }
@@ -71,104 +83,158 @@ func (port *unixPort) Read(p []byte) (int, error) {
fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD()) fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD())
for { for {
// 在每次select之前再次检查端口状态非阻塞检查
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
timeout := time.Duration(-1) timeout := time.Duration(-1)
if port.readTimeout != NoTimeout { if port.readTimeout != NoTimeout {
timeout = time.Until(deadline) timeout = time.Until(deadline)
if timeout < 0 { if timeout < 0 {
// a negative timeout means "no-timeout" in Select(...) // 负超时值在Select(...)中表示"无超时"
timeout = 0 timeout = 0
} }
} }
res, err := unixutils.Select(fds, nil, fds, timeout) res, err := unixutils.Select(fds, nil, fds, timeout)
if err == unix.EINTR { if err == unix.EINTR {
continue continue // 系统调用被中断,重试
} }
if err != nil { if err != nil {
return 0, err // 如果在端口关闭后遇到错误返回PortClosed错误
} if atomic.LoadUint32(&port.opened) != 1 {
if res.IsReadable(port.closeSignal.ReadFD()) {
return 0, &PortError{code: PortClosed} return 0, &PortError{code: PortClosed}
} }
if !res.IsReadable(port.handle) { return 0, &PortError{code: FunctionNotImplemented, causedBy: err}
// Timeout happened
return 0, nil
} }
if res.IsReadable(port.closeSignal.ReadFD()) {
// 收到关闭信号
return 0, &PortError{code: PortClosed}
}
if !res.IsReadable(port.handle) {
// 超时
if port.readTimeout == NoTimeout {
continue // 无超时设置,继续等待
}
// 超时应返回 Timeout 错误,而不是 (0, nil)
return 0, &PortError{code: Timeout, causedBy: unix.ETIMEDOUT}
}
n, err := unix.Read(port.handle, p) n, err := unix.Read(port.handle, p)
if err == unix.EINTR { if err == unix.EINTR {
continue continue // 系统调用被中断,重试
} }
// Linux: when the port is disconnected during a read operation // Linux系统特性:当读取操作期间端口断开连接时,
// the port is left in a "readable with zero-length-data" state. // 端口会进入"可读但返回零长度数据"的状态。
// https://stackoverflow.com/a/34945814/1655275 // https://stackoverflow.com/a/34945814/1655275
if n == 0 && err == nil { if n == 0 && err == nil {
return 0, &PortError{code: PortClosed} return 0, &PortError{code: PortClosed}
} }
if n < 0 { // Do not return -1 unix errors if n < 0 { // 确保不返回负数
n = 0 n = 0
} }
return n, err // 检查读取操作期间端口是否被关闭
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
if err != nil {
return n, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return n, nil
} }
} }
func (port *unixPort) Write(p []byte) (n int, err error) { func (port *unixPort) Write(p []byte) (n int, err error) {
// 首先检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
n, err = unix.Write(port.handle, p) n, err = unix.Write(port.handle, p)
if n < 0 { // Do not return -1 unix errors if n < 0 { // 确保不返回负数
n = 0 n = 0
} }
return if err != nil {
return n, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return n, nil
} }
func (port *unixPort) Break(t time.Duration) error { func (port *unixPort) Break(t time.Duration) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil { if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil {
return err return &PortError{code: FunctionNotImplemented, causedBy: err}
} }
time.Sleep(t) time.Sleep(t)
if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil { if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil {
return err return &PortError{code: FunctionNotImplemented, causedBy: err}
} }
return nil return nil
} }
func (port *unixPort) SetMode(mode *Mode) error { func (port *unixPort) SetMode(mode *Mode) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
settings, err := port.getTermSettings() settings, err := port.getTermSettings()
if err != nil { if err != nil {
return err return &PortError{code: InvalidSerialPort, causedBy: err}
} }
if err := setTermSettingsParity(mode.Parity, settings); err != nil { if err := setTermSettingsParity(mode.Parity, settings); err != nil {
return err return err
} }
if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil { if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil {
return err return err
} }
if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil { if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil {
return err return err
} }
requireSpecialBaudrate := false requireSpecialBaudrate := false
if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil { if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil {
return err return err
} else if special { } else if special {
requireSpecialBaudrate = true requireSpecialBaudrate = true
} }
if err := port.setTermSettings(settings); err != nil { if err := port.setTermSettings(settings); err != nil {
return err return &PortError{code: InvalidSerialPort, causedBy: err}
} }
if requireSpecialBaudrate { if requireSpecialBaudrate {
// MacOSX require this one to be the last operation otherwise an // MacOSX要求这是最后一个操作,否则会产生'Invalid serial port'错误
// 'Invalid serial port' error is produced.
if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil { if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil {
return err return &PortError{code: InvalidSpeed, causedBy: err}
} }
} }
return nil return nil
} }
func (port *unixPort) SetDTR(dtr bool) error { func (port *unixPort) SetDTR(dtr bool) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus() status, err := port.getModemBitsStatus()
if err != nil { if err != nil {
return err return &PortError{code: FunctionNotImplemented, causedBy: err}
} }
if dtr { if dtr {
status |= unix.TIOCM_DTR status |= unix.TIOCM_DTR
@@ -179,9 +245,14 @@ func (port *unixPort) SetDTR(dtr bool) error {
} }
func (port *unixPort) SetRTS(rts bool) error { func (port *unixPort) SetRTS(rts bool) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus() status, err := port.getModemBitsStatus()
if err != nil { if err != nil {
return err return &PortError{code: FunctionNotImplemented, causedBy: err}
} }
if rts { if rts {
status |= unix.TIOCM_RTS status |= unix.TIOCM_RTS
@@ -200,10 +271,16 @@ func (port *unixPort) SetReadTimeout(timeout time.Duration) error {
} }
func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) { func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return nil, &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus() status, err := port.getModemBitsStatus()
if err != nil { if err != nil {
return nil, err return nil, &PortError{code: FunctionNotImplemented, causedBy: err}
} }
return &ModemStatusBits{ return &ModemStatusBits{
CTS: (status & unix.TIOCM_CTS) != 0, CTS: (status & unix.TIOCM_CTS) != 0,
DCD: (status & unix.TIOCM_CD) != 0, DCD: (status & unix.TIOCM_CD) != 0,
@@ -242,7 +319,7 @@ func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
// Explicitly disable RTS/CTS flow control // Explicitly disable RTS/CTS flow control
setTermSettingsCtsRts(false, settings) setTermSettingsCtsRts(false, settings)
if port.setTermSettings(settings) != nil { if err = port.setTermSettings(settings); err != nil {
port.Close() port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting term settings: %w", err)} return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting term settings: %w", err)}
} }
@@ -276,7 +353,9 @@ func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error configuring port: %w", err)} return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error configuring port: %w", err)}
} }
unix.SetNonblock(h, false) // Set non-blocking mode to ensure read() doesn't block after select() returns
// The select() call provides the timeout mechanism, not the read() call
unix.SetNonblock(h, true)
port.acquireExclusiveAccess() port.acquireExclusiveAccess()
@@ -292,16 +371,12 @@ func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
} }
func nativeGetPortsList() ([]string, error) { func nativeGetPortsList() ([]string, error) {
files, err := ioutil.ReadDir(devFolder) files, err := os.ReadDir(devFolder)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ports := make([]string, 0, len(files)) ports := make([]string, 0, len(files))
regex, err := regexp.Compile(regexFilter)
if err != nil {
return nil, err
}
for _, f := range files { for _, f := range files {
// Skip folders // Skip folders
if f.IsDir() { if f.IsDir() {
@@ -309,7 +384,7 @@ func nativeGetPortsList() ([]string, error) {
} }
// Keep only devices with the correct name // Keep only devices with the correct name
if !regex.MatchString(f.Name()) { if !osPortFilter.MatchString(f.Name()) {
continue continue
} }
@@ -441,7 +516,14 @@ func setRawMode(settings *unix.Termios) {
settings.Oflag &^= unix.OPOST settings.Oflag &^= unix.OPOST
// Block reads until at least one char is available (no timeout) // Block reads until at least one char is available (no timeout)
settings.Cc[unix.VMIN] = 1 // VMIN=1: Block until at least 1 byte is available
// VTIME=0: No inter-character timeout (not used with VMIN=1)
//
// NOTE: When readTimeout is set, the select() system call provides the timeout
// and VMIN/VTIME settings are not used for timeout purposes.
// However, VMIN=1 can cause issues with select() on some platforms/
// terminal drivers. Using VMIN=0 allows select() to work reliably.
settings.Cc[unix.VMIN] = 0
settings.Cc[unix.VTIME] = 0 settings.Cc[unix.VTIME] = 0
} }

15
serial_wasm.go Normal file
View File

@@ -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")
}

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -18,48 +18,50 @@ package serial
*/ */
import ( import (
"errors"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
) )
type windowsPort struct { type windowsPort struct {
mu sync.Mutex mu sync.Mutex
handle syscall.Handle handle windows.Handle
hasTimeout bool
} }
func nativeGetPortsList() ([]string, error) { func nativeGetPortsList() ([]string, error) {
subKey, err := syscall.UTF16PtrFromString("HARDWARE\\DEVICEMAP\\SERIALCOMM\\") 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()
names, err := key.ReadValueNames(0)
if err != nil { if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts} return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
} }
var h syscall.Handle var values []string
if err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, subKey, 0, syscall.KEY_READ, &h); err != nil { for _, n := range names {
if errno, isErrno := err.(syscall.Errno); isErrno && errno == syscall.ERROR_FILE_NOT_FOUND { v, _, err := key.GetStringValue(n)
return []string{}, nil if err != nil || v == "" {
} continue
return nil, &PortError{code: ErrorEnumeratingPorts}
}
defer syscall.RegCloseKey(h)
var valuesCount uint32
if syscall.RegQueryInfoKey(h, nil, nil, nil, nil, nil, nil, &valuesCount, nil, nil, nil, nil) != nil {
return nil, &PortError{code: ErrorEnumeratingPorts}
} }
list := make([]string, valuesCount) values = append(values, v)
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, &PortError{code: ErrorEnumeratingPorts}
} }
list[i] = syscall.UTF16ToString(data[:])
} return values, nil
return list, nil
} }
func (port *windowsPort) Close() error { func (port *windowsPort) Close() error {
@@ -71,7 +73,7 @@ func (port *windowsPort) Close() error {
if port.handle == 0 { if port.handle == 0 {
return nil return nil
} }
return syscall.CloseHandle(port.handle) return windows.CloseHandle(port.handle)
} }
func (port *windowsPort) Read(p []byte) (int, error) { func (port *windowsPort) Read(p []byte) (int, error) {
@@ -80,16 +82,17 @@ func (port *windowsPort) Read(p []byte) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer syscall.CloseHandle(ev.HEvent) defer windows.CloseHandle(ev.HEvent)
err = syscall.ReadFile(port.handle, p, &readed, ev) for {
if err == syscall.ERROR_IO_PENDING { err = windows.ReadFile(port.handle, p, &readed, ev)
err = getOverlappedResult(port.handle, ev, &readed, true) if err == windows.ERROR_IO_PENDING {
err = windows.GetOverlappedResult(port.handle, ev, &readed, true)
} }
switch err { switch err {
case nil: case nil:
// operation completed successfully // operation completed successfully
case syscall.ERROR_OPERATION_ABORTED: case windows.ERROR_OPERATION_ABORTED:
// port may have been closed // port may have been closed
return int(readed), &PortError{code: PortClosed, causedBy: err} return int(readed), &PortError{code: PortClosed, causedBy: err}
default: default:
@@ -101,8 +104,14 @@ func (port *windowsPort) Read(p []byte) (int, error) {
} }
// Timeout // Timeout
port.mu.Lock()
hasTimeout := port.hasTimeout
port.mu.Unlock()
if hasTimeout {
return 0, nil return 0, nil
} }
}
}
func (port *windowsPort) Write(p []byte) (int, error) { func (port *windowsPort) Write(p []byte) (int, error) {
var writed uint32 var writed uint32
@@ -110,32 +119,25 @@ func (port *windowsPort) Write(p []byte) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer syscall.CloseHandle(ev.HEvent) defer windows.CloseHandle(ev.HEvent)
err = syscall.WriteFile(port.handle, p, &writed, ev) err = windows.WriteFile(port.handle, p, &writed, ev)
if err == syscall.ERROR_IO_PENDING { if err == windows.ERROR_IO_PENDING {
// wait for write to complete // wait for write to complete
err = getOverlappedResult(port.handle, ev, &writed, true) err = windows.GetOverlappedResult(port.handle, ev, &writed, true)
} }
return int(writed), err return int(writed), err
} }
func (port *windowsPort) Drain() (err error) { func (port *windowsPort) Drain() (err error) {
return syscall.FlushFileBuffers(port.handle) return windows.FlushFileBuffers(port.handle)
} }
const (
purgeRxAbort uint32 = 0x0002
purgeRxClear = 0x0008
purgeTxAbort = 0x0001
purgeTxClear = 0x0004
)
func (port *windowsPort) ResetInputBuffer() error { func (port *windowsPort) ResetInputBuffer() error {
return purgeComm(port.handle, purgeRxClear|purgeRxAbort) return windows.PurgeComm(port.handle, windows.PURGE_RXCLEAR|windows.PURGE_RXABORT)
} }
func (port *windowsPort) ResetOutputBuffer() error { func (port *windowsPort) ResetOutputBuffer() error {
return purgeComm(port.handle, purgeTxClear|purgeTxAbort) return windows.PurgeComm(port.handle, windows.PURGE_TXCLEAR|windows.PURGE_TXABORT)
} }
const ( const (
@@ -152,119 +154,44 @@ const (
dcbInX = 0x00000200 dcbInX = 0x00000200
dcbErrorChar = 0x00000400 dcbErrorChar = 0x00000400
dcbNull = 0x00000800 dcbNull = 0x00000800
dcbRTSControlDisbaleMask = ^uint32(0x00003000) dcbRTSControlDisableMask = ^uint32(0x00003000)
dcbRTSControlEnable = 0x00001000 dcbRTSControlEnable = 0x00001000
dcbRTSControlHandshake = 0x00002000 dcbRTSControlHandshake = 0x00002000
dcbRTSControlToggle = 0x00003000 dcbRTSControlToggle = 0x00003000
dcbAbortOnError = 0x00004000 dcbAbortOnError = 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
}
type commTimeouts struct {
ReadIntervalTimeout uint32
ReadTotalTimeoutMultiplier uint32
ReadTotalTimeoutConstant uint32
WriteTotalTimeoutMultiplier uint32
WriteTotalTimeoutConstant uint32
}
const (
noParity = 0
oddParity = 1
evenParity = 2
markParity = 3
spaceParity = 4
)
var parityMap = map[Parity]byte{ var parityMap = map[Parity]byte{
NoParity: noParity, NoParity: windows.NOPARITY,
OddParity: oddParity, OddParity: windows.ODDPARITY,
EvenParity: evenParity, EvenParity: windows.EVENPARITY,
MarkParity: markParity, MarkParity: windows.MARKPARITY,
SpaceParity: spaceParity, SpaceParity: windows.SPACEPARITY,
} }
const (
oneStopBit = 0
one5StopBits = 1
twoStopBits = 2
)
var stopBitsMap = map[StopBits]byte{ var stopBitsMap = map[StopBits]byte{
OneStopBit: oneStopBit, OneStopBit: windows.ONESTOPBIT,
OnePointFiveStopBits: one5StopBits, OnePointFiveStopBits: windows.ONE5STOPBITS,
TwoStopBits: twoStopBits, TwoStopBits: windows.TWOSTOPBITS,
} }
const (
commFunctionSetXOFF = 1
commFunctionSetXON = 2
commFunctionSetRTS = 3
commFunctionClrRTS = 4
commFunctionSetDTR = 5
commFunctionClrDTR = 6
commFunctionSetBreak = 8
commFunctionClrBreak = 9
)
const (
msCTSOn = 0x0010
msDSROn = 0x0020
msRingOn = 0x0040
msRLSDOn = 0x0080
)
func (port *windowsPort) SetMode(mode *Mode) error { func (port *windowsPort) SetMode(mode *Mode) error {
params := dcb{} params := windows.DCB{}
if getCommState(port.handle, &params) != nil { if windows.GetCommState(port.handle, &params) != nil {
port.Close() port.Close()
return &PortError{code: InvalidSerialPort} return &PortError{code: InvalidSerialPort}
} }
port.setModeParams(mode, &params) port.setModeParams(mode, &params)
if setCommState(port.handle, &params) != nil { if windows.SetCommState(port.handle, &params) != nil {
port.Close() port.Close()
return &PortError{code: InvalidSerialPort} return &PortError{code: InvalidSerialPort}
} }
return nil return nil
} }
func (port *windowsPort) setModeParams(mode *Mode, params *dcb) { func (port *windowsPort) setModeParams(mode *Mode, params *windows.DCB) {
if mode.BaudRate == 0 { if mode.BaudRate == 0 {
params.BaudRate = 9600 // Default to 9600 params.BaudRate = windows.CBR_9600 // Default to 9600
} else { } else {
params.BaudRate = uint32(mode.BaudRate) params.BaudRate = uint32(mode.BaudRate)
} }
@@ -278,22 +205,22 @@ func (port *windowsPort) setModeParams(mode *Mode, params *dcb) {
} }
func (port *windowsPort) SetDTR(dtr bool) error { func (port *windowsPort) SetDTR(dtr bool) error {
// Like for RTS there are problems with the escapeCommFunction // Like for RTS there are problems with the windows.EscapeCommFunction
// observed behaviour was that DTR is set from false -> true // observed behaviour was that DTR is set from false -> true
// when setting RTS from true -> false // when setting RTS from true -> false
// 1) Connect -> RTS = true (low) DTR = true (low) OKAY // 1) Connect -> RTS = true (low) DTR = true (low) OKAY
// 2) SetDTR(false) -> RTS = true (low) DTR = false (heigh) OKAY // 2) SetDTR(false) -> RTS = true (low) DTR = false (high) OKAY
// 3) SetRTS(false) -> RTS = false (heigh) DTR = true (low) ERROR: DTR toggled // 3) SetRTS(false) -> RTS = false (high) DTR = true (low) ERROR: DTR toggled
// //
// In addition this way the CommState Flags are not updated // In addition this way the CommState Flags are not updated
/* /*
var res bool var err error
if dtr { if dtr {
res = escapeCommFunction(port.handle, commFunctionSetDTR) err = windows.EscapeCommFunction(port.handle, windows.SETDTR)
} else { } else {
res = escapeCommFunction(port.handle, commFunctionClrDTR) err = windows.EscapeCommFunction(port.handle, windows.CLTDTR)
} }
if !res { if err != nil {
return &PortError{} return &PortError{}
} }
return nil return nil
@@ -301,15 +228,15 @@ func (port *windowsPort) SetDTR(dtr bool) error {
// The following seems a more reliable way to do it // The following seems a more reliable way to do it
params := &dcb{} params := &windows.DCB{}
if err := getCommState(port.handle, params); err != nil { if err := windows.GetCommState(port.handle, params); err != nil {
return &PortError{causedBy: err} return &PortError{causedBy: err}
} }
params.Flags &= dcbDTRControlDisableMask params.Flags &= dcbDTRControlDisableMask
if dtr { if dtr {
params.Flags |= dcbDTRControlEnable params.Flags |= windows.DTR_CONTROL_ENABLE
} }
if err := setCommState(port.handle, params); err != nil { if err := windows.SetCommState(port.handle, params); err != nil {
return &PortError{causedBy: err} return &PortError{causedBy: err}
} }
@@ -325,13 +252,13 @@ func (port *windowsPort) SetRTS(rts bool) error {
// In addition this way the CommState Flags are not updated // In addition this way the CommState Flags are not updated
/* /*
var res bool var err error
if rts { if rts {
res = escapeCommFunction(port.handle, commFunctionSetRTS) err = windows.EscapeCommFunction(port.handle, windows.SETRTS)
} else { } else {
res = escapeCommFunction(port.handle, commFunctionClrRTS) err = windows.EscapeCommFunction(port.handle, windows.CLRRTS)
} }
if !res { if err != nil {
return &PortError{} return &PortError{}
} }
return nil return nil
@@ -339,38 +266,54 @@ func (port *windowsPort) SetRTS(rts bool) error {
// The following seems a more reliable way to do it // The following seems a more reliable way to do it
params := &dcb{} params := &windows.DCB{}
if err := getCommState(port.handle, params); err != nil { if err := windows.GetCommState(port.handle, params); err != nil {
return &PortError{causedBy: err} return &PortError{causedBy: err}
} }
params.Flags &= dcbRTSControlDisbaleMask params.Flags &= dcbRTSControlDisableMask
if rts { if rts {
params.Flags |= dcbRTSControlEnable params.Flags |= windows.RTS_CONTROL_ENABLE
} }
if err := setCommState(port.handle, params); err != nil { if err := windows.SetCommState(port.handle, params); err != nil {
return &PortError{causedBy: err} return &PortError{causedBy: err}
} }
return nil return nil
} }
func (port *windowsPort) GetModemStatusBits() (*ModemStatusBits, error) { 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 var bits uint32
if !getCommModemStatus(port.handle, &bits) { if err := windows.GetCommModemStatus(port.handle, &bits); err != nil {
return nil, &PortError{} return nil, &PortError{}
} }
return &ModemStatusBits{ return &ModemStatusBits{
CTS: (bits & msCTSOn) != 0, CTS: (bits & MS_CTS_ON) != 0,
DCD: (bits & msRLSDOn) != 0, DCD: (bits & MS_RLSD_ON) != 0,
DSR: (bits & msDSROn) != 0, DSR: (bits & MS_DSR_ON) != 0,
RI: (bits & msRingOn) != 0, RI: (bits & MS_RING_ON) != 0,
}, nil }, nil
} }
func (port *windowsPort) SetReadTimeout(timeout time.Duration) error { func (port *windowsPort) SetReadTimeout(timeout time.Duration) error {
commTimeouts := &commTimeouts{ // 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, ReadIntervalTimeout: 0xFFFFFFFF,
ReadTotalTimeoutMultiplier: 0xFFFFFFFF, ReadTotalTimeoutMultiplier: 0xFFFFFFFF,
ReadTotalTimeoutConstant: 0xFFFFFFFE, ReadTotalTimeoutConstant: MaxReadTotalTimeoutConstant,
WriteTotalTimeoutConstant: 0, WriteTotalTimeoutConstant: 0,
WriteTotalTimeoutMultiplier: 0, WriteTotalTimeoutMultiplier: 0,
} }
@@ -379,53 +322,64 @@ func (port *windowsPort) SetReadTimeout(timeout time.Duration) error {
if ms > 0xFFFFFFFE || ms < 0 { if ms > 0xFFFFFFFE || ms < 0 {
return &PortError{code: InvalidTimeoutValue} return &PortError{code: InvalidTimeoutValue}
} }
if ms > MaxReadTotalTimeoutConstant {
ms = MaxReadTotalTimeoutConstant
}
commTimeouts.ReadTotalTimeoutConstant = uint32(ms) commTimeouts.ReadTotalTimeoutConstant = uint32(ms)
} }
if err := setCommTimeouts(port.handle, commTimeouts); err != nil { port.mu.Lock()
defer port.mu.Unlock()
if err := windows.SetCommTimeouts(port.handle, commTimeouts); err != nil {
return &PortError{code: InvalidTimeoutValue, causedBy: err} return &PortError{code: InvalidTimeoutValue, causedBy: err}
} }
port.hasTimeout = (timeout != NoTimeout)
return nil return nil
} }
func (port *windowsPort) Break(d time.Duration) error { func (port *windowsPort) Break(d time.Duration) error {
if err := setCommBreak(port.handle); err != nil { if err := windows.SetCommBreak(port.handle); err != nil {
return &PortError{causedBy: err} return &PortError{causedBy: err}
} }
time.Sleep(d) time.Sleep(d)
if err := clearCommBreak(port.handle); err != nil { if err := windows.ClearCommBreak(port.handle); err != nil {
return &PortError{causedBy: err} return &PortError{causedBy: err}
} }
return nil return nil
} }
func createOverlappedEvent() (*syscall.Overlapped, error) { func createOverlappedEvent() (*windows.Overlapped, error) {
h, err := createEvent(nil, true, false, nil) h, err := windows.CreateEvent(nil, 1, 0, nil)
return &syscall.Overlapped{HEvent: h}, err return &windows.Overlapped{HEvent: h}, err
} }
func nativeOpen(portName string, mode *Mode) (*windowsPort, error) { func nativeOpen(portName string, mode *Mode) (*windowsPort, error) {
portName = "\\\\.\\" + portName if !strings.HasPrefix(portName, `\\.\`) {
path, err := syscall.UTF16PtrFromString(portName) portName = `\\.\` + portName
}
path, err := windows.UTF16PtrFromString(portName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
handle, err := syscall.CreateFile( handle, err := windows.CreateFile(
path, path,
syscall.GENERIC_READ|syscall.GENERIC_WRITE, windows.GENERIC_READ|windows.GENERIC_WRITE,
0, nil, 0, nil,
syscall.OPEN_EXISTING, windows.OPEN_EXISTING,
syscall.FILE_FLAG_OVERLAPPED, windows.FILE_FLAG_OVERLAPPED,
0) 0,
)
if err != nil { if err != nil {
switch err { switch err {
case syscall.ERROR_ACCESS_DENIED: case windows.ERROR_ACCESS_DENIED:
return nil, &PortError{code: PortBusy} return nil, &PortError{code: PortBusy}
case syscall.ERROR_FILE_NOT_FOUND: case windows.ERROR_FILE_NOT_FOUND:
return nil, &PortError{code: PortNotFound} return nil, &PortError{code: PortNotFound}
} }
return nil, err return nil, err
@@ -436,23 +390,23 @@ func nativeOpen(portName string, mode *Mode) (*windowsPort, error) {
} }
// Set port parameters // Set port parameters
params := &dcb{} params := &windows.DCB{}
if getCommState(port.handle, params) != nil { if windows.GetCommState(port.handle, params) != nil {
port.Close() port.Close()
return nil, &PortError{code: InvalidSerialPort} return nil, &PortError{code: InvalidSerialPort}
} }
port.setModeParams(mode, params) port.setModeParams(mode, params)
params.Flags &= dcbDTRControlDisableMask params.Flags &= dcbDTRControlDisableMask
params.Flags &= dcbRTSControlDisbaleMask params.Flags &= dcbRTSControlDisableMask
if mode.InitialStatusBits == nil { if mode.InitialStatusBits == nil {
params.Flags |= dcbDTRControlEnable params.Flags |= windows.DTR_CONTROL_ENABLE
params.Flags |= dcbRTSControlEnable params.Flags |= windows.RTS_CONTROL_ENABLE
} else { } else {
if mode.InitialStatusBits.DTR { if mode.InitialStatusBits.DTR {
params.Flags |= dcbDTRControlEnable params.Flags |= windows.DTR_CONTROL_ENABLE
} }
if mode.InitialStatusBits.RTS { if mode.InitialStatusBits.RTS {
params.Flags |= dcbRTSControlEnable params.Flags |= windows.RTS_CONTROL_ENABLE
} }
} }
params.Flags &^= dcbOutXCTSFlow params.Flags &^= dcbOutXCTSFlow
@@ -468,7 +422,7 @@ func nativeOpen(portName string, mode *Mode) (*windowsPort, error) {
params.XoffLim = 512 params.XoffLim = 512
params.XonChar = 17 // DC1 params.XonChar = 17 // DC1
params.XoffChar = 19 // C3 params.XoffChar = 19 // C3
if setCommState(port.handle, params) != nil { if windows.SetCommState(port.handle, params) != nil {
port.Close() port.Close()
return nil, &PortError{code: InvalidSerialPort} return nil, &PortError{code: InvalidSerialPort}
} }

View File

@@ -1,31 +0,0 @@
//
// Copyright 2014-2023 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
//sys regEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, class *uint16, value *uint16, valueLen *uint32) (regerrno error) = advapi32.RegEnumValueW
//sys getCommState(handle syscall.Handle, dcb *dcb) (err error) = GetCommState
//sys setCommState(handle syscall.Handle, dcb *dcb) (err error) = SetCommState
//sys setCommTimeouts(handle syscall.Handle, timeouts *commTimeouts) (err error) = SetCommTimeouts
//sys escapeCommFunction(handle syscall.Handle, function uint32) (res bool) = EscapeCommFunction
//sys getCommModemStatus(handle syscall.Handle, bits *uint32) (res bool) = GetCommModemStatus
//sys createEvent(eventAttributes *uint32, manualReset bool, initialState bool, name *uint16) (handle syscall.Handle, err error) = CreateEventW
//sys resetEvent(handle syscall.Handle) (err error) = ResetEvent
//sys getOverlappedResult(handle syscall.Handle, overlapEvent *syscall.Overlapped, n *uint32, wait bool) (err error) = GetOverlappedResult
//sys purgeComm(handle syscall.Handle, flags uint32) (err error) = PurgeComm
//sys setCommBreak(handle syscall.Handle) (err error) = SetCommBreak
//sys clearCommBreak(handle syscall.Handle) (err error) = ClearCommBreak

83
test/deadlock/main.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import (
"fmt"
"log"
"time"
"go.bug.st/serial"
)
func main() {
fmt.Println("Testing serial port read/write/close operations...")
// Test case: Opening a non-existent port (should fail gracefully)
_, err := serial.Open("/dev/nonexistent", &serial.Mode{BaudRate: 9600})
if err != nil {
fmt.Printf("Expected error when opening non-existent port: %v\n", err)
} else {
log.Fatal("Expected error when opening non-existent port, but it succeeded")
}
// Find available serial ports
ports, err := serial.GetPortsList()
if err != nil {
log.Fatal(err)
}
if len(ports) > 0 {
fmt.Printf("Found %d available serial port(s)\n", len(ports))
for _, portName := range ports {
fmt.Printf("- %s\n", portName)
}
} else {
fmt.Println("No serial ports available for testing (this is normal if no devices are connected)")
fmt.Println("The fix has been implemented and should resolve the deadlock issue")
return
}
// Test with the first available port
testPort := ports[0]
fmt.Printf("\nTesting port: %s\n", testPort)
// Test 1: Basic read timeout and close without data
mode := &serial.Mode{BaudRate: 9600}
port, err := serial.Open(testPort, mode)
if err != nil {
fmt.Printf("Could not open port %s: %v\n", testPort, err)
fmt.Println("The fix has been implemented and should resolve the deadlock issue")
return
}
// Set a short read timeout
if err := port.SetReadTimeout(200 * time.Millisecond); err != nil {
log.Fatal(err)
}
// Test concurrent read and close (this would deadlock before the fix)
chErr := make(chan error)
go func() {
buf := make([]byte, 1)
fmt.Printf("Reading from port %s with %v timeout...\n", testPort, 200*time.Millisecond)
n, err := port.Read(buf)
if err != nil {
fmt.Printf("Read error: %v\n", err)
} else if n == 0 {
fmt.Println("Read timeout (expected)")
} else {
fmt.Printf("Read %d byte: %v\n", n, buf)
}
chErr <- err
}()
// Wait a little, then close the port
time.Sleep(100 * time.Millisecond)
fmt.Println("Closing port...")
if err := port.Close(); err != nil {
log.Fatal(err)
}
// Wait for read goroutine to complete
<-chErr
fmt.Println("Test passed: No deadlock occurred!")
}

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //
@@ -40,7 +40,7 @@ func (p *Pipe) ReadFD() int {
return p.rd return p.rd
} }
// WriteFD returns the flie handle for the write side of the pipe. // WriteFD returns the file handle for the write side of the pipe.
func (p *Pipe) WriteFD() int { func (p *Pipe) WriteFD() int {
if !p.opened { if !p.opened {
return -1 return -1
@@ -48,7 +48,7 @@ func (p *Pipe) WriteFD() int {
return p.wr return p.wr
} }
// Write to the pipe the content of data. Returns the numbre of bytes written. // Write to the pipe the content of data. Returns the number of bytes written.
func (p *Pipe) Write(data []byte) (int, error) { func (p *Pipe) Write(data []byte) (int, error) {
if !p.opened { if !p.opened {
return 0, fmt.Errorf("Pipe not opened") return 0, fmt.Errorf("Pipe not opened")

View File

@@ -1,5 +1,5 @@
// //
// Copyright 2014-2023 Cristian Maglie. All rights reserved. // Copyright 2014-2024 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// //

View File

@@ -1,161 +0,0 @@
// Code generated by 'go generate'; DO NOT EDIT.
package serial
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)
errERROR_EINVAL error = syscall.EINVAL
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
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 (
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procRegEnumValueW = modadvapi32.NewProc("RegEnumValueW")
procClearCommBreak = modkernel32.NewProc("ClearCommBreak")
procCreateEventW = modkernel32.NewProc("CreateEventW")
procEscapeCommFunction = modkernel32.NewProc("EscapeCommFunction")
procGetCommModemStatus = modkernel32.NewProc("GetCommModemStatus")
procGetCommState = modkernel32.NewProc("GetCommState")
procGetOverlappedResult = modkernel32.NewProc("GetOverlappedResult")
procPurgeComm = modkernel32.NewProc("PurgeComm")
procResetEvent = modkernel32.NewProc("ResetEvent")
procSetCommBreak = modkernel32.NewProc("SetCommBreak")
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 clearCommBreak(handle syscall.Handle) (err error) {
r1, _, e1 := syscall.Syscall(procClearCommBreak.Addr(), 1, uintptr(handle), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func createEvent(eventAttributes *uint32, manualReset bool, initialState bool, name *uint16) (handle syscall.Handle, err error) {
var _p0 uint32
if manualReset {
_p0 = 1
}
var _p1 uint32
if initialState {
_p1 = 1
}
r0, _, e1 := syscall.Syscall6(procCreateEventW.Addr(), 4, uintptr(unsafe.Pointer(eventAttributes)), uintptr(_p0), uintptr(_p1), uintptr(unsafe.Pointer(name)), 0, 0)
handle = syscall.Handle(r0)
if handle == 0 {
err = errnoErr(e1)
}
return
}
func escapeCommFunction(handle syscall.Handle, function uint32) (res bool) {
r0, _, _ := syscall.Syscall(procEscapeCommFunction.Addr(), 2, uintptr(handle), uintptr(function), 0)
res = r0 != 0
return
}
func getCommModemStatus(handle syscall.Handle, bits *uint32) (res bool) {
r0, _, _ := syscall.Syscall(procGetCommModemStatus.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(bits)), 0)
res = r0 != 0
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 {
err = errnoErr(e1)
}
return
}
func getOverlappedResult(handle syscall.Handle, overlapEvent *syscall.Overlapped, n *uint32, wait bool) (err error) {
var _p0 uint32
if wait {
_p0 = 1
}
r1, _, e1 := syscall.Syscall6(procGetOverlappedResult.Addr(), 4, uintptr(handle), uintptr(unsafe.Pointer(overlapEvent)), uintptr(unsafe.Pointer(n)), uintptr(_p0), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func purgeComm(handle syscall.Handle, flags uint32) (err error) {
r1, _, e1 := syscall.Syscall(procPurgeComm.Addr(), 2, uintptr(handle), uintptr(flags), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func resetEvent(handle syscall.Handle) (err error) {
r1, _, e1 := syscall.Syscall(procResetEvent.Addr(), 1, uintptr(handle), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func setCommBreak(handle syscall.Handle) (err error) {
r1, _, e1 := syscall.Syscall(procSetCommBreak.Addr(), 1, uintptr(handle), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
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 {
err = errnoErr(e1)
}
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 {
err = errnoErr(e1)
}
return
}