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 c91211bbc3fe6c1569529ab8593ea6df43e62074
Author: Sebastian Rühl <[email protected]>
AuthorDate: Mon Jul 6 11:21:09 2026 +0200

    feat(plc4go): replace jacobsa/go-serial with internal serialport package
    
    Internalize the unmaintained (2018) github.com/jacobsa/go-serial dependency
    as spi/transports/serial/serialport, a rewrite with net.Conn-like semantics:
    
    - POSIX: non-blocking *os.File on the runtime poller, so
      SetReadDeadline/SetWriteDeadline work natively (Linux TCSETS2+BOTHER,
      Darwin TIOCSETA+IOSSIOSPEED, FreeBSD TIOCSETA; stub elsewhere)
    - Windows: DCB/COMMTIMEOUTS-based port with deadline emulation in bounded
      slices and a cancellation-safe Close (compile-gated, logic spec-tested
      portably)
    - Tested without hardware: spec-pinned tables for termios and DCB values,
      PTY integration tests for the full open/configure/read/write/deadline/
      close path, race-checked, cross-compiled for 
windows/darwin/freebsd/openbsd
    
    The serial TransportInstance now wires real read/write deadlines (removes
    both "TODO: big oof" no-ops) and fixes a latent bug where Connect never set
    the connected flag, so Write always failed with "Not connected".
    
    Also: attribution entry in LICENSE for the two derived files, dependency
    removed from go.mod/go.sum/tools.mod/tools.sum and the code-generation Go
    test fixture.
---
 LICENSE                                            |   6 +
 .../language/go/src/test/resources/plc4go/go.mod   |   1 -
 .../language/go/src/test/resources/plc4go/go.sum   |   2 -
 plc4go/go.mod                                      |   1 -
 plc4go/go.sum                                      |   2 -
 plc4go/internal/bacnetip/Connection_test.go        |   4 +-
 plc4go/internal/bacnetip/ReadRoundtrip_test.go     |   2 +-
 plc4go/spi/transports/serial/TransportInstance.go  |  32 +-
 .../serial/TransportInstance_pty_test.go           |  91 ++++++
 .../transports/serial/TransportInstance_test.go    |  14 +-
 .../transports/serial/serialport/apply_darwin.go   |  66 +++++
 .../transports/serial/serialport/apply_freebsd.go  |  39 +++
 .../transports/serial/serialport/apply_linux.go    |  45 +++
 plc4go/spi/transports/serial/serialport/dcb.go     |  86 ++++++
 .../spi/transports/serial/serialport/dcb_test.go   | 102 +++++++
 .../spi/transports/serial/serialport/mocks_test.go | 323 ++++++++++++++++++++
 .../spi/transports/serial/serialport/port_unix.go  |  71 +++++
 .../serial/serialport/port_unsupported.go          |  31 ++
 .../transports/serial/serialport/port_windows.go   | 326 +++++++++++++++++++++
 .../transports/serial/serialport/pty_linux_test.go | 147 ++++++++++
 .../spi/transports/serial/serialport/serialport.go | 120 ++++++++
 .../serial/serialport/serialport_test.go           | 107 +++++++
 .../transports/serial/serialport/termios_unix.go   |  77 +++++
 .../serial/serialport/termios_unix_test.go         |  99 +++++++
 .../spi/transports/serial/serialport/timeouts.go   |  47 +++
 .../transports/serial/serialport/timeouts_test.go  |  51 ++++
 plc4go/tools.mod                                   |   5 +-
 plc4go/tools.sum                                   |   5 -
 28 files changed, 1860 insertions(+), 42 deletions(-)

diff --git a/LICENSE b/LICENSE
index 4b39e66ae2..711f75f3ed 100644
--- a/LICENSE
+++ b/LICENSE
@@ -208,6 +208,12 @@ 
plc4j/transports/test/src/main/java/io/netty/channel/embedded/Plc4xEmbeddedChann
 
plc4j/transports/test/src/main/java/io/netty/channel/embedded/Plc4xEmbeddedEventLoop.java
 Are copyrighted by the The Netty Project which is distributed under the Apache 
2.0 license.
 
+The files:
+plc4go/spi/transports/serial/serialport/apply_linux.go
+plc4go/spi/transports/serial/serialport/apply_darwin.go
+Contain logic derived from github.com/jacobsa/go-serial, copyrighted by Aaron 
Jacobs,
+which is distributed under the Apache 2.0 license.
+
 The file:
 
code-generation/protocol-base-mspec/src/main/antlr4/org/apache/plc4x/plugins/codegenerator/language/mspec/expression/Expression.g4
 is licensed under the Category A: "UNLICENSE" which is available here:
