This is an automated email from the ASF dual-hosted git repository.

sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git

commit 2a52d8f66871efb654e8c277c5b5d7e30b410e0d
Author: Sebastian Rühl <[email protected]>
AuthorDate: Mon Jul 6 13:45:58 2026 +0200

    feat(plc4go): complete serialport feature set (ControlPort, ListPorts)
    
    Phase 2 of the internalized serial-port package: a ControlPort interface
    (implemented by every Port that Open returns) adds modem control lines
    (DTR/RTS set, CTS/DSR/DCD/RI read), timed break, flush/drain, and runtime
    reconfiguration via SetConfig; Config gains XON/XOFF software flow control
    and mark/space parity (Linux/Windows; clean error elsewhere); ListPorts
    enumerates ports on Linux/macOS/FreeBSD/Windows.
    
    Same testing discipline as phase 1: portable spec-pinned mappings for all
    Windows DCB/escape/modem constants, thin per-OS shims, PTY integration
    tests for everything the pty driver supports, race-checked, cross-compiled
    for six GOOS targets.
---
 plc4go/spi/transports/serial/serialport/dcb.go     |  35 +-
 .../spi/transports/serial/serialport/dcb_test.go   |  46 ++
 .../transports/serial/serialport/ioflush_bsd.go    |  52 ++
 .../transports/serial/serialport/ioflush_linux.go  |  47 ++
 .../transports/serial/serialport/list_darwin.go    |  38 ++
 .../transports/serial/serialport/list_freebsd.go   |  46 ++
 .../spi/transports/serial/serialport/list_linux.go |  43 ++
 .../serial/serialport/list_linux_test.go           |  47 ++
 .../serial/serialport/list_unsupported.go          |  31 +
 .../transports/serial/serialport/list_windows.go   |  56 ++
 .../spi/transports/serial/serialport/mocks_test.go | 682 +++++++++++++++++++++
 .../spi/transports/serial/serialport/parity_bsd.go |  33 +
 .../transports/serial/serialport/parity_linux.go   |  35 ++
 .../spi/transports/serial/serialport/port_unix.go  |  92 ++-
 .../transports/serial/serialport/port_windows.go   | 110 +++-
 .../transports/serial/serialport/pty_linux_test.go | 111 ++++
 .../spi/transports/serial/serialport/serialport.go |  69 ++-
 .../serial/serialport/serialport_test.go           |  26 +
 .../serial/serialport/termios_bsd_test.go          |  35 ++
 .../serial/serialport/termios_linux_test.go        |  40 ++
 .../transports/serial/serialport/termios_unix.go   |  14 +
 .../serial/serialport/termios_unix_test.go         |  10 +
 plc4go/spi/transports/serial/serialport/winctl.go  |  62 ++
 .../transports/serial/serialport/winctl_test.go    |  47 ++
 24 files changed, 1793 insertions(+), 14 deletions(-)

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

Reply via email to