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/08/18 20:22:20 UTC

(plc4x) 02/17: feat(plc4go/eip): add SessionState with routing-address construction

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 694f5de798136268e114199fe4db3e74c2b1b4d4
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Tue Aug 18 16:22:11 2026 +0200

    feat(plc4go/eip): add SessionState with routing-address construction
---
 plc4go/internal/eip/SessionState.go      | 115 +++++++++++++++++++++++++++++++
 plc4go/internal/eip/SessionState_test.go |  83 ++++++++++++++++++++++
 2 files changed, 198 insertions(+)

diff --git a/plc4go/internal/eip/SessionState.go b/plc4go/internal/eip/SessionState.go
new file mode 100644
index 0000000000..b9337f33aa
--- /dev/null
+++ b/plc4go/internal/eip/SessionState.go
@@ -0,0 +1,115 @@
+/*
+ * 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 eip
+
+import (
+	"context"
+	"math/rand/v2"
+	"strconv"
+	"strings"
+	"sync/atomic"
+
+	"github.com/rs/zerolog"
+
+	readWriteModel "github.com/apache/plc4x/plc4go/protocols/eip/readwrite/model"
+)
+
+// SessionState is the per-connection EIP/CIP session context shared between the
+// Connection (which fills it during the handshake) and Reader/Writer (which pick
+// the messaging mode and addressing from it).
+type SessionState struct {
+	sessionHandle          uint32
+	senderContext          []byte
+	connectionId           uint32
+	sequenceCount          atomic.Uint32
+	useMessageRouter       bool
+	useConnectionManager   bool
+	connectionSerialNumber uint16
+	routingAddress         []readWriteModel.PathSegment
+	connectionPathSize     uint8
+}
+
+func NewSessionState(log zerolog.Logger, configuration Configuration) *SessionState {
+	routingAddress, connectionPathSize := buildRoutingAddress(log, configuration)
+	serialNumber := configuration.connectionSerialNumber
+	if serialNumber == 0 {
+		serialNumber = uint16(1 + rand.IntN(0xFFFF)) // [1, 0xFFFF], like plc4j
+	}
+	state := &SessionState{
+		senderContext:          []byte(DefaultSenderContext),
+		connectionSerialNumber: serialNumber,
+		routingAddress:         routingAddress,
+		connectionPathSize:     connectionPathSize,
+	}
+	state.sequenceCount.Store(1)
+	return state
+}
+
+// nextSequenceCount hands out the connected-messaging sequence number and
+// advances it, starting at 1 like plc4j.
+func (s *SessionState) nextSequenceCount() uint16 {
+	return uint16(s.sequenceCount.Add(1) - 1)
+}
+
+func buildRoutingAddress(log zerolog.Logger, configuration Configuration) ([]readWriteModel.PathSegment, uint8) {
+	var routingAddress []readWriteModel.PathSegment
+	if configuration.communicationPath != "" {
+		parts := strings.Split(configuration.communicationPath, ",")
+		if len(parts)%2 == 0 {
+			for i := 0; i+1 < len(parts); i += 2 {
+				switch parts[i] {
+				case "1":
+					slot, err := strconv.Atoi(parts[i+1])
+					if err != nil {
+						log.Error().Str("slot", parts[i+1]).Msg("Invalid slot in communication path")
+						continue
+					}
+					routingAddress = append(routingAddress,
+						readWriteModel.NewPortSegment(readWriteModel.NewPortSegmentNormal(1, uint8(slot))))
+				case "2":
+					address := parts[i+1]
+					addressLength := uint8(len(address))
+					if len(address)%2 != 0 {
+						address += "\x00"
+					}
+					routingAddress = append(routingAddress,
+						readWriteModel.NewPortSegment(readWriteModel.NewPortSegmentExtended(2, addressLength, address)))
+				default:
+					log.Error().Str("port", parts[i]).Msg("Only backplane or Ethernet module routing is supported")
+				}
+			}
+		}
+	} else {
+		routingAddress = append(routingAddress,
+			readWriteModel.NewPortSegment(readWriteModel.NewPortSegmentNormal(1, uint8(configuration.slot))))
+	}
+	routingAddress = append(routingAddress,
+		readWriteModel.NewLogicalSegment(readWriteModel.NewClassID(0, 2)),
+		readWriteModel.NewLogicalSegment(readWriteModel.NewInstanceID(0, 1)),
+	)
+	totalBytes := 0
+	for _, segment := range routingAddress {
+		totalBytes += int(segment.GetLengthInBytes(context.Background()))
+	}
+	if totalBytes%2 != 0 {
+		totalBytes++
+	}
+	return routingAddress, uint8(totalBytes / 2)
+}
diff --git a/plc4go/internal/eip/SessionState_test.go b/plc4go/internal/eip/SessionState_test.go
new file mode 100644
index 0000000000..496c36801f
--- /dev/null
+++ b/plc4go/internal/eip/SessionState_test.go
@@ -0,0 +1,83 @@
+/*
+ * 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 eip
+
+import (
+	"context"
+	"testing"
+
+	"github.com/rs/zerolog"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	readWriteModel "github.com/apache/plc4x/plc4go/protocols/eip/readwrite/model"
+)
+
+func routingAddressBytes(t *testing.T, segments []readWriteModel.PathSegment) int {
+	total := 0
+	for _, segment := range segments {
+		total += int(segment.GetLengthInBytes(context.Background()))
+	}
+	return total
+}
+
+func TestBuildRoutingAddressDefault(t *testing.T) {
+	configuration := Configuration{backplane: 1, slot: 3}
+	segments, pathSize := buildRoutingAddress(zerolog.Nop(), configuration)
+	// port segment (backplane 1 / slot 3) + ClassID 2 + InstanceID 1
+	require.Len(t, segments, 3)
+	portSegment, ok := segments[0].(readWriteModel.PortSegment)
+	require.True(t, ok)
+	normal, ok := portSegment.GetSegmentType().(readWriteModel.PortSegmentNormal)
+	require.True(t, ok)
+	assert.Equal(t, uint8(1), normal.GetPort())
+	assert.Equal(t, uint8(3), normal.GetLinkAddress())
+	totalBytes := routingAddressBytes(t, segments)
+	expected := (totalBytes + totalBytes%2) / 2
+	assert.Equal(t, uint8(expected), pathSize)
+}
+
+func TestBuildRoutingAddressCommunicationPath(t *testing.T) {
+	configuration := Configuration{communicationPath: "1,4,2,192.168.0.1,1,1"}
+	segments, _ := buildRoutingAddress(zerolog.Nop(), configuration)
+	// three routed hops + ClassID + InstanceID
+	require.Len(t, segments, 5)
+	hop2, ok := segments[1].(readWriteModel.PortSegment)
+	require.True(t, ok)
+	extended, ok := hop2.GetSegmentType().(readWriteModel.PortSegmentExtended)
+	require.True(t, ok)
+	assert.Equal(t, uint8(2), extended.GetPort())
+	// "192.168.0.1" is 11 chars -> reported length 11, padded to "192.168.0.1\x00"
+	assert.Equal(t, uint8(11), extended.GetLinkAddressSize())
+	assert.Equal(t, "192.168.0.1\x00", extended.GetAddress())
+}
+
+func TestNewSessionStateSerialNumber(t *testing.T) {
+	fixed := NewSessionState(zerolog.Nop(), Configuration{connectionSerialNumber: 4660})
+	assert.Equal(t, uint16(4660), fixed.connectionSerialNumber)
+	random := NewSessionState(zerolog.Nop(), Configuration{})
+	assert.NotEqual(t, uint16(0), random.connectionSerialNumber)
+}
+
+func TestNextSequenceCount(t *testing.T) {
+	state := NewSessionState(zerolog.Nop(), Configuration{})
+	assert.Equal(t, uint16(1), state.nextSequenceCount())
+	assert.Equal(t, uint16(2), state.nextSequenceCount())
+}