Generating image to IoT DevKit with Azure Durable Functions

I have recently bought an IoT DevKit AZ3166, and playing with it a lot. First, it is a lot of fun. Secondly, it has a good documentation and project catalog to get started in no time. Thirdly, do not try to upload code to the device through and RDP session, that just does not work.

This post will show how I generate a graph in a Durable Function and display it on the IoT DevKit.

Device

Getting Started with IoT DevKit

It is very easy to get started. Just follow the getting started guide from the project catalog. The samples provide plenty of help and code to get on speed.

Writing C++ code for the device seems frightening (at least for me, because I have not written C++ for years), but it was a lot smoother than I expected.

Getting Started with Durable Functions

Azure Functions is one of my favorite Azure services. Mostly, because they are easy deploy, they have a simple hosting model (say compared to k8s or Service Fabric), and with Durable Functions you can do leverage a nice actor like programming model.But most of all, it is ridiculously cheap (even for an eastern European people), so anyone can host his/her services for an utterly cheap price in an "enterprise grade" cloud platform (and no, I am not working for Microsoft).

Note, yes Azure Functions could be run as part of a k8s or Service Fabric cluster, and they are independent from Azure in a sense that you could host the services in AWS or other cloud environment.

Device code

Data Collection

One of the first apps I created, sends temperature, humidity and noise levels to IoT Hub, which message is then processed by an Azure Functions. Data is sent periodically (say every minute). In this section I show how the code looks like to process a message returned from IoT Hub and to display it as an image on the device's screen.

Firstly, need to hook up a callback:

DevKitMQTTClient_SetMessageCallback(ShowHistoricalDataCallback);

Secondly, in certain state of the application it checks if there are any messages addressed to the device:

DevKitMQTTClient_Check(false);

Displaying the images

Let's see the implementation of the callback function:

static void ShowHistoricalDataCallback(const char *msg, int msgLength)
{
  if (app_status != 6 || msg == NULL)
  {
    return;
  }
  Screen.clean();
  DrawTitle("Environment");
  Screen.draw(0, 2, 128, 8, (unsigned char*)msg);
}

Ok, so the function gets a message and a length as parameteres. It validates the application being in a proper state, checks if the message is not null. It clears the screen, draws an app title and the image (with a reinterpreting cast).

It should probably validate the length of the message as well.

It draws from the 2nd row to the 7th row, and from 0th column to the 127th column. Each byte value represents 8 pixels in a column. You can actually prepare images online with this bitmap editor.

  • 0x01 means the top pixel in the column (0b0000_0001)

  • 0x02 means the 2nd pixel from the top (0b0000_0010)

  • 0x04 means the 3rd pixel from the top (0b0000_0100)...

  • 0x80 means the most bottom pixel (1000_0000)

Great, each pixel gets a bit.

Azure Function

On the Azure Function part, I will show the message processing part in separate posts. In this post, I will keep the focus on 2 things: the durable extension, and the way to generate the graph based on the temperature data.

Entity Function

At the time of writing Durable Functions 2.0 is still in preview. Entity functions are part of preview Durable Functions extension. The concept is having an entity function is that it can manage state explicitly. That means the state is 'within' the function.

Below, I have this very simple function, which gets a Measurement (containing temperature data as well) and stores it along the current state. The current size of the state is limited to a couple hundred of entries by the Add method. When it gets full, the oldest entry is overwritten.

[FunctionName(nameof(HistoricalStorageFunction))]
public static void HistoricalStorageFunction([EntityTrigger] IDurableEntityContext context)
{
 if (context.OperationName == "set")
 {
   var data = context.GetInput<Measurement>();
   HistoricalStrip currentState = GetCurrentState(context);
   var updatedState = currentState.Add(data);
   context.SetState(updatedState);
 }

 if (context.OperationName == "delete")
 {
   context.DestructOnExit();
   return;
 }
 context.Return(null);
}

Generating Temperature Graph

When the user presses Button B on the device, a 'temperature graph' request message is sent by the IoT Device. An Azure Functions responds to this with a byte[] to be displayed. This part of the post gets funky with bit manipulations to calculate an image displaying the graph. For a better understanding, I commented to the code itself:

private static byte[] ConvertData(HistoricalStrip data)
{
  List<double> temperatures = new List<double>();
  
  // Convert input parameter to list of doubles and add to temperatures, removed for brevity.
  // Also making sure to have only ScreenWidth amount of values in the list.
  // ...

  double min; // ... find the minimum value of the input data
  double scale; // ... set a scaling value

  // Create a multidimensional array representing the bitmap
  byte[,] image = new byte[ScreenRows, ScreenWidth];
  int tempIndexer = temperatures.Count;
  for (int i = 0; i < temperatures.Count; i++)
  {
    // Calculate a value scaled to screen size, the temperature values are in inverted order
    double currentValue = (temperatures[temperatures.Count - 1 - i] - min) / scale * (ScreenHeight - 1);

    // Calculate the row on the screen where the pixel should show up
    int row = (ScreenRows - ((int)currentValue / screenRowHeight)) - 1;

    // Calculate the byte value of the 8-pixel column in the row.
    // Based on the remainder the 1 in 0b1000_0000 is pushed to the write.
    int shift = (int)Math.Floor(currentValue % screenRowHeight);
    byte bitreprsentation = (byte)(128 >> shift);

    image[row, i] = bitreprsentation;
  }

  // Add a horizontal line at the bottom as the x-axis
  for (int i = 0; i < ScreenWidth; i++)
  {
    // This is an addition so, the lowest row values are not overwritten
    image[ScreenRows - 1, i] += 128;
  }

  // Add a vertical line on the left as the y-axis
  for (int i = 0; i < ScreenRows; i++)
  {
    image[i, 0] = 255;
  }

  //Convert multidimensional array to a single dimensional array
  byte[] result = new byte[ScreenHeight * ScreenWidth];
  int j = 0;
  foreach (byte b in image)
    result[j++] = b;

  return result;
}

This could be implemented using only a single dimensional array to, reducing code length and improving performance. In this post I left it as is for a better understanding.

Here is the final look on the device:

InUse