Random wanderings through Microsoft Azure esp. PaaS plumbing, the IoT bits, AI on Micro controllers, AI on Edge Devices, .NET nanoFramework, .NET Core on *nix and ML.NET+ONNX
After extensive searching I found a couple of relevant blog posts but these had complex approaches and I wanted to keep the churn in the codebase I was working on to an absolute minimum.
With the different versions of the libraries involved (Late March 2019) this was what worked for me so YMMV. To provide the simplest possible example I have created a TimerTrigger which logs information via Log4Net to Azure Application Insights.
Initially the Log4Net configuration wasn’t loaded because its location is usually configured in the AssemblyInfo.cs file and .Net Core 2.x code doesn’t have one.
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: log4net.Config.XmlConfigurator]
I figured I would have to manually load the Log4Net configuration and had to look at the file system of machine running the function to figure out where the Log4Net XML configuration file was getting copied to.
The “Copy to output directory” setting is important
Then I had to get the Dependency Injection (DI) framework to build an ExecutionContext for me so I could get the FunctionAppDirectory to combine with the Log4Net config file name. I used Path.Combine which is more robust and secure than manually concatenating segments of a path together.
/*
Copyright ® 2019 March devMobile Software, All Rights Reserved
MIT License
...
*/
namespace ApplicationInsightsAzureFunctionLog4NetClient
{
using System;
using System.IO;
using System.Reflection;
using log4net;
using log4net.Config;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Azure.WebJobs;
public static class ApplicationInsightsTimer
{
[FunctionName("ApplicationInsightsTimerLog4Net")]
public static void Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, ExecutionContext executionContext)
{
ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
TelemetryConfiguration.Active.InstrumentationKey = Environment.GetEnvironmentVariable("InstrumentationKey", EnvironmentVariableTarget.Process);
var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
XmlConfigurator.Configure(logRepository, new FileInfo(Path.Combine(executionContext.FunctionAppDirectory, "log4net.config")));
log.Debug("This is a Log4Net Debug message");
log.Info("This is a Log4Net Info message");
log.Warn("This is a Log4Net Warning message");
log.Error("This is a Log4Net Error message");
log.Fatal("This is a Log4Net Fatal message");
TelemetryConfiguration.Active.TelemetryChannel.Flush();
}
}
}
Log4Net logging in Azure Application Insights
The latest code for my Azure Function Log4net to Applications Insights sample along with some samples for other logging platforms is available on GitHub.
I found that the application was taking too many photos, plus the way it was storing them in Azure storage was awkward and creating to many BlobTrigger events.
I split the Azure blob storage configuration settings into latest and historic images. This meant the trigger for the image emailer could be more selective.
public static class ImageEmailer
{
[FunctionName("ImageEmailer")]
public async static Task Run(
[BlobTrigger("current/{name}")]
Stream inputBlob,
string name,
[SendGrid(ApiKey = "")]
IAsyncCollector<SendGridMessage> messageCollector,
TraceWriter log)
{
log.Info($"C# Blob trigger function Processed blob Name:{name} Size: {inputBlob.Length} Bytes");
I also found that the positioning of the PIR sensor in relation to the camera field of view was important and required a bit of trial and error.
In this sample configuration the stored images are split into two containers one with the latest image for each device, the other container had a series of folders for each device which contained a historic timestamped pictures
Latest image for each deviceHistoric images for a device
I also added configuration settings for the digital input edge (RisingEdge vs. FallingEdge) which triggered the taking of a photo (the output of one my sensors went low when it detected motion). I also added the device MAC address as a parameter for the format configuration options as I had a couple of cloned devices with the same network name (on different physical networks) which where difficult to distinguish.
I also force azure storage file configuration to lower case to stop failures, but I have not validated the strings for other invalid characters and formatting issues.
/*
Copyright ® 2019 March devMobile Software, All Rights Reserved
MIT License
...
*/
namespace devMobile.Windows10IotCore.IoT.PhotoTimerInputTriggerAzureStorage
{
using System;
using System.IO;
using System.Diagnostics;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading;
using Microsoft.Extensions.Configuration;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Diagnostics;
using Windows.Media.Capture;
using Windows.Media.MediaProperties;
using Windows.Storage;
using Windows.System;
public sealed class StartupTask : IBackgroundTask
{
private BackgroundTaskDeferral backgroundTaskDeferral = null;
private readonly LoggingChannel logging = new LoggingChannel("devMobile Photo Timer Trigger Azure Storage demo", null, new Guid("4bd2826e-54a1-4ba9-bf63-92b73ea1ac4a"));
private const string ConfigurationFilename = "appsettings.json";
private Timer ImageUpdatetimer;
private MediaCapture mediaCapture;
private string deviceMacAddress;
private string azureStorageConnectionString;
private string azureStorageContainerNameLatestFormat;
private string azureStorageimageFilenameLatestFormat;
private string azureStorageContainerNameHistoryFormat;
private string azureStorageImageFilenameHistoryFormat;
private const string ImageFilenameLocal = "latest.jpg";
private volatile bool cameraBusy = false;
public void Run(IBackgroundTaskInstance taskInstance)
{
StorageFolder localFolder = ApplicationData.Current.LocalFolder;
int imageUpdateDueSeconds;
int imageUpdatePeriodSeconds;
this.logging.LogEvent("Application starting");
// Log the Application build, shield information etc.
LoggingFields startupInformation = new LoggingFields();
startupInformation.AddString("Timezone", TimeZoneSettings.CurrentTimeZoneDisplayName);
startupInformation.AddString("OSVersion", Environment.OSVersion.VersionString);
startupInformation.AddString("MachineName", Environment.MachineName);
// This is from the application manifest
Package package = Package.Current;
PackageId packageId = package.Id;
PackageVersion version = packageId.Version;
startupInformation.AddString("ApplicationVersion", string.Format($"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"));
// ethernet mac address
deviceMacAddress = NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.NetworkInterfaceType.ToString().ToLower().Contains("ethernet"))
.FirstOrDefault()
?.GetPhysicalAddress().ToString();
// remove unsupported charachers from MacAddress
deviceMacAddress = deviceMacAddress.Replace("-", "").Replace(" ", "").Replace(":", "");
startupInformation.AddString("MacAddress", deviceMacAddress);
try
{
// see if the configuration file is present if not copy minimal sample one from application directory
if (localFolder.TryGetItemAsync(ConfigurationFilename).AsTask().Result == null)
{
StorageFile templateConfigurationfile = Package.Current.InstalledLocation.GetFileAsync(ConfigurationFilename).AsTask().Result;
templateConfigurationfile.CopyAsync(localFolder, ConfigurationFilename).AsTask();
this.logging.LogMessage("JSON configuration file missing, templated created", LoggingLevel.Warning);
return;
}
IConfiguration configuration = new ConfigurationBuilder().AddJsonFile(Path.Combine(localFolder.Path, ConfigurationFilename), false, true).Build();
azureStorageConnectionString = configuration.GetSection("AzureStorageConnectionString").Value;
startupInformation.AddString("AzureStorageConnectionString", azureStorageConnectionString);
azureStorageContainerNameLatestFormat = configuration.GetSection("AzureContainerNameFormatLatest").Value;
startupInformation.AddString("ContainerNameLatestFormat", azureStorageContainerNameLatestFormat);
azureStorageimageFilenameLatestFormat = configuration.GetSection("AzureImageFilenameFormatLatest").Value;
startupInformation.AddString("ImageFilenameLatestFormat", azureStorageimageFilenameLatestFormat);
azureStorageContainerNameHistoryFormat = configuration.GetSection("AzureContainerNameFormatHistory").Value;
startupInformation.AddString("ContainerNameHistoryFormat", azureStorageContainerNameHistoryFormat);
azureStorageImageFilenameHistoryFormat = configuration.GetSection("AzureImageFilenameFormatHistory").Value;
startupInformation.AddString("ImageFilenameHistoryFormat", azureStorageImageFilenameHistoryFormat);
imageUpdateDueSeconds = int.Parse(configuration.GetSection("ImageUpdateDueSeconds").Value);
startupInformation.AddInt32("ImageUpdateDueSeconds", imageUpdateDueSeconds);
imageUpdatePeriodSeconds = int.Parse(configuration.GetSection("ImageUpdatePeriodSeconds").Value);
startupInformation.AddInt32("ImageUpdatePeriodSeconds", imageUpdatePeriodSeconds);
}
catch (Exception ex)
{
this.logging.LogMessage("JSON configuration file load or settings retrieval failed " + ex.Message, LoggingLevel.Error);
return;
}
try
{
mediaCapture = new MediaCapture();
mediaCapture.InitializeAsync().AsTask().Wait();
}
catch (Exception ex)
{
this.logging.LogMessage("Camera configuration failed " + ex.Message, LoggingLevel.Error);
return;
}
ImageUpdatetimer = new Timer(ImageUpdateTimerCallback, null, new TimeSpan(0,0, imageUpdateDueSeconds), new TimeSpan(0, 0, imageUpdatePeriodSeconds));
this.logging.LogEvent("Application started", startupInformation);
//enable task to continue running in background
backgroundTaskDeferral = taskInstance.GetDeferral();
}
private async void ImageUpdateTimerCallback(object state)
{
DateTime currentTime = DateTime.UtcNow;
Debug.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Timer triggered");
// Just incase - stop code being called while photo already in progress
if (cameraBusy)
{
return;
}
cameraBusy = true;
try
{
StorageFile photoFile = await KnownFolders.PicturesLibrary.CreateFileAsync(ImageFilenameLocal, CreationCollisionOption.ReplaceExisting);
ImageEncodingProperties imageProperties = ImageEncodingProperties.CreateJpeg();
await mediaCapture.CapturePhotoToStorageFileAsync(imageProperties, photoFile);
string azureContainernameLatest = string.Format(azureStorageContainerNameLatestFormat, Environment.MachineName, deviceMacAddress, currentTime).ToLower();
string azureFilenameLatest = string.Format(azureStorageimageFilenameLatestFormat, Environment.MachineName, deviceMacAddress, currentTime);
string azureContainerNameHistory = string.Format(azureStorageContainerNameHistoryFormat, Environment.MachineName, deviceMacAddress, currentTime).ToLower();
string azureFilenameHistory = string.Format(azureStorageImageFilenameHistoryFormat, Environment.MachineName.ToLower(), deviceMacAddress, currentTime);
LoggingFields imageInformation = new LoggingFields();
imageInformation.AddDateTime("TakenAtUTC", currentTime);
imageInformation.AddString("LocalFilename", photoFile.Path);
imageInformation.AddString("AzureContainerNameLatest", azureContainernameLatest);
imageInformation.AddString("AzureFilenameLatest", azureFilenameLatest);
imageInformation.AddString("AzureContainerNameHistory", azureContainerNameHistory);
imageInformation.AddString("AzureFilenameHistory", azureFilenameHistory);
this.logging.LogEvent("Saving image(s) to Azure storage", imageInformation);
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(azureStorageConnectionString);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
// Update the latest image in storage
if (!string.IsNullOrWhiteSpace(azureContainernameLatest) && !string.IsNullOrWhiteSpace(azureFilenameLatest))
{
CloudBlobContainer containerLatest = blobClient.GetContainerReference(azureContainernameLatest);
await containerLatest.CreateIfNotExistsAsync();
CloudBlockBlob blockBlobLatest = containerLatest.GetBlockBlobReference(azureFilenameLatest);
await blockBlobLatest.UploadFromFileAsync(photoFile);
this.logging.LogEvent("Image latest saved to Azure storage");
}
// Upload the historic image to storage
if (!string.IsNullOrWhiteSpace(azureContainerNameHistory) && !string.IsNullOrWhiteSpace(azureFilenameHistory))
{
CloudBlobContainer containerHistory = blobClient.GetContainerReference(azureContainerNameHistory);
await containerHistory.CreateIfNotExistsAsync();
CloudBlockBlob blockBlob = containerHistory.GetBlockBlobReference(azureFilenameHistory);
await blockBlob.UploadFromFileAsync(photoFile);
this.logging.LogEvent("Image historic saved to Azure storage");
}
}
catch (Exception ex)
{
this.logging.LogMessage("Camera photo save or upload failed " + ex.Message, LoggingLevel.Error);
}
finally
{
cameraBusy = false;
}
}
}
}
The code is still pretty short at roughly 200 lines and is all available on GitHub.
After a couple of failed attempts (due to NuGet package versioning mismatches) this was the smallest, reliable enough application I could come up with. Beware BlobTriggers are not really intended for solutions requiring high throughput and/or reliability.
/*
Copyright ® 2019 March devMobile Software, All Rights Reserved
MIT License
...
*/
namespace devMobile.Azure.Storage
{
using System.IO;
using System.Configuration;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using SendGrid.Helpers.Mail;
public static class ImageEmailer
{
[FunctionName("ImageEmailer")]
public async static Task Run(
[BlobTrigger("seeedrpibasehat190321/{name}")]
Stream inputBlob,
string name,
[SendGrid(ApiKey = "")]
IAsyncCollector<SendGridMessage> messageCollector,
TraceWriter log)
{
log.Info($"C# Blob trigger function Processed blob Name:{name} Size: {inputBlob.Length} Bytes");
SendGridMessage message = new SendGridMessage();
message.AddTo(new EmailAddress(ConfigurationManager.AppSettings["EmailAddressTo"]));
message.From = new EmailAddress(ConfigurationManager.AppSettings["EmailAddressFrom"]);
message.SetSubject("RPI Web camera Image attached");
message.AddContent("text/plain", $"{name} {inputBlob.Length} bytes" );
await message.AddAttachmentAsync(name, inputBlob, "image/jpeg");
await messageCollector.AddAsync(message);
}
}
}
Blob container and naming issues
This application highlighted a number of issues with my Windows 10 IoT Core client. They were
Configurable minimum period between images as PIR sensor would trigger multiple times as someone moved across my office.
Configurable Azure Blob Storage container for latest image as my BlobTrigger fired twice (for latest and timestamped images).
Configurable Azure Blob Storage container for image history as my BlobTrigger fired twice (for latest and timestamped images).
Include a unique device identifier (possibly MAC address) with image as I had two machines with the same device name on different networks.
Additional Blob metadata would be useful.
Additional logging would be useful for diagnosing problems.
I’ll look fix these issues in my next couple of posts
While working on this code I realised I had made some invalid assumptions about the stream and the image properties so I refactored the code (which also made it simpler).
The Windows 10 IoT Core application has support for a JSON configuration file using Microsoft.Extensions.Configuration namespace functionality which took a bit of trial and error to get going.
This gets the configuration subsystem to use the specified file in the application’s localstate folder. If there is no configuration file present i.e. the application has just been deployed for the first time or installed a template file is copied from the application install directory.
In the application configuration file you can specify the azure storage connection string, digital input port number, azure container name format (formatted machine name + Universal Coordinated Time(UTC)), the azure storage file name (formatted machine name + UTC) and the name of the file with the most recently uploaded image. These configuration settings are provided so that the image files can stored in “buckets” best suited to the way they are going to be processed.
In my testing the pictures were stored in folders for each device/day and each image file had a timestamp in its name.
Azure Storage Explorer
/*
Copyright ® 2019 March devMobile Software, All Rights Reserved
MIT License
...
*/
namespace devMobile.Windows10IotCore.IoT.PhotoDigitalInputTriggerAzureStorage
{
using System;
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Background;
using Windows.Devices.Gpio;
using Windows.Foundation.Diagnostics;
using Windows.Media.Capture;
using Windows.Media.MediaProperties;
using Windows.Storage;
using Windows.System;
public sealed class StartupTask : IBackgroundTask
{
private BackgroundTaskDeferral backgroundTaskDeferral = null;
private readonly LoggingChannel logging = new LoggingChannel("devMobile Photo Digital Input Trigger Azure Storage demo", null, new Guid("4bd2826e-54a1-4ba9-bf63-92b73ea1ac4a"));
private const string ConfigurationFilename = "appsettings.json";
private GpioPin interruptGpioPin = null;
private int interruptPinNumber;
private MediaCapture mediaCapture;
private string azureStorageConnectionString;
private string azureStorageContainerNameFormat;
private string azureStorageimageFilenameLatest;
private string azureStorageImageFilenameFormat;
private const string ImageFilenameLocal = "latest.jpg";
private volatile bool cameraBusy = false;
public void Run(IBackgroundTaskInstance taskInstance)
{
StorageFolder localFolder = ApplicationData.Current.LocalFolder;
this.logging.LogEvent("Application starting");
// Log the Application build, shield information etc.
LoggingFields startupInformation = new LoggingFields();
startupInformation.AddString("Timezone", TimeZoneSettings.CurrentTimeZoneDisplayName);
startupInformation.AddString("OSVersion", Environment.OSVersion.VersionString);
startupInformation.AddString("MachineName", Environment.MachineName);
// This is from the application manifest
Package package = Package.Current;
PackageId packageId = package.Id;
PackageVersion version = packageId.Version;
startupInformation.AddString("ApplicationVersion", string.Format($"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"));
try
{
// see if the configuration file is present if not copy minimal sample one from application directory
if (localFolder.TryGetItemAsync(ConfigurationFilename).AsTask().Result == null)
{
StorageFile templateConfigurationfile = Package.Current.InstalledLocation.GetFileAsync(ConfigurationFilename).AsTask().Result;
templateConfigurationfile.CopyAsync(localFolder, ConfigurationFilename).AsTask();
this.logging.LogMessage("JSON configuration file missing, templated created", LoggingLevel.Warning);
return;
}
IConfiguration configuration = new ConfigurationBuilder().AddJsonFile(localFolder.Path + @"\" + ConfigurationFilename, false, true).Build();
azureStorageConnectionString = configuration.GetSection("AzureStorageConnectionString").Value;
startupInformation.AddString("AzureStorageConnectionString", azureStorageConnectionString);
azureStorageContainerNameFormat = configuration.GetSection("AzureContainerNameFormat").Value;
startupInformation.AddString("ContainerNameFormat", azureStorageContainerNameFormat);
azureStorageImageFilenameFormat = configuration.GetSection("AzureImageFilenameFormat").Value;
startupInformation.AddString("ImageFilenameFormat", azureStorageImageFilenameFormat);
azureStorageimageFilenameLatest = configuration.GetSection("AzureImageFilenameLatest").Value;
startupInformation.AddString("ImageFilenameLatest", azureStorageimageFilenameLatest);
interruptPinNumber = int.Parse( configuration.GetSection("InterruptPinNumber").Value);
startupInformation.AddInt32("Interrupt pin", interruptPinNumber);
}
catch (Exception ex)
{
this.logging.LogMessage("JSON configuration file load or settings retrieval failed " + ex.Message, LoggingLevel.Error);
return;
}
try
{
mediaCapture = new MediaCapture();
mediaCapture.InitializeAsync().AsTask().Wait();
}
catch (Exception ex)
{
this.logging.LogMessage("Camera configuration failed " + ex.Message, LoggingLevel.Error);
return;
}
try
{
GpioController gpioController = GpioController.GetDefault();
interruptGpioPin = gpioController.OpenPin(interruptPinNumber);
interruptGpioPin.SetDriveMode(GpioPinDriveMode.InputPullUp);
interruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;
}
catch (Exception ex)
{
this.logging.LogMessage("Digital input configuration failed " + ex.Message, LoggingLevel.Error);
return;
}
this.logging.LogEvent("Application started", startupInformation);
//enable task to continue running in background
backgroundTaskDeferral = taskInstance.GetDeferral();
}
private async void InterruptGpioPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
{
DateTime currentTime = DateTime.UtcNow;
Debug.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Digital Input Interrupt {sender.PinNumber} triggered {args.Edge}");
if (args.Edge == GpioPinEdge.RisingEdge)
{
return;
}
// Just incase - stop code being called while photo already in progress
if (cameraBusy)
{
return;
}
cameraBusy = true;
try
{
StorageFile photoFile = await KnownFolders.PicturesLibrary.CreateFileAsync(ImageFilenameLocal, CreationCollisionOption.ReplaceExisting);
ImageEncodingProperties imageProperties = ImageEncodingProperties.CreateJpeg();
await mediaCapture.CapturePhotoToStorageFileAsync(imageProperties, photoFile);
string azureContainername = string.Format(azureStorageContainerNameFormat, Environment.MachineName.ToLower(), currentTime);
string azureStoragefilename = string.Format(azureStorageImageFilenameFormat, Environment.MachineName.ToLower(), currentTime);
LoggingFields imageInformation = new LoggingFields();
imageInformation.AddDateTime("TakenAtUTC", currentTime);
imageInformation.AddString("LocalFilename", photoFile.Path);
imageInformation.AddString("AzureContainerName", azureContainername);
imageInformation.AddString("AzureStorageFilename", azureStoragefilename);
imageInformation.AddString("AzureStorageFilenameLatest", azureStorageimageFilenameLatest);
this.logging.LogEvent("Image saving to Azure storage", imageInformation);
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(azureStorageConnectionString);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference(azureContainername);
await container.CreateIfNotExistsAsync();
CloudBlockBlob blockBlob = container.GetBlockBlobReference(azureStoragefilename);
await blockBlob.UploadFromFileAsync(photoFile);
blockBlob = container.GetBlockBlobReference(azureStorageimageFilenameLatest);
await blockBlob.UploadFromFileAsync(photoFile);
this.logging.LogEvent("Image saved to Azure storage");
}
catch (Exception ex)
{
this.logging.LogMessage("Camera photo save or upload failed " + ex.Message, LoggingLevel.Error);
}
finally
{
cameraBusy = false;
}
}
}
}
I need to add some code to ensure there is a minimum gap between photos and trial some different sensors. For example, an Adjustable Infrared Switch has proved to be a better option for some of my projects.
The code is available on GitHub and is a bit of a work in progress.
One of my students wanted to do some image processing so to help her project along I am writing a series posts about capturing images on a Windows 10 IoT Core device. I’ll cover initiating the capturing of an image, uploading the image too Azure Blob storage, uploading the image to Azure blob storage associated with an Azure IoT Hub, then processing the images with the Azure Custom Vision Service.
USB Camera test rig
First step was to capture an image from a USB web camera and store it in the local file system.
/*
Copyright ® 2019 Feb devMobile Software, All Rights Reserved
MIT License
…
*/
namespace devMobile.Windows10IotCore.IoT.PhotoTimer
{
using System;
using System.Threading;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Diagnostics;
using Windows.Media.Capture;
using Windows.Media.MediaProperties;
using Windows.Storage;
public sealed class StartupTask : IBackgroundTask
{
private readonly LoggingChannel logging = new LoggingChannel("devMobile Timer Photo demo", null, new Guid("4bd2826e-54a1-4ba9-bf63-92b73ea1ac4a"));
private BackgroundTaskDeferral backgroundTaskDeferral = null;
private Timer ImageUpdatetimer;
private readonly TimeSpan ImageUpdateDueDefault = new TimeSpan(0, 0, 15);
private readonly TimeSpan ImageUpdatePeriodDefault = new TimeSpan(0, 5, 0);
private MediaCapture mediaCapture;
private const string ImageFilenameFormat = "Image{0:yyMMddhhmmss}.jpg";
public void Run(IBackgroundTaskInstance taskInstance)
{
LoggingFields startupInformation = new LoggingFields();
this.logging.LogEvent("Application starting");
try
{
mediaCapture = new MediaCapture();
mediaCapture.InitializeAsync().AsTask().Wait();
ImageUpdatetimer = new Timer(ImageUpdateTimerCallback, null, ImageUpdateDueDefault, ImageUpdatePeriodDefault);
}
catch (Exception ex)
{
this.logging.LogMessage("Camera configuration failed " + ex.Message, LoggingLevel.Error);
return;
}
startupInformation.AddString("PrimaryUse", mediaCapture.VideoDeviceController.PrimaryUse.ToString());
startupInformation.AddTimeSpan("Due", ImageUpdateDueDefault);
startupInformation.AddTimeSpan("Period", ImageUpdatePeriodDefault);
this.logging.LogEvent("Application started", startupInformation);
//enable task to continue running in background
backgroundTaskDeferral = taskInstance.GetDeferral();
}
private void ImageUpdateTimerCallback(object state)
{
DateTime currentTime = DateTime.UtcNow;
try
{
string filename = string.Format(ImageFilenameFormat, currentTime);
IStorageFile photoFile = KnownFolders.PicturesLibrary.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting).AsTask().Result;
ImageEncodingProperties imageProperties = ImageEncodingProperties.CreateJpeg();
mediaCapture.CapturePhotoToStorageFileAsync(imageProperties, photoFile).AsTask().Wait();
LoggingFields imageInformation = new LoggingFields();
imageInformation.AddDateTime("TakenAtUTC", currentTime);
imageInformation.AddString("Filename", filename);
imageInformation.AddString("Path", photoFile.Path);
this.logging.LogEvent("Image saved to storage", imageInformation);
}
catch (Exception ex)
{
this.logging.LogMessage("Image capture or save to local storage failed " + ex.Message, LoggingLevel.Error);
}
}
}
}
To get my camera to work I had to enable “pictures library”, “microphone” and “webcam” in the capabilities section of the application manifest.
As the application starts up and captures images it logs information to the Windows 10 IoT Core ETW logging
Windows 10 IoT Core Portal ETW LoggingCamera images in \user folders\pictures
The code is available on GitHub and is a bit of a work in progress.
In an early version of the software I used to provide a sample configuration JSON file in the associated GitHub repository. Users had to download this file to a computer, update it with their Azure IOT Hub or Azure IoT Central connection string or AdafruitIO APIKey , frequency and device address, then upload to the field gateway.
In a later version of the software I added code which created an empty configuration file with defaults for all settings, many of which were a distraction as the majority of users would never change them.
More settings meant there was more scope for users to change settings which broke the device samples and the gateway.
I have removed the code to generate the full configuration file (starting with Azure IOT Hub field gateway) and included a sample configuration file with the minimum required settings in the GitHub repositories and installers.
I am assuming that if a user wants to change advanced settings they can look at the code and/or documentation and figure out the setting names and valid values.
The new sample configuration file for a Azure IoT Hub telemetry only gateway is
Now that the device is running well, I’ll look at reducing power consumption and splitting the the payload packing code into a library. Also noticed an extra “,” on the end of a message so need to come up with a better way of doing the payload packing.
/*
Copyright ® 2018 December devMobile Software, All Rights Reserved
THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
PURPOSE.
You can do what you want with this code, acknowledgment would be nice.
http://www.devmobile.co.nz
*/
#include <stdlib.h>
#include <LoRa.h>
#include <avr/dtostrf.h>
#include "DHT.h"
#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA
// LoRa field gateway configuration (these settings must match your field gateway)
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const char DeviceAddress[] = {"SparkFunX1"};
const float FieldGatewayFrequency = 915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;
// Payload configuration
const int InterruptPin = 12;
const int ChipSelectPin = 6;
// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 10 ;
const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;
#define DHTPIN 4 // what digital pin we're connected to
// Uncomment whatever type you're using!
//#define DHTTYPE DHT11 // DHT 11
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321
//#define DHTTYPE DHT21 // DHT 21 (AM2301)
DHT dht(DHTPIN, DHTTYPE);
void setup()
{
SerialUSB.begin(9600);
#ifdef DEBUG
while (!SerialUSB);
#endif
SerialUSB.println("Setup called");
SerialUSB.println("LoRa setup start");
// override the default chip select and reset pins
LoRa.setPins(InterruptPin, ChipSelectPin);
if (!LoRa.begin(FieldGatewayFrequency))
{
SerialUSB.println("LoRa begin failed");
while (true); // Drop into endless loop requiring restart
}
// Need to do this so field gateways pays attention to messsages from this device
LoRa.enableCrc();
LoRa.setSyncWord(FieldGatewaySyncWord);
#ifdef DEBUG_LORA
LoRa.dumpRegisters(SerialUSB);
#endif
SerialUSB.println("LoRa Setup done.");
// Configure the Seeedstudio TH02 temperature & humidity sensor
SerialUSB.println("DHT setup start");
dht.begin();
delay(100);
SerialUSB.println("DHT setup done");
PayloadHeader((byte*)FieldGatewayAddress,strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));
SerialUSB.println("Setup done");
SerialUSB.println();
}
void loop()
{
float temperature = 17.2;
float humidity = 75.0;
SerialUSB.println("Loop called");
PayloadReset();
// Read the temperature & humidity & battery voltage values then display nicely
temperature = dht.readTemperature();
humidity = dht.readHumidity();
if (isnan(humidity) || isnan(temperature))
{
SerialUSB.println("Failed to read from DHT sensor!");
return;
}
SerialUSB.print("T:");
SerialUSB.print( temperature, 1 ) ;
SerialUSB.println( "C " ) ;
PayloadAdd( "T", temperature, 1);
SerialUSB.print("H:");
SerialUSB.print( humidity, 0 ) ;
SerialUSB.println( "% " ) ;
PayloadAdd( "H", humidity, 0) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println();
SerialUSB.print( "RFM9X/SX127X Payload length:");
SerialUSB.print( payloadLength );
SerialUSB.println( " bytes" );
#endif
LoRa.beginPacket();
LoRa.write( payload, payloadLength );
LoRa.endPacket();
SerialUSB.println("Loop done");
SerialUSB.println();
delay(LoopSleepDelaySeconds * 1000l);
}
void PayloadHeader( byte *to, byte toAddressLength, byte *from, byte fromAddressLength)
{
byte addressesLength = toAddressLength + fromAddressLength ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadHeader- ");
SerialUSB.print( "To Address len:");
SerialUSB.print( toAddressLength );
SerialUSB.print( " From Address len:");
SerialUSB.print( fromAddressLength );
SerialUSB.print( " Addresses length:");
SerialUSB.print( addressesLength );
SerialUSB.println( );
#endif
payloadLength = 0 ;
// prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
payloadLength += 1;
// Copy the "To" address into payload
memcpy(&payload[payloadLength], to, toAddressLength);
payloadLength += toAddressLength ;
// Copy the "From" into payload
memcpy(&payload[payloadLength], from, fromAddressLength);
payloadLength += fromAddressLength ;
}
void PayloadAdd( char *sensorId, float value, byte decimalPlaces)
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadAdd-float ");
SerialUSB.print( "SensorId:");
SerialUSB.print( sensorId );
SerialUSB.print( " sensorIdLen:");
SerialUSB.print( sensorIdLength );
SerialUSB.print( " Value:");
SerialUSB.print( value, decimalPlaces );
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength]));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
SerialUSB.println( );
#endif
}
void PayloadAdd( char *sensorId, int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadAdd-int ");
SerialUSB.print( "SensorId:");
SerialUSB.print( sensorId );
SerialUSB.print( " sensorIdLen:");
SerialUSB.print( sensorIdLength );
SerialUSB.print( " Value:");
SerialUSB.print( value );
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( itoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
SerialUSB.println( );
#endif
}
void PayloadAdd( char *sensorId, unsigned int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadAdd-unsigned int ");
SerialUSB.print( "SensorId:");
SerialUSB.print( sensorId );
SerialUSB.print( " sensorIdLen:");
SerialUSB.print( sensorIdLength );
SerialUSB.print( " Value:");
SerialUSB.print( value );
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( utoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
SerialUSB.println( );
#endif
}
void PayloadReset()
{
byte fromAddressLength = payload[0] & 0xf ;
byte toAddressLength = payload[0] >> 4 ;
byte addressesLength = toAddressLength + fromAddressLength ;
payloadLength = addressesLength + 1;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadReset- ");
SerialUSB.print( "To Address len:");
SerialUSB.print( toAddressLength );
SerialUSB.print( " From Address len:");
SerialUSB.print( fromAddressLength );
SerialUSB.print( " Addresses length:");
SerialUSB.print( addressesLength );
SerialUSB.println( );
#endif
}
Last week a package arrived from LowPowerLab with some Moteino0 devices and accessories . With this gear I have built yet another client for my Azure IoT Hub and AdaFruit.IOLoRa Field Gateways.
It took me a while longer that usual to get the Motenio working as the sketch setup call appeared to hang in DEBUG builds.
After staring at the code for a while I noticed that I hadn’t changed LoRa.dumpRegisters method parameter from Serial to SerialUSB. A couple of hours lost due to a dumb typo by me.
Now that the device is running well, I’ll look at reducing power consumption and splitting the the payload packing code into a library.
/*
Copyright ® 2018 November devMobile Software, All Rights Reserved
THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
PURPOSE.
You can do what you want with this code, acknowledgment would be nice.
http://www.devmobile.co.nz
*/
#include <stdlib.h>
#include <avr/dtostrf.h>
#include <LoRa.h>
#include <TH02_dev.h>
//#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA
// LoRa field gateway configuration (these settings must match your field gateway)
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const char DeviceAddress[] = {"Moteino01"};
const float FieldGatewayFrequency = 915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;
// Payload configuration
const int ChipSelectPin = A2;
const int InterruptPin = 9;
const int ResetPin = -1;
// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 10 ;
const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;
void setup()
{
SerialUSB.begin(9600);
#ifdef DEBUG
while (!SerialUSB);
#endif
SerialUSB.println("Setup called");
SerialUSB.println("LoRa setup start");
// override the default chip select and reset pins
LoRa.setPins( ChipSelectPin, ResetPin, InterruptPin );
if (!LoRa.begin(FieldGatewayFrequency))
{
SerialUSB.println("LoRa begin failed");
while (true); // Drop into endless loop requiring restart
}
// Need to do this so field gateways pays attention to messsages from this device
LoRa.enableCrc();
LoRa.setSyncWord(FieldGatewaySyncWord);
#ifdef DEBUG_LORA
LoRa.dumpRegisters(USBSerial);
#endif
SerialUSB.println("LoRa Setup done.");
// Configure the Seeedstudio TH02 temperature & humidity sensor
SerialUSB.println("TH02 setup start");
TH02.begin();
delay(100);
SerialUSB.println("TH02 setup done");
PayloadHeader((byte*)FieldGatewayAddress,strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));
SerialUSB.println("Setup done");
SerialUSB.println();
}
void loop()
{
float temperature ;
float humidity ;
SerialUSB.println("Loop called");
PayloadReset();
// Read the temperature & humidity & battery voltage values then display nicely
temperature = TH02.ReadTemperature();
SerialUSB.print("T:");
SerialUSB.print( temperature, 1 ) ;
SerialUSB.println( "C " ) ;
PayloadAdd( "T", temperature, 1);
humidity = TH02.ReadHumidity();
SerialUSB.print("H:");
SerialUSB.print( humidity, 0 ) ;
SerialUSB.println( "% " ) ;
PayloadAdd( "H", humidity, 0) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println();
SerialUSB.print( "RFM9X/SX127X Payload length:");
SerialUSB.print( payloadLength );
SerialUSB.println( " bytes" );
#endif
LoRa.beginPacket();
LoRa.write( payload, payloadLength );
LoRa.endPacket();
SerialUSB.println("Loop done");
SerialUSB.println();
delay(LoopSleepDelaySeconds * 1000l);
}
void PayloadHeader( byte *to, byte toAddressLength, byte *from, byte fromAddressLength)
{
byte addressesLength = toAddressLength + fromAddressLength ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadHeader- ");
SerialUSB.print( "To Address len:");
SerialUSB.print( toAddressLength );
SerialUSB.print( " From Address len:");
SerialUSB.print( fromAddressLength );
SerialUSB.print( " Addresses length:");
SerialUSB.print( addressesLength );
SerialUSB.println( );
#endif
payloadLength = 0 ;
// prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
payloadLength += 1;
// Copy the "To" address into payload
memcpy(&payload[payloadLength], to, toAddressLength);
payloadLength += toAddressLength ;
// Copy the "From" into payload
memcpy(&payload[payloadLength], from, fromAddressLength);
payloadLength += fromAddressLength ;
}
void PayloadAdd( const char *sensorId, float value, byte decimalPlaces)
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadAdd-float ");
SerialUSB.print( "SensorId:");
SerialUSB.print( sensorId );
SerialUSB.print( " sensorIdLen:");
SerialUSB.print( sensorIdLength );
SerialUSB.print( " Value:");
SerialUSB.print( value, decimalPlaces );
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength]));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
SerialUSB.println( );
#endif
}
void PayloadAdd( const char *sensorId, int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadAdd-int ");
SerialUSB.print( "SensorId:");
SerialUSB.print( sensorId );
SerialUSB.print( " sensorIdLen:");
SerialUSB.print( sensorIdLength );
SerialUSB.print( " Value:");
SerialUSB.print( value );
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( itoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
SerialUSB.println( );
#endif
}
void PayloadAdd( const char *sensorId, unsigned int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadAdd-unsigned int ");
SerialUSB.print( "SensorId:");
SerialUSB.print( sensorId );
SerialUSB.print( " sensorIdLen:");
SerialUSB.print( sensorIdLength );
SerialUSB.print( " Value:");
SerialUSB.print( value );
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( utoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
SerialUSB.print( " payloadLength:");
SerialUSB.print( payloadLength);
SerialUSB.println( );
#endif
}
void PayloadReset()
{
byte fromAddressLength = payload[0] & 0xf ;
byte toAddressLength = payload[0] >> 4 ;
byte addressesLength = toAddressLength + fromAddressLength ;
payloadLength = addressesLength + 1;
#ifdef DEBUG_TELEMETRY
SerialUSB.println("PayloadReset- ");
SerialUSB.print( "To Address len:");
SerialUSB.print( toAddressLength );
SerialUSB.print( " From Address len:");
SerialUSB.print( fromAddressLength );
SerialUSB.print( " Addresses length:");
SerialUSB.print( addressesLength );
SerialUSB.println( );
#endif
}
For my application I directly access the on-board Semtech SX127X chip by passing the Murata CMWX1ZZABZ functionality. To do this I (November 2018) I had to upgrade the device firmware using the Arduino updater.
Arduino MKR1300 WAN device with Grove Shield & patch antenna
The application is a modified version of my Arduino code with additional debugging support and payload formatting functionality.
/*
Copyright ® 2018 November devMobile Software, All Rights Reserved
THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
PURPOSE.
You can do what you want with this code, acknowledgment would be nice.
http://www.devmobile.co.nz
*/
#include <stdlib.h>
#include <avr/dtostrf.h>
#include <LoRa.h>
#include <TH02_dev.h>
#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA
// LoRa field gateway configuration (these settings must match your field gateway)
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const char DeviceAddress[] = {"MKR1300LoRa1"};
const float FieldGatewayFrequency = 915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;
// Payload configuration
const int InterruptPin = LORA_IRQ_DUMB;
const int ChipSelectPin = 6;
const int ResetPin = 1;
// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 60 ;
const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;
void setup()
{
Serial.begin(9600);
#ifdef DEBUG
while (!Serial);
#endif
Serial.println("Setup called");
Serial.println("LoRa setup start");
// override the default chip select and reset pins
LoRa.setPins(InterruptPin, ChipSelectPin, ResetPin);
if (!LoRa.begin(FieldGatewayFrequency))
{
Serial.println("LoRa begin failed");
while (true); // Drop into endless loop requiring restart
}
// Need to do this so field gateways pays attention to messsages from this device
LoRa.enableCrc();
LoRa.setSyncWord(FieldGatewaySyncWord);
#ifdef DEBUG_LORA
LoRa.dumpRegisters(Serial);
#endif
Serial.println("LoRa Setup done.");
// Configure the Seeedstudio TH02 temperature & humidity sensor
Serial.println("TH02 setup start");
TH02.begin();
delay(100);
Serial.println("TH02 setup done");
PayloadHeader((byte*)FieldGatewayAddress,strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));
Serial.println("Setup done");
Serial.println();
}
void loop()
{
float temperature ;
float humidity ;
Serial.println("Loop called");
PayloadReset();
// Read the temperature & humidity & battery voltage values then display nicely
temperature = TH02.ReadTemperature();
Serial.print("T:");
Serial.print( temperature, 1 ) ;
Serial.println( "C " ) ;
PayloadAdd( "T", temperature, 1);
humidity = TH02.ReadHumidity();
Serial.print("H:");
Serial.print( humidity, 0 ) ;
Serial.println( "% " ) ;
PayloadAdd( "H", humidity, 0) ;
#ifdef DEBUG_TELEMETRY
Serial.println();
Serial.print( "RFM9X/SX127X Payload length:");
Serial.print( payloadLength );
Serial.println( " bytes" );
#endif
LoRa.beginPacket();
LoRa.write( payload, payloadLength );
LoRa.endPacket();
Serial.println("Loop done");
Serial.println();
delay(LoopSleepDelaySeconds * 1000l);
}
void PayloadHeader( byte *to, byte toAddressLength, byte *from, byte fromAddressLength)
{
byte addressesLength = toAddressLength + fromAddressLength ;
#ifdef DEBUG_TELEMETRY
Serial.println("PayloadHeader- ");
Serial.print( "To Address len:");
Serial.print( toAddressLength );
Serial.print( " From Address len:");
Serial.print( fromAddressLength );
Serial.print( " Addresses length:");
Serial.print( addressesLength );
Serial.println( );
#endif
payloadLength = 0 ;
// prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
payloadLength += 1;
// Copy the "To" address into payload
memcpy(&payload[payloadLength], to, toAddressLength);
payloadLength += toAddressLength ;
// Copy the "From" into payload
memcpy(&payload[payloadLength], from, fromAddressLength);
payloadLength += fromAddressLength ;
}
void PayloadAdd( char *sensorId, float value, byte decimalPlaces)
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
Serial.println("PayloadAdd-float ");
Serial.print( "SensorId:");
Serial.print( sensorId );
Serial.print( " sensorIdLen:");
Serial.print( sensorIdLength );
Serial.print( " Value:");
Serial.print( value, decimalPlaces );
Serial.print( " payloadLength:");
Serial.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength]));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLength:");
Serial.print( payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( char *sensorId, int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
Serial.println("PayloadAdd-int ");
Serial.print( "SensorId:");
Serial.print( sensorId );
Serial.print( " sensorIdLen:");
Serial.print( sensorIdLength );
Serial.print( " Value:");
Serial.print( value );
Serial.print( " payloadLength:");
Serial.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( itoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLength:");
Serial.print( payloadLength);
Serial.println( );
#endif
}
void PayloadAdd( char *sensorId, unsigned int value )
{
byte sensorIdLength = strlen( sensorId ) ;
#ifdef DEBUG_TELEMETRY
Serial.println("PayloadAdd-unsigned int ");
Serial.print( "SensorId:");
Serial.print( sensorId );
Serial.print( " sensorIdLen:");
Serial.print( sensorIdLength );
Serial.print( " Value:");
Serial.print( value );
Serial.print( " payloadLength:");
Serial.print( payloadLength);
#endif
memcpy( &payload[payloadLength], sensorId, sensorIdLength) ;
payloadLength += sensorIdLength ;
payload[ payloadLength] = SensorIdValueSeperator;
payloadLength += 1 ;
payloadLength += strlen( utoa( value,(char *)&payload[payloadLength],10));
payload[ payloadLength] = SensorReadingSeperator;
payloadLength += 1 ;
#ifdef DEBUG_TELEMETRY
Serial.print( " payloadLength:");
Serial.print( payloadLength);
Serial.println( );
#endif
}
void PayloadReset()
{
byte fromAddressLength = payload[0] & 0xf ;
byte toAddressLength = payload[0] >> 4 ;
byte addressesLength = toAddressLength + fromAddressLength ;
payloadLength = addressesLength + 1;
#ifdef DEBUG_TELEMETRY
Serial.println("PayloadReset- ");
Serial.print( "To Address len:");
Serial.print( toAddressLength );
Serial.print( " From Address len:");
Serial.print( fromAddressLength );
Serial.print( " Addresses length:");
Serial.print( addressesLength );
Serial.println( );
#endif
}
After updating the firmware configuring the data to display in Azure IoT Central (or AdaFruit.IO) took minimal time.
The unique identifier provided by the SHA204A crypto and authentication chip on the EasySensors shield highlighted this issue. The Binary Coded Decimal(BCD) version of the 72 bit identifier was too long to fit in the from address.
My Arduino MKR1300 sample code has some helper functions to populate the message header, add values, and prepare the message payload for reuse.
On the server side I have added code to log the build version and Raspbery PI shield type
// Log the Application build, shield information etc.
LoggingFields appllicationBuildInformation = new LoggingFields();
#if DRAGINO
appllicationBuildInformation.AddString("Shield", "DraginoLoRaGPSHat");
#endif
...
#if UPUTRONICS_RPIPLUS_CS1
appllicationBuildInformation.AddString("Shield", "UputronicsPiPlusLoRaExpansionBoardCS1");
#endif
appllicationBuildInformation.AddString("Timezone", TimeZoneSettings.CurrentTimeZoneDisplayName);
appllicationBuildInformation.AddString("OSVersion", Environment.OSVersion.VersionString);
appllicationBuildInformation.AddString("MachineName", Environment.MachineName);
// This is from the application manifest
Package package = Package.Current;
PackageId packageId = package.Id;
PackageVersion version = packageId.Version;
appllicationBuildInformation.AddString("ApplicationVersion", string.Format($"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"));
this.logging.LogEvent("Application starting", appllicationBuildInformation, LoggingLevel.Information);
Then when the message payload is populated the from address byte array is converted to BCD
private async void Rfm9XDevice_OnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
{
string addressBcdText;
string messageBcdText;
string messageText = "";
char[] sensorReadingSeparators = new char[] { ',' };
char[] sensorIdAndValueSeparators = new char[] { ' ' };
addressBcdText = BitConverter.ToString(e.Address);
messageBcdText = BitConverter.ToString(e.Data);
try
{
messageText = UTF8Encoding.UTF8.GetString(e.Data);
}
catch (Exception)
{
this.logging.LogMessage("Failure converting payload to text",
LoggingLevel.Error);
return;
}
#if DEBUG
Debug.WriteLine(@"{0:HH:mm:ss}-RX From {1} PacketSnr {2:0.0} Packet
RSSI {3}dBm RSSI {4}dBm = {5} byte message ""{6}""", DateTime.Now,
messageBcdText, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length,
messageText);
#endif
LoggingFields messagePayload = new LoggingFields();
messagePayload.AddInt32("AddressLength", e.Address.Length);
messagePayload.AddString("Address-BCD", addressBcdText);
messagePayload.AddInt32("Message-Length", e.Data.Length);
messagePayload.AddString("Message-BCD", messageBcdText);
messagePayload.AddString("Message-Unicode", messageText);
messagePayload.AddDouble("Packet SNR", e.PacketSnr);
messagePayload.AddInt32("Packet RSSI", e.PacketRssi);
messagePayload.AddInt32("RSSI", e.Rssi);
this.logging.LogEvent("Message Data", messagePayload, LoggingLevel.Verbose);
//...
JObject telemetryDataPoint = new JObject(); // This could be simplified but for field gateway will use this style
LoggingFields sensorData = new LoggingFields();
telemetryDataPoint.Add("DeviceID", addressBcdText);
sensorData.AddString("DeviceID", addressBcdText);
telemetryDataPoint.Add("PacketSNR", e.PacketSnr.ToString("F1"));
sensorData.AddString("PacketSNR", e.PacketSnr.ToString("F1"));
telemetryDataPoint.Add("PacketRSSI", e.PacketRssi);
sensorData.AddInt32("PacketRSSI", e.PacketRssi);
telemetryDataPoint.Add("RSSI", e.Rssi);
sensorData.AddInt32("RSSI", e.Rssi);
//Chop up each sensor read into an ID & value
foreach (string sensorReading in sensorReadings)
{
string[] sensorIdAndValue = sensorReading.Split(sensorIdAndValueSeparators, StringSplitOptions.RemoveEmptyEntries);
// Check that there is an id & value
if (sensorIdAndValue.Length != 2)
{
this.logging.LogMessage("Sensor reading invalid format", LoggingLevel.Warning);
return;
}
string sensorId = sensorIdAndValue[0];
string value = sensorIdAndValue[1];
try
{
if (this.applicationSettings.SensorIDIsDeviceIDSensorID)
{
// Construct the sensor ID from SensordeviceID & Value ID
telemetryDataPoint.Add(string.Format("{0}{1}", addressBcdText, sensorId), value);
sensorData.AddString(string.Format("{0}{1}", addressBcdText, sensorId), value);
Debug.WriteLine(" Sensor {0}{1} Value {2}", addressBcdText, sensorId, value);
}
else
{
telemetryDataPoint.Add(sensorId, value);
sensorData.AddString(sensorId, value);
Debug.WriteLine(" Device {0} Sensor {1} Value {2}", addressBcdText, sensorId, value);
}
}
catch (Exception ex)
{
this.logging.LogMessage("Sensor reading invalid JSON format " + ex.Message, LoggingLevel.Warning);
return;
}
}
this.logging.LogEvent("Sensor readings", sensorData, LoggingLevel.Information);
try
{
using (Message message = new Message(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(telemetryDataPoint))))
{
Debug.WriteLine(" AzureIoTHubClient SendEventAsync start");
await this.azureIoTHubClient.SendEventAsync(message);
Debug.WriteLine(" AzureIoTHubClient SendEventAsync finish");
}
}
catch (Exception ex)
{
this.logging.LogMessage("AzureIoTHubClient SendEventAsync failed " + ex.Message, LoggingLevel.Error);
}
}
This does mean longer field names but I usually copy n paste them from the Arduino serial monitor of the Event Tracing For Windows (ETW) logging.