From 00c7581dc10db67018e40648c009e52b7629dae8 Mon Sep 17 00:00:00 2001 From: Jooyoung Jung Date: Tue, 21 Jul 2026 23:24:48 +0900 Subject: [PATCH] fix(plc4j/ads): cap sum-command item count, configurably The ADS driver's multiRead/multiWrite packed every tag they sent into a single ADS sum command (ADSIGRP_MULTIPLE_READ / ADSIGRP_MULTIPLE_WRITE) with no bound on the number of sub-commands. The item list is now split into consecutive groups of at most max-sum-command-items; the groups are sent sequentially and their results merged into one response. A request within the cap produces the same sum command as before: same items, same order, same payload. max-sum-command-items is an AdsConfiguration parameter (default 500); onConnect rejects a value below 1. Beckhoff's manual [1] says "Number of Sub-ADS calls: Highly recommended to max. 500!". The groups are chained with thenComposeAsync, so the next group is not built from the receive-thread callback that completed the previous one. As a consequence even a request that takes the sum path with a single group now builds and sends that sum command through the common pool instead of on the calling thread; requests left with one tag after resolution keep their separate non-sum path. A group-level ReturnCode != OK fails only that group's tags. A group that fails outright (send failure or timeout) fails the whole request and the remaining groups are not sent; in a multi-group write the earlier groups' writes may already have taken effect and are not rolled back. The same manual section also requires bounding the byte length of the requested data. This change does not do that. Also not covered: the separate, uncapped sum command used for symbolic handle resolution (ADSIGRP_SYM_HNDBYNAME sub-items). Both are left for a follow-up. The shared ADS driver testsuite gains three cases with the cap set to two and three tags: a split read, a split write, and one where the second group fails and only that group's tag reports the error. plc4go's multi-tag paths send one sum command per request and have no such parameter, so its runner skips these three cases. Unit tests cover the grouping helper chunk(), the two merge helpers, and the parameter default. [1] https://download.beckhoff.com/download/document/automation/twincat3/TwinCAT_ADS-DLL_C_EN.pdf TwinCAT 3 ADS-DLL C++ manual v1.1.3, section "ADS-sum command: Read or Write a list of variables with one single ADS-command" Signed-off-by: Jooyoung Jung --- .../testing/protocols/ads/DriverTestsuite.xml | 1000 +++++++++++++++++ plc4go/tests/drivers/tests/ads_driver_test.go | 9 + .../plc4x/java/ads/AdsTcpConnection.java | 176 ++- .../ads/configuration/AdsConfiguration.java | 13 + .../plc4x/java/ads/AdsSumChunkTest.java | 116 ++ .../plc4x/java/ads/AdsSumMergeTest.java | 167 +++ .../configuration/AdsConfigurationTest.java | 29 + .../protocols/ads/DriverTestsuite.xml | 1000 +++++++++++++++++ .../asciidoc/modules/users/partials/ads.adoc | 1 + 9 files changed, 2473 insertions(+), 38 deletions(-) create mode 100644 plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumChunkTest.java create mode 100644 plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumMergeTest.java diff --git a/plc4go/assets/testing/protocols/ads/DriverTestsuite.xml b/plc4go/assets/testing/protocols/ads/DriverTestsuite.xml index b708cff6d55..256aec2bac1 100644 --- a/plc4go/assets/testing/protocols/ads/DriverTestsuite.xml +++ b/plc4go/assets/testing/protocols/ads/DriverTestsuite.xml @@ -43,6 +43,13 @@ target-ams-port 851 + + + max-sum-command-items + 2 + @@ -2242,4 +2249,997 @@ + + Multi-element direct read request split across two sum commands + + Three direct addresses with max-sum-command-items lowered to two. The driver has to send + two ADSIGRP_MULTIPLE_READ sum commands, the first carrying two sub-commands and the second + the remaining one, and merge the two responses back into one response covering all three + tags in request order. + + + + + + + hurz1 +
00004040/00000001:BOOL
+
+ + hurz2 +
00004040/00000002:BOOL
+
+ + hurz3 +
00004040/00000003:BOOL
+
+
+
+
+ + + 0 + 72 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 40 + + 0 + + 9 + + 61568 + 2 + 10 + 24 + + + + 4040 + 1 + 1 + + + + + 4040 + 2 + 1 + + + + 0x + + + + + + + + 0 + 50 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 18 + + 0 + + 9 + + + 0 + + 10 + 0x00000000000000000100 + + + + + + + + 0 + 60 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 28 + + 0 + + 10 + + 61568 + 1 + 5 + 12 + + + + 4040 + 3 + 1 + + + + 0x + + + + + + + + 0 + 45 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 13 + + 0 + + 10 + + + 0 + + 5 + 0x0000000001 + + + + + + + + + + + + + 4040 + 1 + 1 + BOOL + + + + + 4040 + 2 + 1 + BOOL + + + + + 4040 + 3 + 1 + BOOL + + + + + + + + + OK + + true + + + + + + OK + + false + + + + + + OK + + true + + + + + + + 1000 +
+
+ + + Multi-element direct write request split across two sum commands + + The write counterpart of the split read above: three direct addresses with + max-sum-command-items lowered to two, so the driver has to send two + ADSIGRP_MULTIPLE_WRITE sum commands, the first carrying two sub-commands and their two + payload bytes and the second the remaining one. Merging the per-group return codes is + covered at unit level by AdsSumMergeTest; what this adds is that the connection really + does issue one sum command per group on the write path too. + + + + + + + hurz1 +
00004040/00000001:BOOL
+ + true + +
+ + hurz2 +
00004040/00000002:BOOL
+ + false + +
+ + hurz3 +
00004040/00000003:BOOL
+ + true + +
+
+
+
+ + + 0 + 74 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 42 + + 0 + + 9 + + 61569 + 2 + 8 + 26 + + + + 4040 + 1 + 1 + + + + + 4040 + 2 + 1 + + + + 0x0100 + + + + + + + + 0 + 48 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 16 + + 0 + + 9 + + + 0 + + 8 + 0x0000000000000000 + + + + + + + + 0 + 61 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 29 + + 0 + + 10 + + 61569 + 1 + 4 + 13 + + + + 4040 + 3 + 1 + + + + 0x01 + + + + + + + + 0 + 44 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 12 + + 0 + + 10 + + + 0 + + 4 + 0x00000000 + + + + + + + + + + OK + + + OK + + + OK + + + + +
+
+ + + One failing group does not fail the tags of the other group + + Same three direct addresses and the same cap of two, but the device answers the second sum + command with ADSERR_DEVICE_SYMBOLNOTFOUND. The tags of the first group keep their real + values, and only the tag of the failing group carries the mapped error code, rather than the + whole request failing or every tag degrading to INTERNAL_ERROR. + + + + + + + hurz1 +
00004040/00000001:BOOL
+
+ + hurz2 +
00004040/00000002:BOOL
+
+ + hurz3 +
00004040/00000003:BOOL
+
+
+
+
+ + + 0 + 72 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 40 + + 0 + + 9 + + 61568 + 2 + 10 + 24 + + + + 4040 + 1 + 1 + + + + + 4040 + 2 + 1 + + + + 0x + + + + + + + + 0 + 50 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 18 + + 0 + + 9 + + + 0 + + 10 + 0x00000000000000000100 + + + + + + + + 0 + 60 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 28 + + 0 + + 10 + + 61568 + 1 + 5 + 12 + + + + 4040 + 3 + 1 + + + + 0x + + + + + + + + 0 + 40 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 8 + + 0 + + 10 + + + 1808 + + 0 + 0x + + + + + + + + + + + + + 4040 + 1 + 1 + BOOL + + + + + 4040 + 2 + 1 + BOOL + + + + + 4040 + 3 + 1 + BOOL + + + + + + + + + OK + + true + + + + + + OK + + false + + + + + + INVALID_ADDRESS + + + + + +
+
+ diff --git a/plc4go/tests/drivers/tests/ads_driver_test.go b/plc4go/tests/drivers/tests/ads_driver_test.go index 204de1357b9..4f3a31dbdff 100644 --- a/plc4go/tests/drivers/tests/ads_driver_test.go +++ b/plc4go/tests/drivers/tests/ads_driver_test.go @@ -41,6 +41,15 @@ func TestAdsDriver(t *testing.T) { adsIO.AdsXmlParserHelper{}, append(optionsForTesting, testutils.WithRootTypeParser(parser), + testutils.WithSkippedTestCases( + // The Java driver splits a request into groups of at most + // max-sum-command-items sub-commands; Reader.go and Writer.go each build + // one sum command for the whole request and have no such option yet, so + // they answer the three tags in one exchange instead of two. + "Multi-element direct read request split across two sum commands", + "Multi-element direct write request split across two sum commands", + "One failing group does not fail the tags of the other group", + ), )..., ) } diff --git a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/AdsTcpConnection.java b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/AdsTcpConnection.java index 9b874c8ec62..95b819aa3e6 100644 --- a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/AdsTcpConnection.java +++ b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/AdsTcpConnection.java @@ -142,6 +142,13 @@ public boolean isConnected() { @Override protected void onConnect() throws PlcConnectionException { + // chunk() rejects a non-positive group size, and onRead/onWrite call it on the caller's + // thread - so an unusable cap would surface as an unchecked exception out of read() + // rather than as a failed future. Reject it here, where the caller expects to be told. + if (getConfiguration().getMaxSumCommandItems() < 1) { + throw new PlcConnectionException("max-sum-command-items must be at least 1, got " + + getConfiguration().getMaxSumCommandItems()); + } messageCodec = new AdsTcpMessageCodec(transportInstance, this::handleIncomingMessage); startReceiving(() -> { @@ -596,7 +603,32 @@ private CompletableFuture multiRead(PlcReadRequest readRequest, List orderedNames = readRequest.getTagNames().stream() .filter(resolved::containsKey).toList(); - long expectedDataSize = orderedNames.stream() + // Cap the number of sub-commands per sum command: send consecutive groups of at most + // max-sum-command-items sequentially and merge the per-group results. + // thenComposeAsync, not thenCompose: the previous group's stage completes on the driver's + // receive thread, so composing synchronously would build and write the next sum command + // from that callback. The throttle itself no longer blocks the caller, so this is about + // where the work runs, not about a parked permit. + // Failure granularity: a group-level ReturnCode != OK fails only that group's tags; the + // other groups still return their real results. For a single group the outcome is the + // previous all-or-nothing behaviour, except that an unparseable sum response no longer + // overwrites tags that had already failed to resolve with INTERNAL_ERROR. + CompletableFuture>> merged = + CompletableFuture.completedFuture(new LinkedHashMap<>()); + for (List groupNames : chunk(orderedNames, getConfiguration().getMaxSumCommandItems())) { + merged = merged.thenComposeAsync(accumulated -> + multiReadChunk(groupNames, resolved).thenApply(groupValues -> { + accumulated.putAll(groupValues); + return accumulated; + })); + } + + return merged.thenApply(accumulated -> assembleReadResponse(readRequest, initialFailures, accumulated)); + } + + private CompletableFuture>> multiReadChunk(List groupNames, + Map resolved) { + long expectedDataSize = groupNames.stream() .mapToLong(n -> 4L + resolved.get(n).sizeInBytes()) // 4-byte per-item return code + data .sum(); @@ -604,9 +636,9 @@ private CompletableFuture multiRead(PlcReadRequest readRequest, getConfiguration().getTargetAmsNetId(), getConfiguration().getTargetAmsPort(), getConfiguration().getSourceAmsNetId(), getConfiguration().getSourceAmsPort(), ReturnCode.OK, getInvokeId(), - ReservedIndexGroups.ADSIGRP_MULTIPLE_READ.getValue(), (long) orderedNames.size(), + ReservedIndexGroups.ADSIGRP_MULTIPLE_READ.getValue(), (long) groupNames.size(), expectedDataSize, - orderedNames.stream().map(n -> { + groupNames.stream().map(n -> { ResolvedAdsTag t = resolved.get(n); return new AdsMultiRequestItemRead(t.indexGroup(), t.indexOffset(), t.sizeInBytes()); }).collect(Collectors.toList()), @@ -615,11 +647,11 @@ ReturnCode.OK, getInvokeId(), return sendAmsRequest(request, AdsReadWriteResponse.class).thenApply(response -> { Map> values = new LinkedHashMap<>(); if (response.getResult() != ReturnCode.OK) { - for (String name : readRequest.getTagNames()) { - PlcResponseCode code = initialFailures.getOrDefault(name, parsePlcResponseCode(response.getResult())); + PlcResponseCode code = parsePlcResponseCode(response.getResult()); + for (String name : groupNames) { values.put(name, new DefaultPlcResponseItem<>(code, null)); } - return new DefaultPlcReadResponse(readRequest, values); + return values; } // Sum-up response layout: N x ReturnCode (4 bytes each), then concatenated data. @@ -628,19 +660,18 @@ ReturnCode.OK, getInvokeId(), // Read per-item return codes. Map resultPerTag = new LinkedHashMap<>(); try { - for (String n : orderedNames) { + for (String n : groupNames) { resultPerTag.put(n, ReturnCode.enumForValue(rb.readUnsignedLong(32))); } } catch (BufferException e) { - return buildFailureReadResponse(readRequest, Collections.emptyMap()); + for (String name : groupNames) { + values.put(name, new DefaultPlcResponseItem<>(PlcResponseCode.INTERNAL_ERROR, null)); + } + return values; } // Now decode each item's payload — for items whose result wasn't OK, we still need // to advance the cursor by sizeInBytes to keep alignment for the rest. - for (String name : readRequest.getTagNames()) { - if (initialFailures.containsKey(name)) { - values.put(name, new DefaultPlcResponseItem<>(initialFailures.get(name), null)); - continue; - } + for (String name : groupNames) { ResolvedAdsTag tag = resolved.get(name); ReturnCode code = resultPerTag.get(name); if (code != ReturnCode.OK) { @@ -664,10 +695,66 @@ ReturnCode.OK, getInvokeId(), values.put(name, new DefaultPlcResponseItem<>(PlcResponseCode.INTERNAL_ERROR, null)); } } - return new DefaultPlcReadResponse(readRequest, values); + return values; }); } + /** + * Merges the per-group read results into one response. A tag that failed before the request + * went out keeps that failure code, a tag that was sent but came back in no group is reported + * as {@code INTERNAL_ERROR}, and the response follows the order the caller asked in rather + * than the order the groups completed. + */ + static PlcReadResponse assembleReadResponse(PlcReadRequest readRequest, + Map initialFailures, + Map> accumulated) { + Map> values = new LinkedHashMap<>(); + for (String name : readRequest.getTagNames()) { + if (initialFailures.containsKey(name)) { + values.put(name, new DefaultPlcResponseItem<>(initialFailures.get(name), null)); + continue; + } + values.put(name, accumulated.getOrDefault(name, + new DefaultPlcResponseItem<>(PlcResponseCode.INTERNAL_ERROR, null))); + } + return new DefaultPlcReadResponse(readRequest, values); + } + + /** The write side of {@link #assembleReadResponse}, with the same three rules. */ + static PlcWriteResponse assembleWriteResponse(PlcWriteRequest writeRequest, + Map initialFailures, + Map accumulated) { + Map codes = new LinkedHashMap<>(); + for (String name : writeRequest.getTagNames()) { + if (initialFailures.containsKey(name)) { + codes.put(name, initialFailures.get(name)); + } else { + codes.put(name, accumulated.getOrDefault(name, PlcResponseCode.INTERNAL_ERROR)); + } + } + return new DefaultPlcWriteResponse(writeRequest, codes); + } + + /** + * Partitions {@code items} into consecutive sub-lists of at most {@code maxPerChunk} + * elements, preserving order. An empty input yields an empty list of chunks. The chunks are + * copies rather than {@code subList} views because they outlive the loop in the async chain; + * both current call sites pass a list nobody mutates, so that is belt and braces. + */ + static List> chunk(List items, int maxPerChunk) { + if (maxPerChunk <= 0) { + throw new IllegalArgumentException("maxPerChunk must be positive, got " + maxPerChunk); + } + List> chunks = new ArrayList<>(Math.ceilDiv(items.size(), maxPerChunk)); + for (int i = 0; i < items.size(); i += maxPerChunk) { + // Defensive copy rather than a subList view: the chunks are captured in async + // lambdas and outlive this loop. Both call sites pass a list nobody mutates, so + // this is belt and braces. + chunks.add(new ArrayList<>(items.subList(i, Math.min(i + maxPerChunk, items.size())))); + } + return chunks; + } + private PlcResponseItem decodePayload(ResolvedAdsTag tag, byte[] data) { try { ReadBufferByteBased rb = newLittleEndianBuffer(data); @@ -807,12 +894,35 @@ private CompletableFuture multiWrite(PlcWriteRequest writeRequ Map resolved, Map serialized, Map initialFailures) { + List names = new ArrayList<>(serialized.keySet()); + + // Cap the number of sub-commands per sum command: send consecutive groups of at most + // max-sum-command-items sequentially and merge the per-group results. + // thenComposeAsync for the same reason as the read side: the next group must not be built + // and written from the receive-thread callback that completed the previous one. + // A group that fails outright fails the whole request, so in a multi-group write that fails + // midway the earlier groups' writes may already have taken effect; they are not rolled back. + CompletableFuture> merged = + CompletableFuture.completedFuture(new LinkedHashMap<>()); + for (List groupNames : chunk(names, getConfiguration().getMaxSumCommandItems())) { + merged = merged.thenComposeAsync(accumulated -> + multiWriteChunk(groupNames, resolved, serialized).thenApply(groupCodes -> { + accumulated.putAll(groupCodes); + return accumulated; + })); + } + + return merged.thenApply(accumulated -> assembleWriteResponse(writeRequest, initialFailures, accumulated)); + } + + private CompletableFuture> multiWriteChunk(List groupNames, + Map resolved, + Map serialized) { // Concatenate per-tag payloads in request order. - int totalDataBytes = serialized.values().stream().mapToInt(b -> b.length).sum(); + int totalDataBytes = groupNames.stream().mapToInt(n -> serialized.get(n).length).sum(); byte[] payload = new byte[totalDataBytes]; int p = 0; - List names = new ArrayList<>(serialized.keySet()); - for (String name : names) { + for (String name : groupNames) { byte[] b = serialized.get(name); System.arraycopy(b, 0, payload, p, b.length); p += b.length; @@ -822,9 +932,9 @@ private CompletableFuture multiWrite(PlcWriteRequest writeRequ getConfiguration().getTargetAmsNetId(), getConfiguration().getTargetAmsPort(), getConfiguration().getSourceAmsNetId(), getConfiguration().getSourceAmsPort(), ReturnCode.OK, getInvokeId(), - ReservedIndexGroups.ADSIGRP_MULTIPLE_WRITE.getValue(), (long) names.size(), - (long) names.size() * 4L, - names.stream().map(n -> { + ReservedIndexGroups.ADSIGRP_MULTIPLE_WRITE.getValue(), (long) groupNames.size(), + (long) groupNames.size() * 4L, + groupNames.stream().map(n -> { ResolvedAdsTag t = resolved.get(n); return new AdsMultiRequestItemWrite(t.indexGroup(), t.indexOffset(), t.sizeInBytes()); }).collect(Collectors.toList()), @@ -833,33 +943,23 @@ ReturnCode.OK, getInvokeId(), return sendAmsRequest(request, AdsReadWriteResponse.class).thenApply(response -> { Map codes = new LinkedHashMap<>(); if (response.getResult() != ReturnCode.OK) { - for (String n : writeRequest.getTagNames()) { - codes.put(n, initialFailures.getOrDefault(n, parsePlcResponseCode(response.getResult()))); + PlcResponseCode code = parsePlcResponseCode(response.getResult()); + for (String n : groupNames) { + codes.put(n, code); } - return new DefaultPlcWriteResponse(writeRequest, codes); + return codes; } ReadBufferByteBased rb = newLittleEndianBuffer(response.getData()); - Map perTag = new LinkedHashMap<>(); try { - for (String n : names) { - perTag.put(n, ReturnCode.enumForValue(rb.readUnsignedLong(32))); + for (String n : groupNames) { + codes.put(n, parsePlcResponseCode(ReturnCode.enumForValue(rb.readUnsignedLong(32)))); } } catch (BufferException e) { - for (String n : writeRequest.getTagNames()) { - codes.put(n, initialFailures.getOrDefault(n, PlcResponseCode.INTERNAL_ERROR)); - } - return new DefaultPlcWriteResponse(writeRequest, codes); - } - for (String n : writeRequest.getTagNames()) { - if (initialFailures.containsKey(n)) { - codes.put(n, initialFailures.get(n)); - } else if (perTag.containsKey(n)) { - codes.put(n, parsePlcResponseCode(perTag.get(n))); - } else { + for (String n : groupNames) { codes.put(n, PlcResponseCode.INTERNAL_ERROR); } } - return new DefaultPlcWriteResponse(writeRequest, codes); + return codes; }); } diff --git a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/configuration/AdsConfiguration.java b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/configuration/AdsConfiguration.java index 046545ade1b..5bde7e44c4b 100644 --- a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/configuration/AdsConfiguration.java +++ b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/configuration/AdsConfiguration.java @@ -69,6 +69,11 @@ public class AdsConfiguration implements Configuration { @Description("Maximum nesting depth accepted when parsing the data-type table uploaded from the device. An entry may contain further entries, so without a limit the depth of the tree is dictated by the device rather than by the driver, and a table of well under a megabyte can nest deeply enough to exhaust the parser's stack. Real type hierarchies are only a handful of levels deep, so the default is already generous; raise it for a device that is known to need more. Note that the JVM's own stack imposes a practical ceiling of a few thousand levels regardless of what is configured here.") protected int maxDataTypeTableDepth; + @ConfigurationParameter("max-sum-command-items") + @IntDefaultValue(500) + @Description("Maximum number of sub-commands packed into a single ADS sum command (ADSIGRP_MULTIPLE_READ / ADSIGRP_MULTIPLE_WRITE). Requests referencing more tags are split into consecutive groups of at most this size and sent sequentially. The ceiling and the reason for it are Beckhoff's: the PLC works off a whole ADS request before starting its next PLC cycle, so a request carrying very many sub-commands makes it collect everything into one response first, and \"this large number of ads-sub-commands will jitter the PLC execution\". See \"Number of Sub-ADS calls: Highly recommended to max. 500!\" in the TwinCAT 3 ADS-DLL C++ manual version 1.1.3, section \"ADS-sum command: Read or Write a list of variables with one single ADS-command\", https://download.beckhoff.com/download/document/automation/twincat3/TwinCAT_ADS-DLL_C_EN.pdf . This bounds the item count only; the same section separately requires the caller to bound the response byte length, which this driver does not do yet. Must be at least 1.") + protected int maxSumCommandItems; + @ConfigurationParameter("load-symbol-and-data-type-tables") @BooleanDefaultValue(true) @Description("Configures, if when connecting the data-type- and symbol-table should be read. This is an optimization that can help in cases, where the PLC program is pretty large and downloading the full tables is causing problems. When disabled, reading and writing is limited to direct addresses (`{IndexGroup}/{IndexOffset}:{TYPE}`): symbolic addresses cannot be resolved without the tables and are rejected with a corresponding error. Browsing is unavailable for the same reason. Subscriptions are unaffected, as they resolve symbol handles on the device.") @@ -122,6 +127,14 @@ public void setMaxDataTypeTableDepth(int maxDataTypeTableDepth) { this.maxDataTypeTableDepth = maxDataTypeTableDepth; } + public int getMaxSumCommandItems() { + return maxSumCommandItems; + } + + public void setMaxSumCommandItems(int maxSumCommandItems) { + this.maxSumCommandItems = maxSumCommandItems; + } + public boolean isLoadSymbolAndDataTypeTables() { return loadSymbolAndDataTypeTables; } diff --git a/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumChunkTest.java b/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumChunkTest.java new file mode 100644 index 00000000000..293470d4a00 --- /dev/null +++ b/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumChunkTest.java @@ -0,0 +1,116 @@ +/* + * 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 + * + * http://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.ads; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link AdsTcpConnection#chunk(List, int)}, the helper that partitions the item + * list of a sum command (multi-read/multi-write) into groups no larger than the + * per-sum-command item cap. + */ +class AdsSumChunkTest { + + private static List items(int count) { + List items = new ArrayList<>(count); + IntStream.range(0, count).forEach(items::add); + return items; + } + + @Test + void emptyListYieldsNoChunks() { + assertTrue(AdsTcpConnection.chunk(Collections.emptyList(), 500).isEmpty()); + } + + @Test + void singleItemYieldsOneChunk() { + List> chunks = AdsTcpConnection.chunk(items(1), 500); + assertEquals(1, chunks.size()); + assertEquals(List.of(0), chunks.get(0)); + } + + @Test + void listSmallerThanCapYieldsOneChunk() { + List> chunks = AdsTcpConnection.chunk(items(499), 500); + assertEquals(1, chunks.size()); + assertEquals(499, chunks.get(0).size()); + } + + @Test + void listEqualToCapYieldsOneChunk() { + List> chunks = AdsTcpConnection.chunk(items(500), 500); + assertEquals(1, chunks.size()); + assertEquals(500, chunks.get(0).size()); + } + + @Test + void listOneOverCapYieldsTwoChunks() { + List> chunks = AdsTcpConnection.chunk(items(501), 500); + assertEquals(2, chunks.size()); + assertEquals(500, chunks.get(0).size()); + assertEquals(1, chunks.get(1).size()); + } + + @Test + void largeListYieldsFullChunksPlusRemainder() { + List> chunks = AdsTcpConnection.chunk(items(1001), 500); + assertEquals(3, chunks.size()); + assertEquals(500, chunks.get(0).size()); + assertEquals(500, chunks.get(1).size()); + assertEquals(1, chunks.get(2).size()); + } + + @Test + void orderIsPreservedAcrossChunks() { + List input = items(1001); + List> chunks = AdsTcpConnection.chunk(input, 500); + List flattened = new ArrayList<>(input.size()); + chunks.forEach(flattened::addAll); + assertEquals(input, flattened); + } + + @Test + void chunkSizeSmallerThanCapWorks() { + List> chunks = AdsTcpConnection.chunk(items(7), 3); + assertEquals(3, chunks.size()); + assertEquals(List.of(0, 1, 2), chunks.get(0)); + assertEquals(List.of(3, 4, 5), chunks.get(1)); + assertEquals(List.of(6), chunks.get(2)); + } + + @Test + void maximumCapYieldsOneChunk() { + List> chunks = AdsTcpConnection.chunk(items(2), Integer.MAX_VALUE); + assertEquals(1, chunks.size()); + assertEquals(List.of(0, 1), chunks.get(0)); + } + + @Test + void nonPositiveCapIsRejected() { + assertThrows(IllegalArgumentException.class, () -> AdsTcpConnection.chunk(items(1), 0)); + assertThrows(IllegalArgumentException.class, () -> AdsTcpConnection.chunk(items(1), -1)); + } +} diff --git a/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumMergeTest.java b/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumMergeTest.java new file mode 100644 index 00000000000..7033fc26b2c --- /dev/null +++ b/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/AdsSumMergeTest.java @@ -0,0 +1,167 @@ +/* + * 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.ads; + +import org.apache.plc4x.java.ads.tag.SymbolicAdsTag; +import org.apache.plc4x.java.api.messages.PlcReadResponse; +import org.apache.plc4x.java.api.messages.PlcWriteResponse; +import org.apache.plc4x.java.api.model.PlcTag; +import org.apache.plc4x.java.api.types.PlcResponseCode; +import org.apache.plc4x.java.api.value.PlcValue; +import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcReadRequest; +import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcReadResponse; +import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcWriteRequest; +import org.apache.plc4x.java.spi.drivers.messages.items.DefaultPlcResponseItem; +import org.apache.plc4x.java.spi.drivers.messages.items.DefaultPlcTagItem; +import org.apache.plc4x.java.spi.drivers.messages.items.DefaultPlcTagValueItem; +import org.apache.plc4x.java.spi.drivers.messages.items.PlcResponseItem; +import org.apache.plc4x.java.spi.drivers.messages.items.PlcTagItem; +import org.apache.plc4x.java.spi.drivers.messages.items.PlcTagValueItem; +import org.apache.plc4x.java.spi.values.PlcDINT; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * How the per-group results of a split request are merged back into one response. + * + *

{@link AdsSumChunkTest} covers the partitioning arithmetic, and the driver testsuite + * ({@code AdsDriverIT} over {@code protocols/ads/DriverTestsuite.xml}) covers the connection + * issuing one sum command per group, on the read and the write path. This covers the merge + * itself: each tag keeps its own group's result, a tag that failed before anything was sent + * keeps that failure code, and a tag no group answered for is reported rather than dropped. + */ +public class AdsSumMergeTest { + + private static List names(int count) { + List names = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + names.add("tag" + i); + } + return names; + } + + /** A real request rather than a mock: only the tag names matter here. */ + private static DefaultPlcReadRequest requestFor(List tagNames) { + LinkedHashMap> tags = new LinkedHashMap<>(); + for (String name : tagNames) { + tags.put(name, new DefaultPlcTagItem<>(SymbolicAdsTag.of("Main." + name))); + } + return new DefaultPlcReadRequest(null, tags); + } + + /** The write counterpart of {@link #requestFor}: again only the tag names matter. */ + private static DefaultPlcWriteRequest writeRequestFor(List tagNames) { + LinkedHashMap> tags = new LinkedHashMap<>(); + for (String name : tagNames) { + tags.put(name, new DefaultPlcTagValueItem<>(SymbolicAdsTag.of("Main." + name), new PlcDINT(0))); + } + return new DefaultPlcWriteRequest(null, tags); + } + + private static PlcResponseItem ok(int value) { + return new DefaultPlcResponseItem<>(PlcResponseCode.OK, new PlcDINT(value)); + } + + /** + * Asserting on {@code response.getTagNames()} would prove nothing: {@link DefaultPlcReadResponse} + * answers that from the request, so it reads back in request order whatever the merge did. What + * can actually break is the pairing of each name with its own group's value. + */ + @Test + void eachTagKeepsItsOwnValueWhateverOrderTheGroupsCameBackIn() { + List tagNames = names(3); + // The second group came back first and put its result in ahead of the first group's. + Map> accumulated = new LinkedHashMap<>(); + accumulated.put("tag2", ok(22)); + accumulated.put("tag0", ok(0)); + accumulated.put("tag1", ok(11)); + + PlcReadResponse response = + AdsTcpConnection.assembleReadResponse(requestFor(tagNames), Map.of(), accumulated); + + assertEquals(0, response.getInteger("tag0")); + assertEquals(11, response.getInteger("tag1")); + assertEquals(22, response.getInteger("tag2")); + } + + @Test + void aTagThatFailedBeforeSendingKeepsItsOwnCode() { + List tagNames = names(2); + Map initialFailures = Map.of("tag0", PlcResponseCode.INVALID_ADDRESS); + Map> accumulated = new LinkedHashMap<>(); + accumulated.put("tag1", ok(1)); + + PlcReadResponse response = + AdsTcpConnection.assembleReadResponse(requestFor(tagNames), initialFailures, accumulated); + + assertEquals(PlcResponseCode.INVALID_ADDRESS, response.getResponseCode("tag0")); + assertEquals(PlcResponseCode.OK, response.getResponseCode("tag1")); + } + + @Test + void aTagNoGroupAnsweredForIsReportedRatherThanDropped() { + List tagNames = names(2); + Map> accumulated = new LinkedHashMap<>(); + accumulated.put("tag0", ok(0)); + + PlcReadResponse response = + AdsTcpConnection.assembleReadResponse(requestFor(tagNames), Map.of(), accumulated); + + assertEquals(PlcResponseCode.INTERNAL_ERROR, response.getResponseCode("tag1")); + } + + /** The write side pairs names with codes the same way; see the read case above on why. */ + @Test + void eachWrittenTagKeepsItsOwnCodeWhateverOrderTheGroupsCameBackIn() { + List tagNames = names(3); + Map accumulated = new LinkedHashMap<>(); + accumulated.put("tag2", PlcResponseCode.OK); + accumulated.put("tag0", PlcResponseCode.INVALID_DATA); + accumulated.put("tag1", PlcResponseCode.ACCESS_DENIED); + + PlcWriteResponse response = + AdsTcpConnection.assembleWriteResponse(writeRequestFor(tagNames), Map.of(), accumulated); + + assertEquals(PlcResponseCode.INVALID_DATA, response.getResponseCode("tag0")); + assertEquals(PlcResponseCode.ACCESS_DENIED, response.getResponseCode("tag1")); + assertEquals(PlcResponseCode.OK, response.getResponseCode("tag2")); + } + + /** A tag that never made it onto the wire keeps its own code; one no group answered for is reported. */ + @Test + void writeKeepsPreSendFailuresAndReportsUnansweredTags() { + List tagNames = names(3); + Map initialFailures = Map.of("tag0", PlcResponseCode.INVALID_DATA); + Map accumulated = new LinkedHashMap<>(); + accumulated.put("tag1", PlcResponseCode.OK); + + PlcWriteResponse response = AdsTcpConnection.assembleWriteResponse( + writeRequestFor(tagNames), initialFailures, accumulated); + + assertEquals(PlcResponseCode.INVALID_DATA, response.getResponseCode("tag0")); + assertEquals(PlcResponseCode.OK, response.getResponseCode("tag1")); + assertEquals(PlcResponseCode.INTERNAL_ERROR, response.getResponseCode("tag2")); + } +} diff --git a/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/configuration/AdsConfigurationTest.java b/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/configuration/AdsConfigurationTest.java index 7fba5d1c70c..ab9e1b0a70b 100644 --- a/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/configuration/AdsConfigurationTest.java +++ b/plc4j/drivers/ads/src/test/java/org/apache/plc4x/java/ads/configuration/AdsConfigurationTest.java @@ -39,6 +39,7 @@ void gettersAndSetters() { cfg.setTimeoutRequest(1234); cfg.setLoadSymbolAndDataTypeTables(false); cfg.setMaxDataTypeTableDepth(64); + cfg.setMaxSumCommandItems(2); assertEquals(target, cfg.getTargetAmsNetId()); assertEquals(851, cfg.getTargetAmsPort()); @@ -47,6 +48,7 @@ void gettersAndSetters() { assertEquals(1234, cfg.getTimeoutRequest()); assertFalse(cfg.isLoadSymbolAndDataTypeTables()); assertEquals(64, cfg.getMaxDataTypeTableDepth()); + assertEquals(2, cfg.getMaxSumCommandItems()); assertTrue(cfg.toString().contains("targetAmsPort=851")); } @@ -69,6 +71,33 @@ void maxDataTypeTableDepthDefaultsToTwenty() throws Exception { "an unconfigured connection must get a usable budget, never 0"); } + /** + * The cap is Beckhoff's recommended ceiling for sub-ADS calls in one sum command. A default + * that failed to resolve would arrive as 0, and chunk() rejects a non-positive chunk size, so + * every sum read and write would fail outright - the default is pinned here rather than + * assumed. + */ + @Test + void maxSumCommandItemsDefaultsToFiveHundred() throws Exception { + AdsConfiguration defaults = new ConfigurationFactory() + .createConfiguration(AdsConfiguration.class, REQUIRED_PARAMS); + + assertEquals(500, defaults.getMaxSumCommandItems(), + "Beckhoff recommends at most 500 sub-ADS calls per sum command"); + } + + /** + * Lowering the cap is what makes the split testable end-to-end: the driver testsuite drives + * three tags with a cap of two rather than putting 501 tags in XML. + */ + @Test + void maxSumCommandItemsCanBeLowered() throws Exception { + AdsConfiguration lowered = new ConfigurationFactory().createConfiguration( + AdsConfiguration.class, REQUIRED_PARAMS + "&max-sum-command-items=2"); + + assertEquals(2, lowered.getMaxSumCommandItems()); + } + @Test void maxDataTypeTableDepthCanBeRaised() throws Exception { AdsConfiguration raised = new ConfigurationFactory().createConfiguration( diff --git a/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml b/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml index b708cff6d55..256aec2bac1 100644 --- a/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml +++ b/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml @@ -43,6 +43,13 @@ target-ams-port 851 + + + max-sum-command-items + 2 + @@ -2242,4 +2249,997 @@ + + Multi-element direct read request split across two sum commands + + Three direct addresses with max-sum-command-items lowered to two. The driver has to send + two ADSIGRP_MULTIPLE_READ sum commands, the first carrying two sub-commands and the second + the remaining one, and merge the two responses back into one response covering all three + tags in request order. + + + + + + + hurz1 +

00004040/00000001:BOOL
+ + + hurz2 +
00004040/00000002:BOOL
+
+ + hurz3 +
00004040/00000003:BOOL
+
+ + + + + + 0 + 72 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 40 + + 0 + + 9 + + 61568 + 2 + 10 + 24 + + + + 4040 + 1 + 1 + + + + + 4040 + 2 + 1 + + + + 0x + + + + + + + + 0 + 50 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 18 + + 0 + + 9 + + + 0 + + 10 + 0x00000000000000000100 + + + + + + + + 0 + 60 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 28 + + 0 + + 10 + + 61568 + 1 + 5 + 12 + + + + 4040 + 3 + 1 + + + + 0x + + + + + + + + 0 + 45 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 13 + + 0 + + 10 + + + 0 + + 5 + 0x0000000001 + + + + + + + + + + + + + 4040 + 1 + 1 + BOOL + + + + + 4040 + 2 + 1 + BOOL + + + + + 4040 + 3 + 1 + BOOL + + + + + + + + + OK + + true + + + + + + OK + + false + + + + + + OK + + true + + + + + + + 1000 + + + + + Multi-element direct write request split across two sum commands + + The write counterpart of the split read above: three direct addresses with + max-sum-command-items lowered to two, so the driver has to send two + ADSIGRP_MULTIPLE_WRITE sum commands, the first carrying two sub-commands and their two + payload bytes and the second the remaining one. Merging the per-group return codes is + covered at unit level by AdsSumMergeTest; what this adds is that the connection really + does issue one sum command per group on the write path too. + + + + + + + hurz1 +
00004040/00000001:BOOL
+ + true + +
+ + hurz2 +
00004040/00000002:BOOL
+ + false + +
+ + hurz3 +
00004040/00000003:BOOL
+ + true + +
+
+
+
+ + + 0 + 74 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 42 + + 0 + + 9 + + 61569 + 2 + 8 + 26 + + + + 4040 + 1 + 1 + + + + + 4040 + 2 + 1 + + + + 0x0100 + + + + + + + + 0 + 48 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 16 + + 0 + + 9 + + + 0 + + 8 + 0x0000000000000000 + + + + + + + + 0 + 61 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 29 + + 0 + + 10 + + 61569 + 1 + 4 + 13 + + + + 4040 + 3 + 1 + + + + 0x01 + + + + + + + + 0 + 44 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 12 + + 0 + + 10 + + + 0 + + 4 + 0x00000000 + + + + + + + + + + OK + + + OK + + + OK + + + + +
+
+ + + One failing group does not fail the tags of the other group + + Same three direct addresses and the same cap of two, but the device answers the second sum + command with ADSERR_DEVICE_SYMBOLNOTFOUND. The tags of the first group keep their real + values, and only the tag of the failing group carries the mapped error code, rather than the + whole request failing or every tag degrading to INTERNAL_ERROR. + + + + + + + hurz1 +
00004040/00000001:BOOL
+
+ + hurz2 +
00004040/00000002:BOOL
+
+ + hurz3 +
00004040/00000003:BOOL
+
+
+
+
+ + + 0 + 72 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 40 + + 0 + + 9 + + 61568 + 2 + 10 + 24 + + + + 4040 + 1 + 1 + + + + + 4040 + 2 + 1 + + + + 0x + + + + + + + + 0 + 50 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 18 + + 0 + + 9 + + + 0 + + 10 + 0x00000000000000000100 + + + + + + + + 0 + 60 + + + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + 9 + + false + false + false + false + false + true + false + false + false + 0 + 28 + + 0 + + 10 + + 61568 + 1 + 5 + 12 + + + + 4040 + 3 + 1 + + + + 0x + + + + + + + + 0 + 40 + + + + + 192 + 168 + 23 + 220 + 1 + 1 + + + 65534 + + + 192 + 168 + 23 + 20 + 1 + 1 + + + 851 + + 9 + + false + false + false + false + false + true + false + true + false + 0 + 8 + + 0 + + 10 + + + 1808 + + 0 + 0x + + + + + + + + + + + + + 4040 + 1 + 1 + BOOL + + + + + 4040 + 2 + 1 + BOOL + + + + + 4040 + 3 + 1 + BOOL + + + + + + + + + OK + + true + + + + + + OK + + false + + + + + + INVALID_ADDRESS + + + + + +
+
+ diff --git a/website/asciidoc/modules/users/partials/ads.adoc b/website/asciidoc/modules/users/partials/ads.adoc index 536f0c65325..a455bb7a1c1 100644 --- a/website/asciidoc/modules/users/partials/ads.adoc +++ b/website/asciidoc/modules/users/partials/ads.adoc @@ -42,6 +42,7 @@ |`source-ams-port` |INT | |required |AMS port of the source. |`request-timeout-ms` |INT |4000| |Default timeout for all types of requests. |`max-data-type-table-depth` |INT |20| |Maximum nesting depth accepted when parsing the data-type table uploaded from the device. An entry may contain further entries, so without a limit the depth of the tree is dictated by the device rather than by the driver, and a table of well under a megabyte can nest deeply enough to exhaust the parser's stack. Real type hierarchies are only a handful of levels deep, so the default is already generous; raise it for a device that is known to need more. Note that the JVM's own stack imposes a practical ceiling of a few thousand levels regardless of what is configured here. +|`max-sum-command-items` |INT |500| |Maximum number of sub-commands packed into a single ADS sum command (ADSIGRP_MULTIPLE_READ / ADSIGRP_MULTIPLE_WRITE). Requests referencing more tags are split into consecutive groups of at most this size and sent sequentially. The ceiling and the reason for it are Beckhoff's: the PLC works off a whole ADS request before starting its next PLC cycle, so a request carrying very many sub-commands makes it collect everything into one response first, and "this large number of ads-sub-commands will jitter the PLC execution". See "Number of Sub-ADS calls: Highly recommended to max. 500!" in the TwinCAT 3 ADS-DLL C++ manual version 1.1.3, section "ADS-sum command: Read or Write a list of variables with one single ADS-command", https://download.beckhoff.com/download/document/automation/twincat3/TwinCAT_ADS-DLL_C_EN.pdf . This bounds the item count only; the same section separately requires the caller to bound the response byte length, which this driver does not do yet. Must be at least 1. |`load-symbol-and-data-type-tables` |BOOLEAN |true| |Configures, if when connecting the data-type- and symbol-table should be read. This is an optimization that can help in cases, where the PLC program is pretty large and downloading the full tables is causing problems. When disabled, reading and writing is limited to direct addresses (`\{IndexGroup}/\{IndexOffset}:\{TYPE}`): symbolic addresses cannot be resolved without the tables and are rejected with a corresponding error. Browsing is unavailable for the same reason. Subscriptions are unaffected, as they resolve symbol handles on the device. 5+|Transport config options: 5+|