# Sending Remote Commands to an Edge Module
This is the second in a series of tutorials which explain how to accomplish various functions within the context of an ABB Ability™ Edge. On the ABB Ability™ Edge runtime, the proxy and broker modules provided by the Platform are responsible for handling any communication to or from the ABB Ability™ Edge environment. Thus, any code written within custom modules has built-in security and can be relatively simple.
This tutorial will build upon the first “01-send-telemetry” module and will demonstrate:
- How to create parameterized methods that can be invoked remotely
- How to invoke those methods using the ABB Ability™ Instance API
- Upon method invocation, how to send a response data back to the requestor
# Before You Start
The tutorial is written in C# using the .Net Core framework.
A basic console application will be created. Familiarity with .Net
C# is
recommended.
Once the application is compiled, Docker will be used to create and push an image to a remote container registry.
Once the image is deployed, the ABB Ability™ Edge runtime is responsible for identifying custom modules to be loaded and then pulling down the appropriate image and creating a container instance. The corresponding code for this tutorial can be found at this ABB Codebits repository under the “03-remote-commands” folder. The “RemoteCommands” project references the ABB.Ability.IotEdge.CST.Modules.CSharp.Common project, which can be found under the “common” folder in the same repository.
For this tutorial, we are using an MIT-licensed nuget package called MQTTnet, but other MQTT (Message Queuing Telemetry Transport) wrappers can be used.
# Steps
Open the
ABB.Ability.IotEdge.CSTModules.CSharp.RemoteCommands
project in Visual Studio, VS Code, or the IDE of your choice. The point of focus in this tutorial is theRemoteCommandsModule
, which is created on startup in theProgram.cs
class.In the
StartModuleAsync
method, a few things are done which should look familiar from previous tutorial modules: subscriptions to topics are created, the simulated device being used to send telemetry is created, and aManualResetEventSlim
object is used to keep the container in a running state while the code waits to receive remote commands.One thing to note in the
SubscribeToTopics
method is that in addition to the “model in” topic, a subscription is now also created for the “methods in” topic. This will ensure that any remote commands are correctly delivered as messages to the inbound queue for this module:private async Task SubscribeToTopics() { var topics = new[] { $"{ this.Configuration.ModelInTopic}/#", $"{this.Configuration.MethodsInTopic}/#" }; var topicFilters = topics.Select(e => new TopicFilter(e, MqttQualityOfServiceLevel.AtLeastOnce)); await _mqttClient.SubscribeAsync(topicFilters) .ContinueWith((e) => Console.WriteLine( $"Subscribed to topics: {string.Join(", ", topics)}")); }
In the
OnApplicationMessageReceived
method which handles all incoming messages, notice that there is now code to check for the additional “methods in” topic, in addition to “model in”. If the message’s topic starts with the module’s “methods in” topic (which is predefined by the ABB Ability™ Edge runtime), then the message will be passed to theHandleCloudToDeviceMethod
method which is defined in theModuleBase
class:else if((e.ApplicationMessage.Topic ?? string.Empty) .StartsWith(this.Configuration.MethodsInTopic, StringComparison.InvariantCultureIgnoreCase)) { HandleCloudToDeviceMethod(e.ApplicationMessage.Topic, messageBody); }
In the
HandleCloudToDeviceMethod
in ModuleBase, the first thing that is done is to determine which method is being requested, and what the request Id is so that a proper acknowledgement can be sent once the request is completed.Both pieces of information can be gleaned from the message’s topic, which follows the format
{MethodsInTopic}/{MethodName}/{RequestId}
. The code is using a Regex object defined in the class’s constructor to parse this in a reliable manner:_methodInvocationRegex = new Regex( $@"^{Regex.Escape(this.Configuration.MethodsInTopic)}" + $"/(?<{"methodName"}>.+)/(?<{"requestId"}>.+)$", RegexOptions.Compiled);
The method name and request Id are then placed in variables to be used later:
var match = _methodInvocationRegex.Match(topic); var methodName = match.Groups["methodName"].Value; var requestId = match.Groups["requestId"].Value;
Next, reflection is used to determine if there is a matching method that can be called within the current class. A custom attribute called
InvokableMethod
is also used to explicitly indicate which methods can be invoked remotely. However, there is validation that occurs at the API level which will prevent any method that is not defined in the module’s type definition from being invoked.var methodInfo = GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase); if (methodInfo == null) { Console.WriteLine($"The requested method '{methodName}'" + "was not found in the module."); return; } var attr = methodInfo.GetCustomAttribute<InvokableMethod>(); if (attr == null) { Console.WriteLine($"The requested method '{methodName}'" + "is not invokable via remote commanding."); return; }
Any parameters to be passed to the invoked method are then parsed from the payload for the message. The payload will be in JSON format, and parameters are contained within a property called input. The code finds the expected parameters for the method, and then parses the payload to deserialize into the expected parameter types. Parameter validation also occurs at the API level; any missing parameters or parameters of an invalid type will cause an error.
var parameterInfo = methodInfo.GetParameters().FirstOrDefault(); var parameters = parameterInfo != null ? new[] { JsonConvert.DeserializeObject(messageBody, parameterInfo.ParameterType) } : Array.Empty<object>();
The last steps to handle a command invocation are to send an acknowledgement response that the method has been invoked, and to call the underlying method using reflection. A response message with a successful status code (20x) must be placed on the
{MethodsOutTopic}/{HttpStatusCode}/{RequestId}
topic in order for the Data Access API call which invoked the method to return successfully. If no acknowledgement is sent, the method invocation is attempted from the API level multiple times until the original request times out.Additionally, a JSON payload can be sent with the response to include any requested data, such as the result of a query. It will be returned as part of the response body to the API call. In this instance, the data being sent back is simple - a count of the number of times a remote method has been invoked, and the time at which it was invoked:
_methodsInvoked++; var confirmTopic = $"{this.Configuration.MethodsOutTopic}/{(int)HttpStatusCode.Accepted}/{requestId}"; var payload = JsonConvert.SerializeObject(new { MethodsInvoked = _methodsInvoked, TimeInvoked = DateTime.UtcNow.ToString("o") }); _mqttClient.PublishAsync(confirmTopic, payload, MqttQualityOfServiceLevel.AtLeastOnce); Console.WriteLine($"Requested method '{methodName}' found and will be executed."); Console.Write("Confirmation published to: {confirmTopic} with payload: {payload}"); methodInfo.Invoke(this, parameters);
Ideally, the response or acknowledgement would be sent after the method is successfully invoked, but in this case the Start method will run indeterminately, so a response needs to be sent back to the API as soon as the method is found in order to prevent multiple attempts to invoke the method.
Once the API has received the response, the response message will reflect the status code and payload sent in the acknowledgement message:
{ "syncMethodResponse": { "output": { "status": 202, "payload": "{\"MethodsInvoked\":5,\"TimeInvoked\":\"2019-04-03T13:52:59.3426703Z\"}" } } }
For methods in a module to be invoked remotely, they must first be defined in the type definition for the module. The Instance API will validate this when any requests are made to the method invocation endpoint; requested methods that are not included in the type definition will return errors and no message will be sent to the module.
The methods for this module are defined in the corresponding type definition for the abb.ability.device model. In the template subfolder, open the
03-abb.ability.device.edge.modules.csharp.sample.json
file.There is a “methods” property which contains information about the methods for the module. A description for each method can be provided but is not required. For the start method, an input parameter is also defined, along with a datatype and description. For each parameter, the data type is required but a description is not:
"methods": { "start": { "description": "Start sending simulating telemetry messages", "input": { "interval": { "description": "Interval between telemetry messages", "dataType": "integer" } } }, "stop": { "description": "Stop sending simulating telemetry messages" } }
The invokable methods
start
andstop
that match the type definition are included in theModuleBase
class. Both are decorated with theInvokableMethod
attribute. Within these methods aCancellationTokenSource
object is used to control application flow. The method names are case sensitive.start
ultimately callsStartSendingTelemetryAsync
, in which the method overrides can handle any cancellation requests resulting from the invocation of thestop
method. The signature for thestart
method also contains a dynamic parameter, which will be parsed from the message payload (more explanation of that can be found in the “Running the sample program” section below).[InvokableMethod] protected void Start(dynamic parameter) { if (_cancelTelemetry == null) { _cancelTelemetry = new CancellationTokenSource(); StartSendingTelemetryAsync(parameter); } else { Console.WriteLine("Start command received, but " + "telemetry sending is already in progress."); } } [InvokableMethod] protected void Stop() { Console.WriteLine("Request to stop timeseries " + "publishing received."); //stop sending telemetry if (_cancelTelemetry != null) { _cancelTelemetry.Cancel(); _cancelTelemetry = null; } }
Returning to the
RemoteCommandsModule
class, the override of theStartSendingTelemetryAsync
method determines what happens when thestart
method is remotely invoked. In this case, the expectation is that the payload of thestart
command will contain an integer value called interval within the input object. This interval value will determine, in seconds, how long the code will wait before sending more telemetry data (previously it was hard-coded to 10 seconds). However, if no valid interval value can be parsed from the message payload, the code will default back to 10 seconds:public async override void StartSendingTelemetryAsync (dynamic parameter) { int interval = int.TryParse(parameter.interval.ToString(), out interval) ? interval : DEFAULT_TELEMETRY_INTERVAL_SECONDS;
Once the interval is determined, telemetry sending continues as it did before in an infinite loop. The only change occurs at the end of this method. Instead of waiting for 10 seconds and then continuing every time, the code now waits for the time determined by the method parameter, unless a Cancel request has been received from the CancellationToken via the Stop method being invoked. If a Cancel request has been received, a
TaskCanceledException
is thrown and the code can safely exit out of the telemetry sending loop.try { //wait to send another telemetry message, //unless the stop method has been invoked await Task.Delay(interval * 1000, _cancelTelemetry.Token); } catch (TaskCanceledException) { Console.WriteLine("Timeseries publishing stopped."); break; }
# Running the Sample Module
There are two ways to run this custom module. CST provides types for a given version of Ability Platform. If your CST contact has not already loaded these types into your Ability environment, these types can be manually uploaded to the environment. Once the types are loaded into the instance, any supported Edge setup can be used to run the Edge types provided by the tutorials.
Once you are familiar with the Edge, it is possible to build custom versions of the tutorials. Once a custom modules is created and the type(s) are loaded into an instance, any valid Edge can run the custom type.
# Verify Module Functionality
An application has been provided to simplify verification. Before proceeding, verify the InstanceApp is configured correctly and has access to your Ability Platform.
- Once the Edge starts, it should automatically pull the image for this module and start a container using this image.
- To verify that telemetry messages are being sent, the logs for this tutorial
module can be viewed by running the following command:
docker service logs -f remoteCommands
After this module starts, the log can be used to determine theobjectId
for the remoteCommands module. In the above example output, theobjectId
is4f30790c-2c04-4e04-8be4-6f170ba563a5
. For consistency, this objectId will be used in the examples below.
ability@myhost:service logs remoteCommands
remoteCommands.1.i92adxwfuasc@hveunb117 | Module started at 2020071911511256
remoteCommands.1.i92adxwfuasc@hveunb117 | Sleeping for 10000ms to allow broker to restart...
remoteCommands.1.i92adxwfuasc@hveunb117 | Is module running as a Docker container: True
remoteCommands.1.i92adxwfuasc@hveunb117 | MQTT client created and started.
remoteCommands.1.i92adxwfuasc@hveunb117 | Starting C# Remote Commands Module...
remoteCommands.1.i92adxwfuasc@hveunb117 | Subscribed to topics: modules/remoteCommands/model/desired/#, modules/remoteCommands/methods/req/#
remoteCommands.1.i92adxwfuasc@hveunb117 | Message received on topic 'modules/remoteCommands/model/desired/abb.ability.device/4f30790c-2c04-4e04-8be4-6f170ba563a5': ...
Within the ABB Ability™ Instance API, to remotely invoke commands, a POST request must be made to the endpoint located at:
/api/v1/objects/{objectId}/models/{modelId}/methods/{methodName}
. Methods can be defined to accpect JSON-formatted payload can be provided but is not required. The values for each parameter can be determined as follows:objectId
: The object Id of the custom module which contains the method. This can be found by looking at the logs for tutorialModule in the step above. Upon module startup, the model definitions for the module are retrieved on the “model in” topic. The object Id can be found in the device model for the module’s type returned on themodules/referenceModule/model/desired/abb.ability.device
topic.modelId
: The type of model on which the method is defined. As mentioned above, thestart
andstop
methods (along with the interval parameter forstart
) are defined in the type definition for this modules. Download all the tutorial types and review03-03-abb.ability.device.edge.modules.csharp.remote.commands.tutorial.json
. The model for that type isabb.ability.device
and should be the value here.methodName
: The name of the remote command that the user is invoking (in this case, eitherstart
orstop
).
When invoking the
start
method, a payload is required to include the value for the telemetry sending interval. This can be defined as the input property in a JSON object as follows:{ "input": { "interval": 5 } }
Use the commands below to
start
andstop
telemetry. After invoking each method, view the logs for the tutorial module to verify that the appropriate messages are being received and sent. Upon receiving each remote command, the module should be sending an acknowledgement response back, which will then trigger a successful response to the original REST API call to invoke the method. By following the logs, the appropriate interval between telemetry messages (as defined by the payload in thestart
method) can be confirmed.# Send the start command with an interval of 5 seconds # dotnet run -i <myEnv> -C start:4f30790c-2c04-4e04-8be4-6f170ba563a5 -J {\"input\":{\"interval\":15}} # Send the stop command # dotnet run -i <myEnv> -C stop:4f30790c-2c04-4e04-8be4-6f170ba563a5
# Next Steps
The “Configurable” module builds upon this tutorial and demonstrates how modules can be adapted based on the configuration model and can react in real-time to any changes in that model.