diff --git a/commands/include/aws/iotcommands/CommandExecutionResult.h b/commands/include/aws/iotcommands/CommandExecutionResult.h new file mode 100644 index 0000000000..c4f6d68c41 --- /dev/null +++ b/commands/include/aws/iotcommands/CommandExecutionResult.h @@ -0,0 +1,56 @@ +#pragma once + +/* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ + +#include + +#include +#include + +namespace Aws +{ + namespace Iotcommands + { + + /** + * 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. + * + */ + class AWS_IOTCOMMANDS_API CommandExecutionResult final + { + public: + CommandExecutionResult() = default; + + CommandExecutionResult(const Crt::JsonView &doc); + CommandExecutionResult &operator=(const Crt::JsonView &doc); + + void SerializeToObject(Crt::JsonObject &doc) const; + + /** + * An attribute of type String. + * + */ + Aws::Crt::Optional S; + + /** + * An attribute of type Boolean. + * + */ + Aws::Crt::Optional B; + + /** + * An attribute of type Binary. + * + */ + Aws::Crt::Optional> Bin; + + private: + static void LoadFromObject(CommandExecutionResult &obj, const Crt::JsonView &doc); + }; + } // namespace Iotcommands +} // namespace Aws diff --git a/commands/include/aws/iotcommands/UpdateCommandExecutionRequest.h b/commands/include/aws/iotcommands/UpdateCommandExecutionRequest.h index 6e2f045b8b..5a0f64b033 100644 --- a/commands/include/aws/iotcommands/UpdateCommandExecutionRequest.h +++ b/commands/include/aws/iotcommands/UpdateCommandExecutionRequest.h @@ -6,6 +6,7 @@ * This file is generated */ +#include #include #include #include @@ -66,6 +67,14 @@ namespace Aws */ Aws::Crt::Optional 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. + * + */ + Aws::Crt::Optional> Result; + private: static void LoadFromObject(UpdateCommandExecutionRequest &obj, const Crt::JsonView &doc); }; diff --git a/commands/source/CommandExecutionResult.cpp b/commands/source/CommandExecutionResult.cpp new file mode 100644 index 0000000000..5c8d5bc06f --- /dev/null +++ b/commands/source/CommandExecutionResult.cpp @@ -0,0 +1,66 @@ +/* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + * + * This file is generated + */ +#include + +namespace Aws +{ + namespace Iotcommands + { + + void CommandExecutionResult::LoadFromObject(CommandExecutionResult &val, const Aws::Crt::JsonView &doc) + { + (void)val; + (void)doc; + + if (doc.ValueExists("s")) + { + val.S = doc.GetString("s"); + } + + if (doc.ValueExists("b")) + { + val.B = doc.GetBool("b"); + } + + if (doc.ValueExists("bin")) + { + val.Bin = Aws::Crt::Base64Decode(doc.GetString("bin")); + } + } + + void CommandExecutionResult::SerializeToObject(Aws::Crt::JsonObject &object) const + { + (void)object; + + if (S) + { + object.WithString("s", *S); + } + + if (B) + { + object.WithBool("b", *B); + } + + if (Bin) + { + object.WithString("bin", Aws::Crt::Base64Encode(*Bin)); + } + } + + CommandExecutionResult::CommandExecutionResult(const Crt::JsonView &doc) + { + LoadFromObject(*this, doc); + } + + CommandExecutionResult &CommandExecutionResult::operator=(const Crt::JsonView &doc) + { + *this = CommandExecutionResult(doc); + return *this; + } + + } // namespace Iotcommands +} // namespace Aws diff --git a/commands/source/IotCommandsClientV2.cpp b/commands/source/IotCommandsClientV2.cpp index 04bf84e15a..d062caebf5 100644 --- a/commands/source/IotCommandsClientV2.cpp +++ b/commands/source/IotCommandsClientV2.cpp @@ -210,6 +210,10 @@ namespace Aws CommandExecutionEvent &modeledEvent) { auto segmentExecutionId = s_getSegmentFromTopic(publishEvent.GetTopic(), 5); + if (segmentExecutionId.ptr == nullptr || segmentExecutionId.len == 0) + { + return false; + } modeledEvent.SetExecutionId(segmentExecutionId); modeledEvent.SetPayload(publishEvent.GetPayload()); auto contentType = publishEvent.GetContentType(); diff --git a/commands/source/UpdateCommandExecutionRequest.cpp b/commands/source/UpdateCommandExecutionRequest.cpp index f1cc28a6b3..a2f2454974 100644 --- a/commands/source/UpdateCommandExecutionRequest.cpp +++ b/commands/source/UpdateCommandExecutionRequest.cpp @@ -26,6 +26,18 @@ namespace Aws { val.StatusReason = doc.GetJsonObject("statusReason"); } + + if (doc.ValueExists("result")) + { + auto resultMap = doc.GetJsonObject("result"); + val.Result = Aws::Crt::Map(); + for (auto &resultMapMember : resultMap.GetAllObjects()) + { + Aws::Iotcommands::CommandExecutionResult resultMapValMember; + resultMapValMember = resultMapMember.second.AsObject(); + val.Result->emplace(resultMapMember.first, std::move(resultMapValMember)); + } + } } void UpdateCommandExecutionRequest::SerializeToObject(Aws::Crt::JsonObject &object) const @@ -43,6 +55,20 @@ namespace Aws StatusReason->SerializeToObject(jsonObject); object.WithObject("statusReason", std::move(jsonObject)); } + + if (Result) + { + Aws::Crt::JsonObject resultMap; + for (auto &resultMapMember : *Result) + { + Aws::Crt::JsonObject resultMapValMember; + Aws::Crt::JsonObject jsonObject; + resultMapMember.second.SerializeToObject(jsonObject); + resultMapValMember.AsObject(std::move(jsonObject)); + resultMap.WithObject(resultMapMember.first, std::move(resultMapValMember)); + } + object.WithObject("result", std::move(resultMap)); + } } UpdateCommandExecutionRequest::UpdateCommandExecutionRequest(const Crt::JsonView &doc) diff --git a/docsrc/doxygen.config b/docsrc/doxygen.config index 33cb7aa896..6ccae6cd4b 100644 --- a/docsrc/doxygen.config +++ b/docsrc/doxygen.config @@ -889,6 +889,8 @@ INPUT = crt/aws-crt-cpp/include \ iotdevicecommon/source \ jobs/include \ jobs/source \ + commands/include \ + commands/source \ secure_tunneling/include \ secure_tunneling/source \ shadow/include \ diff --git a/docsrc/mainpage.md b/docsrc/mainpage.md index 02671af9dc..f302091343 100644 --- a/docsrc/mainpage.md +++ b/docsrc/mainpage.md @@ -19,6 +19,7 @@ GitHub: [https://github.com/aws/aws-iot-device-sdk-cpp-v2](https://github.com/aw * [Iotjobs](https://aws.github.io/aws-iot-device-sdk-cpp-v2/namespace_aws_1_1_iotjobs.html) * [Iotsecuretunneling](https://aws.github.io/aws-iot-device-sdk-cpp-v2/namespace_aws_1_1_iotsecuretunneling.html) * [Iotshadow](https://aws.github.io/aws-iot-device-sdk-cpp-v2/namespace_aws_1_1_iotshadow.html) +* [Iotcommands](https://aws.github.io/aws-iot-device-sdk-cpp-v2/namespace_aws_1_1_iotcommands.html) ## AWS Crt C++ diff --git a/samples/service_clients/commands/commands-sandbox/command_stream_handler.cpp b/samples/service_clients/commands/commands-sandbox/CommandStreamHandler.cpp similarity index 97% rename from samples/service_clients/commands/commands-sandbox/command_stream_handler.cpp rename to samples/service_clients/commands/commands-sandbox/CommandStreamHandler.cpp index 6430400df3..a71b5fc207 100644 --- a/samples/service_clients/commands/commands-sandbox/command_stream_handler.cpp +++ b/samples/service_clients/commands/commands-sandbox/CommandStreamHandler.cpp @@ -3,10 +3,11 @@ * SPDX-License-Identifier: Apache-2.0. */ -#include "command_stream_handler.h" +#include "CommandStreamHandler.h" #include #include +#include #include #include #include @@ -101,7 +102,8 @@ namespace Aws const Crt::String &executionId, Aws::Iotcommands::CommandExecutionStatus status, const Aws::Crt::String &reasonCode, - const Aws::Crt::String &reasonDescription) + const Aws::Crt::String &reasonDescription, + const Aws::Crt::Map &result) { CommandExecutionContext commandExecutionContext; @@ -133,6 +135,11 @@ namespace Aws request.StatusReason->ReasonDescription = reasonDescription; } + if (!result.empty()) + { + request.Result = result; + } + fprintf(stdout, "Updating command execution '%s'\n", commandExecutionContext.event.ExecutionId->c_str()); std::promise updateWaiter; diff --git a/samples/service_clients/commands/commands-sandbox/command_stream_handler.h b/samples/service_clients/commands/commands-sandbox/CommandStreamHandler.h similarity index 93% rename from samples/service_clients/commands/commands-sandbox/command_stream_handler.h rename to samples/service_clients/commands/commands-sandbox/CommandStreamHandler.h index 08f56a1cf7..2f7f05ed6c 100644 --- a/samples/service_clients/commands/commands-sandbox/command_stream_handler.h +++ b/samples/service_clients/commands/commands-sandbox/CommandStreamHandler.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -22,7 +23,7 @@ namespace Aws /** * Sample wrapper around client for the AWS IoT command service. - * It helps + * It helps handle all the streaming operations and command execution updates. */ class CommandStreamHandler { @@ -55,13 +56,15 @@ namespace Aws * @param status One of the following statuses: IN_PROGRESS, SUCCEEDED, FAILED, REJECTED, TIMED_OUT. * @param reasonCode A short code in the [A-Z0-9_-]+ format and not exceeding 64 characters in length. * @param reasonDescription Detailed description of the reason. + * @param result A map of result entries. Each entry value is either a string or boolean. * @return Success status. */ bool updateCommandExecutionStatus( const Aws::Crt::String &executionId, Aws::Iotcommands::CommandExecutionStatus status, const Aws::Crt::String &reasonCode, - const Aws::Crt::String &reasonDescription); + const Aws::Crt::String &reasonDescription, + const Aws::Crt::Map &result); void listOpenedStreams(); diff --git a/samples/service_clients/commands/commands-sandbox/README.md b/samples/service_clients/commands/commands-sandbox/README.md index 20d4e65059..8baf77fe40 100644 --- a/samples/service_clients/commands/commands-sandbox/README.md +++ b/samples/service_clients/commands/commands-sandbox/README.md @@ -58,13 +58,16 @@ API calls. Once connected, the sample supports the following commands: * open-thing-stream - subscribe to a stream of AWS IoT command executions with a specified payload format -targeting the IoT Thing set on the application startup +targeting the IoT Thing set on the application startup. Supported payload formats: `json`, `cbor`, or any other value for `generic`. * 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 +targeting the MQTT client ID set on the application startup. Supported payload formats: `json`, `cbor`, or any other value for `generic`. +* 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 is required if reason-description is specified + * result format: `key1:value1;key2:value2` + * values of `true`/`false` are treated as boolean, others as string + * use quotes for values with spaces: `key:"hello world"` Miscellaneous * list-streams - list all open streaming operations @@ -432,9 +435,17 @@ Take an AWS IoT command execution ID your sample received at the end of the prev update-command-execution IN_PROGRESS ``` +You can also provide a result with the update: +``` +update-command-execution IN_PROGRESS result=battery_ok:true;message:"doing something" +``` + +> [!NOTE] +> You can also pass binary data in the result field using the CommandExecutionResult::Bin member, which is not supported in this sample. + Then this AWS CLI command ```shell -aws iot get-command-execution --target-arn "" --execution-id +aws iot get-command-execution --target-arn "" --execution-id --include-result ``` should return something like @@ -445,6 +456,14 @@ should return something like "commandArn": "arn:aws:iot:...:command/MyJsonCommand", "targetArn": "arn:aws:iot:...:thing/MyIotThing", "status": "IN_PROGRESS", + "result": { + "battery_ok": { + "B": true + }, + "message": { + "S": "doing something" + } + }, "executionTimeoutSeconds": 300 } ``` @@ -463,7 +482,7 @@ update-command-execution SUCCEEDED ``` or ``` -update-command-execution FAILED SHORT_FAILURE_CODE A longer description +update-command-execution FAILED reason-code=SHORT_FAILURE_CODE reason-description="A longer description" result=status:"task complete";success:false ``` will yield something like @@ -505,6 +524,14 @@ which will yield "reasonCode": "SHORT_FAILURE_CODE", "reasonDescription": "A longer description" }, + "result": { + "success": { + "B": false + }, + "status": { + "S": "task complete" + } + }, "executionTimeoutSeconds": 300 } ``` @@ -552,4 +579,4 @@ Additionally, example code might theoretically modify or delete existing AWS res - Be aware of the resources that these examples create or delete. - Be aware of the costs that might be charged to your account as a result. -- Back up your important data. \ No newline at end of file +- Back up your important data. diff --git a/samples/service_clients/commands/commands-sandbox/main.cpp b/samples/service_clients/commands/commands-sandbox/main.cpp index 5b280806d3..defeb56982 100644 --- a/samples/service_clients/commands/commands-sandbox/main.cpp +++ b/samples/service_clients/commands/commands-sandbox/main.cpp @@ -6,15 +6,16 @@ #include #include #include +#include -#include "command_stream_handler.h" +#include "CommandStreamHandler.h" #include #include #include #include #include -#include +#include struct ApplicationContext { @@ -26,31 +27,42 @@ struct ApplicationContext static Aws::Crt::String s_nibbleNextToken(Aws::Crt::String &input) { - Aws::Crt::String token; - Aws::Crt::String remaining; - auto delimPosition = input.find(' '); - if (delimPosition != Aws::Crt::String::npos) + // Skip leading spaces + auto it = std::find_if(input.begin(), input.end(), [](char c) { return c != ' '; }); + if (it == input.end()) { - token = input.substr(0, delimPosition); + input.clear(); + return {}; + } - auto untrimmedRemaining = input.substr(delimPosition, Aws::Crt::String::npos); - auto firstNonSpacePosition = untrimmedRemaining.find_first_not_of(' '); - if (firstNonSpacePosition != Aws::Crt::String::npos) + // Find end of token, respecting quotes + bool inQuotes = false; + auto tokenEnd = std::find_if(it, input.end(), [&inQuotes](char c) + { + if (c == '"') { - remaining = untrimmedRemaining.substr(firstNonSpacePosition, Aws::Crt::String::npos); + inQuotes = !inQuotes; } - else + return c == ' ' && !inQuotes; + }); + + Aws::Crt::String token(it, tokenEnd); + + // Strip surrounding quotes from the value part (after '=') + auto eqPos = token.find('='); + if (eqPos != Aws::Crt::String::npos) + { + auto value = token.substr(eqPos + 1); + if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { - remaining = ""; + token = token.substr(0, eqPos + 1) + value.substr(1, value.size() - 2); } } - else - { - token = input; - remaining = ""; - } - input = remaining; + // Set remaining input + auto nextStart = std::find_if(tokenEnd, input.end(), [](char c) { return c != ' '; }); + input = Aws::Crt::String(nextStart, input.end()); + return token; } @@ -60,15 +72,22 @@ static void s_printHelp() fprintf(stdout, " open-thing-stream \n"); fprintf(stdout, " subscribe to a stream of command executions with a specified payload format\n"); fprintf(stdout, " targeting the IoT Thing set on the application startup\n"); + fprintf(stdout, " supported payload formats: json, cbor, or any other value for generic\n"); fprintf(stdout, " open-client-stream \n"); fprintf(stdout, " subscribe to a stream of command executions with a specified payload format\n"); fprintf(stdout, " targeting the MQTT client ID set on the application startup\n"); - fprintf(stdout, " update-command-execution [] []\n"); + fprintf(stdout, " supported payload formats: json, cbor, or any other value for generic\n"); + fprintf( + stdout, + " update-command-execution [reason-code=] [reason-description=] " + "[result=]\n"); fprintf(stdout, " update status for specified command execution ID;\n"); fprintf(stdout, " can be one of the following:\n"); fprintf(stdout, " IN_PROGRESS, SUCCEEDED, REJECTED, FAILED, TIMED_OUT\n"); - fprintf(stdout, " and may be optionally provided for\n"); - fprintf(stdout, " the REJECTED, FAILED, or TIMED_OUT statuses\n"); + fprintf(stdout, " reason-code is required if reason-description is specified\n"); + fprintf(stdout, " result format: key1:value1;key2:value2\n"); + fprintf(stdout, " values of 'true'/'false' are treated as boolean, others as string\n"); + fprintf(stdout, " use quotes for values with spaces: key:\"hello world\"\n"); fprintf(stdout, " list-streams list all open streaming operations\n"); fprintf(stdout, " close-stream \n"); fprintf(stdout, " close a specified stream;\n"); @@ -86,7 +105,7 @@ static void s_handleOpenStream( if (payloadFormat.empty()) { - fprintf(stdout, "Invalid arguments to open-*-stream command!\n\n"); + fprintf(stdout, "Invalid arguments to open-stream command!\n\n"); s_printHelp(); return; } @@ -102,6 +121,58 @@ static void s_handleOpenStream( } } +static Aws::Crt::String s_extractValue(std::istringstream &stream) +{ + Aws::Crt::String value; + if (stream.peek() == '"') + { + stream.get(); // skip opening quote + std::getline(stream, value, '"'); + if (stream.peek() == ';') + { + stream.get(); + } + } + else + { + std::getline(stream, value, ';'); + } + return value; +} + +/* */ +static Aws::Iotcommands::CommandExecutionResult s_makeResultEntry(const Aws::Crt::String &value) +{ + Aws::Iotcommands::CommandExecutionResult entry; + // NOTE: CommandExecutionResult also supports binary data via the `Bin` member, which is not demonstrated in this + // sample. + if (value == "true" || value == "false") + { + entry.B = (value == "true"); + } + else + { + entry.S = value; + } + return entry; +} + +static Aws::Crt::Map s_parseResult( + const Aws::Crt::String &input) +{ + Aws::Crt::Map result; + std::istringstream stream(std::string(input.c_str(), input.size())); + + Aws::Crt::String key; + while (std::getline(stream, key, ':')) + { + auto value = s_extractValue(stream); + result.emplace(std::move(key), s_makeResultEntry(value)); + } + + return result; +} + static void s_handleUpdateCommandExecution(const Aws::Crt::String ¶ms, ApplicationContext &context) { Aws::Crt::String paramCopy = params; @@ -115,15 +186,49 @@ static void s_handleUpdateCommandExecution(const Aws::Crt::String ¶ms, Appli return; } - // NOTE: For debug builds, invalid values will cause abort here. Aws::Iotcommands::CommandExecutionStatus status = Aws::Iotcommands::CommandExecutionStatusMarshaller::FromString(statusStr); - Aws::Crt::String reasonCode = s_nibbleNextToken(paramCopy); - Aws::Crt::String reasonDescription = paramCopy; + Aws::Crt::String reasonCode; + Aws::Crt::String reasonDescription; + Aws::Crt::Map result; + + // Parse optional key=value arguments + while (!paramCopy.empty()) + { + Aws::Crt::String token = s_nibbleNextToken(paramCopy); + auto eqPos = token.find('='); + if (eqPos == Aws::Crt::String::npos) + { + continue; + } + + Aws::Crt::String key = token.substr(0, eqPos); + Aws::Crt::String value = token.substr(eqPos + 1); + + if (key == "reason-code") + { + reasonCode = value; + } + else if (key == "reason-description") + { + reasonDescription = value; + } + else if (key == "result") + { + result = s_parseResult(value); + } + } + + if (!reasonDescription.empty() && reasonCode.empty()) + { + fprintf(stdout, "reason-code is required when reason-description is specified!\n\n"); + s_printHelp(); + return; + } context.commandStreamHandler->updateCommandExecutionStatus( - commandExecutionId, status, reasonCode, reasonDescription); + commandExecutionId, status, reasonCode, reasonDescription, result); } static void s_handleListStreams(ApplicationContext &context) @@ -276,7 +381,7 @@ int main(int argc, char *argv[]) // Create the MQTT5 builder and populate it with data from cmdData. auto builder = Aws::Iot::Mqtt5ClientBuilder::CreateMqtt5ClientBuilderWithMtlsFromPath( - cmdData.endpoint, cmdData.cert.c_str(), cmdData.key.c_str()); + cmdData.endpoint, cmdData.cert.c_str(), cmdData.key.c_str()); // Check if the builder setup correctly. if (builder == nullptr) { @@ -331,6 +436,7 @@ int main(int argc, char *argv[]) auto commandStreamHandler = Aws::Crt::MakeShared( Aws::Crt::DefaultAllocatorImplementation(), std::move(commandClient)); + // Establish connection protocolClient->Start(); auto isConnected = connectedWaiter.get_future().get(); connectedWaiter = {};