diff --git a/code-generation/language/go/src/test/resources/plc4go/go.mod 
b/code-generation/language/go/src/test/resources/plc4go/go.mod
index 5880bc91e5..8900a7b3e6 100644
--- a/code-generation/language/go/src/test/resources/plc4go/go.mod
+++ b/code-generation/language/go/src/test/resources/plc4go/go.mod
@@ -28,7 +28,6 @@ require (
        github.com/fatih/color v1.19.0
        github.com/google/uuid v1.6.0
        github.com/gopacket/gopacket v1.5.0
-       github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4
        github.com/libp2p/go-reuseport v0.4.0
        github.com/rs/zerolog v1.35.1
        github.com/stretchr/testify v1.11.1
diff --git a/code-generation/language/go/src/test/resources/plc4go/go.sum 
b/code-generation/language/go/src/test/resources/plc4go/go.sum
index 42ad20b66c..28be973e55 100644
--- a/code-generation/language/go/src/test/resources/plc4go/go.sum
+++ b/code-generation/language/go/src/test/resources/plc4go/go.sum
@@ -11,8 +11,6 @@ github.com/google/uuid v1.6.0 
h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopacket/gopacket v1.5.0 
h1:9s9fcSUVKFlRV97B77Bq9XNV3ly2gvvsneFMQUGjc+M=
 github.com/gopacket/gopacket v1.5.0/go.mod 
h1:i3NaGaqfoWKAr1+g7qxEdWsmfT+MXuWkAe9+THv8LME=
-github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 
h1:G2ztCwXov8mRvP0ZfjE6nAlaCX2XbykaeHdbT6KwDz0=
-github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod 
h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs=
 github.com/kr/pretty v0.2.1/go.mod 
h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod 
h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
diff --git a/plc4go/go.mod b/plc4go/go.mod
index 60f974c9dc..1d8a034cfb 100644
--- a/plc4go/go.mod
+++ b/plc4go/go.mod
@@ -28,7 +28,6 @@ require (
        github.com/fatih/color v1.19.0
        github.com/google/uuid v1.6.0
        github.com/gopacket/gopacket v1.7.0
-       github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4
        github.com/rs/zerolog v1.35.1
        github.com/stretchr/testify v1.11.1
        golang.org/x/net v0.56.0
diff --git a/plc4go/go.sum b/plc4go/go.sum
index 67241d04da..2bb63966bd 100644
--- a/plc4go/go.sum
+++ b/plc4go/go.sum
@@ -11,8 +11,6 @@ github.com/google/uuid v1.6.0 
h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopacket/gopacket v1.7.0 
h1:GdmF8ytGnjtSvyy30CTZhIwX1ybDWH3Q0MNK0blIKzA=
 github.com/gopacket/gopacket v1.7.0/go.mod 
h1:QKowPlTLrQU2rqV5C5I14Aoaid3l8da3kbddibc/Wgk=
-github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 
h1:G2ztCwXov8mRvP0ZfjE6nAlaCX2XbykaeHdbT6KwDz0=
-github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod 
h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs=
 github.com/kr/pretty v0.2.1/go.mod 
h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod 
h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
diff --git a/plc4go/internal/bacnetip/Connection_test.go 
b/plc4go/internal/bacnetip/Connection_test.go
index f6f260e2c5..7559a8930d 100644
--- a/plc4go/internal/bacnetip/Connection_test.go
+++ b/plc4go/internal/bacnetip/Connection_test.go
@@ -23,11 +23,11 @@ import (
        "math"
        "testing"
 
-       "github.com/apache/plc4x/plc4go/spi/options"
-       "github.com/apache/plc4x/plc4go/spi/testutils"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 
+       "github.com/apache/plc4x/plc4go/spi/options"
+       "github.com/apache/plc4x/plc4go/spi/testutils"
        "github.com/apache/plc4x/plc4go/spi/transactions"
 )
 
diff --git a/plc4go/internal/bacnetip/ReadRoundtrip_test.go 
b/plc4go/internal/bacnetip/ReadRoundtrip_test.go
index 870687893f..bf2332cbc7 100644
--- a/plc4go/internal/bacnetip/ReadRoundtrip_test.go
+++ b/plc4go/internal/bacnetip/ReadRoundtrip_test.go
@@ -27,7 +27,6 @@ import (
        "testing"
        "time"
 
-       "github.com/apache/plc4x/plc4go/spi/testutils"
        "github.com/rs/zerolog"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
@@ -38,6 +37,7 @@ import (
        apiTransports "github.com/apache/plc4x/plc4go/pkg/api/transports"
        model 
"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
        "github.com/apache/plc4x/plc4go/spi/options"
+       "github.com/apache/plc4x/plc4go/spi/testutils"
 )
 
 // fakeBacnetDevice is a minimal UDP BACnet/IP device used to exercise the real
diff --git a/plc4go/spi/transports/serial/TransportInstance.go 
b/plc4go/spi/transports/serial/TransportInstance.go
index be8cfa0070..5868efbc25 100644
--- a/plc4go/spi/transports/serial/TransportInstance.go
+++ b/plc4go/spi/transports/serial/TransportInstance.go
@@ -29,12 +29,12 @@ import (
        "sync/atomic"
        "time"
 
-       "github.com/jacobsa/go-serial/serial"
        "github.com/rs/zerolog"
 
        "github.com/apache/plc4x/plc4go/spi/errors"
        "github.com/apache/plc4x/plc4go/spi/options"
        "github.com/apache/plc4x/plc4go/spi/transports"
+       "github.com/apache/plc4x/plc4go/spi/transports/serial/serialport"
        transportUtils "github.com/apache/plc4x/plc4go/spi/transports/utils"
        "github.com/apache/plc4x/plc4go/spi/utils"
 )
@@ -50,7 +50,7 @@ type TransportInstance struct {
        stateChangeMutex sync.Mutex
 
        transport  *Transport
-       serialPort io.ReadWriteCloser
+       serialPort serialport.Port
        reader     *bufio.Reader
 
        log zerolog.Logger
@@ -79,22 +79,13 @@ func (m *TransportInstance) Connect(ctx context.Context) 
error {
                return errors.New("Already connected")
        }
 
-       var err error
-       config := serial.OpenOptions{PortName: m.SerialPortName, BaudRate: 
m.BaudRate, DataBits: 8, StopBits: 1, MinimumReadSize: 0, 
InterCharacterTimeout: 100 /*, RTSCTSFlowControl: true*/}
-       m.serialPort, err = serial.Open(config)
+       serialPort, err := serialport.Open(m.SerialPortName, 
serialport.Config{BaudRate: m.BaudRate})
        if err != nil {
                return errors.Wrap(err, "error connecting to serial port")
        }
-       // Add a logging layer ...
-       /*logFile, err := ioutil.TempFile(os.TempDir(), "transport-logger")
-       if err != nil {
-               m.log.Error().Msg("Error creating file for logging transport 
requests")
-       } else {
-               fileLogger := zerolog.New(logFile).With().Logger()
-               m.serialPort = utils.NewTransportLogger(m.serialPort, 
utils.WithLogger(fileLogger))
-               m.log.Trace().Msg("Logging Transport to file %s", 
logFile.Name())
-       }*/
+       m.serialPort = serialPort
        m.reader = bufio.NewReader(m.serialPort)
+       m.connected.Store(true)
 
        return nil
 }
@@ -132,7 +123,11 @@ func (m *TransportInstance) Write(ctx context.Context, 
data []byte) error {
        if m.serialPort == nil {
                return errors.New("error writing to transport. No writer 
available")
        }
-       // TODO: big oof.... there is no way to set a timeout on the write 
operation.
+       if deadline, ok := ctx.Deadline(); ok {
+               if err := m.serialPort.SetWriteDeadline(deadline); err != nil {
+                       return errors.Wrap(err, "error setting write deadline")
+               }
+       }
        num, err := m.serialPort.Write(data)
        if err != nil {
                return errors.Wrap(err, "error writing")
@@ -148,8 +143,11 @@ func (m *TransportInstance) GetReader() 
transports.ExtendedReader {
 }
 
 func (m *TransportInstance) SetReadDeadline(deadline time.Time) error {
-       // TODO: big oof.... there is no way to set a timeout
-       return nil
+       serialPort := m.serialPort
+       if serialPort == nil {
+               return errors.New("error setting read deadline. No serial port 
available")
+       }
+       return serialPort.SetReadDeadline(deadline)
 }
 
 func (m *TransportInstance) String() string {
diff --git a/plc4go/spi/transports/serial/TransportInstance_pty_test.go 
b/plc4go/spi/transports/serial/TransportInstance_pty_test.go
new file mode 100644
index 0000000000..b33456eaa3
--- /dev/null
+++ b/plc4go/spi/transports/serial/TransportInstance_pty_test.go
@@ -0,0 +1,91 @@
+//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 serial
+
+import (
+       "context"
+       "fmt"
+       "os"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       "golang.org/x/sys/unix"
+)
+
+// openPTY allocates a pseudo-terminal pair; the slave path stands in for a
+// real serial device (same pattern as the serialport package's own tests).
+func openPTY(t *testing.T) (master *os.File, slavePath string) {
+       t.Helper()
+       master, err := os.OpenFile("/dev/ptmx", os.O_RDWR|unix.O_NOCTTY, 0)
+       require.NoError(t, err, "opening /dev/ptmx")
+       t.Cleanup(func() { _ = master.Close() })
+       ptn, err := unix.IoctlGetInt(int(master.Fd()), unix.TIOCGPTN)
+       require.NoError(t, err, "TIOCGPTN")
+       require.NoError(t, unix.IoctlSetPointerInt(int(master.Fd()), 
unix.TIOCSPTLCK, 0), "unlocking pty slave")
+       return master, fmt.Sprintf("/dev/pts/%d", ptn)
+}
+
+func TestTransportInstance_EndToEndOnPTY(t *testing.T) {
+       master, slavePath := openPTY(t)
+
+       instance := NewTransportInstance(slavePath, 115200, 1, NewTransport())
+       require.NoError(t, instance.Connect(context.Background()))
+       t.Cleanup(func() { _ = instance.Close() })
+       assert.True(t, instance.IsConnected())
+
+       // Write goes out on the wire (fixes the historic "Not connected" bug:
+       // Connect previously never flipped the connected flag).
+       require.NoError(t, instance.Write(context.Background(), []byte{0x01, 
0x02, 0x03}))
+       wire := make([]byte, 3)
+       _, err := master.Read(wire)
+       require.NoError(t, err)
+       assert.Equal(t, []byte{0x01, 0x02, 0x03}, wire)
+
+       // Reads work and honor context deadlines through the buffered layer.
+       _, err = master.Write([]byte{0xCA, 0xFE})
+       require.NoError(t, err)
+       ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+       defer cancel()
+       data, err := instance.Read(ctx, 2)
+       require.NoError(t, err)
+       assert.Equal(t, []byte{0xCA, 0xFE}, data)
+
+       // A read into silence now actually times out instead of hanging:
+       // SetReadDeadline is no longer a no-op.
+       shortCtx, shortCancel := context.WithTimeout(context.Background(), 
100*time.Millisecond)
+       defer shortCancel()
+       start := time.Now()
+       _, err = instance.Read(shortCtx, 1)
+       require.Error(t, err)
+       assert.Less(t, time.Since(start), 5*time.Second)
+}
+
+func TestTransportInstance_CloseIsIdempotentOnPTY(t *testing.T) {
+       _, slavePath := openPTY(t)
+       instance := NewTransportInstance(slavePath, 9600, 1, NewTransport())
+       require.NoError(t, instance.Connect(context.Background()))
+       require.NoError(t, instance.Close())
+       assert.False(t, instance.IsConnected())
+       require.NoError(t, instance.Close(), "second close must be a no-op")
+}
diff --git a/plc4go/spi/transports/serial/TransportInstance_test.go 
b/plc4go/spi/transports/serial/TransportInstance_test.go
index 132489cad9..e3f5f76145 100644
--- a/plc4go/spi/transports/serial/TransportInstance_test.go
+++ b/plc4go/spi/transports/serial/TransportInstance_test.go
@@ -21,12 +21,12 @@ package serial
 
 import (
        "bufio"
-       "io"
        "testing"
        "time"
 
        "github.com/stretchr/testify/assert"
 
+       "github.com/apache/plc4x/plc4go/spi/transports/serial/serialport"
        transportUtils "github.com/apache/plc4x/plc4go/spi/transports/utils"
 )
 
@@ -60,7 +60,7 @@ func TestTransportInstance_Close(t *testing.T) {
                BaudRate                         uint
                ConnectTimeout                   uint32
                transport                        *Transport
-               serialPort                       io.ReadWriteCloser
+               serialPort                       serialport.Port
                reader                           *bufio.Reader
        }
        tests := []struct {
@@ -95,7 +95,7 @@ func TestTransportInstance_Connect(t *testing.T) {
                BaudRate                         uint
                ConnectTimeout                   uint32
                transport                        *Transport
-               serialPort                       io.ReadWriteCloser
+               serialPort                       serialport.Port
                reader                           *bufio.Reader
        }
        tests := []struct {
@@ -130,7 +130,7 @@ func TestTransportInstance_GetReader(t *testing.T) {
                BaudRate                         uint
                ConnectTimeout                   uint32
                transport                        *Transport
-               serialPort                       io.ReadWriteCloser
+               serialPort                       serialport.Port
                reader                           *bufio.Reader
        }
        tests := []struct {
@@ -165,7 +165,7 @@ func TestTransportInstance_IsConnected(t *testing.T) {
                BaudRate                         uint
                ConnectTimeout                   uint32
                transport                        *Transport
-               serialPort                       io.ReadWriteCloser
+               serialPort                       serialport.Port
                reader                           *bufio.Reader
        }
        tests := []struct {
@@ -200,7 +200,7 @@ func TestTransportInstance_String(t *testing.T) {
                BaudRate                         uint
                ConnectTimeout                   uint32
                transport                        *Transport
-               serialPort                       io.ReadWriteCloser
+               serialPort                       serialport.Port
                reader                           *bufio.Reader
        }
        tests := []struct {
@@ -235,7 +235,7 @@ func TestTransportInstance_Write(t *testing.T) {
                BaudRate                         uint
                ConnectTimeout                   uint32
                transport                        *Transport
-               serialPort                       io.ReadWriteCloser
+               serialPort                       serialport.Port
                reader                           *bufio.Reader
        }
        type args struct {
diff --git a/plc4go/spi/transports/serial/serialport/apply_darwin.go 
b/plc4go/spi/transports/serial/serialport/apply_darwin.go
new file mode 100644
index 0000000000..bc3135c1e8
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/apply_darwin.go
@@ -0,0 +1,66 @@
+//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 (
+       "os"
+
+       "golang.org/x/sys/unix"
+)
+
+// darwinStandardBaudRates are the rates TIOCSETA accepts directly
+// (Darwin speed_t values are numeric, so Bxxx == the rate itself).
+var darwinStandardBaudRates = map[uint]bool{
+       50: true, 75: true, 110: true, 134: true, 150: true, 200: true,
+       300: true, 600: true, 1200: true, 1800: true, 2400: true, 4800: true,
+       7200: true, 9600: true, 14400: true, 19200: true, 28800: true,
+       38400: true, 57600: true, 76800: true, 115200: true, 230400: true,
+}
+
+// ioctlIOSSIOSPEED is IOSSIOSPEED from IOKit's serial/ioss.h, which sets
+// arbitrary baud rates. Not defined in x/sys/unix; the value (with its
+// 4-byte payload encoding) is the one established in the wild by
+// github.com/jacobsa/go-serial (Apache License 2.0).
+const ioctlIOSSIOSPEED = 0x80045402
+
+// applySettings derived from github.com/jacobsa/go-serial (Apache License 
2.0).
+func applySettings(fd int, t *unix.Termios, baud uint) error {
+       if darwinStandardBaudRates[baud] {
+               t.Ispeed = uint64(baud)
+               t.Ospeed = uint64(baud)
+               if err := unix.IoctlSetTermios(fd, unix.TIOCSETA, t); err != 
nil {
+                       return os.NewSyscallError("ioctl TIOCSETA", err)
+               }
+               return nil
+       }
+       // Non-standard rate: configure everything else with a placeholder
+       // standard rate first, then set the real rate via IOSSIOSPEED.
+       t.Ispeed = 19200
+       t.Ospeed = 19200
+       if err := unix.IoctlSetTermios(fd, unix.TIOCSETA, t); err != nil {
+               return os.NewSyscallError("ioctl TIOCSETA", err)
+       }
+       if err := unix.IoctlSetPointerInt(fd, ioctlIOSSIOSPEED, int(baud)); err 
!= nil {
+               return os.NewSyscallError("ioctl IOSSIOSPEED", err)
+       }
+       return nil
+}
diff --git a/plc4go/spi/transports/serial/serialport/apply_freebsd.go 
b/plc4go/spi/transports/serial/serialport/apply_freebsd.go
new file mode 100644
index 0000000000..04293587cd
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/apply_freebsd.go
@@ -0,0 +1,39 @@
+//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 (
+       "os"
+
+       "golang.org/x/sys/unix"
+)
+
+// applySettings sets the rate directly: FreeBSD speed_t values are numeric
+// and the driver rejects rates it cannot provide.
+func applySettings(fd int, t *unix.Termios, baud uint) error {
+       t.Ispeed = uint32(baud)
+       t.Ospeed = uint32(baud)
+       if err := unix.IoctlSetTermios(fd, unix.TIOCSETA, t); err != nil {
+               return os.NewSyscallError("ioctl TIOCSETA", err)
+       }
+       return nil
+}
diff --git a/plc4go/spi/transports/serial/serialport/apply_linux.go 
b/plc4go/spi/transports/serial/serialport/apply_linux.go
new file mode 100644
index 0000000000..2d5e85b5de
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/apply_linux.go
@@ -0,0 +1,45 @@
+//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"
+)
+
+// applySettings configures the port via the kernel termios2 interface
+// (TCSETS2), which accepts arbitrary baud rates through BOTHER + explicit
+// Ispeed/Ospeed — so standard and non-standard rates share one code path.
+// Approach derived from github.com/jacobsa/go-serial (Apache License 2.0);
+// unlike that library we use the per-architecture unix.BOTHER constant
+// (e.g. 0x1000 on amd64 but 0x1f on ppc64) instead of a hardcoded value.
+func applySettings(fd int, t *unix.Termios, baud uint) error {
+       t.Cflag &^= unix.CBAUD
+       t.Cflag |= unix.BOTHER
+       t.Ispeed = uint32(baud)
+       t.Ospeed = uint32(baud)
+       if err := unix.IoctlSetTermios(fd, unix.TCSETS2, t); err != nil {
+               return os.NewSyscallError("ioctl TCSETS2", err)
+       }
+       return nil
+}
diff --git a/plc4go/spi/transports/serial/serialport/dcb.go 
b/plc4go/spi/transports/serial/serialport/dcb.go
new file mode 100644
index 0000000000..f507101cea
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/dcb.go
@@ -0,0 +1,86 @@
+/*
+ * 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 DCB field values, kept in a portable file (no x/sys/windows
+// 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
+
+       dcbOneStopBit   = 0 // ONESTOPBIT
+       dcbOne5StopBits = 1 // ONE5STOPBITS
+       dcbTwoStopBits  = 2 // TWOSTOPBITS
+
+       // Bit positions inside DCB.Flags (the C bitfield block starting at 
fBinary).
+       dcbFlagBinary              = 0x00000001 // fBinary: binary mode, 
required
+       dcbFlagParity              = 0x00000002 // fParity: enable parity 
checking
+       dcbFlagOutxCtsFlow         = 0x00000004 // fOutxCtsFlow: output 
suspended while CTS low
+       dcbFlagDtrControlEnable    = 0x00000010 // fDtrControl = 
DTR_CONTROL_ENABLE (1 << 4)
+       dcbFlagRtsControlEnable    = 0x00001000 // fRtsControl = 
RTS_CONTROL_ENABLE (1 << 12)
+       dcbFlagRtsControlHandshake = 0x00002000 // fRtsControl = 
RTS_CONTROL_HANDSHAKE (2 << 12)
+)
+
+// dcbSettings carries the computed DCB field values; port_windows.go copies
+// them into a windows.DCB.
+type dcbSettings struct {
+       BaudRate uint32
+       ByteSize uint8
+       Parity   uint8
+       StopBits uint8
+       Flags    uint32
+}
+
+// makeDCBSettings translates an already-normalized Config into DCB field
+// values. DTR is asserted on open (DTR_CONTROL_ENABLE); RTS is asserted
+// unless hardware flow control hands it to the driver.
+func makeDCBSettings(cfg Config) dcbSettings {
+       s := dcbSettings{
+               BaudRate: uint32(cfg.BaudRate),
+               ByteSize: uint8(cfg.DataBits),
+               Flags:    dcbFlagBinary | dcbFlagDtrControlEnable,
+       }
+       switch cfg.Parity {
+       case ParityOdd:
+               s.Parity = dcbOddParity
+               s.Flags |= dcbFlagParity
+       case ParityEven:
+               s.Parity = dcbEvenParity
+               s.Flags |= dcbFlagParity
+       default:
+               s.Parity = dcbNoParity
+       }
+       switch cfg.StopBits {
+       case StopBitsOnePointFive:
+               s.StopBits = dcbOne5StopBits
+       case StopBitsTwo:
+               s.StopBits = dcbTwoStopBits
+       default:
+               s.StopBits = dcbOneStopBit
+       }
+       if cfg.RTSCTSFlowControl {
+               s.Flags |= dcbFlagOutxCtsFlow | dcbFlagRtsControlHandshake
+       } else {
+               s.Flags |= dcbFlagRtsControlEnable
+       }
+       return s
+}
diff --git a/plc4go/spi/transports/serial/serialport/dcb_test.go 
b/plc4go/spi/transports/serial/serialport/dcb_test.go
new file mode 100644
index 0000000000..e601aea3a1
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/dcb_test.go
@@ -0,0 +1,102 @@
+/*
+ * 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"
+)
+
+// Expected values quoted from Microsoft's winbase.h documentation:
+//
+//     Parity:   NOPARITY=0, ODDPARITY=1, EVENPARITY=2
+//     StopBits: ONESTOPBIT=0, ONE5STOPBITS=1, TWOSTOPBITS=2
+//     Flags bits: fBinary=1<<0, fParity=1<<1, fOutxCtsFlow=1<<2,
+//                 fDtrControl(2 bits)@4 (DTR_CONTROL_ENABLE=1),
+//                 fRtsControl(2 bits)@12 (RTS_CONTROL_ENABLE=1, 
RTS_CONTROL_HANDSHAKE=2)
+func TestMakeDCBSettings(t *testing.T) {
+       tests := []struct {
+               name string
+               cfg  Config
+               want dcbSettings
+       }{
+               {
+                       name: "9600 8N1 no flow control",
+                       cfg:  Config{BaudRate: 9600, DataBits: 8, StopBits: 
StopBitsOne, Parity: ParityNone},
+                       want: dcbSettings{
+                               BaudRate: 9600,
+                               ByteSize: 8,
+                               Parity:   0,                   // NOPARITY
+                               StopBits: 0,                   // ONESTOPBIT
+                               Flags:    0x1 | 0x10 | 0x1000, // fBinary | 
DTR_CONTROL_ENABLE | RTS_CONTROL_ENABLE
+                       },
+               },
+               {
+                       name: "19200 7E2",
+                       cfg:  Config{BaudRate: 19200, DataBits: 7, StopBits: 
StopBitsTwo, Parity: ParityEven},
+                       want: dcbSettings{
+                               BaudRate: 19200,
+                               ByteSize: 7,
+                               Parity:   2,                         // 
EVENPARITY
+                               StopBits: 2,                         // 
TWOSTOPBITS
+                               Flags:    0x1 | 0x2 | 0x10 | 0x1000, // ... | 
fParity | ...
+                       },
+               },
+               {
+                       name: "odd parity",
+                       cfg:  Config{BaudRate: 9600, DataBits: 8, StopBits: 
StopBitsOne, Parity: ParityOdd},
+                       want: dcbSettings{
+                               BaudRate: 9600,
+                               ByteSize: 8,
+                               Parity:   1, // ODDPARITY
+                               StopBits: 0,
+                               Flags:    0x1 | 0x2 | 0x10 | 0x1000,
+                       },
+               },
+               {
+                       name: "1.5 stop bits with 5 data bits",
+                       cfg:  Config{BaudRate: 9600, DataBits: 5, StopBits: 
StopBitsOnePointFive, Parity: ParityNone},
+                       want: dcbSettings{
+                               BaudRate: 9600,
+                               ByteSize: 5,
+                               Parity:   0,
+                               StopBits: 1, // ONE5STOPBITS
+                               Flags:    0x1 | 0x10 | 0x1000,
+                       },
+               },
+               {
+                       name: "RTS/CTS flow control switches RTS to handshake 
and enables CTS sensitivity",
+                       cfg:  Config{BaudRate: 115200, DataBits: 8, StopBits: 
StopBitsOne, Parity: ParityNone, RTSCTSFlowControl: true},
+                       want: dcbSettings{
+                               BaudRate: 115200,
+                               ByteSize: 8,
+                               Parity:   0,
+                               StopBits: 0,
+                               Flags:    0x1 | 0x4 | 0x10 | 0x2000, // fBinary 
| fOutxCtsFlow | DTR_CONTROL_ENABLE | RTS_CONTROL_HANDSHAKE
+                       },
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert.Equal(t, tt.want, makeDCBSettings(tt.cfg))
+               })
+       }
+}
diff --git a/plc4go/spi/transports/serial/serialport/mocks_test.go 
b/plc4go/spi/transports/serial/serialport/mocks_test.go
new file mode 100644
index 0000000000..7d0e108f4e
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/mocks_test.go
@@ -0,0 +1,323 @@
+/*
+ * 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.
+ */
+
+// Code generated by mockery; DO NOT EDIT.
+// github.com/vektra/mockery
+// template: testify
+
+package serialport
+
+import (
+       "time"
+
+       mock "github.com/stretchr/testify/mock"
+)
+
+// NewMockPort creates a new instance of MockPort. 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 NewMockPort(t interface {
+       mock.TestingT
+       Cleanup(func())
+}) *MockPort {
+       mock := &MockPort{}
+       mock.Mock.Test(t)
+
+       t.Cleanup(func() { mock.AssertExpectations(t) })
+
+       return mock
+}
+
+// MockPort is an autogenerated mock type for the Port type
+type MockPort struct {
+       mock.Mock
+}
+
+type MockPort_Expecter struct {
+       mock *mock.Mock
+}
+
+func (_m *MockPort) EXPECT() *MockPort_Expecter {
+       return &MockPort_Expecter{mock: &_m.Mock}
+}
+
+// Close provides a mock function for the type MockPort
+func (_mock *MockPort) 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
+}
+
+// MockPort_Close_Call is a *mock.Call that shadows Run/Return methods with 
type explicit version for method 'Close'
+type MockPort_Close_Call struct {
+       *mock.Call
+}
+
+// Close is a helper method to define mock.On call
+func (_e *MockPort_Expecter) Close() *MockPort_Close_Call {
+       return &MockPort_Close_Call{Call: _e.mock.On("Close")}
+}
+
+func (_c *MockPort_Close_Call) Run(run func()) *MockPort_Close_Call {
+       _c.Call.Run(func(args mock.Arguments) {
+               run()
+       })
+       return _c
+}
+
+func (_c *MockPort_Close_Call) Return(err error) *MockPort_Close_Call {
+       _c.Call.Return(err)
+       return _c
+}
+
+func (_c *MockPort_Close_Call) RunAndReturn(run func() error) 
*MockPort_Close_Call {
+       _c.Call.Return(run)
+       return _c
+}
+
+// Read provides a mock function for the type MockPort
+func (_mock *MockPort) 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
+}
+
+// MockPort_Read_Call is a *mock.Call that shadows Run/Return methods with 
type explicit version for method 'Read'
+type MockPort_Read_Call struct {
+       *mock.Call
+}
+
+// Read is a helper method to define mock.On call
+//   - p []byte
+func (_e *MockPort_Expecter) Read(p interface{}) *MockPort_Read_Call {
+       return &MockPort_Read_Call{Call: _e.mock.On("Read", p)}
+}
+
+func (_c *MockPort_Read_Call) Run(run func(p []byte)) *MockPort_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 *MockPort_Read_Call) Return(n int, err error) *MockPort_Read_Call {
+       _c.Call.Return(n, err)
+       return _c
+}
+
+func (_c *MockPort_Read_Call) RunAndReturn(run func(p []byte) (int, error)) 
*MockPort_Read_Call {
+       _c.Call.Return(run)
+       return _c
+}
+
+// SetReadDeadline provides a mock function for the type MockPort
+func (_mock *MockPort) 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
+}
+
+// MockPort_SetReadDeadline_Call is a *mock.Call that shadows Run/Return 
methods with type explicit version for method 'SetReadDeadline'
+type MockPort_SetReadDeadline_Call struct {
+       *mock.Call
+}
+
+// SetReadDeadline is a helper method to define mock.On call
+//   - t time.Time
+func (_e *MockPort_Expecter) SetReadDeadline(t interface{}) 
*MockPort_SetReadDeadline_Call {
+       return &MockPort_SetReadDeadline_Call{Call: 
_e.mock.On("SetReadDeadline", t)}
+}
+
+func (_c *MockPort_SetReadDeadline_Call) Run(run func(t time.Time)) 
*MockPort_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 *MockPort_SetReadDeadline_Call) Return(err error) 
*MockPort_SetReadDeadline_Call {
+       _c.Call.Return(err)
+       return _c
+}
+
+func (_c *MockPort_SetReadDeadline_Call) RunAndReturn(run func(t time.Time) 
error) *MockPort_SetReadDeadline_Call {
+       _c.Call.Return(run)
+       return _c
+}
+
+// SetWriteDeadline provides a mock function for the type MockPort
+func (_mock *MockPort) 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
+}
+
+// MockPort_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return 
methods with type explicit version for method 'SetWriteDeadline'
+type MockPort_SetWriteDeadline_Call struct {
+       *mock.Call
+}
+
+// SetWriteDeadline is a helper method to define mock.On call
+//   - t time.Time
+func (_e *MockPort_Expecter) SetWriteDeadline(t interface{}) 
*MockPort_SetWriteDeadline_Call {
+       return &MockPort_SetWriteDeadline_Call{Call: 
_e.mock.On("SetWriteDeadline", t)}
+}
+
+func (_c *MockPort_SetWriteDeadline_Call) Run(run func(t time.Time)) 
*MockPort_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 *MockPort_SetWriteDeadline_Call) Return(err error) 
*MockPort_SetWriteDeadline_Call {
+       _c.Call.Return(err)
+       return _c
+}
+
+func (_c *MockPort_SetWriteDeadline_Call) RunAndReturn(run func(t time.Time) 
error) *MockPort_SetWriteDeadline_Call {
+       _c.Call.Return(run)
+       return _c
+}
+
+// Write provides a mock function for the type MockPort
+func (_mock *MockPort) 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
+}
+
+// MockPort_Write_Call is a *mock.Call that shadows Run/Return methods with 
type explicit version for method 'Write'
+type MockPort_Write_Call struct {
+       *mock.Call
+}
+
+// Write is a helper method to define mock.On call
+//   - p []byte
+func (_e *MockPort_Expecter) Write(p interface{}) *MockPort_Write_Call {
+       return &MockPort_Write_Call{Call: _e.mock.On("Write", p)}
+}
+
+func (_c *MockPort_Write_Call) Run(run func(p []byte)) *MockPort_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 *MockPort_Write_Call) Return(n int, err error) *MockPort_Write_Call {
+       _c.Call.Return(n, err)
+       return _c
+}
+
+func (_c *MockPort_Write_Call) RunAndReturn(run func(p []byte) (int, error)) 
*MockPort_Write_Call {
+       _c.Call.Return(run)
+       return _c
+}
diff --git a/plc4go/spi/transports/serial/serialport/port_unix.go 
b/plc4go/spi/transports/serial/serialport/port_unix.go
new file mode 100644
index 0000000000..5db37c362f
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/port_unix.go
@@ -0,0 +1,71 @@
+//go:build linux || 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 (
+       "fmt"
+       "os"
+       "time"
+
+       "golang.org/x/sys/unix"
+)
+
+// 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).
+//
+// The termios configuration is applied through SyscallConn().Control and
+// never via Fd(): calling Fd() would switch the file to blocking mode and
+// de-register it from the poller, silently breaking deadline support.
+func openPort(portName string, cfg Config) (Port, error) {
+       t, err := makeTermios(cfg)
+       if err != nil {
+               return nil, err
+       }
+       f, err := os.OpenFile(portName, 
os.O_RDWR|unix.O_NOCTTY|unix.O_NONBLOCK, 0)
+       if err != nil {
+               return nil, fmt.Errorf("serialport: opening %s: %w", portName, 
err)
+       }
+       rawConn, err := f.SyscallConn()
+       if err != nil {
+               _ = f.Close()
+               return nil, fmt.Errorf("serialport: accessing raw connection of 
%s: %w", portName, err)
+       }
+       var applyErr error
+       if err := rawConn.Control(func(fd uintptr) {
+               applyErr = applySettings(int(fd), t, cfg.BaudRate)
+       }); err != nil {
+               _ = f.Close()
+               return nil, fmt.Errorf("serialport: raw control on %s: %w", 
portName, err)
+       }
+       if applyErr != nil {
+               _ = f.Close()
+               return nil, fmt.Errorf("serialport: configuring %s: %w", 
portName, applyErr)
+       }
+       // Deadlines are part of the Port contract: fail fast if the runtime
+       // poller could not adopt this fd instead of degrading silently.
+       if err := f.SetReadDeadline(time.Time{}); err != nil {
+               _ = f.Close()
+               return nil, fmt.Errorf("serialport: %s does not support I/O 
deadlines: %w", portName, err)
+       }
+       return f, nil
+}
diff --git a/plc4go/spi/transports/serial/serialport/port_unsupported.go 
b/plc4go/spi/transports/serial/serialport/port_unsupported.go
new file mode 100644
index 0000000000..01d6e8988c
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/port_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 openPort(portName string, cfg Config) (Port, error) {
+       return nil, fmt.Errorf("serialport: %w: %s", ErrUnsupportedPlatform, 
runtime.GOOS)
+}
diff --git a/plc4go/spi/transports/serial/serialport/port_windows.go 
b/plc4go/spi/transports/serial/serialport/port_windows.go
new file mode 100644
index 0000000000..a75668a860
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/port_windows.go
@@ -0,0 +1,326 @@
+//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 (
+       "errors"
+       "fmt"
+       "os"
+       "strings"
+       "sync"
+       "time"
+       "unsafe"
+
+       "golang.org/x/sys/windows"
+)
+
+// winPort implements Port on Windows with synchronous I/O. Deadlines are
+// emulated with COMMTIMEOUTS recomputed before every blocking attempt:
+// each ReadFile/WriteFile call is bounded by the remaining time to the
+// deadline (capped at maxIOSliceMillis so Close and deadline changes are
+// observed even on a port with no traffic).
+//
+// Reads use the documented COMMTIMEOUTS pattern for "return as soon as at
+// least one byte is available, or after the total timeout": interval and
+// multiplier MAXDWORD with a bounded positive total constant.
+//
+// COMMTIMEOUTS is one flat struct per handle governing both directions, so
+// every SetCommTimeouts call (whether issued by Read or Write) repopulates
+// BOTH directions from a single snapshot of both deadlines - leaving the
+// other direction's fields untouched or zeroed would silently corrupt it
+// (a zero write timeout means "wait forever"). Because the two directions
+// recompute independently, a deadline change made while the other
+// direction is mid-attempt can leave that attempt's view of the change
+// 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)
+// 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.
+type winPort struct {
+       mu            sync.Mutex
+       handle        windows.Handle
+       closed        bool
+       ioCount       int // number of Read/Write attempts currently between 
beginIO and endIO
+       readDeadline  time.Time
+       writeDeadline time.Time
+}
+
+const winMaxDword = 0xFFFFFFFF
+
+func openPort(portName string, cfg Config) (Port, error) {
+       // The \\.\ prefix is required for COM10 and above and harmless below.
+       path := portName
+       if !strings.HasPrefix(path, `\\.\`) {
+               path = `\\.\` + path
+       }
+       path16, err := windows.UTF16PtrFromString(path)
+       if err != nil {
+               return nil, fmt.Errorf("serialport: invalid port name %q: %w", 
portName, err)
+       }
+       handle, err := windows.CreateFile(path16,
+               windows.GENERIC_READ|windows.GENERIC_WRITE,
+               0,   // exclusive access
+               nil, // default security attributes
+               windows.OPEN_EXISTING,
+               0, // synchronous I/O
+               0)
+       if err != nil {
+               return nil, fmt.Errorf("serialport: opening %s: %w", portName, 
os.NewSyscallError("CreateFile", err))
+       }
+       p := &winPort{handle: handle}
+       if err := p.configure(cfg); err != nil {
+               _ = windows.CloseHandle(handle)
+               return nil, fmt.Errorf("serialport: configuring %s: %w", 
portName, err)
+       }
+       return p, nil
+}
+
+func (p *winPort) configure(cfg Config) error {
+       if err := windows.SetupComm(p.handle, 4096, 4096); err != nil {
+               return os.NewSyscallError("SetupComm", err)
+       }
+       var dcb windows.DCB
+       dcb.DCBlength = uint32(unsafe.Sizeof(dcb))
+       if err := windows.GetCommState(p.handle, &dcb); err != nil {
+               return os.NewSyscallError("GetCommState", err)
+       }
+       s := makeDCBSettings(cfg)
+       dcb.BaudRate = s.BaudRate
+       dcb.ByteSize = s.ByteSize
+       dcb.Parity = s.Parity
+       dcb.StopBits = s.StopBits
+       dcb.Flags = s.Flags
+       if err := windows.SetCommState(p.handle, &dcb); err != nil {
+               return os.NewSyscallError("SetCommState", err)
+       }
+       return nil
+}
+
+// commTimeouts builds a COMMTIMEOUTS value covering both directions from
+// the two slice lengths already computed for this instant. The read
+// fields always use the MAXDWORD/MAXDWORD/constant "return on first byte
+// or after the constant" pattern; the write multiplier stays 0 so the
+// constant alone bounds the write.
+func commTimeouts(readSliceMs, writeSliceMs uint32) windows.CommTimeouts {
+       return windows.CommTimeouts{
+               ReadIntervalTimeout:        winMaxDword,
+               ReadTotalTimeoutMultiplier: winMaxDword,
+               ReadTotalTimeoutConstant:   readSliceMs,
+               WriteTotalTimeoutConstant:  writeSliceMs,
+       }
+}
+
+// sliceMillisForOtherDirection is like ioSliceMillis but never reports an
+// expired deadline as 0: a 0 total timeout constant means "wait forever"
+// to COMMTIMEOUTS (for the write side) or is at best ambiguous (for the
+// read side), which is exactly the cross-contamination this helper exists
+// to avoid. An already-expired deadline instead gets the smallest positive
+// slice so the other direction times out promptly rather than blocking;
+// that direction's own attempt independently detects the expiry as an
+// error.
+func sliceMillisForOtherDirection(now, deadline time.Time) uint32 {
+       ms, ok := ioSliceMillis(now, deadline)
+       if !ok {
+               return 1
+       }
+       return ms
+}
+
+// beginIO validates the port is still open, registers an in-flight
+// attempt (so Close knows to wait for it), and returns a single
+// consistent snapshot of the handle and both deadlines. Every attempt
+// that successfully calls beginIO must call endIO exactly once when it is
+// done, regardless of outcome.
+func (p *winPort) beginIO() (handle windows.Handle, readDeadline, 
writeDeadline time.Time, err error) {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+       if p.closed {
+               return 0, time.Time{}, time.Time{}, os.ErrClosed
+       }
+       p.ioCount++
+       return p.handle, p.readDeadline, p.writeDeadline, nil
+}
+
+// endIO retires an in-flight attempt registered by beginIO.
+func (p *winPort) endIO() {
+       p.mu.Lock()
+       p.ioCount--
+       p.mu.Unlock()
+}
+
+func (p *winPort) isClosed() bool {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+       return p.closed
+}
+
+func (p *winPort) Read(buf []byte) (int, error) {
+       if len(buf) == 0 {
+               return 0, nil
+       }
+       for {
+               n, done, err := p.readAttempt(buf)
+               if done {
+                       return n, err
+               }
+       }
+}
+
+// readAttempt performs one bounded ReadFile call. done is true when the
+// result should be returned to the caller as-is; false means the read
+// deadline has not yet expired and Read should loop for another attempt.
+func (p *winPort) readAttempt(buf []byte) (n int, done bool, err error) {
+       handle, readDeadline, writeDeadline, err := p.beginIO()
+       if err != nil {
+               return 0, true, err
+       }
+       defer p.endIO()
+
+       now := time.Now()
+       readSliceMs, ok := ioSliceMillis(now, readDeadline)
+       if !ok {
+               return 0, true, os.ErrDeadlineExceeded
+       }
+       timeouts := commTimeouts(readSliceMs, sliceMillisForOtherDirection(now, 
writeDeadline))
+       if err := windows.SetCommTimeouts(handle, &timeouts); err != nil {
+               return 0, true, os.NewSyscallError("SetCommTimeouts", err)
+       }
+       var got uint32
+       if err := windows.ReadFile(handle, buf, &got, nil); err != nil {
+               if errors.Is(err, windows.ERROR_OPERATION_ABORTED) && 
p.isClosed() {
+                       return int(got), true, os.ErrClosed
+               }
+               return int(got), true, os.NewSyscallError("ReadFile", err)
+       }
+       if got > 0 {
+               return int(got), true, nil
+       }
+       // Timed out with no data: expired deadline is an error, absence of
+       // a deadline means keep waiting in bounded slices.
+       if !readDeadline.IsZero() && !time.Now().Before(readDeadline) {
+               return 0, true, os.ErrDeadlineExceeded
+       }
+       return 0, false, nil
+}
+
+func (p *winPort) Write(buf []byte) (int, error) {
+       written := 0
+       for {
+               n, done, err := p.writeAttempt(buf, written)
+               written += n
+               if done {
+                       return written, err
+               }
+       }
+}
+
+// writeAttempt performs one bounded WriteFile call for the remainder of
+// buf starting at offset written. n is the number of bytes written by
+// this attempt alone. done is true when the result should be returned to
+// the caller; false means the write deadline has not yet expired and
+// Write should loop for another attempt.
+func (p *winPort) writeAttempt(buf []byte, written int) (n int, done bool, err 
error) {
+       handle, readDeadline, writeDeadline, err := p.beginIO()
+       if err != nil {
+               return 0, true, err
+       }
+       defer p.endIO()
+
+       now := time.Now()
+       writeSliceMs, ok := ioSliceMillis(now, writeDeadline)
+       if !ok {
+               return 0, true, os.ErrDeadlineExceeded
+       }
+       timeouts := commTimeouts(sliceMillisForOtherDirection(now, 
readDeadline), writeSliceMs)
+       if err := windows.SetCommTimeouts(handle, &timeouts); err != nil {
+               return 0, true, os.NewSyscallError("SetCommTimeouts", err)
+       }
+       var got uint32
+       err = windows.WriteFile(handle, buf[written:], &got, nil)
+       if err != nil {
+               if errors.Is(err, windows.ERROR_OPERATION_ABORTED) && 
p.isClosed() {
+                       return int(got), true, os.ErrClosed
+               }
+               return int(got), true, os.NewSyscallError("WriteFile", err)
+       }
+       if written+int(got) == len(buf) {
+               return int(got), true, nil
+       }
+       // Partial write: the slice timed out before the buffer drained.
+       if !writeDeadline.IsZero() && !time.Now().Before(writeDeadline) {
+               return int(got), true, os.ErrDeadlineExceeded
+       }
+       return int(got), false, nil
+}
+
+func (p *winPort) SetReadDeadline(t time.Time) error {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+       if p.closed {
+               return os.ErrClosed
+       }
+       p.readDeadline = t
+       return nil
+}
+
+func (p *winPort) SetWriteDeadline(t time.Time) error {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+       if p.closed {
+               return os.ErrClosed
+       }
+       p.writeDeadline = t
+       return nil
+}
+
+func (p *winPort) Close() error {
+       p.mu.Lock()
+       if p.closed {
+               p.mu.Unlock()
+               return nil
+       }
+       p.closed = true
+       handle := p.handle
+       // Drain attempts already registered via beginIO before releasing the
+       // handle: closed is now true, so no new attempt can increment
+       // ioCount past this point (beginIO checks closed under mu), but an
+       // attempt that snapshotted the handle just before we set closed may
+       // not have issued its ReadFile/WriteFile yet. Repeatedly cancel
+       // outstanding I/O until every registered attempt has reported back
+       // via endIO; CancelIoEx is a no-op if nothing is in flight yet, so we
+       // retry on a short poll until the racing attempt's syscall has
+       // actually started (and been cancelled) or completed.
+       for p.ioCount > 0 {
+               p.mu.Unlock()
+               _ = windows.CancelIoEx(handle, nil)
+               time.Sleep(time.Millisecond)
+               p.mu.Lock()
+       }
+       err := windows.CloseHandle(handle)
+       p.mu.Unlock()
+       if err != nil {
+               return os.NewSyscallError("CloseHandle", err)
+       }
+       return nil
+}
diff --git a/plc4go/spi/transports/serial/serialport/pty_linux_test.go 
b/plc4go/spi/transports/serial/serialport/pty_linux_test.go
new file mode 100644
index 0000000000..df2ee516ef
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/pty_linux_test.go
@@ -0,0 +1,147 @@
+//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 (
+       "errors"
+       "fmt"
+       "os"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       "golang.org/x/sys/unix"
+)
+
+// openPTY allocates a pseudo-terminal pair and returns the master side plus
+// the slave device path. A PTY slave accepts the same termios ioctls as a
+// real serial device (baud is faked but accepted), which lets the full
+// open→configure→read/write→deadline→close path run without hardware.
+func openPTY(t *testing.T) (master *os.File, slavePath string) {
+       t.Helper()
+       master, err := os.OpenFile("/dev/ptmx", os.O_RDWR|unix.O_NOCTTY, 0)
+       require.NoError(t, err, "opening /dev/ptmx")
+       t.Cleanup(func() { _ = master.Close() })
+       ptn, err := unix.IoctlGetInt(int(master.Fd()), unix.TIOCGPTN)
+       require.NoError(t, err, "TIOCGPTN")
+       require.NoError(t, unix.IoctlSetPointerInt(int(master.Fd()), 
unix.TIOCSPTLCK, 0), "unlocking pty slave")
+       return master, fmt.Sprintf("/dev/pts/%d", ptn)
+}
+
+func TestOpenOnPTY_ReadAndWrite(t *testing.T) {
+       master, slavePath := openPTY(t)
+
+       port, err := Open(slavePath, Config{BaudRate: 115200})
+       require.NoError(t, err)
+       defer port.Close()
+
+       // master -> port
+       _, err = master.Write([]byte("hello"))
+       require.NoError(t, err)
+       require.NoError(t, port.SetReadDeadline(time.Now().Add(5*time.Second)))
+       buf := make([]byte, 16)
+       n, err := port.Read(buf)
+       require.NoError(t, err)
+       assert.Equal(t, "hello", string(buf[:n]))
+
+       // port -> master
+       _, err = port.Write([]byte("world"))
+       require.NoError(t, err)
+       n, err = master.Read(buf)
+       require.NoError(t, err)
+       assert.Equal(t, "world", string(buf[:n]))
+}
+
+func TestOpenOnPTY_FullConfigAccepted(t *testing.T) {
+       // A PTY accepts arbitrary termios settings; this proves applySettings
+       // round-trips a non-default configuration without error.
+       _, slavePath := openPTY(t)
+       port, err := Open(slavePath, Config{
+               BaudRate: 2500000, // non-standard on purpose: exercises the 
BOTHER path
+               DataBits: 7, StopBits: StopBitsTwo, Parity: ParityEven, 
RTSCTSFlowControl: true,
+       })
+       require.NoError(t, err)
+       require.NoError(t, port.Close())
+}
+
+func TestOpenOnPTY_ReadDeadlineExpires(t *testing.T) {
+       _, slavePath := openPTY(t)
+       port, err := Open(slavePath, Config{BaudRate: 9600})
+       require.NoError(t, err)
+       defer port.Close()
+
+       require.NoError(t, 
port.SetReadDeadline(time.Now().Add(50*time.Millisecond)))
+       start := time.Now()
+       _, err = port.Read(make([]byte, 1))
+       elapsed := time.Since(start)
+
+       require.Error(t, err)
+       assert.True(t, errors.Is(err, os.ErrDeadlineExceeded), "want 
os.ErrDeadlineExceeded, got %v", err)
+       assert.GreaterOrEqual(t, elapsed, 50*time.Millisecond)
+       assert.Less(t, elapsed, 5*time.Second)
+}
+
+func TestOpenOnPTY_ClearingDeadlineRestoresBlockingRead(t *testing.T) {
+       master, slavePath := openPTY(t)
+       port, err := Open(slavePath, Config{BaudRate: 9600})
+       require.NoError(t, err)
+       defer port.Close()
+
+       // Expired deadline: read fails immediately.
+       require.NoError(t, port.SetReadDeadline(time.Now().Add(-time.Second)))
+       _, err = port.Read(make([]byte, 1))
+       require.True(t, errors.Is(err, os.ErrDeadlineExceeded))
+
+       // Cleared deadline: read blocks until data arrives.
+       require.NoError(t, port.SetReadDeadline(time.Time{}))
+       go func() {
+               time.Sleep(20 * time.Millisecond)
+               _, _ = master.Write([]byte{0x42})
+       }()
+       buf := make([]byte, 1)
+       n, err := port.Read(buf)
+       require.NoError(t, err)
+       require.Equal(t, 1, n)
+       assert.Equal(t, byte(0x42), buf[0])
+}
+
+func TestOpenOnPTY_UseAfterCloseFails(t *testing.T) {
+       _, slavePath := openPTY(t)
+       port, err := Open(slavePath, Config{BaudRate: 9600})
+       require.NoError(t, err)
+       require.NoError(t, port.Close())
+
+       _, err = port.Read(make([]byte, 1))
+       require.Error(t, err)
+       assert.True(t, errors.Is(err, os.ErrClosed), "read after close: want 
os.ErrClosed, got %v", err)
+
+       _, err = port.Write([]byte{0x01})
+       require.Error(t, err)
+       assert.True(t, errors.Is(err, os.ErrClosed), "write after close: want 
os.ErrClosed, got %v", err)
+}
+
+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")
+}
diff --git a/plc4go/spi/transports/serial/serialport/serialport.go 
b/plc4go/spi/transports/serial/serialport/serialport.go
new file mode 100644
index 0000000000..1b7b0413c4
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/serialport.go
@@ -0,0 +1,120 @@
+/*
+ * 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 provides access to serial ports (RS-232/RS-485 style
+// UART devices) with net.Conn-like read/write deadline semantics.
+//
+// It is an internal replacement for the unmaintained
+// github.com/jacobsa/go-serial dependency, informed by that library
+// (Apache License 2.0) and the underlying platform specifications
+// (POSIX termios, Windows DCB/COMMTIMEOUTS).
+package serialport
+
+import (
+       "errors"
+       "fmt"
+       "io"
+       "time"
+)
+
+// Parity configures the parity bit generated and checked by the UART.
+type Parity int
+
+const (
+       ParityNone Parity = iota
+       ParityOdd
+       ParityEven
+)
+
+// StopBits configures the number of stop bits.
+type StopBits int
+
+const (
+       StopBitsOne          StopBits = iota
+       StopBitsOnePointFive          // only supported on Windows, and only 
with 5 data bits
+       StopBitsTwo
+)
+
+// Config describes how a serial port is configured on Open. The zero value
+// of every field except BaudRate is a valid default (8 data bits, 1 stop
+// bit, no parity, no flow control).
+//
+// There are deliberately no VMIN/VTIME-style read-threshold options: ports
+// 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
+}
+
+// Port is an open serial port. Read blocks until at least one byte is
+// available, the read deadline expires (os.ErrDeadlineExceeded), or the
+// port is closed.
+type Port interface {
+       io.ReadWriteCloser
+       SetReadDeadline(t time.Time) error
+       SetWriteDeadline(t time.Time) error
+}
+
+// ErrUnsupportedPlatform is returned by Open 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
+// "COM3").
+func Open(portName string, cfg Config) (Port, error) {
+       if portName == "" {
+               return nil, errors.New("serialport: port name must not be 
empty")
+       }
+       normalized, err := cfg.normalize()
+       if err != nil {
+               return nil, err
+       }
+       return openPort(portName, normalized)
+}
+
+// normalize validates cfg and fills in defaults.
+func (c Config) normalize() (Config, error) {
+       if c.BaudRate == 0 {
+               return c, errors.New("serialport: baud rate must be greater 
than 0")
+       }
+       if c.DataBits == 0 {
+               c.DataBits = 8
+       }
+       if c.DataBits < 5 || c.DataBits > 8 {
+               return c, fmt.Errorf("serialport: invalid data bits %d (must be 
5..8)", c.DataBits)
+       }
+       switch c.StopBits {
+       case StopBitsOne, StopBitsOnePointFive, StopBitsTwo:
+       default:
+               return c, fmt.Errorf("serialport: invalid stop bits value %d", 
c.StopBits)
+       }
+       switch c.Parity {
+       case ParityNone, ParityOdd, ParityEven:
+       default:
+               return c, fmt.Errorf("serialport: invalid parity value %d", 
c.Parity)
+       }
+       if c.StopBits == StopBitsOnePointFive && c.DataBits != 5 {
+               return c, errors.New("serialport: 1.5 stop bits require 5 data 
bits")
+       }
+       return c, nil
+}
diff --git a/plc4go/spi/transports/serial/serialport/serialport_test.go 
b/plc4go/spi/transports/serial/serialport/serialport_test.go
new file mode 100644
index 0000000000..10c949e22c
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/serialport_test.go
@@ -0,0 +1,107 @@
+/*
+ * 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"
+)
+
+func TestConfigNormalize(t *testing.T) {
+       tests := []struct {
+               name    string
+               in      Config
+               want    Config
+               wantErr string
+       }{
+               {
+                       name: "defaults applied: zero data bits become 8",
+                       in:   Config{BaudRate: 9600},
+                       want: Config{BaudRate: 9600, DataBits: 8, StopBits: 
StopBitsOne, Parity: ParityNone},
+               },
+               {
+                       name: "explicit full config kept",
+                       in:   Config{BaudRate: 115200, DataBits: 7, StopBits: 
StopBitsTwo, Parity: ParityEven, RTSCTSFlowControl: true},
+                       want: Config{BaudRate: 115200, DataBits: 7, StopBits: 
StopBitsTwo, Parity: ParityEven, RTSCTSFlowControl: true},
+               },
+               {
+                       name:    "zero baud rate rejected",
+                       in:      Config{},
+                       wantErr: "baud rate",
+               },
+               {
+                       name:    "data bits below 5 rejected",
+                       in:      Config{BaudRate: 9600, DataBits: 4},
+                       wantErr: "data bits",
+               },
+               {
+                       name:    "data bits above 8 rejected",
+                       in:      Config{BaudRate: 9600, DataBits: 9},
+                       wantErr: "data bits",
+               },
+               {
+                       name:    "invalid stop bits enum rejected",
+                       in:      Config{BaudRate: 9600, StopBits: StopBits(99)},
+                       wantErr: "stop bits",
+               },
+               {
+                       name:    "invalid parity enum rejected",
+                       in:      Config{BaudRate: 9600, Parity: Parity(99)},
+                       wantErr: "parity",
+               },
+               {
+                       // Windows rule (only platform supporting 1.5): 1.5 
stop bits require 5 data bits.
+                       name:    "1.5 stop bits with 8 data bits rejected",
+                       in:      Config{BaudRate: 9600, StopBits: 
StopBitsOnePointFive},
+                       wantErr: "1.5 stop bits",
+               },
+               {
+                       name: "1.5 stop bits with 5 data bits accepted",
+                       in:   Config{BaudRate: 9600, DataBits: 5, StopBits: 
StopBitsOnePointFive},
+                       want: Config{BaudRate: 9600, DataBits: 5, StopBits: 
StopBitsOnePointFive, Parity: ParityNone},
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got, err := tt.in.normalize()
+                       if tt.wantErr != "" {
+                               require.Error(t, err)
+                               assert.Contains(t, err.Error(), tt.wantErr)
+                               return
+                       }
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.want, got)
+               })
+       }
+}
+
+func TestOpenRejectsEmptyPortName(t *testing.T) {
+       _, err := Open("", Config{BaudRate: 9600})
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "port name")
+}
+
+func TestOpenRejectsInvalidConfigBeforeTouchingPort(t *testing.T) {
+       _, err := Open("/dev/does-not-matter", Config{})
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "baud rate")
+}
diff --git a/plc4go/spi/transports/serial/serialport/termios_unix.go 
b/plc4go/spi/transports/serial/serialport/termios_unix.go
new file mode 100644
index 0000000000..00d11328da
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/termios_unix.go
@@ -0,0 +1,77 @@
+//go:build linux || 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"
+)
+
+// makeTermios translates an already-normalized Config into a raw-mode
+// termios structure per POSIX: non-canonical, no echo, no signal or
+// flow-control character processing, receiver enabled, modem control
+// lines ignored (CLOCAL). Baud rate is set separately by the per-OS
+// applySettings, because speed handling differs between kernels.
+func makeTermios(cfg Config) (*unix.Termios, error) {
+       t := &unix.Termios{}
+       t.Cflag |= unix.CLOCAL | unix.CREAD
+
+       switch cfg.DataBits {
+       case 5:
+               t.Cflag |= unix.CS5
+       case 6:
+               t.Cflag |= unix.CS6
+       case 7:
+               t.Cflag |= unix.CS7
+       case 8:
+               t.Cflag |= unix.CS8
+       }
+
+       switch cfg.StopBits {
+       case StopBitsOne:
+               // CSTOPB cleared
+       case StopBitsTwo:
+               t.Cflag |= unix.CSTOPB
+       case StopBitsOnePointFive:
+               return nil, errors.New("serialport: 1.5 stop bits are not 
supported on this platform")
+       }
+
+       switch cfg.Parity {
+       case ParityNone:
+       case ParityOdd:
+               t.Cflag |= unix.PARENB | unix.PARODD
+       case ParityEven:
+               t.Cflag |= unix.PARENB
+       }
+
+       if cfg.RTSCTSFlowControl {
+               t.Cflag |= unix.CRTSCTS
+       }
+
+       // 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).
+       t.Cc[unix.VMIN] = 1
+       t.Cc[unix.VTIME] = 0
+       return t, nil
+}
diff --git a/plc4go/spi/transports/serial/serialport/termios_unix_test.go 
b/plc4go/spi/transports/serial/serialport/termios_unix_test.go
new file mode 100644
index 0000000000..ac6307c003
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/termios_unix_test.go
@@ -0,0 +1,99 @@
+//go:build linux || 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/assert"
+       "github.com/stretchr/testify/require"
+       "golang.org/x/sys/unix"
+)
+
+// Expected flag values per POSIX (IEEE Std 1003.1) termios: raw,
+// non-canonical mode with receiver enabled and modem status ignored.
+func TestMakeTermios(t *testing.T) {
+       base := uint64(unix.CLOCAL | unix.CREAD)
+       tests := []struct {
+               name      string
+               cfg       Config
+               wantCflag uint64
+       }{
+               {
+                       name:      "8N1",
+                       cfg:       Config{BaudRate: 9600, DataBits: 8, 
StopBits: StopBitsOne, Parity: ParityNone},
+                       wantCflag: base | unix.CS8,
+               },
+               {
+                       name:      "7E1",
+                       cfg:       Config{BaudRate: 9600, DataBits: 7, 
StopBits: StopBitsOne, Parity: ParityEven},
+                       wantCflag: base | unix.CS7 | unix.PARENB,
+               },
+               {
+                       name:      "odd parity sets PARENB and PARODD",
+                       cfg:       Config{BaudRate: 9600, DataBits: 8, 
StopBits: StopBitsOne, Parity: ParityOdd},
+                       wantCflag: base | unix.CS8 | unix.PARENB | unix.PARODD,
+               },
+               {
+                       name:      "two stop bits set CSTOPB",
+                       cfg:       Config{BaudRate: 9600, DataBits: 8, 
StopBits: StopBitsTwo, Parity: ParityNone},
+                       wantCflag: base | unix.CS8 | unix.CSTOPB,
+               },
+               {
+                       name:      "5 data bits",
+                       cfg:       Config{BaudRate: 9600, DataBits: 5, 
StopBits: StopBitsOne, Parity: ParityNone},
+                       wantCflag: base | unix.CS5,
+               },
+               {
+                       name:      "6 data bits",
+                       cfg:       Config{BaudRate: 9600, DataBits: 6, 
StopBits: StopBitsOne, Parity: ParityNone},
+                       wantCflag: base | unix.CS6,
+               },
+               {
+                       name:      "hardware flow control sets CRTSCTS",
+                       cfg:       Config{BaudRate: 9600, DataBits: 8, 
StopBits: StopBitsOne, Parity: ParityNone, RTSCTSFlowControl: true},
+                       wantCflag: base | unix.CS8 | unix.CRTSCTS,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got, err := makeTermios(tt.cfg)
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.wantCflag, uint64(got.Cflag), 
"Cflag")
+                       // Raw mode: input/output/local processing fully 
disabled per POSIX
+                       // (no ICANON/ECHO/ISIG, no IXON/ICRNL/INPCK, no OPOST).
+                       assert.Zero(t, got.Iflag, "Iflag must be 0 (raw)")
+                       assert.Zero(t, got.Oflag, "Oflag must be 0 (raw)")
+                       assert.Zero(t, got.Lflag, "Lflag must be 0 (raw)")
+                       // VMIN/VTIME are documented intent only: the fd is 
non-blocking, so
+                       // the runtime poller (deadlines) governs read blocking 
behavior.
+                       assert.EqualValues(t, 1, got.Cc[unix.VMIN], "VMIN")
+                       assert.EqualValues(t, 0, got.Cc[unix.VTIME], "VTIME")
+               })
+       }
+}
+
+func TestMakeTermiosRejectsOnePointFiveStopBits(t *testing.T) {
+       _, err := makeTermios(Config{BaudRate: 9600, DataBits: 5, StopBits: 
StopBitsOnePointFive})
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "1.5 stop bits")
+}
diff --git a/plc4go/spi/transports/serial/serialport/timeouts.go 
b/plc4go/spi/transports/serial/serialport/timeouts.go
new file mode 100644
index 0000000000..ebd969b21f
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/timeouts.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 "time"
+
+// maxIOSliceMillis caps how long a single blocking I/O attempt may last on
+// platforms where blocking calls cannot be interrupted (Windows), so that
+// Close and deadline changes are observed within this bound.
+const maxIOSliceMillis = 60_000
+
+// ioSliceMillis returns how many milliseconds the next blocking I/O attempt
+// may last, given the current time and an absolute deadline (zero = none).
+// ok is false when the deadline has already expired; ms is then 0.
+// A non-expired deadline always yields ms >= 1 (sub-millisecond remainders
+// round up) because a 0 would mean "no timeout" to COMMTIMEOUTS.
+func ioSliceMillis(now, deadline time.Time) (ms uint32, ok bool) {
+       if deadline.IsZero() {
+               return maxIOSliceMillis, true
+       }
+       remaining := deadline.Sub(now)
+       if remaining <= 0 {
+               return 0, false
+       }
+       millis := (remaining + time.Millisecond - 1) / time.Millisecond
+       if millis > maxIOSliceMillis {
+               return maxIOSliceMillis, true
+       }
+       return uint32(millis), true
+}
diff --git a/plc4go/spi/transports/serial/serialport/timeouts_test.go 
b/plc4go/spi/transports/serial/serialport/timeouts_test.go
new file mode 100644
index 0000000000..ff52402dd8
--- /dev/null
+++ b/plc4go/spi/transports/serial/serialport/timeouts_test.go
@@ -0,0 +1,51 @@
+/*
+ * 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"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestIOSliceMillis(t *testing.T) {
+       now := time.Date(2026, 7, 6, 12, 0, 0, 0, time.UTC)
+       tests := []struct {
+               name     string
+               deadline time.Time
+               wantMs   uint32
+               wantOK   bool
+       }{
+               {"no deadline uses the max slice", time.Time{}, 
maxIOSliceMillis, true},
+               {"future deadline: exact milliseconds", now.Add(1500 * 
time.Millisecond), 1500, true},
+               {"sub-millisecond remainder rounds up, never 0", now.Add(100 * 
time.Microsecond), 1, true},
+               {"far deadline capped to max slice", now.Add(time.Hour), 
maxIOSliceMillis, true},
+               {"deadline exactly now: expired", now, 0, false},
+               {"past deadline: expired", now.Add(-time.Second), 0, false},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       ms, ok := ioSliceMillis(now, tt.deadline)
+                       assert.Equal(t, tt.wantOK, ok)
+                       assert.Equal(t, tt.wantMs, ms)
+               })
+       }
+}
diff --git a/plc4go/tools.mod b/plc4go/tools.mod
index 082dd60a47..082e5c9239 100644
--- a/plc4go/tools.mod
+++ b/plc4go/tools.mod
@@ -33,11 +33,10 @@ require (
        github.com/fatih/color v1.18.0
        github.com/google/uuid v1.6.0
        github.com/gopacket/gopacket v1.5.0
-       github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4
-       github.com/libp2p/go-reuseport v0.4.0
        github.com/rs/zerolog v1.33.0
        github.com/stretchr/testify v1.10.0
        golang.org/x/net v0.43.0
+       golang.org/x/sys v0.36.0
        golang.org/x/text v0.28.0
        golang.org/x/tools v0.36.0
 )
@@ -67,7 +66,6 @@ require (
        github.com/mattn/go-runewidth v0.0.16 // indirect
        github.com/mitchellh/copystructure v1.2.0 // indirect
        github.com/mitchellh/reflectwalk v1.0.2 // indirect
-       github.com/pkg/errors v0.9.1 // indirect
        github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 
indirect
        github.com/rivo/uniseg v0.4.7 // indirect
        github.com/spf13/cobra v1.8.1 // indirect
@@ -80,7 +78,6 @@ require (
        golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
        golang.org/x/mod v0.27.0 // indirect
        golang.org/x/sync v0.17.0 // indirect
-       golang.org/x/sys v0.36.0 // indirect
        golang.org/x/term v0.35.0 // indirect
        gopkg.in/yaml.v3 v3.0.1 // indirect
        gotest.tools/gotestsum v1.13.0 // indirect
diff --git a/plc4go/tools.sum b/plc4go/tools.sum
index 0c421ca546..d6efe80f51 100644
--- a/plc4go/tools.sum
+++ b/plc4go/tools.sum
@@ -34,8 +34,6 @@ github.com/inconshreveable/mousetrap v1.1.0 
h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod 
h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/incu6us/goimports-reviser/v3 v3.11.0 
h1:ApPsIVH/pT1XFwA8ewJR1o6u5y4sqcVZvz4dITCN6Nk=
 github.com/incu6us/goimports-reviser/v3 v3.11.0/go.mod 
h1:9PrNQlAiw8j+Z6xKOtheaC9N4T5J4EmwJkmbEQ3+KCQ=
-github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 
h1:G2ztCwXov8mRvP0ZfjE6nAlaCX2XbykaeHdbT6KwDz0=
-github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod 
h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs=
 github.com/jedib0t/go-pretty/v6 v6.6.7 
h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
 github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod 
h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
 github.com/knadh/koanf/maps v0.1.2 
h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
@@ -56,8 +54,6 @@ github.com/kr/pretty v0.3.1 
h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod 
h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod 
h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/libp2p/go-reuseport v0.4.0 
h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
-github.com/libp2p/go-reuseport v0.4.0/go.mod 
h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
 github.com/mattn/go-colorable v0.1.13 
h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod 
h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod 
h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -70,7 +66,6 @@ github.com/mitchellh/copystructure v1.2.0 
h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
 github.com/mitchellh/copystructure v1.2.0/go.mod 
h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/reflectwalk v1.0.2 
h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod 
h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 
h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=

Reply via email to