Working with IoT Edge in F#


Azure IoT Edge is a combination of multiple components which simplifies the development, deployment and operation of more complex IoT devices. IoT Edge builds upon the already successful IoT Hub to allow for management of these devices through an already familiar User Experience. Whilst the default IoT Hub experience is fine for the deployment and management of many simple IoT devices, there are occasions when we need to develop more complex systems. This has been historically true for the industrial IoT world where we have complex device ecosystems communicating through complex device protocols. But we've seen the types of IoT devices available to consumers growing in complexity. Now device manufacturers are not only competing on the scale of individual devices but are fighting to build the next home IoT ecosystem. Companies like Samsung with the SmartThings device ecosystem or Xiaomi's recent smart home offering are striving to build gateways which allow for simplified control of smart devices around the house.

Azure IoT Edge consists of 3 key components: - A device runtime which allows for running multiple distinct application modules on a device which communicate through a message bus. - A web management platform which allows for configuring the modules which are running on a device - A management platform for performing deployments of devices at scale

A module is an application written in one of a number of supported languages or technologies. Some of the supported language runtimes include .Net core and Node.JS, however, we can also deploy some Azure components to devices such as Azure Machine Learning or Azure Stream Analytics. A module can be something as simple as protocol translation to allow low power devices which might not be capable of passing the security specifications required by Azure. It could also map the destinations of messages from IoT Hub to local device identities. Alternatively it could be as complex as a machine learning module which is designed to discover trends in local device data and create alerts without data needing to first be sent to the cloud.

Creating IoT Edge Modules in F#

Since we can write modules in .Net core, this means we can write our modules using F#. In this post, we'll look at how we're able to create and deploy a new Azure IoT Edge module written in F#. I'll assume that you've already got an existing IoT Hub configured in Azure and you've deployed a module at least once before. I'll also assume that you've got the Azure IoT Edge and .Net core command line tooling installed.

The first step is to create a new .Net core application:

dotnet new console --language F#

Next add package references to the Azure Devices APIs in the .fsproj file:

    <PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.6.0-preview-001" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0-preview2-final" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.0-preview2-final" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0-preview2-final" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0-preview2-final" />
    <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0-preview2-final" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0-preview2-final" />
    <PackageReference Include="System.Runtime.Loader" Version="4.3.0" />

Essentialy all we're doing is creating a new console application which will connect to the local IoT Edge runtime. Once it's connected, we can then start to send and receive messages. The IoT Edge runtime effectively acts like a locally available version of the cloud hosted IoT Hub meaning we can communicate with it as we would any other Azure IoT service. The IoT Hub runtime is responsible for acting as the supervisor for all external components and with that provides some guidance to the modules that it runs. For example, the runtime provides it's connection string to the child processes through environment variables, ensuring that modules connect through the Edge hub rather than directly to the cloud hub. In the below example, we can see how we're able to initialise our IoT Edge Hub module.

open Microsoft.Azure.Devices.Client

let Main args =
    let connectionString = Environment.GetEnvironmentVariable("EdgeHubConnectionString")
    let deviceClient = DeviceClient.CreateFromConnectionString(connectionString)
    deviceClient.OpenAsync () |> Async.AwaitTask |> Async.RunSynchronously

Once we have a connection to the IoT Edge Hub we can start to send messages to the hub through the DeviceClient, just as we would with a regular IoT Hub connection.

open System.Text
open Microsoft.Azure.Devices.Client

let str = "Hello world!"
let bytes = Encoding.UTF8.GetBytes(str)
let msg = new Message(bytes)

We can also set up a callback for what should happen when we receive a message from the hub. This is achieved by calling the SetInputEventHandlerAsync method on the DeviceClient with a callback function. We can see below that we add a FilterMessage event handler.

let FilterMessage (msg:Message) (ctx:obj) =
    async {
        return MessageResponse.Completed
    } |> Async.StartAsTask

