diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/Respondable.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/Respondable.java new file mode 100644 index 000000000..b10bcd8fe --- /dev/null +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/Respondable.java @@ -0,0 +1,4 @@ +package net.zscript.javaclient.commandbuilder; + +public interface Respondable { +} diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java index 8f966d8ae..58c85a139 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/ZscriptResponse.java @@ -3,6 +3,8 @@ import java.util.OptionalInt; import net.zscript.javareceiver.tokenizer.BlockIterator; +import net.zscript.model.components.Zchars; +import net.zscript.model.components.ZscriptStatus; public interface ZscriptResponse { /** @@ -14,4 +16,18 @@ public interface ZscriptResponse { OptionalInt getField(char key); + default boolean succeeded() { + OptionalInt status = getField((char) Zchars.Z_STATUS); + return status.isEmpty() || status.getAsInt() == ZscriptStatus.SUCCESS; + } + + default boolean failed() { + OptionalInt status = getField((char) Zchars.Z_STATUS); + return status.isPresent() && ZscriptStatus.isFailure(status.getAsInt()); + } + + default boolean error() { + OptionalInt status = getField((char) Zchars.Z_STATUS); + return status.isPresent() && ZscriptStatus.isError(status.getAsInt()); + } } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ResponseCaptor.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ResponseCaptor.java index 5c16895fc..f0eb8fdcc 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ResponseCaptor.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ResponseCaptor.java @@ -2,6 +2,7 @@ import java.util.NoSuchElementException; +import net.zscript.javaclient.commandbuilder.Respondable; import net.zscript.javaclient.commandbuilder.ZscriptResponse; public class ResponseCaptor { @@ -9,36 +10,16 @@ public static ResponseCaptor create() { return new ResponseCaptor<>(); } - private ZscriptCommandNode command; - private T response = null; - private boolean called = false; - - public T get() { - if (called) { - return response; - } else { - throw new NoSuchElementException("Command was not run, so no response exists"); - } - } - - public boolean wasCalled() { - return called; + private ResponseCaptor() { } - public void setCommand(ZscriptCommandNode command) { - this.command = command; - } - - public ZscriptCommandNode getCommand() { - return command; - } + private Respondable source; - public void resetResponseParsing() { - called = false; + public void setSource(Respondable source) { + this.source = source; } - public void responseReceived(T response) { - this.response = response; - called = true; + public Respondable getSource() { + return source; } } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java index 1aeef6f52..44369a324 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/commandnodes/ZscriptCommandNode.java @@ -8,11 +8,12 @@ import net.zscript.javaclient.commandPaths.ZscriptFieldSet; import net.zscript.javaclient.ZscriptByteString; +import net.zscript.javaclient.commandbuilder.Respondable; import net.zscript.javaclient.commandbuilder.ZscriptResponse; import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandBuilder.BigField; import net.zscript.javareceiver.tokenizer.ZscriptExpression; -public abstract class ZscriptCommandNode extends CommandSequenceNode { +public abstract class ZscriptCommandNode extends CommandSequenceNode implements Respondable { private final ResponseCaptor captor; @@ -32,10 +33,14 @@ protected ZscriptCommandNode(ResponseCaptor captor, List bigFields, this.bigFields = bigFields; this.fields = fields; if (captor != null) { - captor.setCommand(this); + captor.setSource(this); } } + public ResponseCaptor getCaptor() { + return captor; + } + public abstract T parseResponse(ZscriptExpression response); public abstract Class getResponseType(); @@ -44,18 +49,6 @@ public List getChildren() { return Collections.emptyList(); } - public void responseArrived(ZscriptResponse response) { - if (captor != null) { - captor.responseReceived(getResponseType().cast(response)); - } - } - - public void resetResponseParsing() { - if (captor != null) { - captor.resetResponseParsing(); - } - } - public ZscriptFieldSet asFieldSet() { return ZscriptFieldSet.fromMap(bigFields.stream().map(BigField::getData).collect(Collectors.toList()), bigFields.stream().map(BigField::isString).collect(Collectors.toList()), fields); diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java index 700e04039..73b1702ea 100644 --- a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/defaultCommands/DefaultResponse.java @@ -21,4 +21,5 @@ public boolean isValid() { public OptionalInt getField(char key) { return expression.getField(key); } + } diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationHandle.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationHandle.java new file mode 100644 index 000000000..16088b971 --- /dev/null +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationHandle.java @@ -0,0 +1,11 @@ +package net.zscript.javaclient.commandbuilder.notifications; + +import java.util.List; + +import net.zscript.javaclient.commandbuilder.ZscriptResponse; + +public abstract class NotificationHandle { + public abstract NotificationSection getSection(NotificationSectionId response); + + public abstract List> getSections(); +} diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationId.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationId.java new file mode 100644 index 000000000..9552a7151 --- /dev/null +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationId.java @@ -0,0 +1,9 @@ +package net.zscript.javaclient.commandbuilder.notifications; + +public abstract class NotificationId { + public abstract int getId(); + + public abstract Class getHandleType(); + + public abstract T newHandle(); +} diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationSection.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationSection.java new file mode 100644 index 000000000..3b8559dce --- /dev/null +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationSection.java @@ -0,0 +1,17 @@ +package net.zscript.javaclient.commandbuilder.notifications; + +import net.zscript.javaclient.commandPaths.ZscriptFieldSet; +import net.zscript.javaclient.commandbuilder.Respondable; +import net.zscript.javaclient.commandbuilder.ZscriptResponse; +import net.zscript.javaclient.commandbuilder.commandnodes.ResponseCaptor; +import net.zscript.javareceiver.tokenizer.ZscriptExpression; + +public abstract class NotificationSection implements Respondable { + public abstract Class getResponseType(); + + public void setCaptor(ResponseCaptor captor) { + captor.setSource(this); + } + + public abstract T parseResponse(ZscriptExpression expression); +} diff --git a/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationSectionId.java b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationSectionId.java new file mode 100644 index 000000000..449a4dc87 --- /dev/null +++ b/clients/java-client-lib/client-command-api/src/main/java/net/zscript/javaclient/commandbuilder/notifications/NotificationSectionId.java @@ -0,0 +1,6 @@ +package net.zscript.javaclient.commandbuilder.notifications; + +import net.zscript.javaclient.commandbuilder.ZscriptResponse; + +public class NotificationSectionId { +} diff --git a/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache b/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache index 3bed5954e..b799af772 100644 --- a/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache +++ b/clients/java-client-lib/client-command-builders/src/main/resources/templates/commands/JavaCommandBuilder.mustache @@ -12,6 +12,7 @@ import net.zscript.javaclient.commandbuilder.commandnodes.*; import net.zscript.javaclient.commandbuilder.defaultCommands.*; import net.zscript.javaclient.commandbuilder.*; import net.zscript.javaclient.commandbuilder.commandnodes.*; +import net.zscript.javaclient.commandbuilder.notifications.*; import net.zscript.javareceiver.tokenizer.*; import net.zscript.model.components.*; @@ -166,28 +167,92 @@ public class {{#upperCamel}}{{moduleName}}{{/upperCamel}}Module { } } + {{#notificationSections}} + /** {{description}} */ + public static class {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId extends NotificationSectionId<{{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent> { + private static final {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId id = new {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId(); + + public static {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId {{#lowerCamel}}{{name}}{{/lowerCamel}}NotificationSectionId(){ + return id; + } + public static {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId get(){ + return id; + } + private {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId(){ + } + } + public static class {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSection extends NotificationSection<{{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent> { + @Override + public Class<{{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent> getResponseType(){ + return {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent.class; + } + + @Override + public {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent parseResponse(ZscriptExpression expression){ + return new {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent(expression); + } + } + + public static class {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionContent extends ValidatingResponse { + public {{#upperCamel}}{{name}}NotificationSectionContent{{/upperCamel}}(ZscriptExpression response) { + super(response, new byte[] { {{#responseFields}}{{#required}}(byte) '{{key}}', {{/required}}{{/responseFields}} }); + } + {{#fields}} + {{>responseField.mustache}} + {{/fields}} + } + {{/notificationSections}} + {{#notifications}} /** {{description}} */ - public static class {{#upperCamel}}{{notificationName}}{{/upperCamel}}Notification { + public static class {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationId extends NotificationId<{{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle>{ public static final {{#upperCamel}}{{moduleName}}{{/upperCamel}}Notifications NTFN = {{#upperCamel}}{{moduleName}}{{/upperCamel}}Notifications.{{#upperCamel}}{{notificationName}}{{/upperCamel}}; + private static final {{#upperCamel}}{{name}}{{/upperCamel}}NotificationId id = new {{#upperCamel}}{{name}}{{/upperCamel}}NotificationId(); + + private {{#upperCamel}}{{name}}{{/upperCamel}}NotificationId(){} - public {{#upperCamel}}{{notificationName}}{{/upperCamel}}Notification(String zscriptResponse) { + public static {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationId {{#lowerCamel}}{{notificationName}}{{/lowerCamel}}NotificationId(){ + return id; + } + public static {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationId get(){ + return id; } - {{#sections}} - {{#section}} - /** {{description}} */ - public static class {{#upperCamel}}{{sectionName}}{{/upperCamel}}Section extends ValidatingResponse { - public {{#upperCamel}}{{sectionName}}{{/upperCamel}}Section(final ZscriptExpression response) { - super(response, new byte[] { {{#responseFields}}{{#required}}(byte) '{{key}}', {{/required}}{{/responseFields}} }); - } - {{#responseFields}} - {{>responseField.mustache}} - {{/responseFields}} + @Override + public int getId(){ + return (MODULE_ID << 4) | (int) NTFN.getNotification(); + } + + @Override + public Class<{{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle> getHandleType(){ + return {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle.class; } - {{/section}} + @Override + public {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle newHandle(){ + return new {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle(); + } + } + public static class {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle extends NotificationHandle{ + private final LinkedHashMap, NotificationSection> sections = new LinkedHashMap<>(); + + public {{#upperCamel}}{{notificationName}}{{/upperCamel}}NotificationHandle() { + {{#sections}} + {{#section}} + sections.put({{#upperCamel}}{{name}}{{/upperCamel}}NotificationSectionId.get(), new {{#upperCamel}}{{name}}{{/upperCamel}}NotificationSection()); + {{/section}} {{/sections}} + } + @Override + public NotificationSection getSection(NotificationSectionId response) { + return (NotificationSection) sections.get(response); + } + @Override + public List> getSections() { + return new ArrayList<>(sections.values()); + } } + {{/notifications}} + } diff --git a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java index 604962475..1e7803382 100644 --- a/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java +++ b/clients/java-client-lib/client-command-builders/src/test/java/net/zscript/model/modules/testing/test/JavaCommandBuilderResponseTest.java @@ -234,12 +234,10 @@ private void checkResponse(final String responseChar final Consumer listener) { responseChars.chars().forEach(c -> tokenizer.accept((byte) c)); - ResponseCaptor captor = ResponseCaptor.create(); - ZscriptCommandNode cmd = commandBuilder.capture(captor) - .build(); + ResponseCaptor captor = ResponseCaptor.create(); + ZscriptCommandNode cmd = commandBuilder.capture(captor).build(); - cmd.responseArrived(cmd.parseResponse(new ZscriptTokenExpression(tokenReader::iterator))); - listener.accept(captor.get()); + listener.accept(cmd.parseResponse(new ZscriptTokenExpression(tokenReader::iterator))); } } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/AddressedCommand.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/AddressedCommand.java index 0af38ad9e..5567c0056 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/AddressedCommand.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/AddressedCommand.java @@ -31,6 +31,7 @@ public void toBytes(ZscriptByteString.ZscriptByteStringBuilder builder) { address.writeTo(builder); } content.toBytes(builder); + builder.appendByte('\n'); } public CommandSequence getContent() { diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java index b19880152..2cf81a476 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/addressing/CompleteAddressedResponse.java @@ -3,11 +3,13 @@ import static net.zscript.javareceiver.tokenizer.TokenBuffer.TokenReader; import static net.zscript.javareceiver.tokenizer.TokenBuffer.TokenReader.ReadToken; +import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.Optional; import net.zscript.javaclient.sequence.ResponseSequence; +import net.zscript.javareceiver.tokenizer.Tokenizer; import net.zscript.model.components.Zchars; import net.zscript.util.OptIterator; @@ -16,6 +18,14 @@ public class CompleteAddressedResponse { private final ResponseSequence content; public static CompleteAddressedResponse parse(TokenReader reader) { + OptIterator iterEnding = reader.iterator(); + for (Optional opt = iterEnding.next(); opt.isPresent(); opt = iterEnding.next()) { + if (opt.get().isSequenceEndMarker()) { + if (opt.get().getKey() != Tokenizer.NORMAL_SEQUENCE_END) { + throw new RuntimeException("Parse failed with Tokenizer error: " + Integer.toHexString(opt.get().getKey())); + } + } + } OptIterator iter = reader.iterator(); List addresses = new ArrayList<>(); ResponseSequence seq = null; diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java index bd42166b2..9e8c8b2fb 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/commandPaths/CommandExecutionPath.java @@ -234,6 +234,15 @@ public void toSequence(ZscriptByteString.ZscriptByteStringBuilder out) { } } + public boolean matchesResponses(ResponseExecutionPath resps) { + try { + compareResponses(resps); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + public List compareResponses(ResponseExecutionPath resps) { Deque parenStarts = new ArrayDeque<>(); // fill out parenStarts so that we can have as many ')' as we want... @@ -260,45 +269,45 @@ public List compareResponses(ResponseExecutionPath resps if (lastSucceeded) { if (lastEndedClose) { if (parenStarts.peek().getOnFail() == current.getOnFail()) { - throw new IllegalStateException("Response has ')' without valid opening '('"); + throw new IllegalArgumentException("Response has ')' without valid opening '('"); } Command tmp2 = parenStarts.pop().getOnFail(); while (tmp2 != null && tmp2 != current) { tmp2 = tmp2.getOnSuccess(); } if (tmp2 != current) { - throw new IllegalStateException("Response has ')' without command sequence merging"); + throw new IllegalArgumentException("Response has ')' without command sequence merging"); } lastEndedClose = false; } else if (lastEndedOpen) { parenStarts.push(current); lastEndedOpen = false; } else if (lastFail != null && current.getOnFail() != lastFail) { - throw new IllegalStateException("Fail conditions don't match up around '&'"); + throw new IllegalArgumentException("Fail conditions don't match up around '&'"); } } else { for (int i = 0; i < lastParenCount; i++) { if (parenStarts.isEmpty()) { - throw new IllegalStateException("Command sequence ran out of parens before response sequence"); + throw new IllegalArgumentException("Command sequence ran out of parens before response sequence"); } Command tmp3 = parenStarts.peek().getOnFail(); while (tmp3 != null && tmp3.getOnFail() != current) { tmp3 = tmp3.getOnSuccess(); } if (tmp3 == null) { - throw new IllegalStateException("Response has ')' without command sequence merging"); + throw new IllegalArgumentException("Response has ')' without command sequence merging"); } tmp3 = parenStarts.peek().getOnFail(); while (tmp3 != null && tmp3 != current) { tmp3 = tmp3.getOnFail(); } if (tmp3 != current) { - throw new IllegalStateException("Response has ')' without command sequence merging"); + throw new IllegalArgumentException("Response has ')' without command sequence merging"); } parenStarts.pop(); } if (parenStarts.isEmpty() || parenStarts.peek().getOnFail() != current) { - throw new IllegalStateException("Response has failure divergence without parenthesis"); + throw new IllegalArgumentException("Response has failure divergence without parenthesis"); } } if (currentResp.wasSuccess()) { @@ -335,7 +344,7 @@ public Optional next() { while (true) { while (toVisit.hasNext()) { Command c = toVisit.next(); - if (!visited.contains(c)) { + if (c != null && !visited.contains(c)) { visited.add(c); Command s = c.getOnSuccess(); if (!visited.contains(s)) { 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/AddressingSystem.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/AddressingSystem.java index cbcf24a7f..053e0b38c 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/AddressingSystem.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/AddressingSystem.java @@ -7,8 +7,9 @@ import net.zscript.javaclient.addressing.AddressedCommand; import net.zscript.javaclient.addressing.AddressedResponse; import net.zscript.javaclient.addressing.ZscriptAddress; +import net.zscript.javaclient.threading.ZscriptWorkerThread; -public class AddressingSystem { +class AddressingSystem { class AddressingConnection implements Connection { private final ZscriptAddress address; private Consumer respConsumer = null; @@ -38,13 +39,18 @@ public void response(AddressedResponse resp) { public void responseReceived(AddressedCommand found) { node.responseReceived(found); } + + @Override + public ZscriptWorkerThread getAssociatedThread() { + return node.getParentConnection().getAssociatedThread(); + } } private final Map connections = new HashMap<>(); - private final ZscriptNode node; + private final ZscriptBasicNode node; - public AddressingSystem(ZscriptNode node) { + AddressingSystem(ZscriptBasicNode node) { this.node = node; } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/Connection.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/Connection.java index 8951114b3..14b868d95 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/Connection.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/Connection.java @@ -4,6 +4,8 @@ import net.zscript.javaclient.addressing.AddressedCommand; import net.zscript.javaclient.addressing.AddressedResponse; +import net.zscript.javaclient.threading.ZscriptCallbackThreadpool; +import net.zscript.javaclient.threading.ZscriptWorkerThread; public interface Connection { void send(AddressedCommand cmd); @@ -11,4 +13,6 @@ public interface Connection { void onReceive(Consumer resp); void responseReceived(AddressedCommand found); + + ZscriptWorkerThread getAssociatedThread(); } 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 cbf542df6..69d54481c 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,10 +1,15 @@ 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; import java.util.Iterator; +import java.util.List; import java.util.Queue; +import java.util.concurrent.TimeUnit; import net.zscript.javaclient.addressing.AddressedCommand; import net.zscript.javaclient.commandPaths.CommandExecutionPath; @@ -19,27 +24,31 @@ class BufferElement { private final boolean sameLayer; private final boolean hadEchoBefore; private final int length; + private final long nanoTimeTimeout; - BufferElement(CommandSequence seq) { + BufferElement(CommandSequence seq, long nanoTimeTimeout) { this.cmd = new AddressedCommand(seq); this.sameLayer = true; this.hadEchoBefore = true; this.length = seq.getBufferLength(); + this.nanoTimeTimeout = nanoTimeTimeout; } - BufferElement(CommandExecutionPath cmd) { - CommandSequence seq = CommandSequence.from(cmd, currentEcho, supports32Locks, lockConditions); + BufferElement(CommandExecutionPath cmd, long nanoTimeTimeout) { + CommandSequence seq = CommandSequence.from(cmd, echo.getEcho(), supports32Locks, lockConditions); this.cmd = new AddressedCommand(seq); this.sameLayer = true; this.hadEchoBefore = false; this.length = seq.getBufferLength(); + this.nanoTimeTimeout = nanoTimeTimeout; } - BufferElement(AddressedCommand cmd) { + BufferElement(AddressedCommand cmd, long nanoTimeTimeout) { this.cmd = cmd; this.sameLayer = !cmd.hasAddressLayer(); this.hadEchoBefore = true; this.length = cmd.getBufferLength(); + this.nanoTimeTimeout = nanoTimeTimeout; } public boolean isSameLayer() { @@ -49,27 +58,26 @@ public boolean isSameLayer() { public AddressedCommand getCommand() { return cmd; } + + public long getNanoTimeTimeout() { + return nanoTimeTimeout; + } } private final Connection connection; private final Queue buffer = new ArrayDeque<>(); - private final int bufferSize; - private int currentBufferContent = 0; - private int currentEcho = 0x100; + private final EchoAssigner echo; + + private int bufferSize; + private int currentBufferContent = 0; 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; } @@ -96,10 +104,18 @@ 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 (!element.getCommand().getContent().getExecutionPath().matchesResponses(sequence.getExecutionPath())) { + return element.getCommand(); + } + // if the echo value is auto-generated, clear the marker + echo.responseArrivedNormal(sequence.getEchoValue()); if (removeUpTo) { clearOutTo(element); } else { @@ -114,6 +130,24 @@ public AddressedCommand match(ResponseSequence sequence) { return null; } + public Collection checkTimeouts() { + List timedOut = new ArrayList<>(2); + long currentNano = System.nanoTime(); + for (Iterator iter = buffer.iterator(); iter.hasNext(); ) { + BufferElement element = iter.next(); + //subtracting first to avoid wrapping issues. + if (currentNano - element.getNanoTimeTimeout() > 0) { + if (element.isSameLayer()) { + timedOut.add(element.getCommand().getContent()); + echo.timeout(element.getCommand().getContent().getEchoValue()); + } + iter.remove(); + currentBufferContent -= element.length; + } + } + return timedOut; + } + public boolean responseReceived(AddressedCommand cmd) { boolean removeUpTo = true; for (Iterator iter = buffer.iterator(); iter.hasNext(); ) { @@ -145,23 +179,30 @@ 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()); return true; } - public boolean send(AddressedCommand cmd, boolean ignoreLength) { - return send(new BufferElement(cmd), ignoreLength); + public boolean send(AddressedCommand cmd, boolean ignoreLength, long timeout, TimeUnit unit) { + return send(new BufferElement(cmd, System.nanoTime() + unit.toNanos(timeout)), ignoreLength); } - public boolean send(CommandSequence seq, boolean ignoreLength) { - return send(new BufferElement(seq), ignoreLength); + public boolean send(CommandSequence seq, boolean ignoreLength, long timeout, TimeUnit unit) { + return send(new BufferElement(seq, System.nanoTime() + unit.toNanos(timeout)), ignoreLength); } - public boolean send(CommandExecutionPath path, boolean ignoreLength) { - return send(new BufferElement(path), ignoreLength); + public boolean send(CommandExecutionPath path, boolean ignoreLength, long timeout, TimeUnit unit) { + return send(new BufferElement(path, System.nanoTime() + unit.toNanos(timeout)), ignoreLength); } public boolean hasNonAddressedInBuffer() { @@ -181,4 +222,7 @@ public int getCurrentBufferContent() { return currentBufferContent; } + 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/QueuingStrategy.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/QueuingStrategy.java index a4aa445ff..e7e81119a 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/QueuingStrategy.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/QueuingStrategy.java @@ -12,4 +12,6 @@ public interface QueuingStrategy { void send(CommandExecutionPath seq); void send(AddressedCommand seq); + + void setBuffer(ConnectionBuffer buffer); } diff --git a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/StandardQueuingStrategy.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/StandardQueuingStrategy.java index adf30981c..8beae6086 100644 --- a/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/StandardQueuingStrategy.java +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/StandardQueuingStrategy.java @@ -2,6 +2,7 @@ import java.util.ArrayDeque; import java.util.Queue; +import java.util.concurrent.TimeUnit; import net.zscript.javaclient.addressing.AddressedCommand; import net.zscript.javaclient.commandPaths.CommandExecutionPath; @@ -22,7 +23,7 @@ private SequenceQueueElement(CommandSequence seq) { @Override public boolean addToBuffer(boolean ignoreLength) { - return buffer.send(seq, ignoreLength); + return buffer.send(seq, ignoreLength, timeout, unit); } } @@ -35,7 +36,7 @@ private PathQueueElement(CommandExecutionPath path) { @Override public boolean addToBuffer(boolean ignoreLength) { - return buffer.send(path, ignoreLength); + return buffer.send(path, ignoreLength, timeout, unit); } } @@ -48,7 +49,7 @@ private AddressedQueueElement(AddressedCommand addr) { @Override public boolean addToBuffer(boolean ignoreLength) { - return buffer.send(addr, ignoreLength); + return buffer.send(addr, ignoreLength, timeout, unit); } } @@ -56,6 +57,14 @@ public boolean addToBuffer(boolean ignoreLength) { private ConnectionBuffer buffer; + private final long timeout; + private final TimeUnit unit; + + public StandardQueuingStrategy(long timeout, TimeUnit unit) { + this.timeout = timeout; + this.unit = unit; + } + public void setBuffer(ConnectionBuffer buffer) { this.buffer = buffer; } @@ -66,7 +75,7 @@ public void mayHaveSpace() { waiting.poll(); } if (!waiting.isEmpty() && !buffer.hasNonAddressedInBuffer()) { - buffer.send(CommandExecutionPath.blank(), true); + buffer.send(CommandExecutionPath.blank(), true, timeout, unit); } } 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 new file mode 100644 index 000000000..fc10a8c1d --- /dev/null +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/nodes/ZscriptBasicNode.java @@ -0,0 +1,200 @@ +package net.zscript.javaclient.nodes; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import net.zscript.javaclient.addressing.AddressedCommand; +import net.zscript.javaclient.addressing.AddressedResponse; +import net.zscript.javaclient.addressing.ZscriptAddress; +import net.zscript.javaclient.commandPaths.CommandExecutionPath; +import net.zscript.javaclient.commandPaths.ResponseExecutionPath; +import net.zscript.javaclient.sequence.CommandSequence; +import net.zscript.javaclient.sequence.ResponseSequence; +import net.zscript.javaclient.threading.ZscriptCallbackThreadpool; + +class ZscriptBasicNode implements ZscriptNode { + + private static final Logger LOG = LoggerFactory.getLogger(ZscriptBasicNode.class); + + private final ZscriptCallbackThreadpool callbackPool; + + private final AddressingSystem addressingSystem; + + private final ConnectionBuffer connectionBuffer; + private final Connection parentConnection; + + private QueuingStrategy strategy = new StandardQueuingStrategy(1000, TimeUnit.MILLISECONDS); // should be enough for almost all cases + + private BiConsumer badCommandResponseMatchHandler = (c, r) -> { + LOG.error("Command and response do not match: {} ; {}", c.getContent().toBytes().asString(), r.getContent().toSequence().asString()); + }; + + private Consumer unknownNotificationHandler = r -> { + LOG.warn("Unknown notification received: {}", r.getContent().toSequence().asString()); + }; + + private Consumer unknownResponseHandler = r -> { + throw new IllegalStateException("Unknown response received: " + r.getContent().toSequence().asString()); + }; + + private Consumer callbackExceptionHandler = e -> { + LOG.error("Exception caught from callback: ", e); + }; + + private final Map> notificationHandlers = new HashMap<>(); + + private final Map> pathCallbacks = new HashMap<>(); + private final Map> fullSequenceCallbacks = new HashMap<>(); + + private final EchoAssigner echoSystem; + + ZscriptBasicNode(ZscriptCallbackThreadpool callbackPool, Connection parentConnection, int bufferSize) { + this(callbackPool, parentConnection, bufferSize, 100, TimeUnit.MILLISECONDS); + } + + ZscriptBasicNode(ZscriptCallbackThreadpool callbackPool, Connection parentConnection, int bufferSize, long minSegmentChangeTime, TimeUnit unit) { + this.callbackPool = callbackPool; + this.addressingSystem = new AddressingSystem(this); + this.parentConnection = parentConnection; + this.echoSystem = new EchoAssigner(unit.toNanos(minSegmentChangeTime)); + this.connectionBuffer = new ConnectionBuffer(parentConnection, echoSystem, bufferSize); + this.strategy.setBuffer(connectionBuffer); + parentConnection.onReceive(r -> { + try { + if (r.hasAddress()) { + if (!addressingSystem.response(r)) { + callbackPool.sendCallback(unknownResponseHandler, r, callbackExceptionHandler); + } + } else { + response(r); + } + } catch (Exception e) { + callbackPool.sendCallback(callbackExceptionHandler, e); // catches all callback exceptions + } + }); + } + + public void setUnknownResponseHandler(Consumer unknownResponseHandler) { + this.unknownResponseHandler = unknownResponseHandler; + } + + public void setUnknownNotificationHandler(Consumer unknownNotificationHandler) { + this.unknownNotificationHandler = unknownNotificationHandler; + } + + public void setBadCommandResponseMatchHandler( + BiConsumer badCommandResponseMatchHandler) { + this.badCommandResponseMatchHandler = badCommandResponseMatchHandler; + } + + public void setCallbackExceptionHandler(Consumer callbackExceptionHandler) { + this.callbackExceptionHandler = callbackExceptionHandler; + } + + public void setStrategy(QueuingStrategy strategy) { + this.strategy = strategy; + this.strategy.setBuffer(connectionBuffer); + } + + public Connection attach(ZscriptAddress address) { + return addressingSystem.attach(address); + } + + public Connection detach(ZscriptAddress address) { + return addressingSystem.detach(address); + } + + public void send(CommandSequence seq, Consumer callback) { + fullSequenceCallbacks.put(seq, callback); + strategy.send(seq); + } + + public void send(CommandExecutionPath path, Consumer callback) { + pathCallbacks.put(path, callback); + strategy.send(path); + } + + public void send(AddressedCommand addr) { + strategy.send(addr); + } + + public void checkTimeouts() { + Collection timedOut = connectionBuffer.checkTimeouts(); + if (!timedOut.isEmpty()) { + for (CommandSequence seq : timedOut) { + if (fullSequenceCallbacks.get(seq) != null) { + callbackPool.sendCallback(fullSequenceCallbacks.get(seq), ResponseSequence.blank(), callbackExceptionHandler); + } else if (pathCallbacks.get(seq.getExecutionPath()) != null) { + callbackPool.sendCallback(pathCallbacks.get(seq.getExecutionPath()), ResponseExecutionPath.blank(), callbackExceptionHandler); + } + } + } + } + + private void response(AddressedResponse resp) { + if (resp.getContent().getResponseValue() != 0) { + Consumer handler = notificationHandlers.get(resp.getContent().getResponseValue()); + if (handler != null) { + callbackPool.sendCallback(handler, resp.getContent(), callbackExceptionHandler); + } else { + callbackPool.sendCallback(unknownNotificationHandler, resp, callbackExceptionHandler); + } + return; + } + 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; + } + callbackPool.sendCallback(unknownResponseHandler, resp, callbackExceptionHandler); + return; + } else if (!found.getContent().getExecutionPath().matchesResponses(resp.getContent().getExecutionPath())) { + callbackPool.sendCallback(badCommandResponseMatchHandler, found, resp, callbackExceptionHandler); + } + strategy.mayHaveSpace(); + parentConnection.responseReceived(found); + Consumer seqCallback = fullSequenceCallbacks.remove(found.getContent()); + if (seqCallback != null) { + callbackPool.sendCallback(seqCallback, resp.getContent(), callbackExceptionHandler); + } else { + Consumer pathCallback = pathCallbacks.remove(found.getContent().getExecutionPath()); + if (pathCallback != null) { + callbackPool.sendCallback(pathCallback, resp.getContent().getExecutionPath(), callbackExceptionHandler); + } else { + callbackPool.sendCallback(unknownResponseHandler, resp, callbackExceptionHandler); + } + } + } + + public Connection getParentConnection() { + return parentConnection; + } + + public void responseReceived(AddressedCommand found) { + if (connectionBuffer.responseReceived(found)) { + strategy.mayHaveSpace(); + } + parentConnection.responseReceived(found); + } + + public void setNotificationHandler(int notification, Consumer handler) { + notificationHandlers.put(notification, handler); + } + + public void removeNotificationHandler(int notification) { + notificationHandlers.remove(notification); + } + + @Override + public void setBufferSize(int bufferSize) { + connectionBuffer.setBufferSize(bufferSize); + } +} 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 df5761d00..75199243a 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 @@ -1,7 +1,7 @@ package net.zscript.javaclient.nodes; -import java.util.HashMap; -import java.util.Map; +import java.lang.reflect.Proxy; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import net.zscript.javaclient.addressing.AddressedCommand; @@ -11,116 +11,40 @@ import net.zscript.javaclient.commandPaths.ResponseExecutionPath; import net.zscript.javaclient.sequence.CommandSequence; import net.zscript.javaclient.sequence.ResponseSequence; +import net.zscript.javaclient.threading.ZscriptWorkerThread; -public class ZscriptNode { - private final AddressingSystem addressingSystem; - - private final ConnectionBuffer connectionBuffer; - private final Connection parentConnection; - - private QueuingStrategy strategy = new StandardQueuingStrategy(); - - private Consumer unknownResponseHandler = r -> { - throw new IllegalStateException("Unknown response received: " + r); - }; - - private final Map> notificationHandlers = new HashMap<>(); - - private final Map> pathCallbacks = new HashMap<>(); - private final Map> fullSequenceCallbacks = new HashMap<>(); - - public ZscriptNode(Connection parentConnection, int bufferSize) { - this.addressingSystem = new AddressingSystem(this); - this.parentConnection = parentConnection; - this.connectionBuffer = new ConnectionBuffer(parentConnection, bufferSize); - parentConnection.onReceive(r -> { - if (r.hasAddress()) { - if (!addressingSystem.response(r)) { - unknownResponseHandler.accept(r); - } - } else { - response(r); - } - }); - } - - public void setUnknownResponseHandler(Consumer unknownResponseHandler) { - this.unknownResponseHandler = unknownResponseHandler; +public interface ZscriptNode { + static ZscriptNode newNode(Connection parentConnection) { + return newNode(parentConnection, 128, 100, TimeUnit.MILLISECONDS); } - public void setStrategy(QueuingStrategy strategy) { - this.strategy = strategy; + static ZscriptNode newNode(Connection parentConnection, int bufferSize, long minSegmentChangeTime, TimeUnit unit) { + ZscriptWorkerThread thread = parentConnection.getAssociatedThread(); + ZscriptBasicNode node = new ZscriptBasicNode(thread.getCallbackPool(), parentConnection, bufferSize, minSegmentChangeTime, unit); + thread.addTimeoutCheck(node::checkTimeouts); + return (ZscriptNode) Proxy.newProxyInstance(ZscriptNode.class.getClassLoader(), new Class[] { ZscriptNode.class }, + (obj, method, params) -> thread.moveOntoThread(() -> method.invoke(node, params))); } - public Connection attach(ZscriptAddress address) { - return addressingSystem.attach(address); - } + void setUnknownResponseHandler(Consumer unknownResponseHandler); - public Connection detach(ZscriptAddress address) { - return addressingSystem.detach(address); - } + void setStrategy(QueuingStrategy strategy); - public void send(CommandSequence seq, Consumer callback) { - fullSequenceCallbacks.put(seq, callback); - strategy.send(seq); - } + void setBufferSize(int bufferSize); - public void send(CommandExecutionPath path, Consumer callback) { - pathCallbacks.put(path, callback); - strategy.send(path); - } + void setCallbackExceptionHandler(Consumer callbackExceptionHandler); - public void send(AddressedCommand addr) { - strategy.send(addr); - } + Connection attach(ZscriptAddress address); - private void response(AddressedResponse resp) { - if (resp.getContent().getResponseValue() != 0) { - Consumer handler = notificationHandlers.get(resp.getContent().getResponseValue()); - if (handler != null) { - handler.accept(resp.getContent()); - } else { - unknownResponseHandler.accept(resp); - } - return; - } - AddressedCommand found = connectionBuffer.match(resp.getContent()); - if (found == null) { - unknownResponseHandler.accept(resp); - return; - } - strategy.mayHaveSpace(); - parentConnection.responseReceived(found); - Consumer seqCallback = fullSequenceCallbacks.remove(found.getContent()); - if (seqCallback != null) { - seqCallback.accept(resp.getContent()); - } else { - Consumer pathCallback = pathCallbacks.remove(found.getContent().getExecutionPath()); - if (pathCallback != null) { - pathCallback.accept(resp.getContent().getExecutionPath()); - } else { - unknownResponseHandler.accept(resp); - } - } + Connection detach(ZscriptAddress address); - } + void send(CommandSequence seq, Consumer callback); - public Connection getParentConnection() { - return parentConnection; - } + void send(CommandExecutionPath path, Consumer callback); - public void responseReceived(AddressedCommand found) { - if (connectionBuffer.responseReceived(found)) { - strategy.mayHaveSpace(); - } - parentConnection.responseReceived(found); - } + void send(AddressedCommand addr); - public void setNotificationHandler(int notification, Consumer handler) { - notificationHandlers.put(notification, handler); - } + void setNotificationHandler(int notification, Consumer handler); - public void removeNotificationHandler(int notification) { - notificationHandlers.remove(notification); - } + void removeNotificationHandler(int notification); } 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/ZscriptCallbackThreadpool.java b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/threading/ZscriptCallbackThreadpool.java new file mode 100644 index 000000000..541fce797 --- /dev/null +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/threading/ZscriptCallbackThreadpool.java @@ -0,0 +1,52 @@ +package net.zscript.javaclient.threading; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class ZscriptCallbackThreadpool { + private final ExecutorService callbackPool; + + public ZscriptCallbackThreadpool() { + this.callbackPool = Executors.newCachedThreadPool(); + } + + public ZscriptCallbackThreadpool(ExecutorService callbackPool) { + this.callbackPool = callbackPool; + } + + public void sendCallback(Consumer callback, T content, Consumer handler) { + callbackPool.submit(() -> { + try { + callback.accept(content); + } catch (Exception e) { + handler.accept(e); + } + }); + } + + public void sendCallback(BiConsumer callback, T content1, U content2, Consumer handler) { + callbackPool.submit(() -> { + try { + callback.accept(content1, content2); + } catch (Exception e) { + handler.accept(e); + } + }); + } + + public void sendCallback(Consumer callback, T content) { + callbackPool.submit(() -> callback.accept(content)); + } + + public void sendCallback(Runnable callback, Consumer handler) { + callbackPool.submit(() -> { + try { + callback.run(); + } catch (Exception e) { + handler.accept(e); + } + }); + } +} 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 new file mode 100644 index 000000000..bb400810d --- /dev/null +++ b/clients/java-client-lib/client-core/src/main/java/net/zscript/javaclient/threading/ZscriptWorkerThread.java @@ -0,0 +1,130 @@ +package net.zscript.javaclient.threading; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ZscriptWorkerThread { + private final ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + private final ZscriptCallbackThreadpool threadpool; + private final Thread execThread; + private final List> timeoutChecks = new ArrayList<>(); + + public ZscriptWorkerThread() { + this(new ZscriptCallbackThreadpool()); + } + + public ZscriptWorkerThread(ZscriptCallbackThreadpool threadpool) { + this.threadpool = threadpool; + try { + execThread = exec.submit(Thread::currentThread).get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + exec.scheduleAtFixedRate(this::checkTimeouts, 100, 10, TimeUnit.MILLISECONDS); + } + + //TODO: Consider batching this to reduce thread latency if lots of devices are present + private void checkTimeouts() { + for (Iterator> iter = timeoutChecks.iterator(); iter.hasNext(); ) { + Runnable node = iter.next().get(); + if (node == null) { + iter.remove(); + } else { + node.run(); + } + } + } + + public void addTimeoutCheck(Runnable r) { + timeoutChecks.add(new WeakReference<>(r)); + } + + public T moveOntoThread(Callable task) { + if (Thread.currentThread() == execThread) { + try { + return task.call(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + try { + return exec.submit(task).get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + } + + public void moveOntoThread(Runnable task) { + if (Thread.currentThread() == execThread) { + try { + task.run(); + return; + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + try { + exec.submit(task).get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + } + + public Future startOnThread(Runnable task) { + if (Thread.currentThread() == execThread) { + try { + task.run(); + return CompletableFuture.completedFuture(null); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } else { + return exec.submit(task); + } + + } + + public Future startOnThread(Callable task) { + if (Thread.currentThread() == execThread) { + try { + return CompletableFuture.completedFuture(task.call()); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } else { + return exec.submit(task); + } + } + + public ZscriptCallbackThreadpool getCallbackPool() { + return threadpool; + } +} diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/RawConnection.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/RawConnection.java index 9e97e2775..414558840 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/RawConnection.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/RawConnection.java @@ -1,22 +1,40 @@ package net.zscript.javaclient.connectors; +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.slf4j.Logger; + import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.function.Consumer; import net.zscript.javaclient.addressing.AddressedCommand; import net.zscript.javaclient.addressing.AddressedResponse; import net.zscript.javaclient.addressing.CompleteAddressedResponse; import net.zscript.javaclient.nodes.Connection; +import net.zscript.javaclient.threading.ZscriptWorkerThread; import net.zscript.javareceiver.tokenizer.TokenExtendingBuffer; import net.zscript.javareceiver.tokenizer.Tokenizer; public abstract class RawConnection implements Connection, AutoCloseable { + private final ZscriptWorkerThread thread = new ZscriptWorkerThread(); + + private Consumer parseFailHandler = bytes -> { + getLogger().warn("Response failed parsing: {}", new String(bytes, UTF_8)); + }; + + protected abstract Logger getLogger(); @Override public final void send(AddressedCommand cmd) { try { - send(cmd.toBytes().toByteArray()); + byte[] data = cmd.toBytes().toByteArray(); + if (getLogger().isTraceEnabled()) { + getLogger().trace(UTF_8.decode(ByteBuffer.wrap(data)).toString()); + } + send(data); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -24,14 +42,26 @@ public final void send(AddressedCommand cmd) { @Override public final void onReceive(Consumer resp) { - onReceiveBytes(data -> { - TokenExtendingBuffer buffer = new TokenExtendingBuffer(); - Tokenizer t = new Tokenizer(buffer.getTokenWriter(), 2); - for (byte b : data) { - t.accept(b); + onReceiveBytes(data -> thread.moveOntoThread(() -> { + AddressedResponse parsed = null; + try { + TokenExtendingBuffer buffer = new TokenExtendingBuffer(); + Tokenizer t = new Tokenizer(buffer.getTokenWriter(), 2); + for (byte b : data) { + t.accept(b); + } + if (getLogger().isTraceEnabled()) { + getLogger().trace("{}", new String(data, UTF_8)); + } + parsed = CompleteAddressedResponse.parse(buffer.getTokenReader()).asResponse(); + } catch (Exception e) { + parseFailHandler.accept(data); + } + if (parsed != null) { + // don't want to catch exceptions from here: + resp.accept(parsed); } - resp.accept(CompleteAddressedResponse.parse(buffer.getTokenReader()).asResponse()); - }); + })); } @Override @@ -43,4 +73,8 @@ public void responseReceived(AddressedCommand found) { protected abstract void onReceiveBytes(final Consumer responseHandler); + @Override + public ZscriptWorkerThread getAssociatedThread() { + return thread; + } } diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/serial/SerialConnection.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/serial/SerialConnection.java index 9fa324bf5..9614b524d 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/serial/SerialConnection.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/serial/SerialConnection.java @@ -5,7 +5,9 @@ import com.fazecast.jSerialComm.SerialPort; import com.fazecast.jSerialComm.SerialPortEvent; import com.fazecast.jSerialComm.SerialPortIOException; -import com.fazecast.jSerialComm.SerialPortMessageListener; +import com.fazecast.jSerialComm.SerialPortMessageListenerWithExceptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.OutputStream; @@ -18,6 +20,8 @@ */ // Note: SerialPort is hard to use in tests as it contains Android references which cause Mocking failure. public class SerialConnection extends RawConnection { + private static final Logger LOG = LoggerFactory.getLogger(SerialConnection.class); + private final SerialPort commPort; private final OutputStream out; @@ -32,6 +36,11 @@ public SerialConnection(SerialPort commPort) throws IOException { this.out = commPort.getOutputStream(); } + @Override + protected Logger getLogger() { + return LOG; + } + public void send(final byte[] data) throws IOException { out.write(data); } @@ -44,10 +53,14 @@ public void onReceiveBytes(final Consumer responseHandler) { if (this.handler != null) { throw new IllegalStateException("Handler already assigned"); } - this.handler = requireNonNull(responseHandler, "handler"); - commPort.addDataListener(new SerialPortMessageListener() { + commPort.addDataListener(new SerialPortMessageListenerWithExceptions() { + @Override + public void catchException(Exception e) { + LOG.error(e.getMessage()); + } + @Override public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_RECEIVED; @@ -55,7 +68,6 @@ public int getListeningEvents() { @Override public void serialEvent(SerialPortEvent serialPortEvent) { - System.out.println("Event: " + serialPortEvent); if (serialPortEvent.getEventType() == SerialPort.LISTENING_EVENT_DATA_RECEIVED) { responseHandler.accept(serialPortEvent.getReceivedData()); } @@ -70,6 +82,7 @@ public byte[] getMessageDelimiter() { public boolean delimiterIndicatesEndOfMessage() { return true; } + }); } diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/tcp/TcpConnection.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/tcp/TcpConnection.java index 98611c314..90aaf4e26 100644 --- a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/tcp/TcpConnection.java +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/connectors/tcp/TcpConnection.java @@ -1,5 +1,8 @@ package net.zscript.javaclient.connectors.tcp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -16,8 +19,9 @@ import net.zscript.javaclient.connectors.RawConnection; public class TcpConnection extends RawConnection { - private final Socket socket; - private final ExecutorService executor; + private static final Logger LOG = LoggerFactory.getLogger(TcpConnection.class); + private final Socket socket; + private final ExecutorService executor; private final InputStream in; private final OutputStream out; @@ -25,6 +29,11 @@ public class TcpConnection extends RawConnection { private Future future; private Consumer handler; + @Override + protected Logger getLogger() { + return LOG; + } + public TcpConnection(Socket socket) throws IOException { this(socket, Executors.newSingleThreadExecutor()); } diff --git a/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/CommandFailedException.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/CommandFailedException.java new file mode 100644 index 000000000..17765af44 --- /dev/null +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/CommandFailedException.java @@ -0,0 +1,13 @@ +package net.zscript.javaclient.devices; + +public class CommandFailedException extends RuntimeException { + private final ResponseSequenceCallback callback; + + public CommandFailedException(ResponseSequenceCallback callback) { + this.callback = callback; + } + + public ResponseSequenceCallback getCallback() { + return callback; + } +} 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 99502f74e..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 @@ -6,6 +6,11 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import net.zscript.javaclient.commandbuilder.commandnodes.CommandSequenceNode; @@ -17,7 +22,8 @@ import net.zscript.javaclient.commandPaths.CommandExecutionPath; import net.zscript.javaclient.commandPaths.ResponseExecutionPath; import net.zscript.javaclient.commandPaths.ZscriptFieldSet; -import net.zscript.javaclient.commandbuilder.commandnodes.CommandSequenceNode; +import net.zscript.javaclient.commandbuilder.notifications.NotificationHandle; +import net.zscript.javaclient.commandbuilder.notifications.NotificationId; import net.zscript.javaclient.nodes.ZscriptNode; import net.zscript.javaclient.sequence.CommandSequence; import net.zscript.javareceiver.tokenizer.TokenExtendingBuffer; @@ -28,16 +34,105 @@ public class Device { private final ZscriptModel model; private final ZscriptNode node; + private final Map, NotificationHandle> handles = new HashMap<>(); + public Device(ZscriptModel model, ZscriptNode node) { this.model = model; this.node = node; } - public void send(final CommandSequenceNode cmdSeq, final Consumer callback) { + public T getNotificationHandle(NotificationId id) { + if (handles.get(id) == null) { + handles.put(id, id.newHandle()); + } + return id.getHandleType().cast(handles.get(id)); + } + + public void setNotificationListener(NotificationId id, Consumer listener) { + node.setNotificationHandler(id.getId(), respSeq -> { + listener.accept(NotificationSequenceCallback.from(getNotificationHandle(id).getSections(), respSeq.getExecutionPath().getResponses())); + }); + } + + public void sendAsync(final CommandSequenceNode cmdSeq, final Consumer callback) { CommandExecutionTask nodeToPath = convert(cmdSeq, callback); node.send(nodeToPath.getPath(), nodeToPath.getCallback()); } + // if future is present, wasExecuted() is true, otherwise NoResponseException is given. + public Future send(final CommandSequenceNode cmdSeq) { + CompletableFuture future = new CompletableFuture<>(); + CommandExecutionTask nodeToPath = convert(cmdSeq, resp -> { + if (!resp.wasExecuted()) { + future.completeExceptionally(new NoResponseException()); + } else { + future.complete(resp); + } + }); + node.send(nodeToPath.getPath(), nodeToPath.getCallback()); + return future; + } + + public Future sendExpectSuccess(final CommandSequenceNode cmdSeq) { + 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); + } else { + future.completeExceptionally(new CommandFailedException(resp)); + } + }); + node.send(nodeToPath.getPath(), nodeToPath.getCallback()); + return future; + } + + public ResponseSequenceCallback sendAndWait(final CommandSequenceNode cmdSeq) throws InterruptedException { + try { + return send(cmdSeq).get(); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + + public ResponseSequenceCallback sendAndWaitExpectSuccess(final CommandSequenceNode cmdSeq) throws InterruptedException { + try { + return sendExpectSuccess(cmdSeq).get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + + public ResponseSequenceCallback sendAndWait(final CommandSequenceNode cmdSeq, final long timeout, final TimeUnit unit) throws TimeoutException, InterruptedException { + try { + return send(cmdSeq).get(timeout, unit); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + + public ResponseSequenceCallback sendAndWaitExpectSuccess(final CommandSequenceNode cmdSeq, final long timeout, final TimeUnit unit) + throws TimeoutException, InterruptedException { + try { + return sendExpectSuccess(cmdSeq).get(timeout, unit); + } catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + public void send(final byte[] cmdSeq, final Consumer callback) { TokenExtendingBuffer buffer = new TokenExtendingBuffer(); Tokenizer tok = new Tokenizer(buffer.getTokenWriter(), 2); @@ -154,8 +249,7 @@ class Layer { layer.success = prevLayer.success; if (prev instanceof OrSequenceNode) { if (prevLayer.onSuccessHasOpenParen) { - layer.success = new Command(prevLayer.success, prevLayer.failure, - ZscriptFieldSet.blank()); + layer.success = new Command(prevLayer.success, prevLayer.failure, ZscriptFieldSet.blank()); } layer.onSuccessHasOpenParen = true; } @@ -169,8 +263,6 @@ class Layer { CommandExecutionPath path = CommandExecutionPath.from(model, destinations.peek().success); return new CommandExecutionTask(path, resps -> { ResponseSequenceCallback rsCallback = ResponseSequenceCallback.from(path.compareResponses(resps), cmdSeq, commandMap); - ; - rsCallback.getExecutionSummary().forEach(s -> s.getCommand().responseArrived(s.getResponse())); callback.accept(rsCallback); }); } 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/NotificationSequenceCallback.java b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NotificationSequenceCallback.java new file mode 100644 index 000000000..a40aa238e --- /dev/null +++ b/clients/java-client-lib/client-main/src/main/java/net/zscript/javaclient/devices/NotificationSequenceCallback.java @@ -0,0 +1,98 @@ +package net.zscript.javaclient.devices; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import net.zscript.javaclient.commandPaths.Command; +import net.zscript.javaclient.commandPaths.MatchedCommandResponse; +import net.zscript.javaclient.commandPaths.Response; +import net.zscript.javaclient.commandbuilder.ZscriptResponse; +import net.zscript.javaclient.commandbuilder.commandnodes.ResponseCaptor; +import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandNode; +import net.zscript.javaclient.commandbuilder.notifications.NotificationSection; +import net.zscript.model.components.Zchars; + +public class NotificationSequenceCallback { + + public static class NotificationSectionSummary { + private final NotificationSection notif; + private final T response; + + public static NotificationSectionSummary generateExecutionSummary(NotificationSection notif, Response response) { + return new NotificationSectionSummary<>(notif, notif.parseResponse(response.getFields())); + } + + public NotificationSectionSummary(NotificationSection notif, T response) { + this.notif = notif; + this.response = response; + } + + public NotificationSection getNotification() { + return notif; + } + + public T getResponse() { + return response; + } + + public Class getResponseType() { + return notif.getResponseType(); + } + } + + public static NotificationSequenceCallback from(List> sections, List responses) { + LinkedHashMap, ZscriptResponse> map = new LinkedHashMap<>(); + + Iterator> sectionIt = sections.iterator(); + Iterator respIt = responses.iterator(); + + while (sectionIt.hasNext() && respIt.hasNext()) { + NotificationSection section = sectionIt.next(); + map.put(section, section.parseResponse(respIt.next().getFields())); + } + if (respIt.hasNext()) { + throw new IllegalStateException("Responses received longer than notification"); + } + return new NotificationSequenceCallback(map); + } + + private final LinkedHashMap, ZscriptResponse> responses; + + private NotificationSequenceCallback(LinkedHashMap, ZscriptResponse> responses) { + this.responses = responses; + } + + public List getResponses() { + return new ArrayList<>(responses.values()); + } + + public List> getExecuted() { + return new ArrayList<>(responses.keySet()); + } + + public List> getExecutionSummary() { + return responses.entrySet().stream().map(e -> matchExecutionSummary(e.getKey(), e.getValue())).collect(Collectors.toList()); + } + + private NotificationSectionSummary matchExecutionSummary(NotificationSection node, ZscriptResponse resp) { + return new NotificationSectionSummary<>(node, node.getResponseType().cast(resp)); + } + + public Optional getResponseFor(NotificationSection node) { + return Optional.ofNullable(responses.get(node)); + } + + public Optional getResponseFor(ResponseCaptor captor) { + return getResponseFor((NotificationSection) captor.getSource()).map(r -> ((NotificationSection) captor.getSource()).getResponseType().cast(r)); + } +} 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 74dc7c7cb..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; @@ -14,6 +15,7 @@ import net.zscript.javaclient.commandPaths.Command; import net.zscript.javaclient.commandPaths.MatchedCommandResponse; import net.zscript.javaclient.commandPaths.Response; +import net.zscript.javaclient.commandbuilder.commandnodes.ResponseCaptor; import net.zscript.javaclient.commandbuilder.commandnodes.ZscriptCommandNode; import net.zscript.javaclient.commandbuilder.ZscriptResponse; import net.zscript.model.components.Zchars; @@ -47,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)) { @@ -58,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<>(); @@ -92,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) { @@ -100,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() { @@ -137,4 +155,12 @@ private CommandExecutionSummary matchExecutionSum public Optional getResponseFor(ZscriptCommandNode node) { return Optional.ofNullable(responses.get(node)); } + + public Optional getResponseFor(ResponseCaptor captor) { + return getResponseFor((ZscriptCommandNode) captor.getSource()).map(r -> ((ZscriptCommandNode) captor.getSource()).getResponseType().cast(r)); + } + + public boolean wasExecuted() { + return wasExecuted; + } } diff --git a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java index ae37a7e29..7b0975487 100644 --- a/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java +++ b/clients/java-client-lib/client-main/src/test/java/net/zscript/javaclient/connection/LocalZscriptConnection.java @@ -1,5 +1,8 @@ package net.zscript.javaclient.connection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.ByteArrayOutputStream; import java.util.ArrayDeque; import java.util.Queue; @@ -8,6 +11,7 @@ import java.util.function.Consumer; import net.zscript.javaclient.connectors.RawConnection; +import net.zscript.javaclient.connectors.serial.SerialConnection; import net.zscript.javareceiver.core.OutStream; import net.zscript.javareceiver.core.OutputStreamOutStream; import net.zscript.javareceiver.core.Zscript; @@ -19,9 +23,15 @@ import net.zscript.javareceiver.tokenizer.Tokenizer; public class LocalZscriptConnection extends RawConnection { - private final ExecutorService exec = Executors.newSingleThreadExecutor(); - private final Queue dataIn = new ArrayDeque<>(); - private Consumer responseHandler; + private static final Logger LOG = LoggerFactory.getLogger(LocalZscriptConnection.class); + private final ExecutorService exec = Executors.newSingleThreadExecutor(); + private final Queue dataIn = new ArrayDeque<>(); + private Consumer responseHandler; + + @Override + protected Logger getLogger() { + return LOG; + } private class ProgressForever implements Runnable { private final Zscript zscript; diff --git a/demo/demo-morse/client/pom.xml b/demo/demo-morse/client/pom.xml new file mode 100644 index 000000000..460770906 --- /dev/null +++ b/demo/demo-morse/client/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + net.zscript.demo + demo-morse + 0.0.1-SNAPSHOT + + + + demo-morse-client + jar + Demo Morse Client + + + + info.picocli + picocli + 4.7.5 + + + net.zscript + zscript-java-client-main + ${project.version} + + + net.zscript + zscript-java-client-command-builders + ${project.version} + + + ch.qos.logback + logback-classic + ${version.logback} + + + + org.openjfx + javafx-controls + 18.0.2 + + + org.openjfx + javafx-fxml + 18.0.2 + + + + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + net.zscript.demo01.client.ui.UiMain + + + + + + + \ No newline at end of file diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseElement.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseElement.java new file mode 100644 index 000000000..4bdd0f9e3 --- /dev/null +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseElement.java @@ -0,0 +1,43 @@ +package net.zscript.demo.morse; + +import java.util.ArrayList; +import java.util.List; + +public enum MorseElement { + DIT(1, true), + DAR(3, true), + LETTER_SPACE(3, false), + WORD_SPACE(7, false); + //Space is implied between all other elements, so LETTER_SPACE only requires 1 dit of blank + // WORD_SPACE is put in for the space character, so only one dit of blank is required (since there is a letter space either side) + + private final int length; + private final boolean isHigh; + + MorseElement(int length, boolean isHigh) { + this.length = length; + this.isHigh = isHigh; + } + + public int getLength() { + return length; + } + + public boolean isHigh() { + return isHigh; + } + + public static List translate(String ditDar) { + ArrayList elements = new ArrayList<>(); + for (char c : ditDar.toCharArray()) { + if (c == '.') { + elements.add(DIT); + } else if (c == '-') { + elements.add(DAR); + } else { + throw new IllegalArgumentException("Dit-Dar string cannot contain characters other than '.' or '-'\n Found: " + c); + } + } + return elements; + } +} diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseFullCli.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseFullCli.java new file mode 100644 index 000000000..e4c55a5fe --- /dev/null +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseFullCli.java @@ -0,0 +1,512 @@ +package net.zscript.demo.morse; + +import com.fazecast.jSerialComm.SerialPort; + +import com.fazecast.jSerialComm.SerialPortInvalidPortException; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.function.Predicate; + +import net.zscript.javaclient.commandbuilder.commandnodes.CommandSequenceNode; +import net.zscript.javaclient.commandbuilder.commandnodes.ResponseCaptor; +import net.zscript.javaclient.commandbuilder.defaultCommands.BlankCommandNode; +import net.zscript.javaclient.connectors.serial.SerialConnection; +import net.zscript.javaclient.devices.Device; +import net.zscript.javaclient.devices.ResponseSequenceCallback; +import net.zscript.javaclient.nodes.ZscriptNode; +import net.zscript.model.ZscriptModel; +import net.zscript.model.modules.base.CoreModule; +import net.zscript.model.modules.base.PinsModule; +import net.zscript.model.modules.base.PinsModule.DigitalSetupCommand.DigitalSetupResponse; +import net.zscript.model.modules.base.UartModule; + +@Command(mixinStandardHelpOptions = true, version = "Morse code Zscript demo 0.1") +public class MorseFullCli implements Callable { + + @Option(names = { "-r", "--receive-port" }, arity = "0..1", fallbackValue = "__ManualSelect__", defaultValue = "__NoReceive__", + description = "The port to receive data along. No reception done if not present. If specified without parameter, gives list of options.") + private String receivePort; + @Option(names = { "-rb", "--receive-baud-rate" }, arity = "0..1", defaultValue = "-1", + description = "Overrides \"--baud-rate\" The initial baud rate of the receiving port. Default: 9600") + private int receiveBaud; + @Option(names = { "-rbn", "--receive-baud-rate-negotiated" }, arity = "0..1", fallbackValue = "-2", defaultValue = "-1", + description = "Overrides \"--baud-rate-negotiated\" The target baud rate to negotiate to on the receiving port, if possible. Leave blank to read options from device. Default: no renegotiation of baud rate") + private int receiveBaudNegotiate; + @Option(names = { "-rp", "--receive-pin" }, arity = "0..1", defaultValue = "-1", + description = "Sets the pin on which the receiver will listen. Default: interactive setting of pin") + private int receivePin; + + @Option(names = { "-t", "--transmit-port" }, arity = "0..1", fallbackValue = "__ManualSelect__", defaultValue = "__NoTransmit__", + description = "The port to transmit data along. No transmission done if not present. If specified without parameter, gives list of options.") + private String transmitPort; + @Option(names = { "-tb", "--transmit-baud-rate" }, arity = "0..1", defaultValue = "-1", + description = "Overrides \"--baud-rate\" The initial baud rate of the transmitting port. Default: 9600") + private int transmitBaud; + @Option(names = { "-tbn", "--transmit-baud-rate-negotiated" }, arity = "0..1", fallbackValue = "-2", defaultValue = "-1", + description = "Overrides \"--baud-rate-negotiated\" The target baud rate to negotiate to on the transmitting port, if possible. Leave blank to read options from device. Default: no renegotiation of baud rate") + private int transmitBaudNegotiate; + @Option(names = { "-tp", "--transmit-pin" }, arity = "0..1", defaultValue = "-1", + description = "Sets the pin on which the transmitter will send. Default: interactive setting of pin") + private int transmitPin; + + @Option(names = { "-b", "--baud-rate" }, arity = "0..1", defaultValue = "9600", + description = "The initial baud rate of the ports. Default: 9600") + private int generalBaud; + @Option(names = { "-bn", "--baud-rate-negotiated" }, arity = "0..1", fallbackValue = "-2", defaultValue = "-1", + description = "The target baud rate to negotiate to on the ports, if possible. Leave blank to read options from device. Default: no renegotiation of baud rate") + private int generalBaudNegotiate; + + @Option(names = { "-d", "--dit-period" }, required = true, + description = "The length of a dit in ms") + private int ditPeriod; + + @Option(names = { "-m", "--message" }, arity = "0..1", + description = "Message to be transmitted") + private String message; + @Parameters(index = "0..*") + private File[] data; + + private final Scanner stdInScanner = new Scanner(System.in, StandardCharsets.UTF_8); + + private int getInputNumber(int max) { + int choice = -1; + try { + String in = stdInScanner.nextLine(); + choice = Integer.parseInt(in); + if (choice < 0 || choice >= max) { + choice = -1; + } + } catch (NumberFormatException ignored) { + } + return choice; + } + + private SerialPort selectPortOptions(String name) { + System.out.println("Available ports:"); + SerialPort[] options = SerialPort.getCommPorts(); + for (int i = 0; i < options.length; i++) { + System.out.printf("%2d: %s (%s)\n", i, options[i].getDescriptivePortName(), options[i].getSystemPortName()); + } + System.out.print("Please select index of " + name + " port: "); + int choice = getInputNumber(options.length); + System.out.println(); + if (choice == -1) { + System.err.println("Invalid port choice, aborting"); + return null; + } + return options[choice]; + } + + private SerialPort selectPort(String port, String operationName) { + SerialPort selected; + if (port.equals("__ManualSelect__")) { + selected = selectPortOptions(operationName); + if (selected == null) { + return null; + } + } else { + try { + selected = SerialPort.getCommPort(port); + } catch (SerialPortInvalidPortException e) { + System.err.println("No port found with name: " + port); + return null; + } + } + return selected; + } + + private int negotiateBaud(SerialPort port, Device device, int target, String name) throws InterruptedException { + if (target > 16000000) { + System.err.println("Cannot negotiate to frequencies over 16MHz"); + return 1; + } else if (target < -2) { + System.err.println("Invalid frequency selection"); + return 1; + } + if (target != -1) { + // Find the channel index for the current channel + ResponseCaptor infoResp = ResponseCaptor.create(); + int currentChannelIndex = device.sendAndWaitExpectSuccess( + CoreModule.channelInfoBuilder().capture(infoResp).build()).getResponseFor(infoResp).orElseThrow().getCurrentChannel(); + + // get the detailed info on the current channel + ResponseCaptor uartInfo = ResponseCaptor.create(); + UartModule.ChannelInfoCommand.ChannelInfoResponse resp = device.sendAndWaitExpectSuccess( + UartModule.channelInfoBuilder().setChannel(currentChannelIndex).capture(uartInfo).build()).getResponseFor(uartInfo).orElseThrow(); + if (resp.getBitsetCapabilities().contains(UartModule.ChannelInfoCommand.ChannelInfoResponse.BitsetCapabilities.ArbitraryFrequency)) { + // if the channel supports free baud rate setting, use that to set the desired baud rate + return setBaudArbitrary(port, device, target, name, resp, currentChannelIndex); + } else if (resp.hasFrequenciesSupported()) { + if (target == -2) { + // present a menu of frequencies by iterating the frequency menu + return setBaudInteractiveMenu(port, device, name, resp, currentChannelIndex); + } else { + // search the frequency options + return setBaudSearchMenu(port, device, target, name, resp, currentChannelIndex); + } + } else { + System.err.println(name + " does not support frequency selection"); + return 1; + } + } + return 0; + } + + private int setBaudSearchMenu(SerialPort port, Device device, int target, String name, UartModule.ChannelInfoCommand.ChannelInfoResponse resp, int currentChannelIndex) + throws InterruptedException { + // find all the frequency options + List> captors = new ArrayList<>(); + + CommandSequenceNode node = new BlankCommandNode(); + boolean hasFound = false; + for (int i = 0; i < resp.getFrequenciesSupported().orElseThrow(); i++) { + ResponseCaptor captor = ResponseCaptor.create(); + node = node.andThen(UartModule.channelInfoBuilder().setChannel(currentChannelIndex).setFrequencySelection(i).capture(captor).build()); + captors.add(captor); + } + ResponseSequenceCallback result = device.sendAndWaitExpectSuccess(node); + // iterate the frequency options looking for an exact match + for (int i = 0; i < resp.getFrequenciesSupported().orElseThrow(); i++) { + UartModule.ChannelInfoCommand.ChannelInfoResponse respEach = result.getResponseFor(captors.get(i)).orElseThrow(); + + int baudInt = parseBaud(respEach.getBaudRate()); + if (baudInt == target) { + hasFound = true; + device.sendAndWaitExpectSuccess(UartModule.channelSetupBuilder().setChannel(currentChannelIndex).setFrequencySelection(i).build()); + port.setBaudRate(target); + break; + } + } + if (!hasFound) { + System.err.println(name + " does not support target frequency"); + return 1; + } + return 0; + } + + private int setBaudInteractiveMenu(SerialPort port, Device device, String name, UartModule.ChannelInfoCommand.ChannelInfoResponse resp, int currentChannelIndex) + throws InterruptedException { + System.out.println(name + " available frequencies: "); + + List> captors = new ArrayList<>(); + + CommandSequenceNode node = new BlankCommandNode(); + // send all the get commands in one batch, for fun + for (int i = 0; i < resp.getFrequenciesSupported().orElseThrow(); i++) { + ResponseCaptor captor = ResponseCaptor.create(); + node = node.andThen(UartModule.channelInfoBuilder().setChannel(currentChannelIndex).setFrequencySelection(i).capture(captor).build()); + captors.add(captor); + } + int[] freqChoices = new int[resp.getFrequenciesSupported().orElseThrow()]; + + ResponseSequenceCallback result = device.sendAndWaitExpectSuccess(node); + for (int i = 0; i < resp.getFrequenciesSupported().orElseThrow(); i++) { + UartModule.ChannelInfoCommand.ChannelInfoResponse respEach = result.getResponseFor(captors.get(i)).orElseThrow(); + + int baudInt = parseBaud(respEach.getBaudRate()); + freqChoices[i] = baudInt; + int baudIntHuman = baudInt; + String unit = "Hz"; + if (baudIntHuman > 999999) { + baudIntHuman /= 1000000; + unit = "MHz"; + } else if (baudIntHuman > 999) { + baudIntHuman /= 1000; + unit = "kHz"; + } + System.out.printf("%d3: %d8 (%d%s)\n", i, baudInt, baudIntHuman, unit); + } + System.out.print("Please select frequency index: "); + int choice = getInputNumber(resp.getFrequenciesSupported().orElseThrow()); + System.out.println(); + if (choice == -1) { + System.err.println("Invalid frequency choice, aborting"); + return 1; + } + // apply chosen frequency + device.sendAndWaitExpectSuccess(UartModule.channelSetupBuilder().setChannel(currentChannelIndex).setFrequencySelection(choice).build()); + port.setBaudRate(freqChoices[choice]); + return 0; + } + + private int setBaudArbitrary(SerialPort port, Device device, int target, String name, UartModule.ChannelInfoCommand.ChannelInfoResponse resp, int currentChannelIndex) + throws InterruptedException { + int baudMax = parseBaud(resp.getBaudRate()); + int actualTarget = target; + if (target == -2) { + System.out.println("Baud rates supported: 0-" + baudMax); + System.out.print("Please choose a baud rate: "); + actualTarget = getInputNumber(baudMax); + System.out.println(); + if (actualTarget == -1) { + System.err.println("Baud rate out of range, aborting."); + return 1; + } + } + if (baudMax < actualTarget) { + System.err.println(name + " does not support negotiating to frequency given"); + return 1; + } + byte[] baud = new byte[] { (byte) (actualTarget >> 16), (byte) (actualTarget >> 8), (byte) actualTarget }; + device.sendAndWaitExpectSuccess(UartModule.channelSetupBuilder().setChannel(currentChannelIndex).setBaudRate(baud).build()); + port.setBaudRate(actualTarget); + return 0; + } + + private int parseBaud(byte[] baud) { + if (baud.length > 3) { + return 16000000; + } + int baudInt = 0; + for (byte b : baud) { + baudInt <<= 8; + baudInt += b; + } + return baudInt; + } + + private int parsePin(Device device, String name, String verb, Predicate requirements, int toIgnore, String requirementName) + throws InterruptedException { + ResponseCaptor captor = ResponseCaptor.create(); + + int pinCount = device.sendAndWaitExpectSuccess( + PinsModule.capabilitiesBuilder().capture(captor).build()).getResponseFor(captor).orElseThrow().getPinCount(); + System.out.println(name + " has " + pinCount + " pins"); + Set pinsSupporting = new HashSet<>(); + // assemble a big command sequence to check pins in batches... + final int batchSize = 8; + for (int batchStart = 0; batchStart < pinCount; batchStart += batchSize) { + CommandSequenceNode sequence = new BlankCommandNode(); + List> captors = new ArrayList<>(); + for (int i = batchStart; i < pinCount && i < batchStart + batchSize; i++) { + if (i == toIgnore) { + captors.add(ResponseCaptor.create()); + continue; + } + ResponseCaptor digitalCaptor = ResponseCaptor.create(); + sequence = sequence.andThen(PinsModule.digitalSetupBuilder().setPin(i).capture(digitalCaptor).build()); + captors.add(digitalCaptor); + } + ResponseSequenceCallback resp = device.sendAndWaitExpectSuccess(sequence); + // check each pin to see if it supports required functions + for (int i = 0; i < captors.size(); i++) { + if (i + batchStart == toIgnore) { + continue; + } + DigitalSetupResponse pinResp = resp.getResponseFor(captors.get(i)).orElseThrow(); + if (requirements.test(pinResp)) { + pinsSupporting.add(i + batchStart); + } + } + } + if (pinsSupporting.isEmpty()) { + System.err.println("Device has no pins supporting " + requirementName + ", so cannot receive morse"); + return -1; + } + System.out.print(verb + " is supported on pins: "); + int prevStart = -2; + int prevEnd = -2; + boolean isFirst = true; + for (int i = 0; i < pinCount; i++) { + if (pinsSupporting.contains(i)) { + if (prevEnd == i - 1) { + prevEnd++; + } else { + prevStart = i; + prevEnd = i; + } + } else if (prevEnd == i - 1) { + if (!isFirst) { + System.out.print(", "); + } + isFirst = false; + if (prevStart == prevEnd) { + System.out.print(prevStart); + } else { + System.out.print(prevStart + "-" + prevEnd); + } + prevStart = -2; + prevEnd = -2; + } + } + if (prevStart != -2) { + if (!isFirst) { + System.out.print(", "); + } + if (prevStart == prevEnd) { + System.out.print(prevStart); + } else { + System.out.print(prevStart + "-" + prevEnd); + } + } + System.out.println(); + + System.out.print("Please select a pin for " + verb.toLowerCase() + ": "); + int chosen = getInputNumber(pinCount); + System.out.println(); + if (!pinsSupporting.contains(chosen)) { + System.err.println("Chosen pin does not support " + requirementName + ", so cannot receive morse"); + return -1; + } + return chosen; + + } + + @Override + public Integer call() throws Exception { + if (ditPeriod > 100000) { + System.err.println("Dit periods over 100s not supported (and probably not desired)"); + return 1; + } else if (ditPeriod <= 0) { + System.err.println("Dit period must be positive"); + return 1; + } else if (ditPeriod <= 2) { + System.err.println("Dit period too short to transmit accurately"); + return 1; + } + SerialPort rx = null; + if (!receivePort.equals("__NoReceive__")) { + rx = selectPort(receivePort, "receiving"); + if (rx == null) { + return 1; + } + } + SerialPort tx = null; + if (!transmitPort.equals("__NoTransmit__")) { + tx = selectPort(transmitPort, "transmitting"); + if (tx == null) { + return 1; + } + } + if (rx == null && tx == null) { + System.err.println("No operation selected."); + return 1; + } + if (rx == null && (receiveBaud != -1 || receiveBaudNegotiate != -1 || receivePin != -1)) { + System.err.println("Cannot set frequency on receiving port without receive device"); + return 1; + } + if (tx == null && (transmitBaud != -1 || transmitBaudNegotiate != -1 || transmitPin != -1)) { + System.err.println("Cannot set frequency on transmitting port without transmit device"); + return 1; + } + if (tx == null && (message != null || data != null)) { + System.err.println("Cannot transmit without transmitting port"); + return 1; + } + if (tx != null && rx != null && tx.getSystemPortName().equals(rx.getSystemPortName())) { + if (receiveBaud != -1 || receiveBaudNegotiate != -1 || transmitBaud != -1 || transmitBaudNegotiate != -1) { + System.err.println("Cannot set independent baud rates when transmit and receive devices are the same"); + return 1; + } + } + Device rxDevice = null; + if (rx != null) { + rx.setBaudRate(receiveBaud == -1 ? generalBaud : receiveBaud); + rxDevice = new Device(ZscriptModel.standardModel(), ZscriptNode.newNode(new SerialConnection(rx))); + Thread.sleep(2000); + rxDevice.sendAndWaitExpectSuccess(CoreModule.activateBuilder().build()); + if (negotiateBaud(rx, rxDevice, receiveBaudNegotiate == -1 ? generalBaudNegotiate : receiveBaudNegotiate, "Receive device") != 0) { + rx.closePort(); + return 1; + } + if (receivePin == -1) { + // ask user to specify receive pin + receivePin = parsePin(rxDevice, "Receive", "Receiving", d -> d.hasSupportedNotifications() && d.getSupportedNotifications().contains( + DigitalSetupResponse.SupportedNotifications.OnChange) && d.getSupportedModes().contains( + DigitalSetupResponse.SupportedModes.Input), -1, "OnChange notifications"); + if (receivePin == -1) { + rx.closePort(); + return 1; + } + } + } + Device txDevice = null; + if (tx != null) { + if (rx != null && tx.getSystemPortName().equals(rx.getSystemPortName())) { + txDevice = rxDevice; + } else { + tx.setBaudRate(transmitBaud == -1 ? generalBaud : transmitBaud); + txDevice = new Device(ZscriptModel.standardModel(), ZscriptNode.newNode(new SerialConnection(tx))); + Thread.sleep(2000); + txDevice.sendAndWaitExpectSuccess(CoreModule.activateBuilder().build()); + if (negotiateBaud(tx, txDevice, transmitBaudNegotiate == -1 ? generalBaudNegotiate : transmitBaudNegotiate, "Transmit device") != 0) { + tx.closePort(); + if (rx != null) { + rx.closePort(); + } + return 1; + } + } + if (transmitPin == -1) { + // ask user to specify receive pin + boolean txRxSamePinAllowed = txDevice != rxDevice; + transmitPin = parsePin(txDevice, "Transmit", "Transmitting", d -> d.getSupportedModes().contains( + DigitalSetupResponse.SupportedModes.Output), txRxSamePinAllowed ? -1 : receivePin, "Output"); + if (transmitPin == -1) { + tx.closePort(); + if (rx != null) { + rx.closePort(); + } + return 1; + } + } + } + + MorseTranslator translator = new MorseTranslator(); + MorseReceiver receiver = null; + if (rxDevice != null) { + receiver = new MorseReceiver(translator, rxDevice, ditPeriod * 1000L, 2); + receiver.startReceiving(); + } + if (txDevice != null) { + MorseTransmitter transmitter = new MorseTransmitter(txDevice, ditPeriod * 1000L, 13); + transmitter.start(); + if (message != null) { + transmitter.transmit(translator.translate(message)); + } + if (data != null) { + for (File file : data) { + FileInputStream in = new FileInputStream(file); + transmitter.transmit(translator.translate(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(in.readAllBytes())).toString())); + in.close(); + } + } + if (message == null && data == null) { + while (true) { + transmitter.transmit(translator.translate(stdInScanner.nextLine())); + } + } + transmitter.close(); + } + if (receiver != null) { + receiver.close(); + } + if (rx != null) { + rx.closePort(); + } + if (tx != null) { + tx.closePort(); + } + return 0; + } + + public static void main(String[] args) throws IOException, InterruptedException { + int exitCode = new CommandLine(new MorseFullCli()).execute(args); + System.exit(exitCode); + } +} diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseReceiver.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseReceiver.java new file mode 100644 index 000000000..9029c2a61 --- /dev/null +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseReceiver.java @@ -0,0 +1,107 @@ +package net.zscript.demo.morse; + +import static net.zscript.model.modules.base.PinsModule.DigitalNotificationSectionContent.Value; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import net.zscript.javaclient.commandbuilder.commandnodes.ResponseCaptor; +import net.zscript.javaclient.devices.Device; +import net.zscript.model.modules.base.CoreModule; +import net.zscript.model.modules.base.OuterCoreModule; +import net.zscript.model.modules.base.PinsModule; +import net.zscript.model.modules.base.PinsModule.DigitalSetupCommand.Builder.Mode; +import net.zscript.model.modules.base.PinsModule.DigitalSetupCommand.Builder.NotificationMode; + +public class MorseReceiver { + private final ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + private final MorseTranslator translator; + private final Device device; + private final long ditPeriodMaxUs; + private final int pin; + + private final List currentCharacter = new ArrayList<>(); + + private long lastTimeNano; + private boolean isHigh = false; + private boolean hasWrittenWS = true; + + public MorseReceiver(MorseTranslator translator, Device device, long ditPeriodMaxUs, int pin) { + this.translator = translator; + this.device = device; + this.ditPeriodMaxUs = ditPeriodMaxUs; + this.pin = pin; + } + + public void startReceiving() { + try { + lastTimeNano = System.nanoTime(); + ResponseCaptor captor = ResponseCaptor.create(); + device.getNotificationHandle(PinsModule.DigitalNotificationId.get()).getSection(PinsModule.DigitalNotificationSectionId.get()).setCaptor(captor); + device.setNotificationListener(PinsModule.DigitalNotificationId.get(), notificationSequenceCallback -> { + PinsModule.DigitalNotificationSectionContent content = notificationSequenceCallback.getResponseFor(captor).get(); + + long current = System.nanoTime(); + double time = (current - lastTimeNano) / 1000.0 / ditPeriodMaxUs; + if (content.getValue() != Value.High) { + hasWrittenWS = false; + isHigh = false; + if (time > 2) { + currentCharacter.add(MorseElement.DAR); + } else { + currentCharacter.add(MorseElement.DIT); + } + } else { + isHigh = true; + if (time > 2) { + if (!currentCharacter.isEmpty()) { + System.out.print(translator.translate(currentCharacter)); + currentCharacter.clear(); + } + if (!hasWrittenWS) { + if (time > 10) { + System.out.print('\n'); + } else if (time > 6) { + System.out.print(' '); + } + } + hasWrittenWS = true; + } + } + lastTimeNano = current; + }); + device.sendAndWaitExpectSuccess(OuterCoreModule.channelSetupBuilder().setAssignNotification().build()); + device.sendAndWaitExpectSuccess(PinsModule.digitalSetupBuilder().setPin(pin).setMode(Mode.Input).setNotificationMode(NotificationMode.OnChange).build()); + exec.scheduleAtFixedRate(this::checkReceive, 100000, ditPeriodMaxUs * 4, TimeUnit.MICROSECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void checkReceive() { + double time = (System.nanoTime() - lastTimeNano) / 1000.0 / ditPeriodMaxUs; + if (!isHigh && time > 4) { + emptyBuffer(); + if (!hasWrittenWS && time > 10) { + System.out.print('\n'); + hasWrittenWS = true; + } + } + } + + private void emptyBuffer() { + if (!currentCharacter.isEmpty()) { + System.out.print(translator.translate(currentCharacter)); + currentCharacter.clear(); + } + } + + public void close() { + emptyBuffer(); + exec.shutdown(); + } +} diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseSimple.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseSimple.java new file mode 100644 index 000000000..f68afbafb --- /dev/null +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseSimple.java @@ -0,0 +1,45 @@ +package net.zscript.demo.morse; + +import com.fazecast.jSerialComm.SerialPort; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import net.zscript.javaclient.connectors.serial.SerialConnection; +import net.zscript.javaclient.devices.Device; +import net.zscript.javaclient.nodes.ZscriptNode; +import net.zscript.model.ZscriptModel; +import net.zscript.model.modules.base.CoreModule; + +public class MorseSimple { + public static void main(String[] args) throws Exception { + System.out.println(Arrays.stream(SerialPort.getCommPorts()).map(SerialPort::getSystemPortName).collect(Collectors.toList())); + SerialPort rxPort = SerialPort.getCommPorts()[1]; + // SerialPort txPort = SerialPort.getCommPorts()[2]; + + rxPort.setBaudRate(9600); + //txPort.setBaudRate(9600); + + Device rxDevice = new Device(ZscriptModel.standardModel(), ZscriptNode.newNode(new SerialConnection(rxPort))); + Device txDevice = rxDevice; // new Device(ZscriptModel.standardModel(), new ZscriptNode(new SerialConnection(txPort))); // uncomment along with txPort to use 2 targets + + Thread.sleep(2000); // give the ports time to start up + + rxDevice.sendAndWaitExpectSuccess(CoreModule.activateBuilder().build()); + txDevice.sendAndWaitExpectSuccess(CoreModule.activateBuilder().build()); // it's fine to run this twice... + + int ditPeriodUs = 150 * 1000; + + MorseTranslator translator = new MorseTranslator(); + MorseReceiver receiver = new MorseReceiver(translator, rxDevice, ditPeriodUs, 2); + MorseTransmitter transmitter = new MorseTransmitter(txDevice, ditPeriodUs, 13); + transmitter.start(); + receiver.startReceiving(); + transmitter.transmit(translator.translate("Hello world.")); + + receiver.close(); + transmitter.close(); + rxPort.closePort(); + // txPort.closePort(); + } +} diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseTranslator.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseTranslator.java new file mode 100644 index 000000000..101e6b256 --- /dev/null +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseTranslator.java @@ -0,0 +1,127 @@ +package net.zscript.demo.morse; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class MorseTranslator { + private final List morseA = MorseElement.translate(".-"); + private final List morseB = MorseElement.translate("-..."); + private final List morseC = MorseElement.translate("-.-."); + private final List morseD = MorseElement.translate("-.."); + private final List morseE = MorseElement.translate("."); + private final List morseF = MorseElement.translate("..-."); + private final List morseG = MorseElement.translate("--."); + private final List morseH = MorseElement.translate("...."); + private final List morseI = MorseElement.translate(".."); + private final List morseJ = MorseElement.translate(".---"); + private final List morseK = MorseElement.translate("-.-"); + private final List morseL = MorseElement.translate(".-.."); + private final List morseM = MorseElement.translate("--"); + private final List morseN = MorseElement.translate("-."); + private final List morseO = MorseElement.translate("---"); + private final List morseP = MorseElement.translate(".--."); + private final List morseQ = MorseElement.translate("--.-"); + private final List morseR = MorseElement.translate(".-."); + private final List morseS = MorseElement.translate("..."); + private final List morseT = MorseElement.translate("-"); + private final List morseU = MorseElement.translate("..-"); + private final List morseV = MorseElement.translate("...-"); + private final List morseW = MorseElement.translate(".--"); + private final List morseX = MorseElement.translate("-..-"); + private final List morseY = MorseElement.translate("-.--"); + private final List morseZ = MorseElement.translate("--.."); + + private final List morse1 = MorseElement.translate(".----"); + private final List morse2 = MorseElement.translate("..---"); + private final List morse3 = MorseElement.translate("...--"); + private final List morse4 = MorseElement.translate("....-"); + private final List morse5 = MorseElement.translate("....."); + private final List morse6 = MorseElement.translate("-...."); + private final List morse7 = MorseElement.translate("--..."); + private final List morse8 = MorseElement.translate("---.."); + private final List morse9 = MorseElement.translate("----."); + private final List morse0 = MorseElement.translate("-----"); + + private final List morsePeriod = MorseElement.translate(".-.-.-"); + private final List morseComma = MorseElement.translate("--..--"); + private final List morseQMark = MorseElement.translate("..--.."); + private final List morseApos = MorseElement.translate(".----."); + private final List morseSlash = MorseElement.translate("-..-."); + private final List morseOpenP = MorseElement.translate("-.--."); + private final List morseCloseP = MorseElement.translate("-.--.-"); + private final List morseColon = MorseElement.translate("---..."); + private final List morseEquals = MorseElement.translate("-...-"); + private final List morsePlus = MorseElement.translate(".-.-."); + private final List morseMinus = MorseElement.translate("-....-"); + private final List morseQuote = MorseElement.translate(".-..-."); + private final List morseAt = MorseElement.translate(".--.-."); + + private final List morseError = MorseElement.translate("........"); + private final List morseMessageSep = MorseElement.translate("-.-.-"); + + private final List morseSpace = List.of(MorseElement.WORD_SPACE); + + private final List[] morseElArray = new List[] { + morseA, morseB, morseC, morseD, morseE, + morseF, morseG, morseH, morseI, morseJ, + morseK, morseL, morseM, morseN, morseO, + morseP, morseQ, morseR, morseS, morseT, + morseU, morseV, morseW, morseX, morseY, + morseZ, + morse0, morse1, morse2, morse3, morse4, + morse5, morse6, morse7, morse8, morse9, + morsePeriod, morseComma, morseQMark, morseApos, morseSlash, + morseOpenP, morseCloseP, morseColon, morseEquals, morsePlus, + morseMinus, morseQuote, morseAt, + morseError, morseMessageSep, + morseSpace + }; + + private final char[] morseChArray = new char[] { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '.', ',', '?', '\'', '/', '(', ')', ':', '=', '+', + '-', '"', '@', + '�', '\n', + ' ' + }; + + public List translate(String s) { + List elements = new ArrayList<>(); + boolean needsLetterSpace = false; // is needed to avoid letter spaces on word spaces. + for (char c : s.toCharArray()) { + boolean found = false; + char effective = Character.toUpperCase(c); + if (needsLetterSpace && effective != ' ') { + elements.add(MorseElement.LETTER_SPACE); + } + for (int i = 0; i < morseChArray.length; i++) { + if (effective == morseChArray[i]) { + elements.addAll(morseElArray[i]); + found = true; + break; + } + } + if (!found) { + elements.add(MorseElement.LETTER_SPACE); + elements.addAll(morseError); + } + needsLetterSpace = effective != ' '; + } + return elements; + } + + public char translate(List element) { + List elementClean = element.stream().filter(e -> e != MorseElement.LETTER_SPACE).collect(Collectors.toList()); + for (int i = 0; i < morseElArray.length; i++) { + List match = morseElArray[i]; + if (match.equals(elementClean)) { + return morseChArray[i]; + } + } + return '�'; + } +} diff --git a/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseTransmitter.java b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseTransmitter.java new file mode 100644 index 000000000..12910645a --- /dev/null +++ b/demo/demo-morse/client/src/main/java/net/zscript/demo/morse/MorseTransmitter.java @@ -0,0 +1,65 @@ +package net.zscript.demo.morse; + +import static net.zscript.model.modules.base.PinsModule.digitalWriteBuilder; + +import java.util.List; + +import net.zscript.javaclient.devices.Device; +import net.zscript.model.modules.base.PinsModule; +import net.zscript.model.modules.base.PinsModule.DigitalSetupCommand.Builder.Mode; +import net.zscript.model.modules.base.PinsModule.DigitalWriteCommand.Builder.Value; + +public class MorseTransmitter { + private final Device device; + private final long ditPeriodUs; + private final int pin; + + public MorseTransmitter(Device device, long ditPeriodUs, int pin) { + this.device = device; + this.ditPeriodUs = ditPeriodUs; + this.pin = pin; + } + + public void start() { + try { + device.sendAndWaitExpectSuccess(PinsModule.digitalSetupBuilder().setPin(pin).setMode(Mode.Output).build()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void close() { + try { + device.sendAndWaitExpectSuccess(PinsModule.digitalSetupBuilder().setPin(pin).setMode(Mode.Input).build()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void transmit(List elements) { + try { + boolean lastHigh = false; + for (MorseElement element : elements) { + if (lastHigh && element.isHigh()) { + sendElement(1, false); + } + lastHigh = element.isHigh(); + sendElement(element.getLength(), element.isHigh()); + } + sendElement(10, false); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void sendElement(int length, boolean isHigh) throws InterruptedException { + long nsStart = System.nanoTime(); + if (isHigh) { + device.sendAndWaitExpectSuccess(digitalWriteBuilder().setPin(pin).setValue(Value.High).build()); + } else { + device.sendAndWaitExpectSuccess(digitalWriteBuilder().setPin(pin).setValue(Value.Low).build()); + } + while (ditPeriodUs * length - (System.nanoTime() - nsStart) / 1000 > 0) { + } + } +} diff --git a/demo/demo-morse/client/src/main/resources/logback.xml b/demo/demo-morse/client/src/main/resources/logback.xml new file mode 100644 index 000000000..6b0a6ef69 --- /dev/null +++ b/demo/demo-morse/client/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n + + + + + + + + + \ No newline at end of file diff --git a/demo/demo-morse/pom.xml b/demo/demo-morse/pom.xml new file mode 100644 index 000000000..0388d1194 --- /dev/null +++ b/demo/demo-morse/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + net.zscript.demo + demo-all + 0.0.1-SNAPSHOT + + + demo-morse + pom + Demo Morse + + + client + + + \ No newline at end of file diff --git a/demo/pom.xml b/demo/pom.xml index 417fa8bc8..6f4a06d87 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -16,6 +16,7 @@ demo-01 + demo-morse diff --git a/model/schema/src/main/java/net/zscript/model/datamodel/ModelValidator.java b/model/schema/src/main/java/net/zscript/model/datamodel/ModelValidator.java index dd678fb73..0bb2efa26 100644 --- a/model/schema/src/main/java/net/zscript/model/datamodel/ModelValidator.java +++ b/model/schema/src/main/java/net/zscript/model/datamodel/ModelValidator.java @@ -124,15 +124,6 @@ void checkNotification(NotificationModel notification) { sNode.getSection().getName(), notification.getName(), sNode.getNotification()); } checkNotificationSection(sNode); - - boolean lastOne = (i == sections.size() - 1); - if (!lastOne && sNode.getLogicalTermination() == ZscriptDataModel.LogicalTermination.END) { - throw new ZscriptModelException("Only final NotificationSectionNode has logicalTermination of NONE [section=%s, notification=%s]", - sNode.getSection().getName(), notification.getName()); - } else if (lastOne && sNode.getLogicalTermination() != ZscriptDataModel.LogicalTermination.END) { - throw new ZscriptModelException("Final NotificationSectionNode must have logicalTermination of NONE [section=%s, notification=%s]", - sNode.getSection().getName(), notification.getName()); - } } } diff --git a/model/schema/src/main/java/net/zscript/model/datamodel/ZscriptDataModel.java b/model/schema/src/main/java/net/zscript/model/datamodel/ZscriptDataModel.java index 415ff49ad..5b2fccf20 100644 --- a/model/schema/src/main/java/net/zscript/model/datamodel/ZscriptDataModel.java +++ b/model/schema/src/main/java/net/zscript/model/datamodel/ZscriptDataModel.java @@ -1,6 +1,9 @@ package net.zscript.model.datamodel; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import com.fasterxml.jackson.annotation.JsonBackReference; @@ -70,6 +73,16 @@ default int getMemberId(int code) { @JsonManagedReference List getNotifications(); + default Collection getNotificationSections() { + Map sections = new HashMap<>(); + for (NotificationModel model : getNotifications()) { + for (NotificationSectionNodeModel node : model.getSections()) { + sections.put(node.getSection().getSectionName(), node.getSection()); + } + } + return sections.values(); + } + default Optional getCommandById(int id) { for (CommandModel c : getCommands()) { if (c.getCommand() == id) { @@ -171,12 +184,6 @@ default int getFullNotification() { } } - enum LogicalTermination { - END, - ANDTHEN, - ORELSE - } - @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class) interface NotificationSectionNodeModel { @JsonBackReference @@ -184,9 +191,6 @@ interface NotificationSectionNodeModel { @JsonProperty(required = true) NotificationSectionModel getSection(); - - @JsonProperty(required = true) - LogicalTermination getLogicalTermination(); } @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class) diff --git a/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module-2.yaml b/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module-2.yaml index f13aedd21..69e473898 100644 --- a/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module-2.yaml +++ b/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module-2.yaml @@ -30,4 +30,3 @@ notifications: typeDefinition: '@type': bytes required: yes - logicalTermination: end diff --git a/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module.yaml b/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module.yaml index f8d28f56a..a5737c3c2 100644 --- a/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module.yaml +++ b/model/schema/src/test/resources/zscript-datamodel/test-modulebank/my-module.yaml @@ -94,7 +94,6 @@ notifications: typeDefinition: '@type': number required: no - logicalTermination: end - name: packetArrived notification: 0x1 @@ -116,13 +115,11 @@ notifications: typeDefinition: '@type': bytes required: yes - logicalTermination: end - name: packetData notification: 2 description: a UDP packet has been received, and its data is being transmitted via notification sections: - section: *section-packetInfo - logicalTermination: andthen - section: name: packetData description: data from a received UDP packet @@ -133,4 +130,3 @@ notifications: typeDefinition: '@type': bytes required: yes - logicalTermination: end diff --git a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/001x-outercore.yaml b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/001x-outercore.yaml index da7937ced..a0aad95b6 100644 --- a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/001x-outercore.yaml +++ b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/001x-outercore.yaml @@ -171,4 +171,3 @@ notifications: typeDefinition: '@type': number required: no - logicalTermination: end diff --git a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/004x-pins.yaml b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/004x-pins.yaml index 864970b16..8f9c14c5e 100644 --- a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/004x-pins.yaml +++ b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/004x-pins.yaml @@ -363,7 +363,6 @@ notifications: - low - high required: yes - logicalTermination: end - name: analog notification: 1 @@ -395,4 +394,3 @@ notifications: typeDefinition: '@type': number required: yes - logicalTermination: end diff --git a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/005x-i2c.yaml b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/005x-i2c.yaml index 6828c6f08..4dfb1805e 100644 --- a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/005x-i2c.yaml +++ b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/005x-i2c.yaml @@ -360,7 +360,6 @@ notifications: typeDefinition: '@type': number required: yes - logicalTermination: andthen - section: name: smBusAlertResolution description: information on an attempted SMBus alert address read @@ -377,4 +376,3 @@ notifications: typeDefinition: '@type': number required: no - logicalTermination: end diff --git a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/007x-uart.yaml b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/007x-uart.yaml index cfffe18bc..2b8ead925 100644 --- a/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/007x-uart.yaml +++ b/model/standard-module-definitions/src/main/resources/zscript-datamodel/00xx-base-modules/007x-uart.yaml @@ -365,29 +365,13 @@ notifications: typeDefinition: '@type': number required: yes - logicalTermination: end - name: serialReceiveNearCapacity notification: 0x1 description: a serial interface is almost overflowing its data capacity condition: on data exceeding some proportion of the buffer (e.g. 75%) - not sent again until after the fullness drops below this level sections: - - section: §ion_serialData - name: serialData - description: data from a serial system - fields: - - key: M - name: serialMarker - description: the type of terminator ending the data - typeDefinition: *receive_markers - required: yes - - key: + - name: data - description: the data in the buffer - typeDefinition: - '@type': bytes - required: yes - logicalTermination: end + - section: *section_serialDataInfo - name: serialReceiveOverflow notification: 0x2 @@ -404,7 +388,6 @@ notifications: typeDefinition: '@type': number required: yes - logicalTermination: end - name: serialReceiveData notification: 0x3 @@ -412,6 +395,18 @@ notifications: condition: 'on any of: X bytes of data having arrived, Y milliseconds after the first data in the buffer arrived, or when a serial marker arrives, for reasonable chosen values of X,Y' sections: - section: *section_serialDataInfo - logicalTermination: andthen - - section: *section_serialData - logicalTermination: end + - section: + name: serialData + description: data from a serial system + fields: + - key: M + name: serialMarker + description: the type of terminator ending the data + typeDefinition: *receive_markers + required: yes + - key: + + name: data + description: the data in the buffer + typeDefinition: + '@type': bytes + required: yes \ No newline at end of file diff --git a/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/011x-udp.yaml b/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/011x-udp.yaml index 00cabcf76..172a5bd16 100644 --- a/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/011x-udp.yaml +++ b/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/011x-udp.yaml @@ -460,13 +460,11 @@ notifications: typeDefinition: '@type': bytes required: yes - logicalTermination: end - name: packetData notification: 1 description: a UDP packet has been received, and its data is being transmitted via notification sections: - section: *section_packetInfo - logicalTermination: andthen - section: name: packetData description: data from a received UDP packet @@ -477,4 +475,3 @@ notifications: typeDefinition: '@type': bytes required: yes - logicalTermination: end diff --git a/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/012x-tcp.yaml b/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/012x-tcp.yaml index 401687cac..f68857be8 100644 --- a/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/012x-tcp.yaml +++ b/model/standard-module-definitions/src/main/resources/zscript-datamodel/01xx-networking-modules/012x-tcp.yaml @@ -459,7 +459,6 @@ notifications: typeDefinition: '@type': number required: yes - logicalTermination: end - name: socketReceiveNearCapacity notification: 1 @@ -467,7 +466,6 @@ notifications: condition: on data exceeding some proportion of the buffer (e.g. 75%) - not sent again until after the fullness drops below this level sections: - section: *section_socketDataInfo - logicalTermination: end - name: socketReceiveOverflow notification: 2 @@ -484,7 +482,6 @@ notifications: typeDefinition: '@type': number required: yes - logicalTermination: end - name: socketReceiveData notification: 3 @@ -492,7 +489,6 @@ notifications: condition: 'on any of: X bytes of data having arrived, or Y milliseconds after the first data in the buffer arrived, for reasonable chosen values of X,Y' sections: - section: *section_socketDataInfo - logicalTermination: andthen - section: name: socketData description: data from a socket @@ -503,7 +499,6 @@ notifications: typeDefinition: '@type': bytes required: yes - logicalTermination: end - name: serverReceiveConnection notification: 4 @@ -526,4 +521,3 @@ notifications: typeDefinition: '@type': number required: yes - logicalTermination: end diff --git a/util/java-testing-deps/pom.xml b/util/java-testing-deps/pom.xml index d61686d38..138166bf3 100644 --- a/util/java-testing-deps/pom.xml +++ b/util/java-testing-deps/pom.xml @@ -77,7 +77,7 @@ ch.qos.logback - logback-core + logback-classic ${version.logback}