How to Control Downstream Device from IoT Edge

For an IoT Edge device which is deployed in manufacture scenario, it is a common requirement to control the downstream device from a module inside the edge device based on machine's status. This article is about how to control the downstream device from a module based on the below prototype.

Downstream Device

Currently, direct method is the only way to send message to downstream device from a module. In order to support direct method, we need set a callback for the direct call shown below(make sure Microsoft.Azure.Devices.Client 1.18.0 or above is referenced in project file if you are using .net sdk). You may also refer to https://github.com/Azure-Samples/azure-iot-samples-csharp/blob/master/iot-hub/Quickstarts/simulated-device-2/SimulatedDevice.cs for a more details.

 
        private static Task ResetCallback(MethodRequest methodRequest, object userContext)
        {
            var data = Encoding.UTF8.GetString(methodRequest.Data);
            Console.Write(data);
            return Task.FromResult(new MethodResponse(200));
        }
        private static void Main(string[] args)
        {
            Console.WriteLine("IoT Hub Quickstarts #1 - Simulated device. Ctrl-C to exit.\n");

            // Connect to the IoT hub using the MQTT protocol
            s_deviceClient = DeviceClient.CreateFromConnectionString(s_connectionString, TransportType.Amqp);
            s_deviceClient.SetMethodHandlerAsync("reset", ResetCallback, null).Wait();
            while (true)
            {
                // other job        
            }

            Console.ReadLine();
        }

Configure ASA Job

Suppose we have configured a streaming analysis job to analyze incoming telemetry, we can flush expected control command into output endpoint shown below, each output record will be a json text like [{"command":"reset}]. Refer to https://docs.microsoft.com/en-us/azure/iot-edge/tutorial-deploy-stream-analytics for more details about how to configure an asa job.

 
SELECT  
    'reset' AS command 
INTO 
   alert 
FROM 
   temperature TIMESTAMP BY timeCreated 
GROUP BY TumblingWindow(second,30) 
HAVING Avg(machine.temperature) > 70

Custom Module
Firstly, make sure azureiotedge-agent and azureiotedge-hub have been upgraded to 1.0.1 or above.
If you are using .net SDK to build the custom module, make sure Microsoft.Azure.Devices.Client 1.18.0 or above is referenced in project file, also please use dotnet 2.1 for the docker image shown below:

 
FROM microsoft/dotnet:2.1-sdk AS build-env
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

FROM microsoft/dotnet:2.1-runtime
WORKDIR /app
COPY --from=build-env /app/out ./

RUN useradd -ms /bin/bash moduleuser
USER moduleuser

ENTRYPOINT ["dotnet", "mycustommodule.dll"]

The PipeMessage function accepts a message argument, we can get the downstream device id by calling message.ConnectionDeviceId. Then, we can call InvokeMethodAsync shown below to send the reset command.

 
moduleClient.InvokeMethodAsync(message.ConnectionDeviceId, new MethodRequest("reset", Encoding.ASCII.GetBytes(res)));

Routing
After configuring the message routing shown below, the downstream device will be able to receive direct method call from the module.

 
{
  "routes": {
    "downstreamdevice2asa": "FROM /messages/* WHERE NOT IS_DEFINED($connectionModuleId) INTO BrokeredEndpoint(\"/modules/asajob/inputs/temperature\")",
    "asa2custommodule": "FROM /messages/modules/asajob/outputs/* INTO BrokeredEndpoint(\"/modules/custommodule/inputs/input1\")",
    "ToIoTHub": "FROM /messages/modules/custommodule/outputs/output1 INTO $upstream"
   }
}