deviceClient.SetInputMessageHandlerAsync("input1", MessageHandler(FilterMessage), deviceClient) |> Async.AwaitTask |> Async.RunSynchronously

Advanced IoT Edge concepts

This is where ioT Edge starts to seriously differ from IoT Hub though. Rather than just receiving cloud to device messages, individual modules are able to receive messages from other modules. This gets to be particularly useful when you're looking to build real time insights on the device. We can start to write code which is able to alert other modules on the edge device or other systems to which the edge device is connected. When we think of a smart home, we can see how this can be beneficial. The ability to trigger an alarm without the need to send an event to the cloud where it's then processed before a command is sent to the device is a significant advantage in cases where it's crucial the alarm goes off in as little time as possible. IoT Edge makes it easy to wire together the individual modules to route messages where they're needed in the form of a SQL like language. In the example below, we can see how we can define a simple pipeline of modules using the format.

      "sensorToAlarm":"FROM /messages/modules/tempSensor/outputs/temperatureOutput INTO BrokeredEndpoint(\"/modules/alarmModule/inputs/input1\")",
      "alarmToIoTHub":"FROM /messages/modules/alarmModule/outputs/output1 INTO $upstream"

We can then start to route events into specific modules and upstream into the cloud hosted IoT hub. This allows us to get insights with a really low latency whilst also sending all of the data up to the IoT hub. Alternatively, we could use modules to reduce the volume of data we're sending to the IoT hub. For example, if we have a device which works across cellular or Wi-Fi connections, we can start to send smaller volumes of data when we switch to a lower bandwidth cellular connection. This allows for a really powerful programming model, especially when twinned with the management capabilities, which we haven't even touched on in this post.


Ultimately, I've been really impressed with Azure IoT Edge, it definitely simplifies the process of writing larger and more complex IoT devices. I'm really excited to see whether the Azure team brings it to more resource constrained devices, unfortunately an Edge device capable of running Docker containers is fairly heavyweight for a lot of retail products especially given that price and power usage are both key factors.

namespace Microsoft
Multiple items
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken

Full name: Microsoft.FSharp.Control.Async

type Async<'T>

Full name: Microsoft.FSharp.Control.Async<_>
static member Async.AwaitTask : task:System.Threading.Tasks.Task -> Async<unit>
static member Async.AwaitTask : task:System.Threading.Tasks.Task<'T> -> Async<'T>
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:System.Threading.CancellationToken -> 'T
namespace System
namespace System.Threading
Multiple items
type Thread =
  inherit CriticalFinalizerObject
  new : start:ThreadStart -> Thread + 3 overloads
  member Abort : unit -> unit + 1 overload
  member ApartmentState : ApartmentState with get, set
  member CurrentCulture : CultureInfo with get, set
  member CurrentUICulture : CultureInfo with get, set
  member DisableComObjectEagerCleanup : unit -> unit
  member ExecutionContext : ExecutionContext
  member GetApartmentState : unit -> ApartmentState
  member GetCompressedStack : unit -> CompressedStack
  member GetHashCode : unit -> int

Full name: System.Threading.Thread

System.Threading.Thread(start: System.Threading.ThreadStart) : unit
System.Threading.Thread(start: System.Threading.ParameterizedThreadStart) : unit
System.Threading.Thread(start: System.Threading.ThreadStart, maxStackSize: int) : unit
System.Threading.Thread(start: System.Threading.ParameterizedThreadStart, maxStackSize: int) : unit
System.Threading.Thread.Sleep(timeout: System.TimeSpan) : unit
System.Threading.Thread.Sleep(millisecondsTimeout: int) : unit
namespace System.Text
type obj = System.Object

Full name: Microsoft.FSharp.Core.obj
val async : AsyncBuilder

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
static member Async.StartAsTask : computation:Async<'T> * ?taskCreationOptions:System.Threading.Tasks.TaskCreationOptions * ?cancellationToken:System.Threading.CancellationToken -> System.Threading.Tasks.Task<'T>