Overview
This Guidance recommends using an AWS IoT Job from AWS IoT Core to perform an update or action on a physical device remotely. These type of operations are called “commands”. Internet of Things (IoT) jobs will provide the infrastructure (e.g existing MQTT topics and AWS managed job templates) to manage the remote operations and configurability for jobs. Remote reboot, factory reset, or firmware update of a device commands can all utilize this capability.
This capability requires edge and cloud components of this Guidance to work together. The edge implementation provided in this Guidance supports rebooting of a device. For the cloud, customers need to implement their own by following the provided Java reference sample code for a command.
Device reboot is used in this Gidance to demonstrate the end-to-end flow of a command. Customers using this Guidance can add more types of operations (such as factory reset, control the pan/tilt/zoom of the camera) by mimicking the edge implementation for reboot and the cloud Java reference sample code for reboot provided in the section Run. To add new command types, see Instructions to implement a new type of command.
Pre-Requisites
- Successfully deployed
deployment/device-management-cdk/VideoAnalyticsDeviceManagementCDK/lib/stacks/bootstrapStack/deviceManagementBootstrapStack.ts
to account. Successfully deployed
deployment/device-management-cdk/VideoAnalyticsDeviceManagementCDK/lib/stacks/optionalCapabilitiesStack/iotJobsDevicePermissionStack.ts
. Once deployed successfully, this stack will add necessary policies to IoT Thing groupSpecialGroup_EnabledState
and will allow all devices inSpecialGroup_EnabledState
to automatically inherit the policies. The policies will give device permission to IoT Job MQTT topics. Similarly, if customers wants any other thing groups to have access to iot Job MQTT topic, attachIotJobPolicy_<region>
to the thing groups following the pattern iniotJobsDevicePermissionStack.ts
To simplify CDK deployment in production, customers can consider merging the AWS Cloud Development Kit code provided in
iotJobsDevicePermissionStack.ts
intodeviceManagementBootstrapStack.ts
. Note that merging the 2 CDK files are not required for the command capability to work.Use a binary that is compiled with the remote operation capability. Run the following from a command line interface (CLI):
a. for armv7
cross build --features command --target armv7-unknown-linux-gnueabihf --release
If successfully compiled, the binary can be found at
guidance-for-video-analytics-infrastructure-on-aws/source/edge/target/armv7-unknown-linux-gnueabihf/release/edge-process
To build for armv7,
cross
needs to be installed prior to runningcross build
command line. Instructions to install cross-rs can be found at cross.b. for x86_64
cargo build
If successfully compiled, the binary can be found at
guidance-for-video-analytics-infrastructure-on-aws/source/edge/target/debug/edge-process
- An Open Network Video Interface Forum (ONVIF) compliant device to run binary on.
Assumptions
- Device has been added to
SpecialGroup_EnabledState
. If the cloud formation deployment was succesfull and the device was created succesfully using APIStartCreateDevice
, by default the device should already be inSpecialGroup_EnabledState
. If the device is not inSpecialGroup_EnabledState
, see State Management (wip-still need to add url once published) on how to move the device toSpecialGroup_EnabledState
. - The computer used for development is a x86_64 system.
- The device is an ONVIF compliant device that offers ONVIF API
SystemReboot
Instructions to run and verify the capability
Run
- Run the binary on device
./edge-process -c <path to config yaml file>
- Execute the Java code below
Customers can choose to 1/embed the Java code provided here to an existing application or 2/implement the Java code following this guidance’s server-less pattern of APIGW → Lambda
for synchronous operation or APIGW -> Lambda -> DDB -> StepFunction
for asynchronous operations.
import software.amazon.awssdk.services.iot.IotClient;
import software.amazon.awssdk.services.iot.model.*;
import java.util.Collections;
import java.util.UUID;
public void createIoTJobForCommand(
final String thingName,
final String region,
final String accountId,
IotClient iotClient,
) {
// Validate if device exists
System.out.println("Checking if device exist.");
try {
iotClient.describeThing(DescribeThingRequest.builder().thingName(thingName).build());
} catch (ResourceNotFoundException e){
System.out.printf("Thing %s doesn't exist.%n", thingName);
return;
}
final String IOT_FLEET_INDEXING_CONNECTIVITY = "connectivity.connected:";
final String IOT_FLEET_INDEXING_THING_NAME = "thingName:";
final String IOT_FLEET_INDEXING_INDEX_NAME = "AWS_Things";
System.out.println("Checking if device is connected.");
System.out.printf("Searching within index: %s.", IOT_FLEET_INDEXING_INDEX_NAME);
// generate the query "connectivity.connected:true AND thingName:exampleThing"
String deviceConnectedQuery = IOT_FLEET_INDEXING_CONNECTIVITY + "true" + " AND " + IOT_FLEET_INDEXING_THING_NAME + thingName;
SearchIndexRequest searchIndexRequest = SearchIndexRequest
.builder()
.indexName(IOT_FLEET_INDEXING_INDEX_NAME)
.queryString(deviceConnectedQuery)
.build();
SearchIndexResponse searchIndexResponse = iotClient.searchIndex(searchIndexRequest);
// Do nothing if device is not connected. Only proceed to perform remote operations on a device that's connected
if (searchIndexResponse.things().size() != 1) {
System.out.println("Device is not connected. No-op for remote operation!");
return;
};
System.out.println("Device is connected.");
System.out.println("Creating IoT Job for AWS-Run-Command Template");
String target = String.format("arn:aws:iot:%s:%s:thing/%s",
region,
accountId,
thingName);
String jobId = UUID.randomUUID().toString();
CreateJobRequest jobRequest = CreateJobRequest
.builder()
// Required field for IoT API CreateJob
.jobId(jobId)
// Required field for IoT API CreateJob
.targets(Collections.singletonList(target))
// Optional field for IoT API CreateJob
.targetSelection(TargetSelection.SNAPSHOT)
// Optional field for IoT API CreateJob
.description("This is a job for remotely reboot a device")
// Optional field for IoT API CreateJob
// documentParameters payload for reboot device:
// "command" is the default key for the key value pair in this guidance.
// If "command" is replaced with another keyword, make sure edge-process is also updated accordingly <link code>
.documentParameters(Collections.singletonMap("command", "REBOOT"))
// Optional field for IoT API CreateJob. Set timeout to 10 minutes in this example
.timeoutConfig(t -> t.inProgressTimeoutInMinutes(10L))
// Optional field for IoT API CreateJob
.jobExecutionsRetryConfig(j -> {
j.criteriaList(Collections.singleton(
RetryCriteria.builder()
.failureType(RetryableFailureType.ALL)
// Set retries to 10 in this example
.numberOfRetries(10)
.build()
)
);
})
.jobTemplateArn(String.format("arn:aws:iot:%s::jobtemplate/AWS-Run-Command:1.0", region))
.build();
try {
iotClient.createJob(jobRequest);
} catch (IotException e) {
System.out.println(e);
}
System.out.printf("Created a new job %s.", jobId);
return;
}
- As part of the reboot, the binary would stop running when the device is turned off. After device is back on after the reboot, run the binary on device
./edge-process -c <path to config yaml file>
Verify
- After Java code is executed, if LOG_LEVEL on device is set to INFO, DEBUG, or TRACE, customers should see below logs printed on the device terminal
INFO System reboot ONVIF request was sent to process 1 successfully. Response from process 1: "System is now rebooting. rebooting may take about one minute." INFO Initiated reboot
printed in the terminal on device. Alternatively, customers can also find the logs in cloud trail if environement variable LOG_SYNC environment variable is set to TRUE.
- Sign in to AWS IoT console. In the menu bar on the left under Manage, find Remote actions > Jobs. Search the job using JobId (the UUID randomly generated in the Java code above). Job status should be “In progress” after the device is turned off temporarily as part of the reboot.
- Screenshot: IoT Job in progress *
- Device that’s running the binary should experience temporary shut down as part of the reboot process. The binary running on device should stop. Device should also stop streaming to KVS.
- After edge-process binary is up and running after step 3 in the previous section Run:, Job status should be “Completed”. And customers should see below logs printed on the device terminal
INFO Successfully got next pending job execution. INFO Checking if an existing reboot command is in progress. INFO Confirmed a reboot command is in progress. INFO Message published to IoT with topic : $aws/things/<thing-name>/jobs/<job-id>/update INFO Successfully updated command status.
- Once the IoT job status is “Completed” and edge-process is up and running, the device should be connected to IoT and be running again.
- Screenshot: IoT Job completed *
a. There are multiple ways to check if a device is connected. One way is using AWS IoT console. In the menu bar on the left under Manage, find All devices > Things > Advanced Search button. Use the query
"connectivity.connected:true AND thingName:<device-thing-name>”
to search. Replace device-thing-name with the device thing name for your device.- Screenshot: IoT search connected thing *
Instructions to implement a new type of command
Using soft factory reset, an operation supported by ONVIF compliant devices, as an example.
Edge Rust code
Add the ONVIF API request and response Rust struct to
source/edge/crates/onvif-client/src/wsdl_rs/devicemgmt.rs
. See this on how to create Rust structs for ONVIF APIs.Add a new function to OnvifClient as an interface for the ONVIF API that supports this new command.
Add a function to DeviceStateModel.
- Add the new command to
Command
enum insource/edge/crates/device-traits/src/command.rs
and other struct that depends onCommand
, likeas_str
andfrom_str
. For soft factory reset:pub enum Command { /// Reboot Reboot, /// Unknown command type Unknown, /// Soft factory reset <----new command type SoftFactoryReset SoftFactoryReset } impl Command { /// converting enum to str pub fn as_str(&self) -> &'static str { match self { Command::Reboot => "REBOOT", Command::Unknown => "UNKNOWN", Command::SoftFactoryReset => "SOFT_FACTORY_RESET", <----new command type SoftFactoryReset, make sure the string defined matches between edge and cloud } } } impl FromStr for Command { type Err = (); fn from_str(input: &str) -> Result<Command, Self::Err> { match input { "REBOOT" => Ok(Command::Reboot), "SOFT_FACTORY_RESET" => Ok(Command::SoftFactoryReset), <----new command type SoftFactoryReset _ => Err(()), } } }
- Add logic in
source/edge/crates/edge-process/src/bin/main.rs
to handle the new command type. Example for soft factory reset... match command_type { ... Command::SoftFactoryReset => { info!("Trying to soft factory reset device"); let res = onvif_client.soft_factory_reset().await; if res.is_err() { error!("Error soft factory reset device: {:?}", res); update_command_status(CommandStatus::Failed, job_id_str.to_string()); } else { info!("Initiated soft factory reset"); } }, ... } ...
- Compile the binary and test it on device.
Cloud java code
- Create a name for the new command. In this case,
SOFT_FACTORY_RESET
. - For CreateJobRequest’s documentParameters field, use the operation name as the key value pair. For example:
.documentParameters(Collections.singletonMap("command", "SOFT_FACTORY_RESET"))