Files
picoclaw/pkg/tools/hardware/serial_unix.go
T

287 lines
5.4 KiB
Go

//go:build linux || darwin
package hardwaretools
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"time"
"golang.org/x/sys/unix"
)
var (
unixSerialNow = time.Now
unixSerialOpenPort = openAndConfigureSerialPort
unixSerialClosePort = unix.Close
unixSerialPollRead = pollRead
unixSerialPollWrite = pollWrite
)
func serialListPorts() ([]serialPortInfo, error) {
patterns := []string{
"/dev/ttyS*",
"/dev/ttyUSB*",
"/dev/ttyACM*",
"/dev/ttyAMA*",
"/dev/rfcomm*",
"/dev/tty.*",
"/dev/cu.*",
}
seen := make(map[string]struct{})
ports := make([]serialPortInfo, 0)
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
for _, match := range matches {
if _, ok := seen[match]; ok {
continue
}
info, err := os.Stat(match)
if err != nil || info.IsDir() {
continue
}
seen[match] = struct{}{}
ports = append(ports, serialPortInfo{
Name: filepath.Base(match),
Path: match,
})
}
}
sort.Slice(ports, func(i, j int) bool {
return ports[i].Path < ports[j].Path
})
return ports, nil
}
func serialRead(ctx context.Context, cfg serialConfig, length int, timeout time.Duration) ([]byte, error) {
if err := serialContextErr(ctx); err != nil {
return nil, err
}
fd, err := unixSerialOpenPort(cfg)
if err != nil {
return nil, err
}
defer unixSerialClosePort(fd)
buf := make([]byte, length)
total := 0
deadline := unixSerialNow().Add(timeout)
for total < length {
if err := serialContextErr(ctx); err != nil {
return nil, err
}
remaining := deadline.Sub(unixSerialNow())
if remaining <= 0 {
break
}
n, err := unixSerialPollRead(fd, buf[total:], minSerialPollTimeout(remaining))
if err != nil {
return nil, err
}
if n == 0 {
continue
}
total += n
}
return buf[:total], nil
}
func serialWrite(ctx context.Context, cfg serialConfig, data []byte, timeout time.Duration) (int, error) {
if err := serialContextErr(ctx); err != nil {
return 0, err
}
fd, err := unixSerialOpenPort(cfg)
if err != nil {
return 0, err
}
defer unixSerialClosePort(fd)
total := 0
deadline := unixSerialNow().Add(timeout)
for total < len(data) {
if err := serialContextErr(ctx); err != nil {
return total, err
}
remaining := deadline.Sub(unixSerialNow())
if remaining <= 0 {
return total, fmt.Errorf("timeout while writing serial data")
}
n, err := unixSerialPollWrite(fd, data[total:], minSerialPollTimeout(remaining))
if err != nil {
return total, err
}
if n == 0 {
continue
}
total += n
}
return total, nil
}
func openAndConfigureSerialPort(cfg serialConfig) (int, error) {
fd, err := unix.Open(cfg.Port, unix.O_RDWR|unix.O_NOCTTY|unix.O_NONBLOCK, 0)
if err != nil {
return -1, err
}
if err := unix.SetNonblock(fd, false); err != nil {
unix.Close(fd)
return -1, err
}
if err := configureUnixSerialPort(fd, cfg); err != nil {
unix.Close(fd)
return -1, err
}
return fd, nil
}
func configureUnixSerialPort(fd int, cfg serialConfig) error {
tio, err := serialGetTermios(fd)
if err != nil {
return err
}
tio.Iflag = 0
tio.Oflag = 0
tio.Lflag = 0
tio.Cflag = unix.CREAD | unix.CLOCAL
tio.Cc[unix.VMIN] = 0
tio.Cc[unix.VTIME] = 0
switch cfg.DataBits {
case 5:
tio.Cflag |= unix.CS5
case 6:
tio.Cflag |= unix.CS6
case 7:
tio.Cflag |= unix.CS7
default:
tio.Cflag |= unix.CS8
}
switch cfg.Parity {
case "even":
tio.Cflag |= unix.PARENB
case "odd":
tio.Cflag |= unix.PARENB | unix.PARODD
}
if cfg.StopBits == 2 {
tio.Cflag |= unix.CSTOPB
}
speed, err := serialBaudToUnix(cfg.Baud)
if err != nil {
return err
}
if err := serialSetSpeed(tio, speed); err != nil {
return err
}
return serialSetTermios(fd, tio)
}
func serialBaudToUnix(baud int) (uint32, error) {
switch baud {
case 50:
return unix.B50, nil
case 75:
return unix.B75, nil
case 110:
return unix.B110, nil
case 134:
return unix.B134, nil
case 150:
return unix.B150, nil
case 200:
return unix.B200, nil
case 300:
return unix.B300, nil
case 600:
return unix.B600, nil
case 1200:
return unix.B1200, nil
case 1800:
return unix.B1800, nil
case 2400:
return unix.B2400, nil
case 4800:
return unix.B4800, nil
case 9600:
return unix.B9600, nil
case 19200:
return unix.B19200, nil
case 38400:
return unix.B38400, nil
case 57600:
return unix.B57600, nil
case 115200:
return unix.B115200, nil
case 230400:
return unix.B230400, nil
default:
return 0, fmt.Errorf("unsupported baud rate on this platform: %d", baud)
}
}
func pollRead(fd int, dst []byte, timeout time.Duration) (int, error) {
pfd := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
n, err := unix.Poll(pfd, durationToPollTimeout(timeout))
if err != nil {
return 0, err
}
if n == 0 {
return 0, nil
}
return unix.Read(fd, dst)
}
func pollWrite(fd int, src []byte, timeout time.Duration) (int, error) {
pfd := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLOUT}}
n, err := unix.Poll(pfd, durationToPollTimeout(timeout))
if err != nil {
return 0, err
}
if n == 0 {
return 0, nil
}
return unix.Write(fd, src)
}
func durationToPollTimeout(timeout time.Duration) int {
if timeout <= 0 {
return 0
}
ms := int(timeout / time.Millisecond)
if ms == 0 {
return 1
}
return ms
}
func minSerialPollTimeout(timeout time.Duration) time.Duration {
if timeout > serialPollInterval {
return serialPollInterval
}
return timeout
}