You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2026/07/06 18:53:47 UTC

(plc4x) 02/09: feat(plc4go): replace jacobsa/go-serial with internal serialport package

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 <sr...@apache.org>
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=