Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions serial_setserial_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//go:build linux
// +build linux

package serial

import (
"fmt"
"os"
"syscall"
"unsafe"

"golang.org/x/sys/unix"
)

// CSerialStruct is a C-interop struct for linux/serial.h: struct serial_struct
// Field order and types must exactly match the C definition for binary compatibility.
// See: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/serial.h#L135
type CSerialStruct struct {
Type int32
Line int32
Port uint32
IRQ int32
Flags int32
XmitFifoSize int32
CustomDivisor int32
BaudBase int32
CloseDelay uint16
IOType byte
ReservedChar byte
Hub6 int32
ClosingWait uint16
ClosingWait2 uint16
IOMemBase uintptr
IOMemRegShift uint16
PortHigh uint32
// IOMapBase: In C, this is 'unsigned long'. Use uint64 for 64-bit systems.
// For 32-bit systems, you may need to use uint32 for binary compatibility.
IOMapBase uint64
}

// GetSerialStruct opens the device and retrieves the CSerialStruct using TIOCGSERIAL ioctl.
func GetSerialStruct(device string) (*CSerialStruct, error) {
f, err := os.OpenFile(device, os.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0666)
if err != nil {
return nil, fmt.Errorf("failed to open device: %w", err)
}
defer f.Close()

var ser CSerialStruct
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.TIOCGSERIAL, uintptr(unsafe.Pointer(&ser)))
if errno != 0 {
return nil, fmt.Errorf("ioctl TIOCGSERIAL failed: %v", errno)
}
return &ser, nil
}

// SetSerialPortMode sets the port mode using ioctl TIOCSSERIAL
func SetSerialPortMode(device string, portMode uint32) error {
f, err := os.OpenFile(device, os.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0666)
if err != nil {
return fmt.Errorf("failed to open device: %w", err)
}
defer f.Close()

var ser CSerialStruct
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.TIOCGSERIAL, uintptr(unsafe.Pointer(&ser)))
if errno != 0 {
return fmt.Errorf("ioctl TIOCGSERIAL failed: %v", errno)
}

ser.Port = portMode

_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.TIOCSSERIAL, uintptr(unsafe.Pointer(&ser)))
if errno != 0 {
return fmt.Errorf("ioctl TIOCSSERIAL failed: %v", errno)
}
return nil
}
52 changes: 52 additions & 0 deletions serial_setserial_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//go:build linux
// +build linux

package serial

import (
"context"
"os/exec"
"testing"

"github.com/stretchr/testify/require"
)

func startSocatAndWaitForSetserialTest(t *testing.T, ctx context.Context) *exec.Cmd {
cmd := exec.CommandContext(ctx, "socat", "-D", "STDIO", "pty,link=/tmp/faketty_setserial")
r, err := cmd.StderrPipe()
require.NoError(t, err)
require.NoError(t, cmd.Start())
buf := make([]byte, 1024)
_, err = r.Read(buf)
require.NoError(t, err)
return cmd
}

func TestGetSerialStructOnFakeTty(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := startSocatAndWaitForSetserialTest(t, ctx)
go cmd.Wait()

ser, err := GetSerialStruct("/tmp/faketty_setserial")
// Note: socat's virtual TTY may not fully support TIOCGSERIAL.
// If not supported, skip the test (environment dependent).
if err != nil {
t.Skipf("ioctl TIOCGSERIAL not supported on socat pty: %v", err)
}
require.NotNil(t, ser)
}

func TestSetSerialPortModeOnFakeTty(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := startSocatAndWaitForSetserialTest(t, ctx)
go cmd.Wait()

err := SetSerialPortMode("/tmp/faketty_setserial", 0)
// Note: socat's virtual TTY may not fully support TIOCSSERIAL.
// If not supported, skip the test (environment dependent).
if err != nil {
t.Skipf("ioctl TIOCSSERIAL not supported on socat pty: %v", err)
}
}