From af9f5d2398580e3fc284ecfa49bbb0fcccad52f2 Mon Sep 17 00:00:00 2001 From: Alicia Date: Tue, 21 Nov 2023 18:13:19 +0000 Subject: [PATCH] [#139] Implemented bitset approach to command timeouts, completed implementation of timeouts --- .../commandPaths/ResponseExecutionPath.java | 3 + .../javaclient/nodes/ConnectionBuffer.java | 43 ++--- .../javaclient/nodes/EchoAssigner.java | 162 ++++++++++++++++++ .../javaclient/nodes/ZscriptBasicNode.java | 98 ++++------- .../zscript/javaclient/nodes/ZscriptNode.java | 7 +- .../javaclient/sequence/ResponseSequence.java | 21 ++- .../threading/ZscriptWorkerThread.java | 6 +- .../zscript/javaclient/devices/Device.java | 14 +- .../devices/NoResponseException.java | 5 + .../devices/ResponseSequenceCallback.java | 29 +++- 10 files changed, 286 insertions(+), 102 deletions(-) create mode 100644 clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/EchoAssigner.java create mode 100644 clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NoResponseException.java diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/ResponseExecutionPath.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/ResponseExecutionPath.java index 99c156826..5b17b802f 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/ResponseExecutionPath.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/ResponseExecutionPath.java @@ -79,6 +79,9 @@ private static List createLinkedPath(ReadToken start) { return builders; } + public static ResponseExecutionPath blank() { + return new ResponseExecutionPath(null); + } public static ResponseExecutionPath parse(ReadToken start) { List builders = createLinkedPath(start); diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ConnectionBuffer.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ConnectionBuffer.java index 4506cca0d..58a4a34cc 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ConnectionBuffer.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ConnectionBuffer.java @@ -1,5 +1,8 @@ package net.zscript.javaclient.nodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; @@ -32,7 +35,7 @@ class BufferElement { } BufferElement(CommandExecutionPath cmd, long nanoTimeTimeout) { - CommandSequence seq = CommandSequence.from(cmd, currentEcho, supports32Locks, lockConditions); + CommandSequence seq = CommandSequence.from(cmd, echo.getEcho(), supports32Locks, lockConditions); this.cmd = new AddressedCommand(seq); this.sameLayer = true; this.hadEchoBefore = false; @@ -64,22 +67,17 @@ public long getNanoTimeTimeout() { private final Connection connection; private final Queue buffer = new ArrayDeque<>(); + private final EchoAssigner echo; + private int bufferSize; private int currentBufferContent = 0; - private int currentEcho = 0x100; private Collection lockConditions = new ArrayList<>(); private boolean supports32Locks = false; - private void moveEchoValue() { - currentEcho++; - if (currentEcho > 0xffff) { - currentEcho = 0x100; - } - } - - public ConnectionBuffer(Connection connection, int bufferSize) { + public ConnectionBuffer(Connection connection, EchoAssigner echo, int bufferSize) { this.connection = connection; + this.echo = echo; this.bufferSize = bufferSize; } @@ -106,10 +104,15 @@ public AddressedCommand match(ResponseSequence sequence) { if (sequence.getResponseValue() != 0) { throw new IllegalArgumentException("Cannot match notification sequence with command sequence"); } + if (!sequence.hasEchoValue()) { + return null; + } boolean removeUpTo = true; for (Iterator iter = buffer.iterator(); iter.hasNext(); ) { BufferElement element = iter.next(); if (element.isSameLayer() && element.getCommand().getContent().getEchoValue() == sequence.getEchoValue()) { + // if the echo value is auto-generated, clear the marker + echo.responseArrivedNormal(sequence.getEchoValue()); if (removeUpTo) { clearOutTo(element); } else { @@ -133,6 +136,7 @@ public Collection checkTimeouts() { if (currentNano - element.getNanoTimeTimeout() > 0) { if (element.isSameLayer()) { timedOut.add(element.getCommand().getContent()); + echo.timeout(element.getCommand().getContent().getEchoValue()); } iter.remove(); currentBufferContent -= element.length; @@ -172,7 +176,14 @@ private boolean send(BufferElement element, boolean ignoreLength) { if (!ignoreLength && element.length + currentBufferContent >= bufferSize) { return false; } - moveEchoValue(); + // make sure echo system knows about echo usage... + if (element.hadEchoBefore) { + if (element.isSameLayer()) { + echo.manualEchoUse(element.getCommand().getContent().getEchoValue()); + } + } else { + echo.moveEcho(); + } buffer.add(element); currentBufferContent += element.length; connection.send(element.getCommand()); @@ -208,16 +219,6 @@ public int getCurrentBufferContent() { return currentBufferContent; } - // checks if the target is going to be used as an echo value within 0xf00 uses of the current value. - // ignores the offset from wrapping by having a big enough window. - public boolean isApproachingEcho(int target) { - if (currentEcho > 0xf000) { - return target > currentEcho || target < currentEcho - 0xf000; - } else { - return target < currentEcho + 0x1000 && target > currentEcho; - } - } - public void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/EchoAssigner.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/EchoAssigner.java new file mode 100644 index 000000000..f08db0d27 --- /dev/null +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/EchoAssigner.java @@ -0,0 +1,162 @@ +package net.zscript.javaclient.nodes; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.BitSet; + +public class EchoAssigner { + private static final Logger LOG = LoggerFactory.getLogger(EchoAssigner.class); + + private final static int SEGMENT_TIMEOUTS_BEFORE_CHANGE = 0x0010; + private final static int SEGMENT_MAX_WAITING = 0x00C0; + private final static int SEGMENT_SIZE = 0x0100; + + private final long minSegmentChangeTimeNanos; + + private BitSet previousMessages = new BitSet(0); + private BitSet sentMessages = new BitSet(SEGMENT_SIZE); + + private int previousSegmentOffset = SEGMENT_SIZE; + private int currentSegmentOffset = SEGMENT_SIZE; + + private int timeoutCount = 0; + private int waitingCount = 0; + private int currentEcho = 0; + + private long lastSegmentChangeTimeNanos = System.nanoTime(); + + public EchoAssigner(long minSegmentChangeTimeNanos) { + this.minSegmentChangeTimeNanos = minSegmentChangeTimeNanos; + } + + public void moveEcho() { + if (waitingCount >= SEGMENT_MAX_WAITING) { + LOG.error("Too many messages waiting for response ({}). Reduce command rate or latency.", waitingCount); + } + if (sentMessages.get(currentEcho)) { + throw new IllegalStateException("Current echo value invalid"); + } + sentMessages.set(currentEcho); + currentEcho = sentMessages.nextClearBit(currentEcho + 1); + if (currentEcho == SEGMENT_SIZE) { + currentEcho = sentMessages.nextClearBit(0); + if (currentEcho == SEGMENT_SIZE) { + throw new IllegalStateException("Ran out of echo values to assign"); + } + } + waitingCount++; + } + + public void manualEchoUse(int echo) { + int relativeEcho = echo - currentSegmentOffset; + if (relativeEcho >= 0 && relativeEcho < SEGMENT_SIZE) { + if (sentMessages.get(relativeEcho)) { + LOG.warn("Echo manually reused when timed out: {}", Integer.toHexString(echo)); + } else if (relativeEcho == currentEcho) { + moveEcho(); + } else { + sentMessages.set(relativeEcho); + } + } + int relativeEchoPrev = echo - previousSegmentOffset; + if (relativeEchoPrev >= 0 && relativeEchoPrev < SEGMENT_SIZE) { + if (previousMessages.get(relativeEcho)) { + LOG.warn("Echo manually reused when timed out: {}", Integer.toHexString(echo)); + } + previousMessages.set(relativeEcho); + } + } + + public int getEcho() { + return currentEcho + currentSegmentOffset; + } + + public boolean isWaiting(int echo) { + int relativeEcho = echo - currentSegmentOffset; + if (relativeEcho >= 0 && relativeEcho < SEGMENT_SIZE) { + return sentMessages.get(echo); + } + int relativeEchoPrev = echo - previousSegmentOffset; + if (relativeEchoPrev >= 0 && relativeEchoPrev < SEGMENT_SIZE) { + return previousMessages.get(echo); + } + return false; + } + + public void responseArrivedNormal(int echo) { + int relativeEcho = echo - currentSegmentOffset; + BitSet messagesTarget = sentMessages; + boolean count = true; + if (relativeEcho < 0 || relativeEcho >= SEGMENT_SIZE) { + relativeEcho = echo - previousSegmentOffset; + if (relativeEcho < 0 || relativeEcho >= SEGMENT_SIZE) { + return; + } + messagesTarget = previousMessages; + count = false; + } + if (messagesTarget.get(relativeEcho)) { + messagesTarget.clear(relativeEcho); + if (count) { + waitingCount--; + } + } + } + + public void timeout(int echo) { + int relativeEcho = echo - currentSegmentOffset; + if (relativeEcho < 0 || relativeEcho >= SEGMENT_SIZE) { + // if in previous, no action required + return; + } + // if not a current message, no action needed + if (sentMessages.get(relativeEcho)) { + timeoutCount++; + if (timeoutCount >= SEGMENT_TIMEOUTS_BEFORE_CHANGE) { + long time = System.nanoTime(); + if (time - lastSegmentChangeTimeNanos < minSegmentChangeTimeNanos) { + LOG.error("Connection timing out too much."); + } else { + LOG.info("Lingering timeout count: ({}). Changing echo value segment.", timeoutCount); + } + timeoutCount = 0; + waitingCount = 0; + currentEcho = 0; + previousMessages = sentMessages; + sentMessages = new BitSet(SEGMENT_SIZE); + previousSegmentOffset = currentSegmentOffset; + currentSegmentOffset += SEGMENT_SIZE; + if (currentSegmentOffset + SEGMENT_SIZE > 0x10000) { + currentSegmentOffset = SEGMENT_SIZE; // Skip the first segment, to leave space for manual echo + } + } + } + } + + public boolean unmatchedReceive(int echo) { + int relativeEcho = echo - currentSegmentOffset; + BitSet messagesTarget = sentMessages; + boolean count = true; + if (relativeEcho < 0 || relativeEcho >= SEGMENT_SIZE) { + relativeEcho = echo - previousSegmentOffset; + if (relativeEcho < 0 || relativeEcho >= SEGMENT_SIZE) { + // go to the unmatched handler, as message is very old (or not one we're keeping track of) + return false; + } + messagesTarget = previousMessages; + count = false; + } + if (messagesTarget.get(relativeEcho)) { + messagesTarget.clear(relativeEcho); + if (count) { + timeoutCount--; + } + return true; + } else { + // goes to the unmatched handler + return false; + } + } + +} diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptBasicNode.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptBasicNode.java index 742ebace1..40e43747d 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptBasicNode.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptBasicNode.java @@ -3,12 +3,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayDeque; import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; -import java.util.Queue; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -24,24 +21,6 @@ class ZscriptBasicNode implements ZscriptNode { private static final Logger LOG = LoggerFactory.getLogger(ZscriptBasicNode.class); - private static class CommandTimeout { - private final int echo; - private final long timeoutNanoTime; - - private CommandTimeout(int echo, long timeoutNanoTime) { - this.echo = echo; - this.timeoutNanoTime = timeoutNanoTime; - } - - public int getEcho() { - return echo; - } - - public long getTimeoutNanoTime() { - return timeoutNanoTime; - } - } - private final AddressingSystem addressingSystem; private final ConnectionBuffer connectionBuffer; @@ -62,22 +41,34 @@ public long getTimeoutNanoTime() { private final Map> pathCallbacks = new HashMap<>(); private final Map> fullSequenceCallbacks = new HashMap<>(); - private final Queue timeouts = new ArrayDeque<>(); + private final EchoAssigner echoSystem; - // How many timed-out commands are retained to avoid meaningless errors - private final int savedTimeoutCount; - // If the timeout queue wraps in less than this time, too many timeouts are occurring. - private final long timeoutRollTooSoonCount; + ZscriptBasicNode(Connection parentConnection, int bufferSize) { + this.addressingSystem = new AddressingSystem(this); + this.parentConnection = parentConnection; + this.echoSystem = new EchoAssigner(TimeUnit.MILLISECONDS.toNanos(100)); + this.connectionBuffer = new ConnectionBuffer(parentConnection, echoSystem, bufferSize); + this.strategy.setBuffer(connectionBuffer); + parentConnection.onReceive(r -> { + try { + if (r.hasAddress()) { + if (!addressingSystem.response(r)) { + unknownResponseHandler.accept(r); + } + } else { + response(r); + } + } catch (Exception e) { + callbackExceptionHandler.accept(e); // catches all callback exceptions + } + }); + } - ZscriptBasicNode(Connection parentConnection, int bufferSize, int savedTimeoutCount, long timeoutRollTooSoon, TimeUnit unit) { - if (savedTimeoutCount < 0) { - throw new IllegalArgumentException("Saved timeout count cannot be negative"); - } - this.savedTimeoutCount = savedTimeoutCount; - this.timeoutRollTooSoonCount = unit.toNanos(timeoutRollTooSoon); + ZscriptBasicNode(Connection parentConnection, int bufferSize, long minSegmentChangeTime, TimeUnit unit) { this.addressingSystem = new AddressingSystem(this); this.parentConnection = parentConnection; - this.connectionBuffer = new ConnectionBuffer(parentConnection, bufferSize); + this.echoSystem = new EchoAssigner(unit.toNanos(minSegmentChangeTime)); + this.connectionBuffer = new ConnectionBuffer(parentConnection, echoSystem, bufferSize); this.strategy.setBuffer(connectionBuffer); parentConnection.onReceive(r -> { try { @@ -118,56 +109,31 @@ public Connection detach(ZscriptAddress address) { public void send(CommandSequence seq, Consumer callback) { fullSequenceCallbacks.put(seq, callback); strategy.send(seq); - if (seq.hasEchoField()) { - for (Iterator iter = timeouts.iterator(); iter.hasNext(); ) { - int echo = iter.next().getEcho(); - if (echo == seq.getEchoValue()) { - iter.remove(); - //TODO: log: time since last use timed out. - } - } - } else { - checkTimeoutEchoReuse(); - } } public void send(CommandExecutionPath path, Consumer callback) { pathCallbacks.put(path, callback); strategy.send(path); - checkTimeoutEchoReuse(); } public void send(AddressedCommand addr) { strategy.send(addr); } - //TODO: add call to execute this - use worker thread public void checkTimeouts() { Collection timedOut = connectionBuffer.checkTimeouts(); if (!timedOut.isEmpty()) { long nanoTime = System.nanoTime(); - for (CommandSequence cmd : timedOut) { - timeouts.add(new CommandTimeout(cmd.getEchoValue(), nanoTime)); - } - while (timeouts.size() > savedTimeoutCount) { - long diff = nanoTime - timeouts.poll().getTimeoutNanoTime(); - if (diff < timeoutRollTooSoonCount) { - //log: too many timeouts... + for (CommandSequence seq : timedOut) { + if (fullSequenceCallbacks.get(seq) != null) { + fullSequenceCallbacks.get(seq).accept(ResponseSequence.blank()); + } else if (pathCallbacks.get(seq.getExecutionPath()) != null) { + pathCallbacks.get(seq.getExecutionPath()).accept(ResponseExecutionPath.blank()); } } } } - private void checkTimeoutEchoReuse() { - for (Iterator iter = timeouts.iterator(); iter.hasNext(); ) { - int echo = iter.next().getEcho(); - if (connectionBuffer.isApproachingEcho(echo)) { - iter.remove(); - //TODO: log: time since last use timed out. - } - } - } - private void response(AddressedResponse resp) { if (resp.getContent().getResponseValue() != 0) { Consumer handler = notificationHandlers.get(resp.getContent().getResponseValue()); @@ -180,11 +146,14 @@ private void response(AddressedResponse resp) { } AddressedCommand found = connectionBuffer.match(resp.getContent()); if (found == null) { + // if it's a recently timed out message, ignore it. + if (resp.getContent().hasEchoValue() && echoSystem.unmatchedReceive(resp.getContent().getEchoValue())) { + return; + } unknownResponseHandler.accept(resp); return; } strategy.mayHaveSpace(); - checkTimeoutEchoReuse(); parentConnection.responseReceived(found); Consumer seqCallback = fullSequenceCallbacks.remove(found.getContent()); if (seqCallback != null) { @@ -206,7 +175,6 @@ public Connection getParentConnection() { public void responseReceived(AddressedCommand found) { if (connectionBuffer.responseReceived(found)) { strategy.mayHaveSpace(); - checkTimeoutEchoReuse(); } parentConnection.responseReceived(found); } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptNode.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptNode.java index 99cc368e9..3a3a9f558 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptNode.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptNode.java @@ -15,12 +15,13 @@ public interface ZscriptNode { static ZscriptNode newNode(Connection parentConnection) { - return newNode(parentConnection, 128, 100, 100, TimeUnit.MILLISECONDS); + return newNode(parentConnection, 128, 100, TimeUnit.MILLISECONDS); } - static ZscriptNode newNode(Connection parentConnection, int bufferSize, int savedTimeoutCount, long timeoutRollTooSoon, TimeUnit unit) { - ZscriptBasicNode node = new ZscriptBasicNode(parentConnection, bufferSize, savedTimeoutCount, timeoutRollTooSoon, unit); + static ZscriptNode newNode(Connection parentConnection, int bufferSize, long minSegmentChangeTime, TimeUnit unit) { + ZscriptBasicNode node = new ZscriptBasicNode(parentConnection, bufferSize, minSegmentChangeTime, unit); ZscriptWorkerThread thread = parentConnection.getAssociatedThread(); + thread.addTimeoutCheck(node::checkTimeouts); return (ZscriptNode) Proxy.newProxyInstance(ZscriptNode.class.getClassLoader(), new Class[] { ZscriptNode.class }, (obj, method, params) -> thread.moveOntoThread(() -> method.invoke(node, params))); } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java index 98f38ffc1..66cbae063 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/sequence/ResponseSequence.java @@ -9,12 +9,14 @@ public class ResponseSequence { private final ResponseExecutionPath executionPath; - private final int echoField; - private final int responseField; + + private final int echoField; + private final int responseField; + private final boolean timedOut; public static ResponseSequence parse(TokenBuffer.TokenReader.ReadToken start) { if (start == null) { - return new ResponseSequence(ResponseExecutionPath.parse(null), -1, -1); + return new ResponseSequence(ResponseExecutionPath.blank(), -1, -1, false); } int echoField = -1; int responseField = -1; @@ -30,13 +32,18 @@ public static ResponseSequence parse(TokenBuffer.TokenReader.ReadToken start) { echoField = current.getData16(); current = iter.next().orElse(null); } - return new ResponseSequence(ResponseExecutionPath.parse(current), echoField, responseField); + return new ResponseSequence(ResponseExecutionPath.parse(current), echoField, responseField, false); + } + + public static ResponseSequence blank() { + return new ResponseSequence(ResponseExecutionPath.blank(), -1, -1, true); } - private ResponseSequence(ResponseExecutionPath executionPath, int echoField, int responseField) { + private ResponseSequence(ResponseExecutionPath executionPath, int echoField, int responseField, boolean timedOut) { this.executionPath = executionPath; this.echoField = echoField; this.responseField = responseField; + this.timedOut = timedOut; } public ResponseExecutionPath getExecutionPath() { @@ -47,6 +54,10 @@ public int getEchoValue() { return echoField; } + public boolean hasEchoValue() { + return echoField != -1; + } + public int getResponseValue() { return responseField; } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/threading/ZscriptWorkerThread.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/threading/ZscriptWorkerThread.java index 22453c67e..a1f7ecc00 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/threading/ZscriptWorkerThread.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/threading/ZscriptWorkerThread.java @@ -1,5 +1,7 @@ package net.zscript.javaclient.threading; +import java.lang.ref.PhantomReference; +import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; @@ -32,9 +34,9 @@ private void checkTimeouts() { Runnable node = iter.next().get(); if (node == null) { iter.remove(); - continue; + } else { + node.run(); } - node.run(); } } diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java index 0687b657e..d3b31d627 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/Device.java @@ -59,10 +59,16 @@ public void sendAsync(final CommandSequenceNode cmdSeq, final Consumer send(final CommandSequenceNode cmdSeq) { CompletableFuture future = new CompletableFuture<>(); - - CommandExecutionTask nodeToPath = convert(cmdSeq, future::complete); + CommandExecutionTask nodeToPath = convert(cmdSeq, resp -> { + if (!resp.wasExecuted()) { + future.completeExceptionally(new NoResponseException()); + } else { + future.complete(resp); + } + }); node.send(nodeToPath.getPath(), nodeToPath.getCallback()); return future; } @@ -71,6 +77,10 @@ public Future sendExpectSuccess(final CommandSequenceN CompletableFuture future = new CompletableFuture<>(); CommandExecutionTask nodeToPath = convert(cmdSeq, resp -> { + if (!resp.wasExecuted()) { + future.completeExceptionally(new NoResponseException()); + return; + } List> l = resp.getExecutionSummary(); if (l.get(l.size() - 1).getResponse().succeeded()) { future.complete(resp); diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NoResponseException.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NoResponseException.java new file mode 100644 index 000000000..ec87bb11d --- /dev/null +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NoResponseException.java @@ -0,0 +1,5 @@ +package net.zscript.javaclient.devices; + +public class NoResponseException extends Exception { + +} diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java index 9fb937c82..09926f5ff 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/ResponseSequenceCallback.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -48,10 +49,6 @@ public Class getResponseType() { } public static ResponseSequenceCallback from(List matchedCRs, Iterable> nodes, Map, Command> commandMap) { - Map> nodeMap = new HashMap<>(); - for (Map.Entry, Command> e : commandMap.entrySet()) { - nodeMap.put(e.getValue(), e.getKey()); - } Set> notExecuted = new HashSet<>(); for (ZscriptCommandNode node : nodes) { if (!notExecuted.add(node)) { @@ -59,6 +56,14 @@ public static ResponseSequenceCallback from(List matched "Command tree contains duplicate ZscriptCommandNode - this is not supported. Instead share the builder, and call it twice, or create the commands seperately."); } } + if (matchedCRs.isEmpty()) { + // if nothing was executed: + return new ResponseSequenceCallback(notExecuted); + } + Map> nodeMap = new HashMap<>(); + for (Map.Entry, Command> e : commandMap.entrySet()) { + nodeMap.put(e.getValue(), e.getKey()); + } LinkedHashMap, ZscriptResponse> responses = new LinkedHashMap<>(); @@ -93,6 +98,8 @@ public static ResponseSequenceCallback from(List matched private final CommandExecutionSummary abort; + private final boolean wasExecuted; + private ResponseSequenceCallback(LinkedHashMap, ZscriptResponse> responses, Set> notExecuted, Set> succeeded, Set> failed, CommandExecutionSummary abort) { @@ -101,6 +108,16 @@ private ResponseSequenceCallback(LinkedHashMap, ZscriptRes this.succeeded = succeeded; this.failed = failed; this.abort = abort; + this.wasExecuted = true; + } + + private ResponseSequenceCallback(Set> notExecuted) { + this.responses = new LinkedHashMap<>(); + this.notExecuted = notExecuted; + this.succeeded = Collections.emptySet(); + this.failed = Collections.emptySet(); + this.abort = null; + this.wasExecuted = false; } public List getResponses() { @@ -142,4 +159,8 @@ public Optional getResponseFor(ZscriptCommandNode node) { public Optional getResponseFor(ResponseCaptor captor) { return getResponseFor((ZscriptCommandNode) captor.getSource()).map(r -> ((ZscriptCommandNode) captor.getSource()).getResponseType().cast(r)); } + + public boolean wasExecuted() { + return wasExecuted; + } }