This is an automated email from the ASF dual-hosted git repository. cdutz pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push: new bcdb1e662d fix: Accepted the patch provided in order to make working unconnected requests work. feat: Added a connection parameter to "force-unconnected-operation" that forces the use of unconnected operations. bcdb1e662d is described below commit bcdb1e662dde89c08daf35799de1c5929ac0a6cc Author: Christofer Dutz <cd...@apache.org> AuthorDate: Fri Jul 12 14:09:08 2024 +0200 fix: Accepted the patch provided in order to make working unconnected requests work. feat: Added a connection parameter to "force-unconnected-operation" that forces the use of unconnected operations. --- .../eip/base/configuration/EIPConfiguration.java | 22 ++- .../java/eip/base/protocol/EipProtocolLogic.java | 175 ++++++++++++--------- .../logix/ManualEipLogixDriverUnconnectedTest.java | 89 +++++++++++ 3 files changed, 208 insertions(+), 78 deletions(-) diff --git a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/configuration/EIPConfiguration.java b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/configuration/EIPConfiguration.java index b723942bc7..b926a61d56 100644 --- a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/configuration/EIPConfiguration.java +++ b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/configuration/EIPConfiguration.java @@ -21,22 +21,34 @@ package org.apache.plc4x.java.eip.base.configuration; import org.apache.plc4x.java.spi.configuration.PlcConnectionConfiguration; import org.apache.plc4x.java.spi.configuration.annotations.ConfigurationParameter; import org.apache.plc4x.java.spi.configuration.annotations.Description; +import org.apache.plc4x.java.spi.configuration.annotations.Since; +import org.apache.plc4x.java.spi.configuration.annotations.defaults.BooleanDefaultValue; +import org.apache.plc4x.java.spi.configuration.annotations.defaults.IntDefaultValue; import org.apache.plc4x.java.spi.generation.ByteOrder; public class EIPConfiguration implements PlcConnectionConfiguration { @ConfigurationParameter @Description("Without using routing information the backplane defaults to 1. This is overridden if communicationPath is provided.") + @IntDefaultValue(1) private int backplane = 1; @ConfigurationParameter @Description("The slot within the backplane the CPU is located.") + @IntDefaultValue(0) private int slot = 0; - @ConfigurationParameter + @ConfigurationParameter("big-endian") @Description("Configure if the connection should be set to transport data in Big-Endian format, or not.") + @BooleanDefaultValue(true) private boolean bigEndian = true; + @ConfigurationParameter("force-unconnected-operation") + @Description("Forces the driver to use unconnected requests.") + @BooleanDefaultValue(false) + @Since("0.13.0") + private boolean forceUnconnectedOperation = false; + public int getBackplane() { return backplane; } @@ -61,4 +73,12 @@ public class EIPConfiguration implements PlcConnectionConfiguration { this.bigEndian = byteOrder == ByteOrder.BIG_ENDIAN; } + public boolean isForceUnconnectedOperation() { + return forceUnconnectedOperation; + } + + public void setForceUnconnectedOperation(boolean forceUnconnectedOperation) { + this.forceUnconnectedOperation = forceUnconnectedOperation; + } + } diff --git a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/protocol/EipProtocolLogic.java b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/protocol/EipProtocolLogic.java index ffc3a904de..d5fead1556 100644 --- a/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/protocol/EipProtocolLogic.java +++ b/plc4j/drivers/eip/src/main/java/org/apache/plc4x/java/eip/base/protocol/EipProtocolLogic.java @@ -66,6 +66,7 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha private static final byte[] DEFAULT_SENDER_CONTEXT = "PLC4X ".getBytes(StandardCharsets.US_ASCII); private static final long EMPTY_SESSION_HANDLE = 0L; + private static final long EMPTY_INTERFACE_OPTIONS = 0L; private static final long EMPTY_INTERFACE_HANDLE = 0L; private NullAddressItem nullAddressItem; private byte[] senderContext; @@ -375,7 +376,6 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha }); } - @Override public void onDisconnect(ConversationContext<EipPacket> context) { if (this.connectionId != 0L) { @@ -421,7 +421,6 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha } } - public void onDisconnectUnregisterSession(ConversationContext<EipPacket> context) { logger.debug("Sending Un RegisterSession EIP Package"); @@ -443,14 +442,13 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha private CompletableFuture<PlcReadResponse> readWithoutMessageRouter(PlcReadRequest readRequest) { CompletableFuture<PlcReadResponse> future = new CompletableFuture<>(); Map<String, ResponseItem<PlcValue>> values = new HashMap<>(); - + List<CompletableFuture<Void>> internalFutures = new ArrayList<>(); PathSegment classSegment = new LogicalSegment(new ClassID((byte) 0, (short) 6)); PathSegment instanceSegment = new LogicalSegment(new InstanceID((byte) 0, (short) 1)); DefaultPlcReadRequest request = (DefaultPlcReadRequest) readRequest; for (String tagName : request.getTagNames()) { - CompletableFuture<Boolean> internalFuture = new CompletableFuture<>(); - RequestTransactionManager.RequestTransaction transaction = tm.startRequest(); + CompletableFuture<Void> internalFuture = new CompletableFuture<>(); EipTag eipTag = (EipTag) request.getTag(tagName); String tag = eipTag.getTag(); @@ -466,24 +464,24 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha (byte) this.configuration.getBackplane(), (byte) this.configuration.getSlot()); - List<TypeId> typeIds = new ArrayList<>(2); - - typeIds.add(nullAddressItem); - typeIds.add(new UnConnectedDataItem(requestItem)); + List<TypeId> typeIds = Arrays.asList( + nullAddressItem, + new UnConnectedDataItem(requestItem)); - CipRRData pkt = new CipRRData( + CipRRData rrdata = new CipRRData( sessionHandle, CIPStatus.Success.getValue(), DEFAULT_SENDER_CONTEXT, 0L, - 0L, + EMPTY_INTERFACE_HANDLE, 0, typeIds); - transaction.submit(() -> context.sendRequest(pkt) + RequestTransactionManager.RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> context.sendRequest(rrdata) .expectResponse(EipPacket.class, REQUEST_TIMEOUT) - .onTimeout(future::completeExceptionally) - .onError((p, e) -> future.completeExceptionally(e)) + .onTimeout(internalFuture::completeExceptionally) + .onError((p, e) -> internalFuture.completeExceptionally(e)) .check(p -> p instanceof CipRRData) .unwrap(p -> (CipRRData) p) .check(p -> p.getSessionHandle() == sessionHandle) @@ -492,18 +490,22 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha UnConnectedDataItem dataItem = (UnConnectedDataItem) responseTypeIds.get(1); Map<String, ResponseItem<PlcValue>> readResponse = decodeSingleReadResponse(dataItem.getService(), tagName, eipTag); values.putAll(readResponse); - internalFuture.complete(true); - // Finish the request-transaction. + internalFuture.complete(null); transaction.endRequest(); })); + internalFutures.add(internalFuture); } catch (SerializationException e) { - e.printStackTrace(); + internalFuture.completeExceptionally(new PlcRuntimeException("Failed to read field")); } } - // TODO: This seems to be blocking here ... we should probably do this asynchronously - PlcReadResponse readResponse = new DefaultPlcReadResponse(readRequest, values); - future.complete(readResponse); + CompletableFuture.allOf(internalFutures.toArray(new CompletableFuture[0])).thenRun(() -> { + PlcReadResponse readResponse = new DefaultPlcReadResponse(readRequest, values); + future.complete(readResponse); + }).exceptionally(e -> { + future.completeExceptionally(e); + return null; + }); return future; } @@ -526,15 +528,21 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha toAnsi(tag), 1); - CipUnconnectedRequest unreq = new CipUnconnectedRequest( + CipUnconnectedRequest requestItem = new CipUnconnectedRequest( classSegment, instanceSegment, req, (byte) this.configuration.getBackplane(), (byte) this.configuration.getSlot()); - requests.add(unreq); + + // TODO: Possibly check if adding this would make the request/response exceed some + // protocol limits and possibly split up into multiple requests. + requests.add(requestItem); } catch (SerializationException e) { - e.printStackTrace(); + // TODO: Instead of failing the entire request it might be better to return a failure + // status for only this item. + future.completeExceptionally(new PlcRuntimeException("Failed to read field", e)); + return future; } } @@ -600,10 +608,14 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha CipReadRequest req = new CipReadRequest( toAnsi(tag), 1); - + // TODO: Possibly check if adding this would make the request/response exceed some + // protocol limits and possibly split up into multiple requests. requests.add(req); } catch (SerializationException e) { - e.printStackTrace(); + // TODO: Instead of failing the entire request it might be better to return a failure + // status for only this item. + future.completeExceptionally(new PlcRuntimeException("Failed to read field", e)); + return future; } } @@ -661,7 +673,7 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha @Override public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) { CompletableFuture<PlcReadResponse> future; - if (!this.useMessageRouter && !this.useConnectionManager) { + if (configuration.isForceUnconnectedOperation() || (!this.useMessageRouter && !this.useConnectionManager)) { future = readWithoutMessageRouter(readRequest); } else if (this.useMessageRouter && !this.useConnectionManager) { future = readWithoutConnectionManager(readRequest); @@ -887,74 +899,79 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha public CompletableFuture<PlcWriteResponse> writeWithoutMessageRouter(PlcWriteRequest writeRequest) { CompletableFuture<PlcWriteResponse> future = new CompletableFuture<>(); DefaultPlcWriteRequest request = (DefaultPlcWriteRequest) writeRequest; - List<CipWriteRequest> items = new ArrayList<>(writeRequest.getNumberOfTags()); + List<CompletableFuture<Void>> internalFutures = new ArrayList<>(); PathSegment classSegment = new LogicalSegment(new ClassID((byte) 0, (short) 6)); PathSegment instanceSegment = new LogicalSegment(new InstanceID((byte) 0, (short) 1)); Map<String, PlcResponseCode> values = new HashMap<>(); for (String fieldName : writeRequest.getTagNames()) { + CompletableFuture<Void> internalFuture = new CompletableFuture<>(); final EipTag field = (EipTag) request.getTag(fieldName); final PlcValue value = request.getPlcValue(fieldName); String tag = field.getTag(); int elements = Math.max(field.getElementNb(), 1); - byte[] data = encodeValue(value, field.getType()); - CipWriteRequest writeReq = null; try { - writeReq = new CipWriteRequest(toAnsi(tag), field.getType(), elements, data); - } catch (SerializationException e) { - e.printStackTrace(); - } - CompletableFuture<Boolean> internalFuture = new CompletableFuture<>(); - RequestTransactionManager.RequestTransaction transaction = tm.startRequest(); - - tm.startRequest(); + byte[] data = encodeValue(value, field.getType()); + CipWriteRequest writeReq = new CipWriteRequest( + toAnsi(tag), + field.getType(), + elements, + data); - UnConnectedDataItem exchange = new UnConnectedDataItem( - new CipUnconnectedRequest( + CipUnconnectedRequest requestItem = new CipUnconnectedRequest( classSegment, instanceSegment, writeReq, (byte) configuration.getBackplane(), - (byte) configuration.getSlot())); + (byte) configuration.getSlot()); - List<TypeId> typeIds = Arrays.asList(nullAddressItem, exchange); + List<TypeId> typeIds = Arrays.asList( + nullAddressItem, + new UnConnectedDataItem(requestItem)); - CipRRData rrdata = new CipRRData( - sessionHandle, - 0L, - senderContext, - 0L, - EMPTY_INTERFACE_HANDLE, - 0, - typeIds); + CipRRData rrdata = new CipRRData( + sessionHandle, + 0L, + // TODO: Check if this could also be the DEFAULT_SENDER_CONTEXT + senderContext, + EMPTY_INTERFACE_OPTIONS, + EMPTY_INTERFACE_HANDLE, + 0, + typeIds); - transaction.submit(() -> context.sendRequest(rrdata) - .expectResponse(EipPacket.class, REQUEST_TIMEOUT) - .onTimeout(future::completeExceptionally) - .onError((p, e) -> future.completeExceptionally(e)) - .check(p -> p instanceof CipRRData).unwrap(p -> (CipRRData) p) - .check(p -> p.getSessionHandle() == sessionHandle) - //.check(p -> p.getSenderContext() == senderContext) - .check(p -> ((UnConnectedDataItem) p.getTypeIds().get(1)).getService() instanceof CipWriteResponse) - .unwrap(p -> (CipWriteResponse) ((UnConnectedDataItem) p.getTypeIds().get(1)).getService()) - .handle(p -> { - Map<String, PlcResponseCode> responseItem = decodeSingleWriteResponse(p, fieldName); - values.putAll(responseItem); - internalFuture.complete(true); - transaction.endRequest(); - }) - ); - try { - internalFuture.get(REQUEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - future.completeExceptionally(new PlcRuntimeException("Failed to read field")); + RequestTransactionManager.RequestTransaction transaction = tm.startRequest(); + transaction.submit(() -> context.sendRequest(rrdata) + .expectResponse(EipPacket.class, REQUEST_TIMEOUT) + .onTimeout(internalFuture::completeExceptionally) + .onError((p, e) -> internalFuture.completeExceptionally(e)) + .check(p -> p instanceof CipRRData) + .unwrap(p -> (CipRRData) p) + .check(p -> p.getSessionHandle() == sessionHandle) + //.check(p -> p.getSenderContext() == senderContext) + .check(p -> ((UnConnectedDataItem) p.getTypeIds().get(1)).getService() instanceof CipWriteResponse) + .unwrap(p -> (CipWriteResponse) ((UnConnectedDataItem) p.getTypeIds().get(1)).getService()) + .handle(p -> { + Map<String, PlcResponseCode> responseItem = decodeSingleWriteResponse(p, fieldName); + values.putAll(responseItem); + internalFuture.complete(null); + transaction.endRequest(); + }) + ); + internalFutures.add(internalFuture); + } catch (SerializationException e) { + internalFuture.completeExceptionally(new PlcRuntimeException("Failed to read field")); } + CompletableFuture.allOf(internalFutures.toArray(new CompletableFuture[0])).thenRun(() -> { + PlcWriteResponse readResponse = new DefaultPlcWriteResponse(writeRequest, values); + future.complete(readResponse); + }).exceptionally(e -> { + future.completeExceptionally(e); + return null; + }); } - // TODO: This seems to be blocking here ... we should probably do this asynchronously - PlcWriteResponse response = new DefaultPlcWriteResponse(writeRequest, values); - future.complete(response); + return future; } @@ -973,9 +990,11 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha CipWriteRequest writeReq = new CipWriteRequest(toAnsi(tag), field.getType(), elements, data); items.add(writeReq); } catch (SerializationException e) { - e.printStackTrace(); + // TODO: Instead of failing the entire request it might be better to return a failure + // status for only this item. + future.completeExceptionally(new PlcRuntimeException("Failed to write field", e)); + return future; } - } RequestTransactionManager.RequestTransaction transaction = tm.startRequest(); @@ -1096,9 +1115,11 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha CipWriteRequest writeReq = new CipWriteRequest(toAnsi(tag), field.getType(), elements, data); items.add(writeReq); } catch (SerializationException e) { - e.printStackTrace(); + // TODO: Instead of failing the entire request it might be better to return a failure + // status for only this item. + future.completeExceptionally(new PlcRuntimeException("Failed to write field", e)); + return future; } - } RequestTransactionManager.RequestTransaction transaction = tm.startRequest(); @@ -1192,7 +1213,7 @@ public class EipProtocolLogic extends Plc4xProtocolBase<EipPacket> implements Ha @Override public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) { CompletableFuture<PlcWriteResponse> future; - if (!this.useMessageRouter && !this.useConnectionManager) { + if (configuration.isForceUnconnectedOperation() || (!this.useMessageRouter && !this.useConnectionManager)) { future = writeWithoutMessageRouter(writeRequest); } else if (this.useMessageRouter && !this.useConnectionManager) { future = writeWithoutConnectionManager(writeRequest); diff --git a/plc4j/drivers/eip/src/test/java/org/apache/plc4x/java/eip/logix/ManualEipLogixDriverUnconnectedTest.java b/plc4j/drivers/eip/src/test/java/org/apache/plc4x/java/eip/logix/ManualEipLogixDriverUnconnectedTest.java new file mode 100644 index 0000000000..e0eec4e2eb --- /dev/null +++ b/plc4j/drivers/eip/src/test/java/org/apache/plc4x/java/eip/logix/ManualEipLogixDriverUnconnectedTest.java @@ -0,0 +1,89 @@ +/* + * 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.logix; + +import org.apache.plc4x.java.spi.values.PlcBOOL; +import org.apache.plc4x.java.spi.values.PlcDINT; +import org.apache.plc4x.java.spi.values.PlcINT; +import org.apache.plc4x.java.spi.values.PlcREAL; +import org.apache.plc4x.java.spi.values.PlcSINT; +import org.apache.plc4x.test.manual.ManualTest; + +public class ManualEipLogixDriverUnconnectedTest extends ManualTest { + + /* + * Test program code on the PLC with the test-data. + * + * Located in "main" + * + + hurz_BOOL := TRUE; + hurz_BYTE := 42; + hurz_WORD := 42424; + hurz_DWORD := 4242442424; + hurz_LWORD := 4242442424242424242; + hurz_SINT := -42; + hurz_USINT := 42; + hurz_INT := -2424; + hurz_UINT := 42424; + hurz_DINT := -242442424; + hurz_UDINT := 4242442424; + hurz_LINT := -4242442424242424242; + hurz_ULINT := 4242442424242424242; + hurz_REAL := 3.14159265359; + hurz_LREAL := 2.71828182846; + hurz_TIME := T#1S234MS; + hurz_LTIME := LTIME#1000D15H23M12S34MS2US44NS; + hurz_DATE := D#1998-03-28; + //hurz_LDATE:LDATE; + hurz_TIME_OF_DAY := TIME_OF_DAY#15:36:30.123; + hurz_TOD := TOD#16:17:18.123; + //hurz_LTIME_OF_DAY:LTIME_OF_DAY; + //hurz_LTOD:LTOD; + hurz_DATE_AND_TIME := DATE_AND_TIME#1996-05-06-15:36:30; + hurz_DT := DT#1972-03-29-00:00:00; + //hurz_LDATE_AND_TIME:LDATE_AND_TIME; + //hurz_LDT:LDT; + hurz_STRING := 'hurz'; + hurz_WSTRING := "wolf"; + + * + */ + + public ManualEipLogixDriverUnconnectedTest(String connectionString) { + super(connectionString, true, false, true, 10); + } + + public static void main(String[] args) throws Exception { + ManualEipLogixDriverUnconnectedTest test = new ManualEipLogixDriverUnconnectedTest("logix://192.168.23.40?force-unconnected-operation=true"); + // This is the very limited number of types my controller supports. + test.addTestCase("hurz_BOOL", new PlcBOOL(true)); + test.addTestCase("hurz_SINT", new PlcSINT(-42)); + test.addTestCase("hurz_INT", new PlcINT(-2424)); + test.addTestCase("hurz_DINT", new PlcDINT(-242442424)); + test.addTestCase("hurz_REAL", new PlcREAL(3.141593F)); + //test.addTestCase("hurz_UDT", new PlcStruct()); + + long start = System.currentTimeMillis(); + test.run(); + long end = System.currentTimeMillis(); + System.out.printf("Finished in %d ms", end - start); + } + +}