This is an automated email from the ASF dual-hosted git repository. sruehl pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit 2a52d8f66871efb654e8c277c5b5d7e30b410e0d Author: Sebastian Rühl <[email protected]> AuthorDate: Mon Jul 6 13:45:58 2026 +0200 feat(plc4go): complete serialport feature set (ControlPort, ListPorts) Phase 2 of the internalized serial-port package: a ControlPort interface (implemented by every Port that Open returns) adds modem control lines (DTR/RTS set, CTS/DSR/DCD/RI read), timed break, flush/drain, and runtime reconfiguration via SetConfig; Config gains XON/XOFF software flow control and mark/space parity (Linux/Windows; clean error elsewhere); ListPorts enumerates ports on Linux/macOS/FreeBSD/Windows. Same testing discipline as phase 1: portable spec-pinned mappings for all Windows DCB/escape/modem constants, thin per-OS shims, PTY integration tests for everything the pty driver supports, race-checked, cross-compiled for six GOOS targets. --- plc4go/spi/transports/serial/serialport/dcb.go | 35 +- .../spi/transports/serial/serialport/dcb_test.go | 46 ++ .../transports/serial/serialport/ioflush_bsd.go | 52 ++ .../transports/serial/serialport/ioflush_linux.go | 47 ++ .../transports/serial/serialport/list_darwin.go | 38 ++ .../transports/serial/serialport/list_freebsd.go | 46 ++ .../spi/transports/serial/serialport/list_linux.go | 43 ++ .../serial/serialport/list_linux_test.go | 47 ++ .../serial/serialport/list_unsupported.go | 31 + .../transports/serial/serialport/list_windows.go | 56 ++ .../spi/transports/serial/serialport/mocks_test.go | 682 +++++++++++++++++++++ .../spi/transports/serial/serialport/parity_bsd.go | 33 + .../transports/serial/serialport/parity_linux.go | 35 ++ .../spi/transports/serial/serialport/port_unix.go | 92 ++- .../transports/serial/serialport/port_windows.go | 110 +++- .../transports/serial/serialport/pty_linux_test.go | 111 ++++ .../spi/transports/serial/serialport/serialport.go | 69 ++- .../serial/serialport/serialport_test.go | 26 + .../serial/serialport/termios_bsd_test.go | 35 ++ .../serial/serialport/termios_linux_test.go | 40 ++ .../transports/serial/serialport/termios_unix.go | 14 + .../serial/serialport/termios_unix_test.go | 10 + plc4go/spi/transports/serial/serialport/winctl.go | 62 ++ .../transports/serial/serialport/winctl_test.go | 47 ++ 24 files changed, 1793 insertions(+), 14 deletions(-) diff --git a/plc4go/spi/transports/serial/serialport/dcb.go b/plc4go/spi/transports/serial/serialport/dcb.go index f507101cea..211883bb3a 100644 --- a/plc4go/spi/transports/serial/serialport/dcb.go +++ b/plc4go/spi/transports/serial/serialport/dcb.go @@ -23,9 +23,11 @@ package serialport // types) so the mapping is unit-testable on every development platform. // Values per Microsoft's documentation of the DCB structure (winbase.h). const ( - dcbNoParity = 0 // NOPARITY - dcbOddParity = 1 // ODDPARITY - dcbEvenParity = 2 // EVENPARITY + dcbNoParity = 0 // NOPARITY + dcbOddParity = 1 // ODDPARITY + dcbEvenParity = 2 // EVENPARITY + dcbMarkParity = 3 // MARKPARITY + dcbSpaceParity = 4 // SPACEPARITY dcbOneStopBit = 0 // ONESTOPBIT dcbOne5StopBits = 1 // ONE5STOPBITS @@ -38,6 +40,8 @@ const ( dcbFlagDtrControlEnable = 0x00000010 // fDtrControl = DTR_CONTROL_ENABLE (1 << 4) dcbFlagRtsControlEnable = 0x00001000 // fRtsControl = RTS_CONTROL_ENABLE (1 << 12) dcbFlagRtsControlHandshake = 0x00002000 // fRtsControl = RTS_CONTROL_HANDSHAKE (2 << 12) + dcbFlagOutX = 0x00000100 // fOutX: honor XOFF/XON received from the device + dcbFlagInX = 0x00000200 // fInX: send XOFF/XON when the RX buffer fills/drains ) // dcbSettings carries the computed DCB field values; port_windows.go copies @@ -48,6 +52,10 @@ type dcbSettings struct { Parity uint8 StopBits uint8 Flags uint32 + XonLim uint16 + XoffLim uint16 + XonChar byte + XoffChar byte } // makeDCBSettings translates an already-normalized Config into DCB field @@ -66,6 +74,12 @@ func makeDCBSettings(cfg Config) dcbSettings { case ParityEven: s.Parity = dcbEvenParity s.Flags |= dcbFlagParity + case ParityMark: + s.Parity = dcbMarkParity + s.Flags |= dcbFlagParity + case ParitySpace: + s.Parity = dcbSpaceParity + s.Flags |= dcbFlagParity default: s.Parity = dcbNoParity } @@ -82,5 +96,20 @@ func makeDCBSettings(cfg Config) dcbSettings { } else { s.Flags |= dcbFlagRtsControlEnable } + if cfg.XONXOFFFlowControl { + s.Flags |= dcbFlagOutX | dcbFlagInX + } + // XonChar/XoffChar/XonLim/XoffLim are set unconditionally, even when + // XON/XOFF flow control is disabled (fOutX/fInX clear above). Some + // Windows serial drivers reject SetCommState (error 87, "the parameter + // is incorrect") when XonChar == XoffChar == 0x00, which is what a + // zero-value DCB carries — purejavacomm hit this in the field on a + // plain 8N1 configuration. DC1/DC3 are the conventional XON/XOFF + // characters and are harmless when the flags are off. + s.XonChar = 0x11 // DC1 + s.XoffChar = 0x13 // DC3 + // Conventional buffer thresholds for a 4096-byte queue (SetupComm). + s.XonLim = 2048 + s.XoffLim = 512 return s } diff --git a/plc4go/spi/transports/serial/serialport/dcb_test.go b/plc4go/spi/transports/serial/serialport/dcb_test.go index e601aea3a1..520070a57d 100644 --- a/plc4go/spi/transports/serial/serialport/dcb_test.go +++ b/plc4go/spi/transports/serial/serialport/dcb_test.go @@ -47,6 +47,10 @@ func TestMakeDCBSettings(t *testing.T) { Parity: 0, // NOPARITY StopBits: 0, // ONESTOPBIT Flags: 0x1 | 0x10 | 0x1000, // fBinary | DTR_CONTROL_ENABLE | RTS_CONTROL_ENABLE + // XonChar/XoffChar/XonLim/XoffLim are always populated, even + // with XON/XOFF flow control disabled — see makeDCBSettings. + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, }, }, { @@ -58,6 +62,8 @@ func TestMakeDCBSettings(t *testing.T) { Parity: 2, // EVENPARITY StopBits: 2, // TWOSTOPBITS Flags: 0x1 | 0x2 | 0x10 | 0x1000, // ... | fParity | ... + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, }, }, { @@ -69,6 +75,8 @@ func TestMakeDCBSettings(t *testing.T) { Parity: 1, // ODDPARITY StopBits: 0, Flags: 0x1 | 0x2 | 0x10 | 0x1000, + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, }, }, { @@ -80,6 +88,8 @@ func TestMakeDCBSettings(t *testing.T) { Parity: 0, StopBits: 1, // ONE5STOPBITS Flags: 0x1 | 0x10 | 0x1000, + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, }, }, { @@ -91,6 +101,42 @@ func TestMakeDCBSettings(t *testing.T) { Parity: 0, StopBits: 0, Flags: 0x1 | 0x4 | 0x10 | 0x2000, // fBinary | fOutxCtsFlow | DTR_CONTROL_ENABLE | RTS_CONTROL_HANDSHAKE + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, + }, + }, + { + name: "mark parity", + cfg: Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParityMark}, + want: dcbSettings{ + BaudRate: 9600, ByteSize: 8, + Parity: 3, // MARKPARITY + StopBits: 0, + Flags: 0x1 | 0x2 | 0x10 | 0x1000, + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, + }, + }, + { + name: "space parity", + cfg: Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParitySpace}, + want: dcbSettings{ + BaudRate: 9600, ByteSize: 8, + Parity: 4, // SPACEPARITY + StopBits: 0, + Flags: 0x1 | 0x2 | 0x10 | 0x1000, + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, + }, + }, + { + name: "xon/xoff flow control sets fOutX and fInX with DC1/DC3 chars", + cfg: Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParityNone, XONXOFFFlowControl: true}, + want: dcbSettings{ + BaudRate: 9600, ByteSize: 8, Parity: 0, StopBits: 0, + Flags: 0x1 | 0x10 | 0x100 | 0x200 | 0x1000, // fBinary | DTR_CONTROL_ENABLE | fOutX | fInX | RTS_CONTROL_ENABLE + XonChar: 0x11, XoffChar: 0x13, + XonLim: 2048, XoffLim: 512, }, }, } diff --git a/plc4go/spi/transports/serial/serialport/ioflush_bsd.go b/plc4go/spi/transports/serial/serialport/ioflush_bsd.go new file mode 100644 index 0000000000..3471a5efaa --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/ioflush_bsd.go @@ -0,0 +1,52 @@ +//go:build darwin || freebsd + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// Classic BSD sys/file.h direction flags for TIOCFLUSH; not in x/sys. +const ( + bsdFREAD = 0x1 + bsdFWRITE = 0x2 +) + +func (p *unixPort) FlushInput() error { + return p.control(func(fd int) error { + return os.NewSyscallError("ioctl TIOCFLUSH", unix.IoctlSetPointerInt(fd, unix.TIOCFLUSH, bsdFREAD)) + }) +} + +func (p *unixPort) FlushOutput() error { + return p.control(func(fd int) error { + return os.NewSyscallError("ioctl TIOCFLUSH", unix.IoctlSetPointerInt(fd, unix.TIOCFLUSH, bsdFWRITE)) + }) +} + +func (p *unixPort) Drain() error { + return p.control(func(fd int) error { + return os.NewSyscallError("ioctl TIOCDRAIN", unix.IoctlSetInt(fd, unix.TIOCDRAIN, 0)) + }) +} diff --git a/plc4go/spi/transports/serial/serialport/ioflush_linux.go b/plc4go/spi/transports/serial/serialport/ioflush_linux.go new file mode 100644 index 0000000000..21183096dd --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/ioflush_linux.go @@ -0,0 +1,47 @@ +//go:build linux + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func (p *unixPort) FlushInput() error { + return p.control(func(fd int) error { + return os.NewSyscallError("ioctl TCFLSH", unix.IoctlSetInt(fd, unix.TCFLSH, unix.TCIFLUSH)) + }) +} + +func (p *unixPort) FlushOutput() error { + return p.control(func(fd int) error { + return os.NewSyscallError("ioctl TCFLSH", unix.IoctlSetInt(fd, unix.TCFLSH, unix.TCOFLUSH)) + }) +} + +// Drain: ioctl(TCSBRK, 1) is the kernel implementation of tcdrain(3). +func (p *unixPort) Drain() error { + return p.control(func(fd int) error { + return os.NewSyscallError("ioctl TCSBRK", unix.IoctlSetInt(fd, unix.TCSBRK, 1)) + }) +} diff --git a/plc4go/spi/transports/serial/serialport/list_darwin.go b/plc4go/spi/transports/serial/serialport/list_darwin.go new file mode 100644 index 0000000000..3c14d372ca --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/list_darwin.go @@ -0,0 +1,38 @@ +//go:build darwin + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "path/filepath" + "sort" +) + +// listPorts returns the callout devices; /dev/cu.* is the conventional +// non-blocking-open device class for serial adapters on Darwin. +func listPorts() ([]string, error) { + ports, err := filepath.Glob("/dev/cu.*") + if err != nil { + return nil, err + } + sort.Strings(ports) + return ports, nil +} diff --git a/plc4go/spi/transports/serial/serialport/list_freebsd.go b/plc4go/spi/transports/serial/serialport/list_freebsd.go new file mode 100644 index 0000000000..be53a1fb5a --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/list_freebsd.go @@ -0,0 +1,46 @@ +//go:build freebsd + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "path/filepath" + "sort" + "strings" +) + +// listPorts returns callout devices (cuau* for onboard UARTs, cuaU* for +// USB adapters), excluding the .init/.lock control nodes. +func listPorts() ([]string, error) { + matches, err := filepath.Glob("/dev/cua[uU]*") + if err != nil { + return nil, err + } + ports := make([]string, 0, len(matches)) + for _, m := range matches { + if strings.HasSuffix(m, ".init") || strings.HasSuffix(m, ".lock") { + continue + } + ports = append(ports, m) + } + sort.Strings(ports) + return ports, nil +} diff --git a/plc4go/spi/transports/serial/serialport/list_linux.go b/plc4go/spi/transports/serial/serialport/list_linux.go new file mode 100644 index 0000000000..482e59f99e --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/list_linux.go @@ -0,0 +1,43 @@ +//go:build linux + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "path/filepath" + "sort" +) + +// listPorts enumerates /sys/class/tty: entries with a device/ link are +// backed by real hardware (or USB adapters); bare line disciplines and +// virtual consoles have none. +func listPorts() ([]string, error) { + matches, err := filepath.Glob("/sys/class/tty/*/device") + if err != nil { + return nil, err + } + ports := make([]string, 0, len(matches)) + for _, m := range matches { + ports = append(ports, "/dev/"+filepath.Base(filepath.Dir(m))) + } + sort.Strings(ports) + return ports, nil +} diff --git a/plc4go/spi/transports/serial/serialport/list_linux_test.go b/plc4go/spi/transports/serial/serialport/list_linux_test.go new file mode 100644 index 0000000000..f86a2c6a57 --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/list_linux_test.go @@ -0,0 +1,47 @@ +//go:build linux + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListPorts(t *testing.T) { + ports, err := ListPorts() + require.NoError(t, err) + // CI machines may expose zero real UARTs; pin the invariants that hold + // regardless: device paths, no duplicates, sorted output. + for _, p := range ports { + assert.True(t, strings.HasPrefix(p, "/dev/"), "port %q must be a /dev path", p) + } + assert.True(t, sort.StringsAreSorted(ports), "ports must be sorted") + seen := map[string]bool{} + for _, p := range ports { + assert.False(t, seen[p], "duplicate %q", p) + seen[p] = true + } +} diff --git a/plc4go/spi/transports/serial/serialport/list_unsupported.go b/plc4go/spi/transports/serial/serialport/list_unsupported.go new file mode 100644 index 0000000000..8d52c59f68 --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/list_unsupported.go @@ -0,0 +1,31 @@ +//go:build !(linux || darwin || freebsd || windows) + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "fmt" + "runtime" +) + +func listPorts() ([]string, error) { + return nil, fmt.Errorf("serialport: %w: %s", ErrUnsupportedPlatform, runtime.GOOS) +} diff --git a/plc4go/spi/transports/serial/serialport/list_windows.go b/plc4go/spi/transports/serial/serialport/list_windows.go new file mode 100644 index 0000000000..7017733806 --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/list_windows.go @@ -0,0 +1,56 @@ +//go:build windows + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "fmt" + "sort" + + "golang.org/x/sys/windows/registry" +) + +// listPorts reads HKLM\HARDWARE\DEVICEMAP\SERIALCOMM, the canonical +// registry map of active serial devices to COM names. +func listPorts() ([]string, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `HARDWARE\DEVICEMAP\SERIALCOMM`, registry.QUERY_VALUE) + if err == registry.ErrNotExist { + return nil, nil // no serial devices present + } + if err != nil { + return nil, fmt.Errorf("serialport: opening SERIALCOMM registry key: %w", err) + } + defer key.Close() + names, err := key.ReadValueNames(0) + if err != nil { + return nil, fmt.Errorf("serialport: reading SERIALCOMM values: %w", err) + } + ports := make([]string, 0, len(names)) + for _, name := range names { + value, _, err := key.GetStringValue(name) + if err != nil { + return nil, fmt.Errorf("serialport: reading SERIALCOMM value %q: %w", name, err) + } + ports = append(ports, value) + } + sort.Strings(ports) + return ports, nil +} diff --git a/plc4go/spi/transports/serial/serialport/mocks_test.go b/plc4go/spi/transports/serial/serialport/mocks_test.go index 7d0e108f4e..31dc5bd5d8 100644 --- a/plc4go/spi/transports/serial/serialport/mocks_test.go +++ b/plc4go/spi/transports/serial/serialport/mocks_test.go @@ -321,3 +321,685 @@ func (_c *MockPort_Write_Call) RunAndReturn(run func(p []byte) (int, error)) *Mo _c.Call.Return(run) return _c } + +// NewMockControlPort creates a new instance of MockControlPort. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockControlPort(t interface { + mock.TestingT + Cleanup(func()) +}) *MockControlPort { + mock := &MockControlPort{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockControlPort is an autogenerated mock type for the ControlPort type +type MockControlPort struct { + mock.Mock +} + +type MockControlPort_Expecter struct { + mock *mock.Mock +} + +func (_m *MockControlPort) EXPECT() *MockControlPort_Expecter { + return &MockControlPort_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function for the type MockControlPort +func (_mock *MockControlPort) Close() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockControlPort_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockControlPort_Expecter) Close() *MockControlPort_Close_Call { + return &MockControlPort_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockControlPort_Close_Call) Run(run func()) *MockControlPort_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockControlPort_Close_Call) Return(err error) *MockControlPort_Close_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_Close_Call) RunAndReturn(run func() error) *MockControlPort_Close_Call { + _c.Call.Return(run) + return _c +} + +// Drain provides a mock function for the type MockControlPort +func (_mock *MockControlPort) Drain() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Drain") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_Drain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Drain' +type MockControlPort_Drain_Call struct { + *mock.Call +} + +// Drain is a helper method to define mock.On call +func (_e *MockControlPort_Expecter) Drain() *MockControlPort_Drain_Call { + return &MockControlPort_Drain_Call{Call: _e.mock.On("Drain")} +} + +func (_c *MockControlPort_Drain_Call) Run(run func()) *MockControlPort_Drain_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockControlPort_Drain_Call) Return(err error) *MockControlPort_Drain_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_Drain_Call) RunAndReturn(run func() error) *MockControlPort_Drain_Call { + _c.Call.Return(run) + return _c +} + +// FlushInput provides a mock function for the type MockControlPort +func (_mock *MockControlPort) FlushInput() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for FlushInput") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_FlushInput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FlushInput' +type MockControlPort_FlushInput_Call struct { + *mock.Call +} + +// FlushInput is a helper method to define mock.On call +func (_e *MockControlPort_Expecter) FlushInput() *MockControlPort_FlushInput_Call { + return &MockControlPort_FlushInput_Call{Call: _e.mock.On("FlushInput")} +} + +func (_c *MockControlPort_FlushInput_Call) Run(run func()) *MockControlPort_FlushInput_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockControlPort_FlushInput_Call) Return(err error) *MockControlPort_FlushInput_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_FlushInput_Call) RunAndReturn(run func() error) *MockControlPort_FlushInput_Call { + _c.Call.Return(run) + return _c +} + +// FlushOutput provides a mock function for the type MockControlPort +func (_mock *MockControlPort) FlushOutput() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for FlushOutput") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_FlushOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FlushOutput' +type MockControlPort_FlushOutput_Call struct { + *mock.Call +} + +// FlushOutput is a helper method to define mock.On call +func (_e *MockControlPort_Expecter) FlushOutput() *MockControlPort_FlushOutput_Call { + return &MockControlPort_FlushOutput_Call{Call: _e.mock.On("FlushOutput")} +} + +func (_c *MockControlPort_FlushOutput_Call) Run(run func()) *MockControlPort_FlushOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockControlPort_FlushOutput_Call) Return(err error) *MockControlPort_FlushOutput_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_FlushOutput_Call) RunAndReturn(run func() error) *MockControlPort_FlushOutput_Call { + _c.Call.Return(run) + return _c +} + +// ModemStatus provides a mock function for the type MockControlPort +func (_mock *MockControlPort) ModemStatus() (ModemStatus, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for ModemStatus") + } + + var r0 ModemStatus + var r1 error + if returnFunc, ok := ret.Get(0).(func() (ModemStatus, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() ModemStatus); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(ModemStatus) + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockControlPort_ModemStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModemStatus' +type MockControlPort_ModemStatus_Call struct { + *mock.Call +} + +// ModemStatus is a helper method to define mock.On call +func (_e *MockControlPort_Expecter) ModemStatus() *MockControlPort_ModemStatus_Call { + return &MockControlPort_ModemStatus_Call{Call: _e.mock.On("ModemStatus")} +} + +func (_c *MockControlPort_ModemStatus_Call) Run(run func()) *MockControlPort_ModemStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockControlPort_ModemStatus_Call) Return(modemStatus ModemStatus, err error) *MockControlPort_ModemStatus_Call { + _c.Call.Return(modemStatus, err) + return _c +} + +func (_c *MockControlPort_ModemStatus_Call) RunAndReturn(run func() (ModemStatus, error)) *MockControlPort_ModemStatus_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function for the type MockControlPort +func (_mock *MockControlPort) Read(p []byte) (int, error) { + ret := _mock.Called(p) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 int + var r1 error + if returnFunc, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return returnFunc(p) + } + if returnFunc, ok := ret.Get(0).(func([]byte) int); ok { + r0 = returnFunc(p) + } else { + r0 = ret.Get(0).(int) + } + if returnFunc, ok := ret.Get(1).(func([]byte) error); ok { + r1 = returnFunc(p) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockControlPort_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockControlPort_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - p []byte +func (_e *MockControlPort_Expecter) Read(p interface{}) *MockControlPort_Read_Call { + return &MockControlPort_Read_Call{Call: _e.mock.On("Read", p)} +} + +func (_c *MockControlPort_Read_Call) Run(run func(p []byte)) *MockControlPort_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []byte + if args[0] != nil { + arg0 = args[0].([]byte) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_Read_Call) Return(n int, err error) *MockControlPort_Read_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockControlPort_Read_Call) RunAndReturn(run func(p []byte) (int, error)) *MockControlPort_Read_Call { + _c.Call.Return(run) + return _c +} + +// SendBreak provides a mock function for the type MockControlPort +func (_mock *MockControlPort) SendBreak(d time.Duration) error { + ret := _mock.Called(d) + + if len(ret) == 0 { + panic("no return value specified for SendBreak") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(time.Duration) error); ok { + r0 = returnFunc(d) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_SendBreak_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendBreak' +type MockControlPort_SendBreak_Call struct { + *mock.Call +} + +// SendBreak is a helper method to define mock.On call +// - d time.Duration +func (_e *MockControlPort_Expecter) SendBreak(d interface{}) *MockControlPort_SendBreak_Call { + return &MockControlPort_SendBreak_Call{Call: _e.mock.On("SendBreak", d)} +} + +func (_c *MockControlPort_SendBreak_Call) Run(run func(d time.Duration)) *MockControlPort_SendBreak_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 time.Duration + if args[0] != nil { + arg0 = args[0].(time.Duration) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_SendBreak_Call) Return(err error) *MockControlPort_SendBreak_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_SendBreak_Call) RunAndReturn(run func(d time.Duration) error) *MockControlPort_SendBreak_Call { + _c.Call.Return(run) + return _c +} + +// SetConfig provides a mock function for the type MockControlPort +func (_mock *MockControlPort) SetConfig(cfg Config) error { + ret := _mock.Called(cfg) + + if len(ret) == 0 { + panic("no return value specified for SetConfig") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(Config) error); ok { + r0 = returnFunc(cfg) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_SetConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetConfig' +type MockControlPort_SetConfig_Call struct { + *mock.Call +} + +// SetConfig is a helper method to define mock.On call +// - cfg Config +func (_e *MockControlPort_Expecter) SetConfig(cfg interface{}) *MockControlPort_SetConfig_Call { + return &MockControlPort_SetConfig_Call{Call: _e.mock.On("SetConfig", cfg)} +} + +func (_c *MockControlPort_SetConfig_Call) Run(run func(cfg Config)) *MockControlPort_SetConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 Config + if args[0] != nil { + arg0 = args[0].(Config) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_SetConfig_Call) Return(err error) *MockControlPort_SetConfig_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_SetConfig_Call) RunAndReturn(run func(cfg Config) error) *MockControlPort_SetConfig_Call { + _c.Call.Return(run) + return _c +} + +// SetDTR provides a mock function for the type MockControlPort +func (_mock *MockControlPort) SetDTR(assert bool) error { + ret := _mock.Called(assert) + + if len(ret) == 0 { + panic("no return value specified for SetDTR") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(bool) error); ok { + r0 = returnFunc(assert) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_SetDTR_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDTR' +type MockControlPort_SetDTR_Call struct { + *mock.Call +} + +// SetDTR is a helper method to define mock.On call +// - assert bool +func (_e *MockControlPort_Expecter) SetDTR(assert interface{}) *MockControlPort_SetDTR_Call { + return &MockControlPort_SetDTR_Call{Call: _e.mock.On("SetDTR", assert)} +} + +func (_c *MockControlPort_SetDTR_Call) Run(run func(assert bool)) *MockControlPort_SetDTR_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 bool + if args[0] != nil { + arg0 = args[0].(bool) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_SetDTR_Call) Return(err error) *MockControlPort_SetDTR_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_SetDTR_Call) RunAndReturn(run func(assert bool) error) *MockControlPort_SetDTR_Call { + _c.Call.Return(run) + return _c +} + +// SetRTS provides a mock function for the type MockControlPort +func (_mock *MockControlPort) SetRTS(assert bool) error { + ret := _mock.Called(assert) + + if len(ret) == 0 { + panic("no return value specified for SetRTS") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(bool) error); ok { + r0 = returnFunc(assert) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_SetRTS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetRTS' +type MockControlPort_SetRTS_Call struct { + *mock.Call +} + +// SetRTS is a helper method to define mock.On call +// - assert bool +func (_e *MockControlPort_Expecter) SetRTS(assert interface{}) *MockControlPort_SetRTS_Call { + return &MockControlPort_SetRTS_Call{Call: _e.mock.On("SetRTS", assert)} +} + +func (_c *MockControlPort_SetRTS_Call) Run(run func(assert bool)) *MockControlPort_SetRTS_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 bool + if args[0] != nil { + arg0 = args[0].(bool) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_SetRTS_Call) Return(err error) *MockControlPort_SetRTS_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_SetRTS_Call) RunAndReturn(run func(assert bool) error) *MockControlPort_SetRTS_Call { + _c.Call.Return(run) + return _c +} + +// SetReadDeadline provides a mock function for the type MockControlPort +func (_mock *MockControlPort) SetReadDeadline(t time.Time) error { + ret := _mock.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetReadDeadline") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = returnFunc(t) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' +type MockControlPort_SetReadDeadline_Call struct { + *mock.Call +} + +// SetReadDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockControlPort_Expecter) SetReadDeadline(t interface{}) *MockControlPort_SetReadDeadline_Call { + return &MockControlPort_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} +} + +func (_c *MockControlPort_SetReadDeadline_Call) Run(run func(t time.Time)) *MockControlPort_SetReadDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 time.Time + if args[0] != nil { + arg0 = args[0].(time.Time) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_SetReadDeadline_Call) Return(err error) *MockControlPort_SetReadDeadline_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_SetReadDeadline_Call) RunAndReturn(run func(t time.Time) error) *MockControlPort_SetReadDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetWriteDeadline provides a mock function for the type MockControlPort +func (_mock *MockControlPort) SetWriteDeadline(t time.Time) error { + ret := _mock.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetWriteDeadline") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = returnFunc(t) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockControlPort_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' +type MockControlPort_SetWriteDeadline_Call struct { + *mock.Call +} + +// SetWriteDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *MockControlPort_Expecter) SetWriteDeadline(t interface{}) *MockControlPort_SetWriteDeadline_Call { + return &MockControlPort_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} +} + +func (_c *MockControlPort_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockControlPort_SetWriteDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 time.Time + if args[0] != nil { + arg0 = args[0].(time.Time) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_SetWriteDeadline_Call) Return(err error) *MockControlPort_SetWriteDeadline_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockControlPort_SetWriteDeadline_Call) RunAndReturn(run func(t time.Time) error) *MockControlPort_SetWriteDeadline_Call { + _c.Call.Return(run) + return _c +} + +// Write provides a mock function for the type MockControlPort +func (_mock *MockControlPort) Write(p []byte) (int, error) { + ret := _mock.Called(p) + + if len(ret) == 0 { + panic("no return value specified for Write") + } + + var r0 int + var r1 error + if returnFunc, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return returnFunc(p) + } + if returnFunc, ok := ret.Get(0).(func([]byte) int); ok { + r0 = returnFunc(p) + } else { + r0 = ret.Get(0).(int) + } + if returnFunc, ok := ret.Get(1).(func([]byte) error); ok { + r1 = returnFunc(p) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockControlPort_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type MockControlPort_Write_Call struct { + *mock.Call +} + +// Write is a helper method to define mock.On call +// - p []byte +func (_e *MockControlPort_Expecter) Write(p interface{}) *MockControlPort_Write_Call { + return &MockControlPort_Write_Call{Call: _e.mock.On("Write", p)} +} + +func (_c *MockControlPort_Write_Call) Run(run func(p []byte)) *MockControlPort_Write_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []byte + if args[0] != nil { + arg0 = args[0].([]byte) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockControlPort_Write_Call) Return(n int, err error) *MockControlPort_Write_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockControlPort_Write_Call) RunAndReturn(run func(p []byte) (int, error)) *MockControlPort_Write_Call { + _c.Call.Return(run) + return _c +} diff --git a/plc4go/spi/transports/serial/serialport/parity_bsd.go b/plc4go/spi/transports/serial/serialport/parity_bsd.go new file mode 100644 index 0000000000..7378531df7 --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/parity_bsd.go @@ -0,0 +1,33 @@ +//go:build darwin || freebsd + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +// setMarkSpaceParity: BSD termios has no CMSPAR equivalent. +func setMarkSpaceParity(_ *unix.Termios, _ bool) error { + return errors.New("serialport: mark/space parity is not supported on this platform") +} diff --git a/plc4go/spi/transports/serial/serialport/parity_linux.go b/plc4go/spi/transports/serial/serialport/parity_linux.go new file mode 100644 index 0000000000..577bce96ae --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/parity_linux.go @@ -0,0 +1,35 @@ +//go:build linux + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import "golang.org/x/sys/unix" + +// setMarkSpaceParity configures "sticky" parity via the Linux-specific +// CMSPAR flag: PARENB|CMSPAR|PARODD transmits the parity bit as constant 1 +// (mark), PARENB|CMSPAR as constant 0 (space). +func setMarkSpaceParity(t *unix.Termios, mark bool) error { + t.Cflag |= unix.PARENB | unix.CMSPAR + if mark { + t.Cflag |= unix.PARODD + } + return nil +} diff --git a/plc4go/spi/transports/serial/serialport/port_unix.go b/plc4go/spi/transports/serial/serialport/port_unix.go index 5db37c362f..00c10dab5a 100644 --- a/plc4go/spi/transports/serial/serialport/port_unix.go +++ b/plc4go/spi/transports/serial/serialport/port_unix.go @@ -22,6 +22,7 @@ package serialport import ( + "errors" "fmt" "os" "time" @@ -29,6 +30,95 @@ import ( "golang.org/x/sys/unix" ) +// unixPort is an open POSIX serial port: a runtime-poller-managed *os.File +// (providing Read/Write/Close and deadlines) plus ioctl-backed control +// operations. Control ioctls go through SyscallConn().Control — never +// .Fd(), which would de-register the file from the poller. +type unixPort struct { + *os.File +} + +var _ ControlPort = (*unixPort)(nil) + +// control runs op on the raw fd while the runtime holds it stable. +// +// SyscallConn() and rawConn.Control() only fail for an invalid or closing +// *os.File — internal/poll surfaces that as "use of closed file", which +// does not satisfy errors.Is(err, os.ErrClosed) and would otherwise diverge +// from Read/Write-after-close (which do) and from the Windows ControlPort +// implementation (which returns os.ErrClosed). Normalize to os.ErrClosed +// here; op's own error is returned separately as opErr. +func (p *unixPort) control(op func(fd int) error) error { + rawConn, err := p.SyscallConn() + if err != nil { + return os.ErrClosed + } + var opErr error + if err := rawConn.Control(func(fd uintptr) { opErr = op(int(fd)) }); err != nil { + return os.ErrClosed + } + return opErr +} + +func (p *unixPort) setModemBits(bits int, assert bool) error { + return p.control(func(fd int) error { + if assert { + return os.NewSyscallError("ioctl TIOCMBIS", unix.IoctlSetPointerInt(fd, unix.TIOCMBIS, bits)) + } + return os.NewSyscallError("ioctl TIOCMBIC", unix.IoctlSetPointerInt(fd, unix.TIOCMBIC, bits)) + }) +} + +func (p *unixPort) SetDTR(assert bool) error { return p.setModemBits(unix.TIOCM_DTR, assert) } +func (p *unixPort) SetRTS(assert bool) error { return p.setModemBits(unix.TIOCM_RTS, assert) } + +func (p *unixPort) ModemStatus() (ModemStatus, error) { + var status ModemStatus + err := p.control(func(fd int) error { + bits, err := unix.IoctlGetInt(fd, unix.TIOCMGET) + if err != nil { + return os.NewSyscallError("ioctl TIOCMGET", err) + } + status = ModemStatus{ + CTS: bits&unix.TIOCM_CTS != 0, + DSR: bits&unix.TIOCM_DSR != 0, + DCD: bits&unix.TIOCM_CAR != 0, + RI: bits&unix.TIOCM_RNG != 0, + } + return nil + }) + return status, err +} + +func (p *unixPort) SendBreak(d time.Duration) error { + if d <= 0 { + return errors.New("serialport: break duration must be greater than 0") + } + if err := p.control(func(fd int) error { + return os.NewSyscallError("ioctl TIOCSBRK", unix.IoctlSetInt(fd, unix.TIOCSBRK, 0)) + }); err != nil { + return err + } + time.Sleep(d) + return p.control(func(fd int) error { + return os.NewSyscallError("ioctl TIOCCBRK", unix.IoctlSetInt(fd, unix.TIOCCBRK, 0)) + }) +} + +func (p *unixPort) SetConfig(cfg Config) error { + normalized, err := cfg.normalize() + if err != nil { + return err + } + t, err := makeTermios(normalized) + if err != nil { + return err + } + return p.control(func(fd int) error { + return applySettings(fd, t, normalized.BaudRate) + }) +} + // openPort opens the device non-blocking so the Go runtime poller manages // it — that is what makes SetReadDeadline/SetWriteDeadline work on a plain // *os.File (which therefore is the Port implementation on POSIX). @@ -67,5 +157,5 @@ func openPort(portName string, cfg Config) (Port, error) { _ = f.Close() return nil, fmt.Errorf("serialport: %s does not support I/O deadlines: %w", portName, err) } - return f, nil + return &unixPort{File: f}, nil } diff --git a/plc4go/spi/transports/serial/serialport/port_windows.go b/plc4go/spi/transports/serial/serialport/port_windows.go index a75668a860..01432e2757 100644 --- a/plc4go/spi/transports/serial/serialport/port_windows.go +++ b/plc4go/spi/transports/serial/serialport/port_windows.go @@ -53,15 +53,25 @@ import ( // stale by at most one slice iteration; it self-corrects on that // direction's next attempt. // -// Close() drains in-flight Read/Write attempts (tracked via ioCount) +// Close() drains in-flight I/O attempts (Read/Write/Drain, tracked via ioCount) // before releasing the handle, repeatedly cancelling outstanding I/O with // CancelIoEx, so a syscall can never start on, or run past, a closed/ // recycled handle. +// +// Locking discipline: short, bounded comm calls (escape codes, modem +// status, purge, SetCommState) run under mu via withHandle, since they +// complete quickly and never block on the wire. Read, Write, and Drain +// instead run on the in-flight I/O path (beginIO/endIO) and hold no lock +// while their syscall is outstanding, because those syscalls can block for +// an unbounded time (Drain's FlushFileBuffers in particular can stall +// indefinitely under a hardware flow-control deadlock); Close's CancelIoEx +// drain loop needs the syscall to actually be running, not queued up +// behind mu, in order to cancel it. type winPort struct { mu sync.Mutex handle windows.Handle closed bool - ioCount int // number of Read/Write attempts currently between beginIO and endIO + ioCount int // number of I/O attempts (Read/Write/Drain) currently between beginIO and endIO readDeadline time.Time writeDeadline time.Time } @@ -111,6 +121,10 @@ func (p *winPort) configure(cfg Config) error { dcb.Parity = s.Parity dcb.StopBits = s.StopBits dcb.Flags = s.Flags + dcb.XonLim = s.XonLim + dcb.XoffLim = s.XoffLim + dcb.XonChar = s.XonChar + dcb.XoffChar = s.XoffChar if err := windows.SetCommState(p.handle, &dcb); err != nil { return os.NewSyscallError("SetCommState", err) } @@ -324,3 +338,95 @@ func (p *winPort) Close() error { } return nil } + +var _ ControlPort = (*winPort)(nil) + +// withHandle runs a short, non-blocking comm call under the state lock. +// Only bounded, buffer-level calls may use this (escape codes, status +// reads, purge); blocking I/O keeps using beginIO/endIO. +func (p *winPort) withHandle(name string, op func(h windows.Handle) error) error { + p.mu.Lock() + defer p.mu.Unlock() + if p.closed { + return os.ErrClosed + } + return os.NewSyscallError(name, op(p.handle)) +} + +func (p *winPort) SetDTR(assert bool) error { + return p.withHandle("EscapeCommFunction", func(h windows.Handle) error { + return windows.EscapeCommFunction(h, winEscapeFor(true, assert)) + }) +} + +func (p *winPort) SetRTS(assert bool) error { + return p.withHandle("EscapeCommFunction", func(h windows.Handle) error { + return windows.EscapeCommFunction(h, winEscapeFor(false, assert)) + }) +} + +func (p *winPort) ModemStatus() (ModemStatus, error) { + var bits uint32 + err := p.withHandle("GetCommModemStatus", func(h windows.Handle) error { + return windows.GetCommModemStatus(h, &bits) + }) + if err != nil { + return ModemStatus{}, err + } + return modemStatusFromWinBits(bits), nil +} + +func (p *winPort) SendBreak(d time.Duration) error { + if d <= 0 { + return errors.New("serialport: break duration must be greater than 0") + } + if err := p.withHandle("SetCommBreak", windows.SetCommBreak); err != nil { + return err + } + time.Sleep(d) // deliberately outside the lock + return p.withHandle("ClearCommBreak", windows.ClearCommBreak) +} + +func (p *winPort) FlushInput() error { + return p.withHandle("PurgeComm", func(h windows.Handle) error { + return windows.PurgeComm(h, windows.PURGE_RXCLEAR) + }) +} + +func (p *winPort) FlushOutput() error { + return p.withHandle("PurgeComm", func(h windows.Handle) error { + return windows.PurgeComm(h, windows.PURGE_TXCLEAR) + }) +} + +// Drain blocks until all written data has been transmitted. It runs on the +// in-flight I/O path (beginIO/endIO) rather than under the state lock: +// FlushFileBuffers is unbounded when hardware flow control stalls the +// transmitter, and Close's CancelIoEx drain loop must be able to abort it. +func (p *winPort) Drain() error { + handle, _, _, err := p.beginIO() + if err != nil { + return err + } + defer p.endIO() + if err := windows.FlushFileBuffers(handle); err != nil { + if errors.Is(err, windows.ERROR_OPERATION_ABORTED) && p.isClosed() { + return os.ErrClosed + } + return os.NewSyscallError("FlushFileBuffers", err) + } + return nil +} + +func (p *winPort) SetConfig(cfg Config) error { + normalized, err := cfg.normalize() + if err != nil { + return err + } + p.mu.Lock() + defer p.mu.Unlock() + if p.closed { + return os.ErrClosed + } + return p.configure(normalized) +} diff --git a/plc4go/spi/transports/serial/serialport/pty_linux_test.go b/plc4go/spi/transports/serial/serialport/pty_linux_test.go index df2ee516ef..9d236c578d 100644 --- a/plc4go/spi/transports/serial/serialport/pty_linux_test.go +++ b/plc4go/spi/transports/serial/serialport/pty_linux_test.go @@ -140,8 +140,119 @@ func TestOpenOnPTY_UseAfterCloseFails(t *testing.T) { assert.True(t, errors.Is(err, os.ErrClosed), "write after close: want os.ErrClosed, got %v", err) } +func TestOpenOnPTY_ControlOpsAfterCloseReturnErrClosed(t *testing.T) { + _, slavePath := openPTY(t) + port, err := Open(slavePath, Config{BaudRate: 9600}) + require.NoError(t, err) + cp := port.(ControlPort) + require.NoError(t, port.Close()) + + require.ErrorIs(t, cp.FlushInput(), os.ErrClosed) + require.ErrorIs(t, cp.Drain(), os.ErrClosed) + require.ErrorIs(t, cp.SetDTR(true), os.ErrClosed) + _, err = cp.ModemStatus() + require.ErrorIs(t, err, os.ErrClosed) + require.ErrorIs(t, cp.SetConfig(Config{BaudRate: 19200}), os.ErrClosed) + require.ErrorIs(t, cp.SendBreak(10*time.Millisecond), os.ErrClosed) +} + func TestOpenNonexistentDeviceFails(t *testing.T) { _, err := Open("/dev/plc4x-does-not-exist", Config{BaudRate: 9600}) require.Error(t, err) assert.Contains(t, err.Error(), "plc4x-does-not-exist") } + +func TestOpenOnPTY_ImplementsControlPort(t *testing.T) { + _, slavePath := openPTY(t) + port, err := Open(slavePath, Config{BaudRate: 9600}) + require.NoError(t, err) + defer port.Close() + _, ok := port.(ControlPort) + require.True(t, ok, "Open's result must implement ControlPort") +} + +func TestOpenOnPTY_SetConfigReconfiguresLive(t *testing.T) { + _, slavePath := openPTY(t) + port, err := Open(slavePath, Config{BaudRate: 9600}) + require.NoError(t, err) + defer port.Close() + cp := port.(ControlPort) + + require.NoError(t, cp.SetConfig(Config{BaudRate: 19200, DataBits: 7, StopBits: StopBitsTwo, Parity: ParityEven})) + + // Read back the termios state to prove the reconfiguration landed. + rawConn, err := port.(*unixPort).SyscallConn() + require.NoError(t, err) + var readBack *unix.Termios + var ioctlErr error + require.NoError(t, rawConn.Control(func(fd uintptr) { + readBack, ioctlErr = unix.IoctlGetTermios(int(fd), unix.TCGETS) + })) + require.NoError(t, ioctlErr) + assert.NotZero(t, readBack.Cflag&unix.CS7, "CS7") + assert.NotZero(t, readBack.Cflag&unix.CSTOPB, "CSTOPB") + // PARENB is deliberately not asserted: pty drivers may drop or reject + // parity flags (observed on Linux 7.x), while CS7/CSTOPB reliably stick + // and prove the reconfiguration reached the kernel. + + // Invalid reconfiguration is rejected by validation, not the kernel. + require.ErrorContains(t, cp.SetConfig(Config{}), "baud rate") +} + +func TestOpenOnPTY_FlushInputDiscardsPendingData(t *testing.T) { + master, slavePath := openPTY(t) + port, err := Open(slavePath, Config{BaudRate: 9600}) + require.NoError(t, err) + defer port.Close() + cp := port.(ControlPort) + + _, err = master.Write([]byte("stale")) + require.NoError(t, err) + time.Sleep(50 * time.Millisecond) // let the pty deliver into the input queue + require.NoError(t, cp.FlushInput()) + + require.NoError(t, port.SetReadDeadline(time.Now().Add(100*time.Millisecond))) + _, err = port.Read(make([]byte, 8)) + require.True(t, errors.Is(err, os.ErrDeadlineExceeded), "flushed data must be gone, got %v", err) +} + +func TestOpenOnPTY_FlushOutputAndDrainSucceed(t *testing.T) { + _, slavePath := openPTY(t) + port, err := Open(slavePath, Config{BaudRate: 9600}) + require.NoError(t, err) + defer port.Close() + cp := port.(ControlPort) + require.NoError(t, cp.FlushOutput()) + require.NoError(t, cp.Drain()) +} + +func TestOpenOnPTY_SendBreak(t *testing.T) { + _, slavePath := openPTY(t) + port, err := Open(slavePath, Config{BaudRate: 9600}) + require.NoError(t, err) + defer port.Close() + cp := port.(ControlPort) + + require.Error(t, cp.SendBreak(0), "non-positive duration must be rejected") + + // Linux ptys accept break as a no-op (send_break returns 0 without a + // driver break_ctl), so success plus elapsed time is what we can pin. + start := time.Now() + require.NoError(t, cp.SendBreak(60*time.Millisecond)) + assert.GreaterOrEqual(t, time.Since(start), 60*time.Millisecond) +} + +func TestOpenOnPTY_ModemOpsSurfaceDriverErrors(t *testing.T) { + // Linux ptys have no tiocmget/tiocmset — the calls must fail cleanly + // (on real UARTs they succeed; this pins error propagation, not values). + _, slavePath := openPTY(t) + port, err := Open(slavePath, Config{BaudRate: 9600}) + require.NoError(t, err) + defer port.Close() + cp := port.(ControlPort) + + _, err = cp.ModemStatus() + require.Error(t, err) + require.Error(t, cp.SetDTR(true)) + require.Error(t, cp.SetRTS(false)) +} diff --git a/plc4go/spi/transports/serial/serialport/serialport.go b/plc4go/spi/transports/serial/serialport/serialport.go index 1b7b0413c4..944128637b 100644 --- a/plc4go/spi/transports/serial/serialport/serialport.go +++ b/plc4go/spi/transports/serial/serialport/serialport.go @@ -40,6 +40,8 @@ const ( ParityNone Parity = iota ParityOdd ParityEven + ParityMark // parity bit always 1 — Linux and Windows only + ParitySpace // parity bit always 0 — Linux and Windows only ) // StopBits configures the number of stop bits. @@ -59,11 +61,16 @@ const ( // are opened non-blocking and Read returns as soon as at least one byte is // available, bounded by SetReadDeadline — the same semantics as net.Conn. type Config struct { - BaudRate uint - DataBits uint // 5..8; 0 defaults to 8 - StopBits StopBits - Parity Parity - RTSCTSFlowControl bool + // BaudRate accepts any positive rate, including non-standard ones: + // Linux uses the termios2/BOTHER interface, Darwin falls back to + // IOSSIOSPEED, Windows passes the value through DCB; FreeBSD accepts + // whatever the driver supports. + BaudRate uint + DataBits uint // 5..8; 0 defaults to 8 + StopBits StopBits + Parity Parity + RTSCTSFlowControl bool + XONXOFFFlowControl bool // mutually exclusive with RTSCTSFlowControl } // Port is an open serial port. Read blocks until at least one byte is @@ -75,8 +82,45 @@ type Port interface { SetWriteDeadline(t time.Time) error } -// ErrUnsupportedPlatform is returned by Open on operating systems without -// a serial port implementation. +// ModemStatus is a snapshot of the input modem-control lines. +type ModemStatus struct { + CTS bool // Clear To Send + DSR bool // Data Set Ready + DCD bool // Data Carrier Detect (RLSD on Windows) + RI bool // Ring Indicator +} + +// ControlPort extends Port with modem-control, line-discipline and runtime +// reconfiguration operations. Every Port returned by Open also implements +// ControlPort; callers needing these operations type-assert: +// +// cp := port.(ControlPort) +type ControlPort interface { + Port + // SetDTR asserts (true) or clears (false) the DTR output line. + SetDTR(assert bool) error + // SetRTS asserts or clears the RTS output line. Meaningless while + // RTSCTSFlowControl is active (the driver owns the line then). + SetRTS(assert bool) error + // ModemStatus reads the current input line states. + ModemStatus() (ModemStatus, error) + // SendBreak holds the TX line in break condition for d (must be > 0). + SendBreak(d time.Duration) error + // FlushInput discards received-but-unread data. + FlushInput() error + // FlushOutput discards written-but-unsent data. + FlushOutput() error + // Drain blocks until all written data has been transmitted. Platform + // behavior around a concurrent Close differs: on Windows, closing the + // port aborts a blocked Drain; on POSIX, a Drain stalled by hardware + // flow control is not interruptible by Close (tcdrain(3) semantics). + Drain() error + // SetConfig re-validates and applies cfg to the open port. + SetConfig(cfg Config) error +} + +// ErrUnsupportedPlatform is returned by Open, and by ListPorts, on +// operating systems without a serial port implementation. var ErrUnsupportedPlatform = errors.New("serial ports are not supported on this platform") // Open opens and configures the named serial port (e.g. "/dev/ttyUSB0" or @@ -92,6 +136,12 @@ func Open(portName string, cfg Config) (Port, error) { return openPort(portName, normalized) } +// ListPorts returns the device paths (POSIX) or names (Windows, e.g. +// "COM3") of serial ports present on the system, sorted. +func ListPorts() ([]string, error) { + return listPorts() +} + // normalize validates cfg and fills in defaults. func (c Config) normalize() (Config, error) { if c.BaudRate == 0 { @@ -109,10 +159,13 @@ func (c Config) normalize() (Config, error) { return c, fmt.Errorf("serialport: invalid stop bits value %d", c.StopBits) } switch c.Parity { - case ParityNone, ParityOdd, ParityEven: + case ParityNone, ParityOdd, ParityEven, ParityMark, ParitySpace: default: return c, fmt.Errorf("serialport: invalid parity value %d", c.Parity) } + if c.RTSCTSFlowControl && c.XONXOFFFlowControl { + return c, errors.New("serialport: RTS/CTS and XON/XOFF flow control are mutually exclusive") + } if c.StopBits == StopBitsOnePointFive && c.DataBits != 5 { return c, errors.New("serialport: 1.5 stop bits require 5 data bits") } diff --git a/plc4go/spi/transports/serial/serialport/serialport_test.go b/plc4go/spi/transports/serial/serialport/serialport_test.go index 10c949e22c..5e6f4fe803 100644 --- a/plc4go/spi/transports/serial/serialport/serialport_test.go +++ b/plc4go/spi/transports/serial/serialport/serialport_test.go @@ -79,6 +79,26 @@ func TestConfigNormalize(t *testing.T) { in: Config{BaudRate: 9600, DataBits: 5, StopBits: StopBitsOnePointFive}, want: Config{BaudRate: 9600, DataBits: 5, StopBits: StopBitsOnePointFive, Parity: ParityNone}, }, + { + name: "mark parity accepted", + in: Config{BaudRate: 9600, Parity: ParityMark}, + want: Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParityMark}, + }, + { + name: "space parity accepted", + in: Config{BaudRate: 9600, Parity: ParitySpace}, + want: Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParitySpace}, + }, + { + name: "xon/xoff flow control accepted", + in: Config{BaudRate: 9600, XONXOFFFlowControl: true}, + want: Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParityNone, XONXOFFFlowControl: true}, + }, + { + name: "both flow controls rejected", + in: Config{BaudRate: 9600, RTSCTSFlowControl: true, XONXOFFFlowControl: true}, + wantErr: "mutually exclusive", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -105,3 +125,9 @@ func TestOpenRejectsInvalidConfigBeforeTouchingPort(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "baud rate") } + +func TestControlPortEmbedsPort(t *testing.T) { + // Compile-time containment check: a ControlPort must be assignable to + // Port (i.e. ControlPort embeds Port). Fails to compile otherwise. + var _ Port = ControlPort(nil) +} diff --git a/plc4go/spi/transports/serial/serialport/termios_bsd_test.go b/plc4go/spi/transports/serial/serialport/termios_bsd_test.go new file mode 100644 index 0000000000..fb012f0219 --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/termios_bsd_test.go @@ -0,0 +1,35 @@ +//go:build darwin || freebsd + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMakeTermiosMarkSpaceParityUnsupported(t *testing.T) { + _, err := makeTermios(Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParityMark}) + require.ErrorContains(t, err, "mark/space parity") + _, err = makeTermios(Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParitySpace}) + require.ErrorContains(t, err, "mark/space parity") +} diff --git a/plc4go/spi/transports/serial/serialport/termios_linux_test.go b/plc4go/spi/transports/serial/serialport/termios_linux_test.go new file mode 100644 index 0000000000..61b179afcf --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/termios_linux_test.go @@ -0,0 +1,40 @@ +//go:build linux + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func TestMakeTermiosMarkSpaceParity(t *testing.T) { + markTermios, err := makeTermios(Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParityMark}) + require.NoError(t, err) + spaceTermios, err2 := makeTermios(Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParitySpace}) + require.NoError(t, err2) + base := uint64(unix.CLOCAL | unix.CREAD | unix.CS8) + assert.EqualValues(t, base|unix.PARENB|unix.CMSPAR|unix.PARODD, uint64(markTermios.Cflag), "mark = PARENB|CMSPAR|PARODD") + assert.EqualValues(t, base|unix.PARENB|unix.CMSPAR, uint64(spaceTermios.Cflag), "space = PARENB|CMSPAR") +} diff --git a/plc4go/spi/transports/serial/serialport/termios_unix.go b/plc4go/spi/transports/serial/serialport/termios_unix.go index 00d11328da..94fdff67e0 100644 --- a/plc4go/spi/transports/serial/serialport/termios_unix.go +++ b/plc4go/spi/transports/serial/serialport/termios_unix.go @@ -62,12 +62,26 @@ func makeTermios(cfg Config) (*unix.Termios, error) { t.Cflag |= unix.PARENB | unix.PARODD case ParityEven: t.Cflag |= unix.PARENB + case ParityMark: + if err := setMarkSpaceParity(t, true); err != nil { + return nil, err + } + case ParitySpace: + if err := setMarkSpaceParity(t, false); err != nil { + return nil, err + } } if cfg.RTSCTSFlowControl { t.Cflag |= unix.CRTSCTS } + if cfg.XONXOFFFlowControl { + t.Iflag |= unix.IXON | unix.IXOFF + t.Cc[unix.VSTART] = 0x11 // DC1 + t.Cc[unix.VSTOP] = 0x13 // DC3 + } + // The port is opened O_NONBLOCK and driven by the runtime poller, which // makes the kernel ignore VMIN/VTIME; set the conventional raw-mode // values anyway to document intent (and for any future blocking use). diff --git a/plc4go/spi/transports/serial/serialport/termios_unix_test.go b/plc4go/spi/transports/serial/serialport/termios_unix_test.go index ac6307c003..38dc884742 100644 --- a/plc4go/spi/transports/serial/serialport/termios_unix_test.go +++ b/plc4go/spi/transports/serial/serialport/termios_unix_test.go @@ -97,3 +97,13 @@ func TestMakeTermiosRejectsOnePointFiveStopBits(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "1.5 stop bits") } + +func TestMakeTermiosXONXOFF(t *testing.T) { + got, err := makeTermios(Config{BaudRate: 9600, DataBits: 8, StopBits: StopBitsOne, Parity: ParityNone, XONXOFFFlowControl: true}) + require.NoError(t, err) + // POSIX software flow control: IXON (honor received XOFF/XON) and + // IXOFF (emit XOFF/XON) with the conventional DC1/DC3 characters. + assert.EqualValues(t, unix.IXON|unix.IXOFF, got.Iflag, "Iflag") + assert.EqualValues(t, 0x11, got.Cc[unix.VSTART], "VSTART must be DC1") + assert.EqualValues(t, 0x13, got.Cc[unix.VSTOP], "VSTOP must be DC3") +} diff --git a/plc4go/spi/transports/serial/serialport/winctl.go b/plc4go/spi/transports/serial/serialport/winctl.go new file mode 100644 index 0000000000..a5df968853 --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/winctl.go @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +// Windows comm-control values kept portable (no x/sys/windows import) so the +// mapping is unit-testable on every development platform. Values per +// Microsoft documentation for EscapeCommFunction (x/sys/windows defines the +// same SETRTS/CLRRTS/SETDTR/CLRDTR values, but only under GOOS=windows) and +// GetCommModemStatus (whose MS_*_ON masks x/sys/windows does not define). +const ( + winEscapeSetRTS = 3 // SETRTS + winEscapeClrRTS = 4 // CLRRTS + winEscapeSetDTR = 5 // SETDTR + winEscapeClrDTR = 6 // CLRDTR + + winModemCTSOn = 0x0010 // MS_CTS_ON + winModemDSROn = 0x0020 // MS_DSR_ON + winModemRingOn = 0x0040 // MS_RING_ON + winModemRLSDOn = 0x0080 // MS_RLSD_ON (carrier detect) +) + +// winEscapeFor returns the EscapeCommFunction code for asserting/clearing +// the DTR (dtr=true) or RTS (dtr=false) line. +func winEscapeFor(dtr, assert bool) uint32 { + switch { + case dtr && assert: + return winEscapeSetDTR + case dtr: + return winEscapeClrDTR + case assert: + return winEscapeSetRTS + default: + return winEscapeClrRTS + } +} + +// modemStatusFromWinBits translates a GetCommModemStatus bit mask. +func modemStatusFromWinBits(bits uint32) ModemStatus { + return ModemStatus{ + CTS: bits&winModemCTSOn != 0, + DSR: bits&winModemDSROn != 0, + RI: bits&winModemRingOn != 0, + DCD: bits&winModemRLSDOn != 0, + } +} diff --git a/plc4go/spi/transports/serial/serialport/winctl_test.go b/plc4go/spi/transports/serial/serialport/winctl_test.go new file mode 100644 index 0000000000..5d6cfcc3c4 --- /dev/null +++ b/plc4go/spi/transports/serial/serialport/winctl_test.go @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package serialport + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Values per Microsoft documentation: EscapeCommFunction dwFunc codes +// (SETRTS=3, CLRRTS=4, SETDTR=5, CLRDTR=6) and GetCommModemStatus masks +// (MS_CTS_ON=0x10, MS_DSR_ON=0x20, MS_RING_ON=0x40, MS_RLSD_ON=0x80). +func TestWinEscapeFor(t *testing.T) { + assert.EqualValues(t, 5, winEscapeFor(true, true), "SETDTR") + assert.EqualValues(t, 6, winEscapeFor(true, false), "CLRDTR") + assert.EqualValues(t, 3, winEscapeFor(false, true), "SETRTS") + assert.EqualValues(t, 4, winEscapeFor(false, false), "CLRRTS") +} + +func TestModemStatusFromWinBits(t *testing.T) { + assert.Equal(t, ModemStatus{}, modemStatusFromWinBits(0)) + assert.Equal(t, + ModemStatus{CTS: true, DSR: true, RI: true, DCD: true}, + modemStatusFromWinBits(0x10|0x20|0x40|0x80)) + assert.Equal(t, ModemStatus{CTS: true}, modemStatusFromWinBits(0x10)) + assert.Equal(t, ModemStatus{DSR: true}, modemStatusFromWinBits(0x20)) + assert.Equal(t, ModemStatus{RI: true}, modemStatusFromWinBits(0x40)) + assert.Equal(t, ModemStatus{DCD: true}, modemStatusFromWinBits(0x80)) +}
