From ddd733fceba0c4493b2c0b0fc4ba9e620ef0231f Mon Sep 17 00:00:00 2001 From: Anderson Vasconcelos Pires Date: Tue, 4 Aug 2026 10:25:57 -0300 Subject: [PATCH 1/5] feat(plc4j/eip): Implement "Get Attribute Single" on EIP protocol for cases where "Get Attribute All" is not supported by the device. --- .../plc4x/java/eip/readwrite/AttributeID.java | 126 ++++++++++++++++ .../readwrite/GetAttributeSingleRequest.java | 90 +++++++++++- .../readwrite/GetAttributeSingleResponse.java | 112 +++++++++++++- .../eip/readwrite/LogicalSegmentType.java | 2 + .../plc4x/java/eip/base/EipTcpConnection.java | 59 +++++++- .../main/resources/protocols/eip/eip.mspec | 15 +- .../protocols/eip/DriverTestsuite.xml | 139 ++++++++++++++++++ 7 files changed, 525 insertions(+), 18 deletions(-) create mode 100644 plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/AttributeID.java diff --git a/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/AttributeID.java b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/AttributeID.java new file mode 100644 index 00000000000..9982e11ecfc --- /dev/null +++ b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/AttributeID.java @@ -0,0 +1,126 @@ +// 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 org.apache.plc4x.java.eip.readwrite; + +import org.apache.plc4x.java.spi.buffers.api.Message; +import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; +import org.apache.plc4x.java.spi.buffers.api.WithOption; +import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; +import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; +import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; + +/** + * Code generated by code-generation. DO NOT EDIT. + */ +public class AttributeID extends LogicalSegmentType implements Message { + protected final byte format; + + protected final short attribute; + + public AttributeID(Byte format, Short attribute) { + this.format = format; + this.attribute = attribute; + } + + /** + * Discriminator field logicalSegmentType + */ + @Override + public byte getLogicalSegmentType() { + return (byte) 0x04; + } + + /** + * Property field format + */ + public byte getFormat() { + return format; + } + + /** + * Property field attribute + */ + public short getAttribute() { + return attribute; + } + + public static LogicalSegmentTypeBuilder staticParseLogicalSegmentTypeBuilder( + ReadBuffer readBuffer) throws BufferException { + readBuffer.pushContext(WithOption.WithName("AttributeID")); + int startPos = readBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: format + byte format = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedByte(readBuffer, 2), WithOption.WithName("format")); + + // Simple Field: attribute + short attribute = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("attribute")); + + readBuffer.popContext(); + return new LogicalSegmentTypeBuilderImpl(format, attribute); + } + + protected void serializeLogicalSegmentTypeChild(WriteBuffer writeBuffer) throws BufferException { + writeBuffer.pushContext(WithOption.WithName("AttributeID")); + int startPos = writeBuffer.getPositionInBits(); + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: format + FieldWriterFactory.writeSimpleField((byte) format, DataWriterFactory.writeUnsignedByte(writeBuffer, 2), WithOption.WithName("format")); + + // Simple Field: attribute + FieldWriterFactory.writeSimpleField((short) attribute, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("attribute")); + + writeBuffer.popContext(); + } + + @Override + public int getLengthInBytes() { + return (int) Math.ceil((float) getLengthInBits() / 8.0); + } + + @Override + public int getLengthInBits() { + int lengthInBits = super.getLengthInBits(); + AttributeID _value = this; + boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Simple Field: format + lengthInBits += 2; + + // Simple Field: attribute + lengthInBits += 8; + + return lengthInBits; + } + + public static class LogicalSegmentTypeBuilderImpl implements LogicalSegmentType.LogicalSegmentTypeBuilder { + private final byte format; + + private final short attribute; + + public LogicalSegmentTypeBuilderImpl(byte format, short attribute) { + this.format = format; + this.attribute = attribute; + } + + public LogicalSegmentType build() { + return new AttributeID(format, attribute); + } + } +} diff --git a/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleRequest.java b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleRequest.java index a14814da0ab..efea7404ffa 100644 --- a/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleRequest.java +++ b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleRequest.java @@ -21,13 +21,27 @@ import org.apache.plc4x.java.spi.buffers.api.WithOption; import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; /** * Code generated by code-generation. DO NOT EDIT. */ public class GetAttributeSingleRequest extends CipService implements Message { - public GetAttributeSingleRequest() { + protected final PathSegment classSegment; + + protected final PathSegment instanceSegment; + + protected final PathSegment attributeSegment; + + public GetAttributeSingleRequest(PathSegment classSegment, PathSegment instanceSegment, + PathSegment attributeSegment) { + this.classSegment = classSegment; + this.instanceSegment = instanceSegment; + this.attributeSegment = attributeSegment; } /** @@ -54,19 +68,65 @@ public boolean getConnected() { return false; } + /** + * Property field classSegment + */ + public PathSegment getClassSegment() { + return classSegment; + } + + /** + * Property field instanceSegment + */ + public PathSegment getInstanceSegment() { + return instanceSegment; + } + + /** + * Property field attributeSegment + */ + public PathSegment getAttributeSegment() { + return attributeSegment; + } + public static CipServiceBuilder staticParseCipServiceBuilder(ReadBuffer readBuffer, boolean connected, int serviceLen) throws BufferException { readBuffer.pushContext(WithOption.WithName("GetAttributeSingleRequest")); int startPos = readBuffer.getPositionInBits(); boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Implicit Field: requestPathSize + short requestPathSize = FieldReaderFactory.readImplicitField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("requestPathSize")); + + // Simple Field: classSegment + PathSegment classSegment = FieldReaderFactory.readSimpleField(DataReaderFactory.readComplex(() -> (PathSegment) PathSegment.staticParse(readBuffer), readBuffer), WithOption.WithName("classSegment")); + + // Simple Field: instanceSegment + PathSegment instanceSegment = FieldReaderFactory.readSimpleField(DataReaderFactory.readComplex(() -> (PathSegment) PathSegment.staticParse(readBuffer), readBuffer), WithOption.WithName("instanceSegment")); + + // Simple Field: attributeSegment + PathSegment attributeSegment = FieldReaderFactory.readSimpleField(DataReaderFactory.readComplex(() -> (PathSegment) PathSegment.staticParse(readBuffer), readBuffer), WithOption.WithName("attributeSegment")); + readBuffer.popContext(); - return new CipServiceBuilderImpl(); + return new CipServiceBuilderImpl(classSegment, instanceSegment, attributeSegment); } protected void serializeCipServiceChild(WriteBuffer writeBuffer) throws BufferException { writeBuffer.pushContext(WithOption.WithName("GetAttributeSingleRequest")); int startPos = writeBuffer.getPositionInBits(); boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Implicit Field: requestPathSize + short requestPathSize = (short) (((((classSegment.getLengthInBytes()) + (instanceSegment.getLengthInBytes())) + (attributeSegment.getLengthInBytes()))) / (2)); + FieldWriterFactory.writeImplicitField((short) requestPathSize, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("requestPathSize")); + + // Simple Field: classSegment + FieldWriterFactory.writeSimpleField((PathSegment) classSegment, DataWriterFactory.writeComplex(writeBuffer), WithOption.WithName("classSegment")); + + // Simple Field: instanceSegment + FieldWriterFactory.writeSimpleField((PathSegment) instanceSegment, DataWriterFactory.writeComplex(writeBuffer), WithOption.WithName("instanceSegment")); + + // Simple Field: attributeSegment + FieldWriterFactory.writeSimpleField((PathSegment) attributeSegment, DataWriterFactory.writeComplex(writeBuffer), WithOption.WithName("attributeSegment")); + writeBuffer.popContext(); } @@ -80,15 +140,37 @@ public int getLengthInBits() { int lengthInBits = super.getLengthInBits(); GetAttributeSingleRequest _value = this; boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Implicit Field: requestPathSize + lengthInBits += 8; + + // Simple Field: classSegment + lengthInBits += classSegment.getLengthInBits(); + + // Simple Field: instanceSegment + lengthInBits += instanceSegment.getLengthInBits(); + + // Simple Field: attributeSegment + lengthInBits += attributeSegment.getLengthInBits(); + return lengthInBits; } public static class CipServiceBuilderImpl implements CipService.CipServiceBuilder { - public CipServiceBuilderImpl() { + private final PathSegment classSegment; + + private final PathSegment instanceSegment; + + private final PathSegment attributeSegment; + + public CipServiceBuilderImpl(PathSegment classSegment, PathSegment instanceSegment, + PathSegment attributeSegment) { + this.classSegment = classSegment; + this.instanceSegment = instanceSegment; + this.attributeSegment = attributeSegment; } public CipService build() { - return new GetAttributeSingleRequest(); + return new GetAttributeSingleRequest(classSegment, instanceSegment, attributeSegment); } } } diff --git a/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleResponse.java b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleResponse.java index 2e060937302..0e5228907ea 100644 --- a/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleResponse.java +++ b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/GetAttributeSingleResponse.java @@ -16,18 +16,36 @@ // under the License. package org.apache.plc4x.java.eip.readwrite; +import java.util.List; import org.apache.plc4x.java.spi.buffers.api.Message; import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; import org.apache.plc4x.java.spi.buffers.api.WithOption; import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.fields.data.reader.DataReaderFactory; +import org.apache.plc4x.java.spi.fields.data.writer.DataWriterFactory; +import org.apache.plc4x.java.spi.fields.fields.reader.FieldReaderFactory; +import org.apache.plc4x.java.spi.fields.fields.writer.FieldWriterFactory; import org.apache.plc4x.java.spi.fields.utils.ThreadLocalHelper; /** * Code generated by code-generation. DO NOT EDIT. */ public class GetAttributeSingleResponse extends CipService implements Message { - public GetAttributeSingleResponse() { + protected final short status; + + protected final short extStatusSize; + + protected final List extStatus; + + protected final byte[] servicesData; + + public GetAttributeSingleResponse(Short status, Short extStatusSize, List extStatus, + byte[] servicesData) { + this.status = status; + this.extStatusSize = extStatusSize; + this.extStatus = extStatus; + this.servicesData = servicesData; } /** @@ -54,19 +72,77 @@ public boolean getConnected() { return false; } + /** + * Property field status + */ + public short getStatus() { + return status; + } + + /** + * Property field extStatusSize + */ + public short getExtStatusSize() { + return extStatusSize; + } + + /** + * Property field extStatus + */ + public List getExtStatus() { + return extStatus; + } + + /** + * Property field servicesData + */ + public byte[] getServicesData() { + return servicesData; + } + public static CipServiceBuilder staticParseCipServiceBuilder(ReadBuffer readBuffer, boolean connected, int serviceLen) throws BufferException { readBuffer.pushContext(WithOption.WithName("GetAttributeSingleResponse")); int startPos = readBuffer.getPositionInBits(); boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Reserved Field + FieldReaderFactory.readReservedField(DataReaderFactory.readUnsignedShort(readBuffer, 8), (short) 0x00, WithOption.WithName("GetAttributeSingleResponse.reserved0")); + + // Simple Field: status + short status = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("status")); + + // Simple Field: extStatusSize + short extStatusSize = FieldReaderFactory.readSimpleField(DataReaderFactory.readUnsignedShort(readBuffer, 8), WithOption.WithName("extStatusSize")); + + // Array Field: extStatus + List extStatus = FieldReaderFactory.readCountArrayField(DataReaderFactory.readUnsignedShort(readBuffer, 8), extStatusSize, WithOption.WithName("extStatus")); + + // Array Field: servicesData + byte[] servicesData = readBuffer.readBits(Math.toIntExact((((serviceLen) - (4)) - (extStatusSize)) * 8), WithOption.WithName("servicesData")); + readBuffer.popContext(); - return new CipServiceBuilderImpl(); + return new CipServiceBuilderImpl(status, extStatusSize, extStatus, servicesData); } protected void serializeCipServiceChild(WriteBuffer writeBuffer) throws BufferException { writeBuffer.pushContext(WithOption.WithName("GetAttributeSingleResponse")); int startPos = writeBuffer.getPositionInBits(); boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Reserved Field + FieldWriterFactory.writeReservedField((short) 0x00, DataWriterFactory.writeUnsignedShort(writeBuffer, 8)); + + // Simple Field: status + FieldWriterFactory.writeSimpleField((short) status, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("status")); + + // Simple Field: extStatusSize + FieldWriterFactory.writeSimpleField((short) extStatusSize, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("extStatusSize")); + + // Array Field: extStatus + FieldWriterFactory.writeSimpleTypeArrayField(extStatus, DataWriterFactory.writeUnsignedShort(writeBuffer, 8), WithOption.WithName("extStatus")); + + // Array Field: servicesData + FieldWriterFactory.writeByteArrayField(servicesData, DataWriterFactory.writeByteArray(writeBuffer, (int) ((servicesData != null) ? servicesData.length : 0)), WithOption.WithName("servicesData")); + writeBuffer.popContext(); } @@ -80,15 +156,43 @@ public int getLengthInBits() { int lengthInBits = super.getLengthInBits(); GetAttributeSingleResponse _value = this; boolean _lastItem = ThreadLocalHelper.lastItemThreadLocal.get(); + // Reserved Field + lengthInBits += 8; + + // Simple Field: status + lengthInBits += 8; + + // Simple Field: extStatusSize + lengthInBits += 8; + + // Array Field: extStatus + lengthInBits += 8 * extStatus.size(); + + // Array Field: servicesData + lengthInBits += 8 * ((servicesData != null) ? servicesData.length : 0); + return lengthInBits; } public static class CipServiceBuilderImpl implements CipService.CipServiceBuilder { - public CipServiceBuilderImpl() { + private final short status; + + private final short extStatusSize; + + private final List extStatus; + + private final byte[] servicesData; + + public CipServiceBuilderImpl(short status, short extStatusSize, List extStatus, + byte[] servicesData) { + this.status = status; + this.extStatusSize = extStatusSize; + this.extStatus = extStatus; + this.servicesData = servicesData; } public CipService build() { - return new GetAttributeSingleResponse(); + return new GetAttributeSingleResponse(status, extStatusSize, extStatus, servicesData); } } } diff --git a/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/LogicalSegmentType.java b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/LogicalSegmentType.java index 28776467a52..a02ac13ea8b 100644 --- a/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/LogicalSegmentType.java +++ b/plc4j/drivers/eip/src/main/generated/org/apache/plc4x/java/eip/readwrite/LogicalSegmentType.java @@ -58,6 +58,8 @@ public static LogicalSegmentType staticParse(ReadBuffer readBuffer) throws Buffe builder = InstanceID.staticParseLogicalSegmentTypeBuilder(readBuffer); } else if (EvaluationHelper.equals(logicalSegmentType, (byte) (0x02))) { builder = MemberID.staticParseLogicalSegmentTypeBuilder(readBuffer); + } else if (EvaluationHelper.equals(logicalSegmentType, (byte) (0x04))) { + builder = AttributeID.staticParseLogicalSegmentTypeBuilder(readBuffer); } if (builder == null) { throw new BufferException("Unsupported case for discriminated type parameters parameters [logicalSegmentType]"); diff --git a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java index a2a920bbbdd..ddf538ecdc7 100644 --- a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java +++ b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java @@ -249,16 +249,13 @@ private CompletableFuture probeAttributes() { CipRRData eipWrapper = new CipRRData(sessionHandle, CIPStatus.Success.getValue(), DEFAULT_SENDER_CONTEXT, 0L, EMPTY_INTERFACE_HANDLE, 0, typeIds); - return sendRequest(eipWrapper).thenAccept(response -> { - if (!(response instanceof CipRRData rr) || rr.getStatus() != CIPStatus.Success.getValue()) { - return; - } - UnConnectedDataItem dataItem = (UnConnectedDataItem) rr.getTypeIds().get(1); - if (!(dataItem.getService() instanceof GetAttributeAllResponse gar)) { - return; + return sendRequest(eipWrapper).thenCompose(response -> { + if (!(response instanceof CipRRData rr) || rr.getStatus() != CIPStatus.Success.getValue() || + !(rr.getTypeIds().get(1) instanceof UnConnectedDataItem di && di.getService() instanceof GetAttributeAllResponse gar)) { + return CompletableFuture.completedFuture(null); } if (gar.getStatus() == CIPStatus.ServiceNotSupported.getValue()) { - return; + return checkAttributesSingle(); } if (gar.getAttributes() != null) { for (Integer classId : gar.getAttributes().getClassId()) { @@ -270,6 +267,7 @@ private CompletableFuture probeAttributes() { } } } + return CompletableFuture.completedFuture(null); }).exceptionally(e -> { // Treat any probe failure (timeout, parse error, malformed response, // ServiceNotSupported) as "device has no message router / connection @@ -280,6 +278,51 @@ private CompletableFuture probeAttributes() { }); } + private CompletableFuture checkAttributesSingle() { + LOGGER.debug("Checking MessageRouter and ConnectionManager using GetAttributeSingle"); + + CompletableFuture connectionManagerFuture = checkSingleAttribute(CIPClassID.ConnectionManager, 1); + CompletableFuture messageRouterFuture = checkSingleAttribute(CIPClassID.MessageRouter, 0); + return CompletableFuture.allOf(messageRouterFuture, connectionManagerFuture).thenRun(() -> { + try { + this.useConnectionManager = connectionManagerFuture.get(); + this.useMessageRouter = messageRouterFuture.get(); + } catch (InterruptedException | ExecutionException e) { + LOGGER.warn("Error checking single attributes", e); + } + }); + } + + private CompletableFuture checkSingleAttribute(CIPClassID classId, int instanceId) { + CompletableFuture future = new CompletableFuture<>(); + + PathSegment classSegment = new LogicalSegment(new ClassID((byte) 0, (short) classId.getValue())); + PathSegment instanceSegment = new LogicalSegment(new InstanceID((byte) 0, (short) instanceId)); + PathSegment attributeSegment = new LogicalSegment(new AttributeID((byte) 0, (short) 1)); + + UnConnectedDataItem exchange = new UnConnectedDataItem( + new GetAttributeSingleRequest(classSegment, instanceSegment, attributeSegment)); + + List typeIds = Arrays.asList(nullAddressItem, exchange); + CipRRData eipWrapper = new CipRRData(sessionHandle, CIPStatus.Success.getValue(), + DEFAULT_SENDER_CONTEXT, 0L, EMPTY_INTERFACE_HANDLE, 0, typeIds); + + sendRequest(eipWrapper).thenAccept(response -> { + if (!(response instanceof CipRRData rr) || rr.getStatus() != CIPStatus.Success.getValue() || + !(rr.getTypeIds().get(1) instanceof UnConnectedDataItem di && di.getService() instanceof GetAttributeSingleResponse gar)) { + return; + } + if (gar.getStatus() == CIPStatus.Success.getValue()) { + LOGGER.debug("{} is supported", classId); + future.complete(true); + } else { + LOGGER.debug("{} is not supported, status: {}", classId, gar.getStatus()); + future.complete(false); + } + }); + return future; + } + private CompletableFuture openConnectionManager() { UnConnectedDataItem exchange = new UnConnectedDataItem( new CipConnectionManagerRequest( diff --git a/protocols/eip/src/main/resources/protocols/eip/eip.mspec b/protocols/eip/src/main/resources/protocols/eip/eip.mspec index e7e2957e7c2..95de1909f96 100644 --- a/protocols/eip/src/main/resources/protocols/eip/eip.mspec +++ b/protocols/eip/src/main/resources/protocols/eip/eip.mspec @@ -157,10 +157,17 @@ [array byte servicesData count 'serviceLen - 6 - (2 * serviceNb)' ] ] ['0x0E','false' GetAttributeSingleRequest - // TODO: Implement + [implicit uint 8 requestPathSize '(classSegment.lengthInBytes + instanceSegment.lengthInBytes + attributeSegment.lengthInBytes)/2'] + [simple PathSegment classSegment ] + [simple PathSegment instanceSegment ] + [simple PathSegment attributeSegment ] ] ['0x0E','true' GetAttributeSingleResponse - // TODO: Implement + [reserved uint 8 '0x00' ] + [simple uint 8 status ] + [simple uint 8 extStatusSize ] + [array uint 8 extStatus count 'extStatusSize' ] + [array byte servicesData count 'serviceLen - 4 - extStatusSize' ] ] ['0x10','false' SetAttributeSingleRequest // TODO: Implement @@ -358,6 +365,10 @@ [simple uint 2 format] [simple uint 8 instance] ] + ['0x04' AttributeID + [simple uint 2 format] + [simple uint 8 attribute] + ] ] ] diff --git a/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml b/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml index 72c9e6afda5..f81d80e583b 100644 --- a/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml +++ b/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml @@ -427,4 +427,143 @@ + + Get Attribute Single Request + + + + false + + + 111 + 22 + 1074224669 + 0 + 0x504c433458202020 + 0 + + 0 + 0 + 2 + + + 0 + + 0 + + + + 178 + + 8 + + + false + 14 + + 3 + + + 1 + + + + 0 + + 0 + 1 + + + + + + + + + 1 + + + + 1 + + 0 + 1 + + + + + + + + + 1 + + + + 4 + + 0 + 1 + + + + + + + + + + + + + + + + + + true + + + 111 + 22 + 1074224669 + 0 + 0x504c433458202020 + 0 + + 0 + 0 + 2 + + + 0 + + 0 + + + + 178 + + 6 + + + true + 14 + + 0 + 0 + 0 + 0x0100 + + + + + + + + + + + + \ No newline at end of file From fa7a737476db25368bfe6d460f16936daae51884 Mon Sep 17 00:00:00 2001 From: Anderson Vasconcelos Pires Date: Thu, 6 Aug 2026 07:07:07 -0300 Subject: [PATCH 2/5] Addressing review comments. --- .../plc4x/java/eip/base/EipTcpConnection.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java index ddf538ecdc7..de723d50bf1 100644 --- a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java +++ b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java @@ -250,12 +250,12 @@ private CompletableFuture probeAttributes() { DEFAULT_SENDER_CONTEXT, 0L, EMPTY_INTERFACE_HANDLE, 0, typeIds); return sendRequest(eipWrapper).thenCompose(response -> { - if (!(response instanceof CipRRData rr) || rr.getStatus() != CIPStatus.Success.getValue() || - !(rr.getTypeIds().get(1) instanceof UnConnectedDataItem di && di.getService() instanceof GetAttributeAllResponse gar)) { + if (!(getCipService(response) instanceof GetAttributeAllResponse gar)) { return CompletableFuture.completedFuture(null); } if (gar.getStatus() == CIPStatus.ServiceNotSupported.getValue()) { - return checkAttributesSingle(); + // Not all devices support Get_Attribute_All. Let's try using Get_Attribute_Single. + return probeAttributesUsingSingleAttributeRequest(); } if (gar.getAttributes() != null) { for (Integer classId : gar.getAttributes().getClassId()) { @@ -278,11 +278,21 @@ private CompletableFuture probeAttributes() { }); } - private CompletableFuture checkAttributesSingle() { + private CipService getCipService(EipPacket response) { + if (response instanceof CipRRData rr + && rr.getStatus() == CIPStatus.Success.getValue() + && rr.getTypeIds().size() > 1 + && rr.getTypeIds().get(1) instanceof UnConnectedDataItem di) { + return di.getService(); + } + return null; + } + + private CompletableFuture probeAttributesUsingSingleAttributeRequest() { LOGGER.debug("Checking MessageRouter and ConnectionManager using GetAttributeSingle"); - CompletableFuture connectionManagerFuture = checkSingleAttribute(CIPClassID.ConnectionManager, 1); - CompletableFuture messageRouterFuture = checkSingleAttribute(CIPClassID.MessageRouter, 0); + CompletableFuture connectionManagerFuture = checkSupportOfAttribute(CIPClassID.ConnectionManager, 1); + CompletableFuture messageRouterFuture = checkSupportOfAttribute(CIPClassID.MessageRouter, 0); return CompletableFuture.allOf(messageRouterFuture, connectionManagerFuture).thenRun(() -> { try { this.useConnectionManager = connectionManagerFuture.get(); @@ -293,7 +303,7 @@ private CompletableFuture checkAttributesSingle() { }); } - private CompletableFuture checkSingleAttribute(CIPClassID classId, int instanceId) { + private CompletableFuture checkSupportOfAttribute(CIPClassID classId, int instanceId) { CompletableFuture future = new CompletableFuture<>(); PathSegment classSegment = new LogicalSegment(new ClassID((byte) 0, (short) classId.getValue())); @@ -308,17 +318,13 @@ private CompletableFuture checkSingleAttribute(CIPClassID classId, int DEFAULT_SENDER_CONTEXT, 0L, EMPTY_INTERFACE_HANDLE, 0, typeIds); sendRequest(eipWrapper).thenAccept(response -> { - if (!(response instanceof CipRRData rr) || rr.getStatus() != CIPStatus.Success.getValue() || - !(rr.getTypeIds().get(1) instanceof UnConnectedDataItem di && di.getService() instanceof GetAttributeSingleResponse gar)) { + if (!(getCipService(response) instanceof GetAttributeSingleResponse gsr)) { + future.complete(false); return; } - if (gar.getStatus() == CIPStatus.Success.getValue()) { - LOGGER.debug("{} is supported", classId); - future.complete(true); - } else { - LOGGER.debug("{} is not supported, status: {}", classId, gar.getStatus()); - future.complete(false); - } + boolean hasSupport = gsr.getStatus() == CIPStatus.Success.getValue(); + LOGGER.debug("ClassId: {} status: {} hasSupport: {}", classId, gsr.getStatus(), hasSupport); + future.complete(hasSupport); }); return future; } From cc159f6c334585c614f8e40699e3c63b8c49f799 Mon Sep 17 00:00:00 2001 From: Anderson Vasconcelos Pires Date: Sat, 8 Aug 2026 10:18:36 -0300 Subject: [PATCH 3/5] Try to make the code on checkAttributeSupport more robust because force the return on processing the CompletableFuture instead of setting the future as completed. Refactoring. --- .../plc4x/java/eip/base/EipTcpConnection.java | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java index de723d50bf1..0b46ca231bc 100644 --- a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java +++ b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java @@ -291,42 +291,33 @@ private CipService getCipService(EipPacket response) { private CompletableFuture probeAttributesUsingSingleAttributeRequest() { LOGGER.debug("Checking MessageRouter and ConnectionManager using GetAttributeSingle"); - CompletableFuture connectionManagerFuture = checkSupportOfAttribute(CIPClassID.ConnectionManager, 1); - CompletableFuture messageRouterFuture = checkSupportOfAttribute(CIPClassID.MessageRouter, 0); - return CompletableFuture.allOf(messageRouterFuture, connectionManagerFuture).thenRun(() -> { - try { - this.useConnectionManager = connectionManagerFuture.get(); - this.useMessageRouter = messageRouterFuture.get(); - } catch (InterruptedException | ExecutionException e) { - LOGGER.warn("Error checking single attributes", e); - } + return checkAttributeSupport(CIPClassID.ConnectionManager).thenCompose(hasSupport -> { + useConnectionManager = hasSupport; + return checkAttributeSupport(CIPClassID.MessageRouter); + }).thenAccept(hasSupport -> { + useMessageRouter = hasSupport; }); } - private CompletableFuture checkSupportOfAttribute(CIPClassID classId, int instanceId) { - CompletableFuture future = new CompletableFuture<>(); - - PathSegment classSegment = new LogicalSegment(new ClassID((byte) 0, (short) classId.getValue())); - PathSegment instanceSegment = new LogicalSegment(new InstanceID((byte) 0, (short) instanceId)); - PathSegment attributeSegment = new LogicalSegment(new AttributeID((byte) 0, (short) 1)); - - UnConnectedDataItem exchange = new UnConnectedDataItem( - new GetAttributeSingleRequest(classSegment, instanceSegment, attributeSegment)); + private CompletableFuture checkAttributeSupport(CIPClassID classId) { + UnConnectedDataItem exchange = new UnConnectedDataItem(new GetAttributeSingleRequest( + new LogicalSegment(new ClassID((byte) 0, (short) classId.getValue())), + new LogicalSegment(new InstanceID((byte) 0, (short) 0)), // Class level discovery + new LogicalSegment(new AttributeID((byte) 0, (short) 1))) // Attribute ID 1: Revision + ); List typeIds = Arrays.asList(nullAddressItem, exchange); CipRRData eipWrapper = new CipRRData(sessionHandle, CIPStatus.Success.getValue(), DEFAULT_SENDER_CONTEXT, 0L, EMPTY_INTERFACE_HANDLE, 0, typeIds); - sendRequest(eipWrapper).thenAccept(response -> { + return sendRequest(eipWrapper).thenCompose(response -> { if (!(getCipService(response) instanceof GetAttributeSingleResponse gsr)) { - future.complete(false); - return; + return CompletableFuture.completedFuture(false); } boolean hasSupport = gsr.getStatus() == CIPStatus.Success.getValue(); LOGGER.debug("ClassId: {} status: {} hasSupport: {}", classId, gsr.getStatus(), hasSupport); - future.complete(hasSupport); + return CompletableFuture.completedFuture(hasSupport); }); - return future; } private CompletableFuture openConnectionManager() { From 7bcbf37ea3a7f9ff7bef2e098925630dca8208d3 Mon Sep 17 00:00:00 2001 From: Anderson Vasconcelos Pires Date: Mon, 10 Aug 2026 18:35:37 -0300 Subject: [PATCH 4/5] Addressing Chris comments. --- .../plc4x/java/eip/base/EipTcpConnection.java | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java index 0b46ca231bc..1194502f131 100644 --- a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java +++ b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/EipTcpConnection.java @@ -249,26 +249,17 @@ private CompletableFuture probeAttributes() { CipRRData eipWrapper = new CipRRData(sessionHandle, CIPStatus.Success.getValue(), DEFAULT_SENDER_CONTEXT, 0L, EMPTY_INTERFACE_HANDLE, 0, typeIds); - return sendRequest(eipWrapper).thenCompose(response -> { - if (!(getCipService(response) instanceof GetAttributeAllResponse gar)) { - return CompletableFuture.completedFuture(null); - } - if (gar.getStatus() == CIPStatus.ServiceNotSupported.getValue()) { + return sendRequest(eipWrapper).thenCompose(response -> + switch (extractCipService(response)) { // Not all devices support Get_Attribute_All. Let's try using Get_Attribute_Single. - return probeAttributesUsingSingleAttributeRequest(); - } - if (gar.getAttributes() != null) { - for (Integer classId : gar.getAttributes().getClassId()) { - if (CIPClassID.enumForValue(classId) == CIPClassID.MessageRouter) { - this.useMessageRouter = true; - } - if (CIPClassID.enumForValue(classId) == CIPClassID.ConnectionManager) { - this.useConnectionManager = true; - } + case GetAttributeAllResponse gar when gar.getStatus() == CIPStatus.ServiceNotSupported.getValue() + -> probeAttributesUsingSingleAttributeRequest(); + case GetAttributeAllResponse gar -> { + recordSupportedClasses(gar.getAttributes()); + yield CompletableFuture.completedFuture(null); } - } - return CompletableFuture.completedFuture(null); - }).exceptionally(e -> { + case null, default -> CompletableFuture.completedFuture(null); + }).exceptionally(e -> { // Treat any probe failure (timeout, parse error, malformed response, // ServiceNotSupported) as "device has no message router / connection // manager" and fall through to the unconnected code path. This keeps @@ -278,7 +269,21 @@ private CompletableFuture probeAttributes() { }); } - private CipService getCipService(EipPacket response) { + private void recordSupportedClasses(CIPAttributes attributes) { + if (attributes == null) { + return; + } + for (Integer classId : attributes.getClassId()) { + if (CIPClassID.enumForValue(classId) == CIPClassID.MessageRouter) { + this.useMessageRouter = true; + } + if (CIPClassID.enumForValue(classId) == CIPClassID.ConnectionManager) { + this.useConnectionManager = true; + } + } + } + + private CipService extractCipService(EipPacket response) { if (response instanceof CipRRData rr && rr.getStatus() == CIPStatus.Success.getValue() && rr.getTypeIds().size() > 1 @@ -311,12 +316,13 @@ private CompletableFuture checkAttributeSupport(CIPClassID classId) { DEFAULT_SENDER_CONTEXT, 0L, EMPTY_INTERFACE_HANDLE, 0, typeIds); return sendRequest(eipWrapper).thenCompose(response -> { - if (!(getCipService(response) instanceof GetAttributeSingleResponse gsr)) { - return CompletableFuture.completedFuture(false); - } - boolean hasSupport = gsr.getStatus() == CIPStatus.Success.getValue(); - LOGGER.debug("ClassId: {} status: {} hasSupport: {}", classId, gsr.getStatus(), hasSupport); + + if (extractCipService(response) instanceof GetAttributeSingleResponse gsr) { + boolean hasSupport = gsr.getStatus() == CIPStatus.Success.getValue(); + LOGGER.debug("ClassId: {} status: {} hasSupport: {}", classId, gsr.getStatus(), hasSupport); return CompletableFuture.completedFuture(hasSupport); + } + return CompletableFuture.completedFuture(false); }); } From 203bc93ac74b95d52183f0614d36fd7e64510869 Mon Sep 17 00:00:00 2001 From: Anderson Vasconcelos Pires Date: Mon, 10 Aug 2026 20:17:15 -0300 Subject: [PATCH 5/5] Addressing Copilot comments. --- .../eip/src/test/resources/protocols/eip/DriverTestsuite.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml b/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml index f81d80e583b..12215d767bc 100644 --- a/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml +++ b/protocols/eip/src/test/resources/protocols/eip/DriverTestsuite.xml @@ -553,7 +553,8 @@ 0 0 0 - 0x0100 + + 0x0100