diff --git a/samples/ServiceClients/CommandsSandbox/README.md b/samples/ServiceClients/CommandsSandbox/README.md index f40c3905..05f16c68 100644 --- a/samples/ServiceClients/CommandsSandbox/README.md +++ b/samples/ServiceClients/CommandsSandbox/README.md @@ -51,10 +51,11 @@ Data Plane targeting the IoT Thing set on the application startup * `open-client-stream ` - subscribe to a stream of AWS IoT command executions with a specified payload format targeting the MQTT client ID set on the application startup -* `update-command-execution \ \[\] \[\]` - update status for specified - execution ID; +* `update-command-execution [reason-code=] [reason-description=] [result=:;:]` - + update status for specified execution ID; * status can be one of the following: IN_PROGRESS, SUCCEEDED, REJECTED, FAILED, TIMED_OUT - * reason-code and reason-description may be optionally provided for the REJECTED, FAILED, or TIMED_OUT statuses + * reason-code and reason-description may be optionally provided for any status + * result is a semicolon-separated list of key:value pairs; if a value is true/false it is treated as boolean, otherwise as string Miscellaneous * `list-streams` - list all open streaming operations @@ -353,6 +354,15 @@ Take an AWS IoT command execution ID your sample received at the end of the prev update-command-execution 33333333-3333-3333-3333-333333333333 IN_PROGRESS ``` +You can also provide execution results when updating the status. Results are specified as semicolon-separated +key:value pairs. Values of `true` or `false` are treated as booleans, everything else is treated as a string: +``` +update-command-execution 33333333-3333-3333-3333-333333333333 IN_PROGRESS result=batteryStatus:"unknown status";alive:true +``` + +> [!NOTE] +> You can also pass binary data in the result field using the CommandExecutionResult::bin member, which is not supported in this sample. + Then checking once again for the AWS IoT command execution status with ``` get-command-execution 33333333-3333-3333-3333-333333333333 @@ -362,6 +372,9 @@ should return ``` Status of Command execution '33333333-3333-3333-3333-333333333333' is IN_PROGRESS + Result: + alive: true (boolean) + batteryStatus: unknown (string) ``` `IN_PROGRESS` is an intermediary execution status, i.e. it's possible to change this status. @@ -378,7 +391,7 @@ update-command-execution 33333333-3333-3333-3333-333333333333 SUCCEEDED ``` or ``` -update-command-execution 33333333-3333-3333-3333-333333333333 FAILED SHORT_FAILURE_CODE A longer description +update-command-execution 33333333-3333-3333-3333-333333333333 FAILED reason-code=SHORT_FAILURE_CODE reason-description="A longer description" ``` If you try to update the status of the same AWS IoT command execution to something else, it'll fail: diff --git a/samples/ServiceClients/CommandsSandbox/src/main/java/commands/CommandsSandbox.java b/samples/ServiceClients/CommandsSandbox/src/main/java/commands/CommandsSandbox.java index 11973d00..39e58505 100644 --- a/samples/ServiceClients/CommandsSandbox/src/main/java/commands/CommandsSandbox.java +++ b/samples/ServiceClients/CommandsSandbox/src/main/java/commands/CommandsSandbox.java @@ -319,12 +319,12 @@ private static void printCommandHelp() { System.out.println(" application/json - subscribe to commands with JSON payload"); System.out.println(" application/cbor - subscribe to commands with CBOR payload"); System.out.println(" for any other value, subscribe to a generic topic"); - System.out.println(" update-command-execution [] []"); + System.out.println(" update-command-execution [reason-code=] [reason-description=] [result=:;:]"); System.out.println(" updates a command execution with a new status"); System.out.println(" can be one of the following:"); System.out.println(" IN_PROGRESS, SUCCEEDED, REJECTED, FAILED, TIMED_OUT"); - System.out.println(" and may be optionally provided for"); - System.out.println(" the REJECTED, FAILED, or TIMED_OUT statuses\n"); + System.out.println(" reason-code and reason-description may be optionally provided for any status"); + System.out.println(" result is a semicolon-separated list of key:value pairs; if a value is true/false it is treated as boolean, otherwise as string\n"); System.out.println(" Miscellaneous commands:"); System.out.println(" list-streams list all open streaming operations"); System.out.println(" close-stream "); @@ -426,6 +426,7 @@ private static void handleGetCommandExecution(ApplicationContext context, String GetCommandExecutionRequest getCommandExecutionRequest = GetCommandExecutionRequest.builder() .executionId(commandExecutionId) .targetArn(commandExecutionContext.deviceArn) + .includeResult(true) .build(); GetCommandExecutionResponse getCommandExecutionResponse = context.controlPlaneClient.getCommandExecution(getCommandExecutionRequest); System.out.printf("Status of command execution '%s' is %s\n", commandExecutionId, getCommandExecutionResponse.status()); @@ -433,32 +434,78 @@ private static void handleGetCommandExecution(ApplicationContext context, String System.out.printf(" Reason code: %s\n", getCommandExecutionResponse.statusReason().reasonCode()); System.out.printf(" Reason description: %s\n", getCommandExecutionResponse.statusReason().reasonDescription()); } + if (getCommandExecutionResponse.hasResult()) { + System.out.println(" Result:"); + getCommandExecutionResponse.result().forEach((key, value) -> { + if (value.b() != null) { + System.out.printf(" %s: %s (boolean)\n", key, value.b()); + } else if (value.s() != null) { + System.out.printf(" %s: %s (string)\n", key, value.s()); + } + }); + } } catch (Exception ex) { handleOperationException("get-command-execution", ex, context); } } + private static Map parseKeyValueArgs(String input) { + Pattern pattern = Pattern.compile("(\\S+?)=([^\\s\"]*(?:\"[^\"]*\"[^\\s\"]*)*)"); + Matcher matcher = pattern.matcher(input); + + Map result = new HashMap<>(); + + while (matcher.find()) { + String key = matcher.group(1); + String value = matcher.group(2); + result.put(key, value); + } + + return result; + } + + private static HashMap parseResult(String resultStr) { + HashMap result = new HashMap<>(); + + Pattern pattern = Pattern.compile("([^;:]*):([^;\"]*(?:\"[^\"]*\"[^;\"]*)*)"); + Matcher matcher = pattern.matcher(resultStr); + + while (matcher.find()) { + String key = matcher.group(1); + String value = matcher.group(2).replaceAll("^\"|\"$", ""); + + software.amazon.awssdk.iot.iotcommands.model.CommandExecutionResult entry = + new software.amazon.awssdk.iot.iotcommands.model.CommandExecutionResult(); + + /* NOTE: CommandExecutionResult also supports binary data via the `bin` member, which is not demonstrated in this + * sample. */ + if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + entry.b = Boolean.parseBoolean(value); + } else { + entry.s = value; + } + + result.put(key, entry); + } + + return result; + } + private static void handleUpdateCommandExecution(ApplicationContext context, String arguments) { - String[] argumentSplit = arguments.trim().split(" ", 4); - if (argumentSplit.length < 2) { + String[] parts = arguments.trim().split(" ", 3); + if (parts.length < 2) { printCommandHelp(); return; } - String commandExecutionId = argumentSplit[0]; + String commandExecutionId = parts[0]; if (!context.activeCommandExecutions.containsKey(commandExecutionId)) { System.out.printf("Failed to update command execution status: unknown command execution ID '%s'\n", commandExecutionId); return; } - String statusStr = argumentSplit[1]; - - String reasonCode = null; - String reasonDescription = null; - if (argumentSplit.length > 3) { - reasonCode = argumentSplit[2]; - reasonDescription = argumentSplit[3]; - } + String statusStr = parts[1]; + Map kvArgs = parseKeyValueArgs(parts.length > 2 ? parts[2] : ""); try { CommandExecutionContext commandExecutionContext = context.activeCommandExecutions.get(commandExecutionId); @@ -467,10 +514,18 @@ private static void handleUpdateCommandExecution(ApplicationContext context, Str request.deviceType = commandExecutionContext.deviceType; request.deviceId = commandExecutionContext.deviceId; request.status = CommandExecutionStatus.valueOf(statusStr); - if (reasonCode != null && reasonDescription != null) { + + String reasonCode = kvArgs.get("reason-code"); + String reasonDescription = kvArgs.get("reason-description"); + if (reasonCode != null || reasonDescription != null) { request.statusReason = new StatusReason(); request.statusReason.reasonCode = reasonCode; - request.statusReason.reasonDescription = reasonDescription; + request.statusReason.reasonDescription = reasonDescription.replaceAll("^\"|\"$", ""); + } + + String resultStr = kvArgs.get("result"); + if (resultStr != null) { + request.result = parseResult(resultStr); } UpdateCommandExecutionResponse response = context.commandsClient.updateCommandExecution(request).get(); diff --git a/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/IotCommandsV2Client.java b/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/IotCommandsV2Client.java index 2516f467..89991f62 100644 --- a/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/IotCommandsV2Client.java +++ b/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/IotCommandsV2Client.java @@ -86,7 +86,11 @@ public void close() { private CommandExecutionEvent createCommandExecutionEvent(IncomingPublishEvent publishEvent) { CommandExecutionEvent event = new CommandExecutionEvent(); - event.executionId = publishEvent.getTopic().split("/")[5]; + String[] segments = publishEvent.getTopic().split("/"); + if (segments.length <= 5) { + throw new CrtRuntimeException("Invalid topic: " + publishEvent.getTopic()); + } + event.executionId = segments[5]; event.payload = publishEvent.getPayload(); String contentType = publishEvent.getContentType(); if (contentType != null) { diff --git a/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/model/CommandExecutionResult.java b/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/model/CommandExecutionResult.java new file mode 100644 index 00000000..338d0e23 --- /dev/null +++ b/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/model/CommandExecutionResult.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated. + */ + +package software.amazon.awssdk.iot.iotcommands.model; + + +/** + * The result value of the command execution. The device can use the result field to share additional details about the execution such as a return value of a remote function call. + * + */ +public class CommandExecutionResult { + + /** + * An attribute of type String. + * + */ + public String s; + + + /** + * An attribute of type Boolean. + * + */ + public Boolean b; + + + /** + * An attribute of type Binary. + * + */ + public byte[] bin; + + +} diff --git a/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/model/UpdateCommandExecutionRequest.java b/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/model/UpdateCommandExecutionRequest.java index 54f83da3..6439f05b 100644 --- a/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/model/UpdateCommandExecutionRequest.java +++ b/sdk/src/main/java/software/amazon/awssdk/iot/iotcommands/model/UpdateCommandExecutionRequest.java @@ -7,6 +7,8 @@ package software.amazon.awssdk.iot.iotcommands.model; +import java.util.HashMap; +import software.amazon.awssdk.iot.iotcommands.model.CommandExecutionResult; import software.amazon.awssdk.iot.iotcommands.model.CommandExecutionStatus; import software.amazon.awssdk.iot.iotcommands.model.DeviceType; import software.amazon.awssdk.iot.iotcommands.model.StatusReason; @@ -52,4 +54,11 @@ public class UpdateCommandExecutionRequest { public StatusReason statusReason; + /** + * The result value for the current state of the command execution. The status provides information about the progress of the command execution. The device can use the result field to share additional details about the execution such as a return value of a remote function call. + * + */ + public HashMap result; + + }