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