You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2026/07/06 18:53:49 UTC
(plc4x) 04/09: feat(plc4go): complete serialport feature set (ControlPort, ListPorts)
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 <sr...@apache.org>
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))
+}