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))
+}