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
To connect to an Azure IoT Hub I copied the connection string from the portal.
Azure IoT Hub connection string components
Retrieving a connection string for a device connected to Azure IoT Central (without using the Device Provisioning Service(DPS)) is a bit more involved. There is a deprecated command line application dps-keygen which calls the DPS with a device ID , device SAS key and scope ID and returns a connection string.
Azure IoT Central Device Connection InformationAzure DPS-Keygen command-line
Using Azure IoT Explorer I could see reformatted JSON messages from my client application.
Azure IoT Explorer displaying message payload
These two approaches are fine for testing but wouldn’t scale well and would be painful to use it there were 1000s, 100s or even 10s of devices.
Unpacking the payload_fields property was causing me some issues. I tried many different approaches but they all failed.
public class PayloadV4
{
public string app_id { get; set; }
public string dev_id { get; set; }
public string hardware_serial { get; set; }
public int port { get; set; }
public int counter { get; set; }
public bool is_retry { get; set; }
public string payload_raw { get; set; }
//public JsonObject payload_fields { get; set; }
//public JObject payload_fields { get; set; }
//public JToken payload_fields { get; set; }
//public JContainer payload_fields { get; set; }
//public dynamic payload_fields { get; set; }
public Object payload_fields { get; set; }
public MetadataV4 metadata { get; set; }
public string downlink_url { get; set; }
}
I tried using the excellent JsonSubTypes library to build a polymorphic convertor, which failed.
...
public class PolymorphicJsonConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject item = JObject.Load(reader);
var type = item["type"].Value<string>();
if (type == "PayloadFieldDigitalInput")
{
return item.ToObject<PayloadFieldDigitalInput>();
}
else if (type == "PayloadFieldDigitalInput")
{
return item.ToObject<PayloadFieldDigitalOutput>();
}
else if (type == "PayloadFieldAnalogInput")
{
return item.ToObject<PayloadFieldDigitalOutput>();
}
else if (type == "PayloadFieldAnalogInput")
{
return item.ToObject<PayloadFieldDigitalOutput>();
}
else
{
return null;
}
}
...
}
It was about this point I figured that I was down a very deep rabbit hole and I should just embrace my “stupid”.
I realised I shouldn’t unpack the payload as the number of generated classes required and the complexity of other approaches was going to rapidly get out of hand. Using an Object and recursively traversing its contents with System.Text.Json looked like a viable approach.
public class GatewayV4
{
public string gtw_id { get; set; }
public ulong timestamp { get; set; }
public DateTime time { get; set; }
public int channel { get; set; }
public int rssi { get; set; }
public double snr { get; set; }
public int rf_chain { get; set; }
public double latitude { get; set; }
public double longitude { get; set; }
public int altitude { get; set; }
}
public class MetadataV4
{
public string time { get; set; }
public double frequency { get; set; }
public string modulation { get; set; }
public string data_rate { get; set; }
public string coding_rate { get; set; }
public List<GatewayV4> gateways { get; set; }
}
public class PayloadV4
{
public string app_id { get; set; }
public string dev_id { get; set; }
public string hardware_serial { get; set; }
public int port { get; set; }
public int counter { get; set; }
public bool is_retry { get; set; }
public string payload_raw { get; set; }
// finally settled on an Object
public Object payload_fields { get; set; }
public MetadataV4 metadata { get; set; }
public string downlink_url { get; set; }
}
So, I added yet another new to controller to my application to deserialise the body of the POST from the TTN Application Integration.
[Route("[controller]")]
[ApiController]
public class ClassSerialisationV4Fields : ControllerBase
{
private static readonly ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public string Index()
{
return "move along nothing to see";
}
[HttpPost]
public IActionResult Post([FromBody] PayloadV4 payload)
{
string payloadFieldsUnpacked = string.Empty;
// Check that the post data is good
if (!this.ModelState.IsValid)
{
log.WarnFormat("ClassSerialisationV4Fields validation failed {0}", this.ModelState.Messages());
return this.BadRequest(this.ModelState);
}
JsonElement jsonElement = (JsonElement)payload.payload_fields;
foreach (var property in jsonElement.EnumerateObject())
{
// Special handling for nested properties
if (property.Name.StartsWith("gps_") || property.Name.StartsWith("accelerometer_") || property.Name.StartsWith("gyrometer_"))
{
payloadFieldsUnpacked += $"Property Name:{property.Name}\r\n";
JsonElement gpsElement = (JsonElement)property.Value;
foreach (var gpsProperty in gpsElement.EnumerateObject())
{
payloadFieldsUnpacked += $" Property Name:{gpsProperty.Name} Property Value:{gpsProperty.Value}\r\n";
}
}
else
{
payloadFieldsUnpacked += $"Property Name:{property.Name} Property Value:{property.Value}\r\n";
}
}
log.Info(payloadFieldsUnpacked);
return this.Ok();
}
}
In the body of the events in Azure Application Insights I could see messages and the format looked fine for simple payloads
In part 1 & part 2 I had been ignoring the payload_fields property of the Payload class. The documentation indicates that payload_fields property is populated when an uplink message is Decoded.
There is a built in decoder for Low Power Payload(LPP) which looked like the simplest option to start with.
I used JSON2Csharp to generate C# classes which would deserialise the above uplink message.
// Third version of classes for unpacking HTTP payload
public class Gps1V3
{
public int altitude { get; set; }
public double latitude { get; set; }
public double longitude { get; set; }
}
public class PayloadFieldsV3
{
public double analog_in_1 { get; set; }
public int digital_in_1 { get; set; }
public Gps1V3 gps_1 { get; set; }
public int luminosity_1 { get; set; }
public double temperature_1 { get; set; }
}
public class GatewayV3
{
public string gtw_id { get; set; }
public ulong timestamp { get; set; }
public DateTime time { get; set; }
public int channel { get; set; }
public int rssi { get; set; }
public double snr { get; set; }
public int rf_chain { get; set; }
public double latitude { get; set; }
public double longitude { get; set; }
public int altitude { get; set; }
}
public class MetadataV3
{
public string time { get; set; }
public double frequency { get; set; }
public string modulation { get; set; }
public string data_rate { get; set; }
public string coding_rate { get; set; }
public List<GatewayV3> gateways { get; set; }
}
public class PayloadV3
{
public string app_id { get; set; }
public string dev_id { get; set; }
public string hardware_serial { get; set; }
public int port { get; set; }
public int counter { get; set; }
public bool is_retry { get; set; }
public string payload_raw { get; set; }
public PayloadFieldsV3 payload_fields { get; set; }
public MetadataV3 metadata { get; set; }
public string downlink_url { get; set; }
}
I added a new to controller to my application which used the generated classes to deserialise the body of the POST from the TTN Application Integration.
[Route("[controller]")]
[ApiController]
public class ClassSerialisationV3Fields : ControllerBase
{
private static readonly ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public string Index()
{
return "move along nothing to see";
}
[HttpPost]
public IActionResult Post([FromBody] PayloadV3 payload)
{
// Check that the post data is good
if (!this.ModelState.IsValid)
{
log.WarnFormat("ClassSerialisationV3Fields validation failed {0}", this.ModelState.Messages());
return this.BadRequest(this.ModelState);
}
log.Info($"DevEUI:{payload.hardware_serial} Payload Base64:{payload.payload_raw} analog_in_1:{payload.payload_fields.analog_in_1} digital_in_1:{payload.payload_fields.digital_in_1} gps_1:{payload.payload_fields.gps_1.latitude},{payload.payload_fields.gps_1.longitude},{payload.payload_fields.gps_1.altitude} luminosity_1:{payload.payload_fields.luminosity_1} temperature_1:{payload.payload_fields.temperature_1}");
return this.Ok();
}
}
I then updated the TTN application integration to send messages to my new endpoint. In the body of the Application Insights events I could see the devEUI, port, and the payload fields had been extracted from the message.
This arrangement was pretty nasty and sort of worked but in the “real world” would not have been viable. I would need to generate lots of custom classes for each application taking into account the channel numbers (e,g, analog_in_1,analog_in_2) and datatypes used.
I also explored which datatypes were supported by the TTN decoder, after some experimentation (Aug 2019) it looks like only the LPPV1 ones are.
AnalogInput
AnalogOutput
DigitalInput
DigitalOutput
GPS
Accelerometer
Gyrometer
Luminosity
Presence
BarometricPressure
RelativeHumidity
Temperature
What I need is a more flexible way to stored and decode payload_fields property..
I used JSON2Csharp and a payload I downloaded in Part 1 to generate C# classes which would deserialise my minimalist messages.
// First version of classes for unpacking HTTP payload https://json2csharp.com/
public class GatewayV1
{
public string gtw_id { get; set; }
public int timestamp { get; set; }
public DateTime time { get; set; }
public int channel { get; set; }
public int rssi { get; set; }
public double snr { get; set; }
public int rf_chain { get; set; }
public double latitude { get; set; }
public double longitude { get; set; }
public int altitude { get; set; }
}
public class MetadataV1
{
public string time { get; set; }
public double frequency { get; set; }
public string modulation { get; set; }
public string data_rate { get; set; }
public string coding_rate { get; set; }
public List<GatewayV1> gateways { get; set; }
}
public class PayloadV1
{
public string app_id { get; set; }
public string dev_id { get; set; }
public string hardware_serial { get; set; }
public int port { get; set; }
public int counter { get; set; }
public bool confirmed { get; set; }
public string payload_raw { get; set; }
public MetadataV1 metadata { get; set; }
public string downlink_url { get; set; }
}
I added a new to controller to my application which used the generated classes to deserialise the body of the POST from the TTN Application Integration.
[Route("[controller]")]
[ApiController]
public class ClassSerialisationV1 : ControllerBase
{
private static readonly ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public string Index()
{
return "move along nothing to see";
}
[HttpPost]
public IActionResult Post([FromBody] PayloadV1 payload)
{
// Check that the post data is good
if (!this.ModelState.IsValid)
{
log.WarnFormat("ClassSerialisationV1 validation failed {0}", this.ModelState.Messages());
return this.BadRequest(this.ModelState);
}
log.Info($"DevEUI:{payload.hardware_serial} Payload Base64:{payload.payload_raw}");
return this.Ok();
}
}
I then updated the TTN application integration to send messages to my new endpoint.
TTN Application configuration overview
In the body of the Application Insights events I could see the devEUI, port, and the raw payload had been extracted from the message.
I then added another controller which decoded the Base64 encoded payload_raw.
[Route("[controller]")]
[ApiController]
public class ClassSerialisationV2Base64Decoded : ControllerBase
{
private static readonly ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public string Index()
{
return "move along nothing to see";
}
[HttpPost]
public IActionResult Post([FromBody] PayloadV2 payload)
{
// Check that the post data is good
if (!this.ModelState.IsValid)
{
log.WarnFormat("ClassSerialisationV2BCDDecoded validation failed {0}", this.ModelState.Messages());
return this.BadRequest(this.ModelState);
}
log.Info($"DevEUI:{payload.hardware_serial} Port:{payload.port} Payload:{ Encoding.UTF8.GetString(Convert.FromBase64String(payload.payload_raw))}");
return this.Ok();
}
}
Then after a while the deserialisation started to fail with an HTTP 400-Bad request. When I ran the same request with Telerik Fiddler on my desktop the raw response was
HTTP/1.1 400 Bad Request
Transfer-Encoding: chunked
Content-Type: application/problem+json; charset=utf-8
Server: Microsoft-IIS/10.0
Request-Context: appId=cid-v1:f4f72f2e-1144-4578-923f-d3ebdcfb7766
X-Powered-By: ASP.NET
Date: Mon, 31 Aug 2020 09:07:30 GMT
17a
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title":"One or more validation errors occurred.",
"status":400,
"traceId":"00-45033ec030b63d4ebb82b95b67cb8142-9fc52a18be202848-00",
"errors":{
"$.metadata.gateways[0].timestamp":["The JSON value could not be converted to System.Int32.
Path: $.metadata.gateways[0].timestamp | LineNumber: 21 | BytePositionInLine: 35."]}}
0
The line in the payload was the gateway timestamp. The value was 2,426,973,100 which larger than 2,147,483,647 the maximum number that can be stored in a signed 32 bit integer. The JSON2CSharp generator had made a reasonable choice of datatype but in this case the range was not sufficient.
public class GatewayV2
{
public string gtw_id { get; set; }
public ulong timestamp { get; set; }
public DateTime time { get; set; }
public int channel { get; set; }
public int rssi { get; set; }
public double snr { get; set; }
public int rf_chain { get; set; }
public double latitude { get; set; }
public double longitude { get; set; }
public int altitude { get; set; }
}
I checked the TTN code where the variable was declared as an unsigned 64 bit integer.
This issue could occur for other variables so I need to manually check all the generated classes.
This is the first in a series of posts about building an HTTP Integration for a The Things Network(TTN) application. I have assumed that readers are familiar with the configuration and operation of a TTN instance so I’m not going to cover that in detail.
#include <LoRaWan.h>
unsigned char data[] = {0x53, 0x65, 0x65, 0x65, 0x64, 0x75, 0x69, 0x6E, 0x6F, 0x20, 0x4C, 0x6F, 0x52, 0x61, 0x57, 0x41, 0x4E};
char buffer[256];
void setup(void)
{
SerialUSB.begin(9600);
while (!SerialUSB);
lora.init();
memset(buffer, 0, 256);
lora.getVersion(buffer, 256, 1);
SerialUSB.print("Ver:");
SerialUSB.print(buffer);
memset(buffer, 0, 256);
lora.getId(buffer, 256, 1);
SerialUSB.print(buffer);
SerialUSB.print("ID:");
lora.setKey(NULL, NULL, "12345678901234567890123456789012");
lora.setId(NULL, "1234567890123456", "1234567890123456");
lora.setPort(10);
lora.setDeciveMode(LWOTAA);
lora.setDataRate(DR0, AS923);
lora.setDutyCycle(false);
lora.setJoinDutyCycle(false);
lora.setPower(14);
while (!lora.setOTAAJoin(JOIN, 10))
{
SerialUSB.println("");
}
SerialUSB.println( "Joined");
}
void loop(void)
{
bool result = false;
//result = lora.transferPacket("Hello World!", 10);
result = lora.transferPacket(data, sizeof(data));
if (result)
{
short length;
short rssi;
memset(buffer, 0, 256);
length = lora.receivePacket(buffer, 256, &rssi);
if (length)
{
SerialUSB.print("Length is: ");
SerialUSB.println(length);
SerialUSB.print("RSSI is: ");
SerialUSB.println(rssi);
SerialUSB.print("Data is: ");
for (unsigned char i = 0; i < length; i ++)
{
SerialUSB.print("0x");
SerialUSB.print(buffer[i], HEX);
SerialUSB.print(" ");
}
SerialUSB.println();
}
}
delay( 10000);
}
The SetKey and SetId parameters are not obvious and it would be much easier if there were two methods one for OTTA and the other for Activation by-Personalization(ABP). I then built an Net Core 3.1 Web API application which had a single controller to receive messages from TTN.
[Route("[controller]")]
[ApiController]
public class Raw : ControllerBase
{
private static readonly ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
[HttpGet]
public string Index()
{
return "move along nothing to see";
}
[HttpPost]
public void PostRaw([FromBody]JsonElement body)
{
string json = JsonSerializer.Serialize(body);
log.Info(json);
}
}
I then configured my TTN application integration to send messages to my shinny new endpoint
TTN Application configuration overview
My controller logged events to Azure application Insights so I could see if there were any errors and inspect message payloads. The TTN developers provide sample payloads to illustrate the message format but they were a bit chunky for my initial testing.
Application Insights event list
I could then inspect individual events and payloads
Application Insights event display
At this point I could download message payloads and save them locally.
These were useful because I could then use a tool like Telerik Fiddler to submit messages to my application when it was running locally in the Visual Studio 2019 debugger.
I did notice that the .DeviceConnected and .DeviceDisconnected events did take a while to arrive. When I started the field gateway application on the Windows 10 IoT Core device I would get several DeviceTelemetry events before the DeviceConnected event arrived.
I was using Advanced Message Queueing Protocol (AMQP) so I modified the configuration file so I could try all the available options.
C# TransportType enumeration
namespace Microsoft.Azure.Devices.Client
{
//
// Summary:
// Transport types supported by DeviceClient - AMQP/TCP, HTTP 1.1, MQTT/TCP, AMQP/WS,
// MQTT/WS
public enum TransportType
{
//
// Summary:
// Advanced Message Queuing Protocol transport. Try Amqp over TCP first and fallback
// to Amqp over WebSocket if that fails
Amqp = 0,
//
// Summary:
// HyperText Transfer Protocol version 1 transport.
Http1 = 1,
//
// Summary:
// Advanced Message Queuing Protocol transport over WebSocket only.
Amqp_WebSocket_Only = 2,
//
// Summary:
// Advanced Message Queuing Protocol transport over native TCP only
Amqp_Tcp_Only = 3,
//
// Summary:
// Message Queuing Telemetry Transport. Try Mqtt over TCP first and fallback to
// Mqtt over WebSocket if that fails
Mqtt = 4,
//
// Summary:
// Message Queuing Telemetry Transport over Websocket only.
Mqtt_WebSocket_Only = 5,
//
// Summary:
// Message Queuing Telemetry Transport over native TCP only
Mqtt_Tcp_Only = 6
}
}
The first telemetry data arrived 00:57:18, the DeviceConnected arrived 01:01:28 so approximately a 4 minute delay, the DeviceDisconnected arrived within a minute of me shutting the device down.
The first telemetry data arrived 04:16:48, the DeviceConnected arrived 04:20:39 so approximately a 4 minute delay, the DeviceDisconnected arrived within a minute of me shutting the device down.
The first telemetry data arrived 04:05:36, DeviceConnected arrived 04:09:52 so approximately a 4 minute delay, the DeviceDisconnected arrived within a minute of me shutting the device down.
HTTP
I waited for 20 minutes and there wasn’t a DeviceConnected message which I sort of expected as HTTP is a connectionless protocol.
The first telemetry data arrived 01:11:33, the DeviceConnected arrived 01:11:25 so they arrived in order and within 10 seconds, the DeviceDisconnected arrived within a 15 seconds of me shutting the device down.
The first telemetry data arrived 04:42:15, the DeviceConnected arrived 04:42:06 so they arrived in order and within 10 seconds, the DeviceDisconnected arrived within a 20 seconds of me shutting device down.
The first telemetry data arrived 04:36:08, the DeviceConnected arrived 04:36:03 so they arrived in order and within 10 seconds, the DeviceDisconnected arrived within a 30 seconds of me shutting device down.
Summary
My LoRa sensors nodes are sending data roughly every minute which reduces the precision of the times.
It looks like for AMQP based messaging it can take 4-5 minutes for a Devices.DeviceConnected message to arrive, for based MQTT messaging it’s 5-10 seconds.
I’m working on an project for a customer which implements a number of application programming Interfaces(API) for a Single Page Application(SPA) and other clients. We are using entity tags (ETags) for versioning and the front end developers found the couldn’t access them from javascipt running in mainstream browser clients (June 2018).
The problems was understanding how Cross-Origin Resource Sharing (CORS) worked and how it interacted with our security model (API key and OAuth2.0 depending on the client)
In our scenario we first found the pre-flight check wasn’t working because in the HyperText Transfer Protocol (HTTP) OPTIONS method our X-API-KEY check was failing
OPTIONS http://xyz.azurewebsites.net/api/portfolio HTTP/1.1
...
Access-Control-Request-Headers: x-api-key
Access-Control-Request-Method: GET
Accept-Encoding: gzip, deflate
Content-Length: 0
Host: xyz.azurewebsites.net
Connection: Keep-Alive
Pragma: no-cache
HTTP/1.1 400 Bad Request
Transfer-Encoding: chunked
Server: Kestrel
X-Powered-By: ASP.NET
...
Date: Sun, 24 Jun 2018 05:48:30 GMT
13
API Key is invalid.
0
So I disabled X-API-KEY validation in startup.cs
public async Task Invoke(HttpContext context)
{
if (context.Request.Method == "OPTIONS")
{
await this.next.Invoke(context);
return;
}
var claims = new List();
…
OPTIONS then worked
OPTIONS http://xyz.azurewebsites.net/api/portfolio HTTP/1.1
...
Access-Control-Request-Headers: x-api-key
Access-Control-Request-Method: GET
Accept-Encoding: gzip, deflate
Content-Length: 0
Host: xyz.azurewebsites.net
Connection: Keep-Alive
Pragma: no-cache
HTTP/1.1 404 Not Found
Server: Kestrel
X-Powered-By: ASP.NET
...
Date: Sun, 24 Jun 2018 05:52:20 GMT
Content-Length: 0
I then turned on CORS allowing pretty much anything
From the Application Insights logging and RestTest client (which I ran locally and remotely) I could see that the client side code couldn’t access the value of our eTag. It had to be “exposed”
I had some oddness with releasing code updates which I think was down to caching of pre-flight request responses.
Next steps tidy up the headers etc. and lock the CORS configuration down to expose the minimum necessary required for the application to work.
I use Netduino devices for teaching and my students often build projects which need a cloud based service like AdaFruit.IO to capture, store and display their sensor data.
The Netduino 3 Wifi device also supports https for improved security and privacy. They also make great field gateways as they can run off solar/battery power.
The devices have been uploading temperature and humidity measurements from a Silicon labs Si7005 sensor. (Outside sensor suffering from sunstrike)
program.cs
*
Copyright ® 2017 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.
http://www.devmobile.co.nz
*/
using System;
using System.Net;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using Microsoft.SPOT.Net.NetworkInformation;
using SecretLabs.NETMF.Hardware.Netduino;
using devMobile.NetMF.Sensor;
using devMobile.IoT.NetMF;
namespace devMobile.IoT.AdaFruitIO.NetMF.Client
{
public class Program
{
private const string adaFruitIOApiBaseUrl = @"https://IO.adafruit.com/api/v2/";
private const string group = "netduino3";
private const string temperatureFeedKey = "t";
private const string humidityFeedKey = "h";
private const string adaFruitUserName = "YourUserName";
private const string adaFruitIOApiKey = "YourAPIKey";
private static readonly TimeSpan timerDueAfter = new TimeSpan(0, 0, 15);
private static readonly TimeSpan timerPeriod = new TimeSpan(0, 0, 30);
private static OutputPort led = new OutputPort(Pins.ONBOARD_LED, false);
private static SiliconLabsSI7005 sensor = new SiliconLabsSI7005();
private static AdaFruitIoClient adaFruitIoClient = new AdaFruitIoClient(adaFruitUserName, adaFruitIOApiKey, adaFruitIOApiBaseUrl);
public static void Main()
{
// Wait for Network address if DHCP
NetworkInterface networkInterface = NetworkInterface.GetAllNetworkInterfaces()[0];
if (networkInterface.IsDhcpEnabled)
{
Debug.Print(" Waiting for DHCP IP address");
while (NetworkInterface.GetAllNetworkInterfaces()[0].IPAddress == IPAddress.Any.ToString())
{
Debug.Print(" .");
led.Write(!led.Read());
Thread.Sleep(250);
}
led.Write(false);
}
// Display network config for debugging
Debug.Print("Network configuration");
Debug.Print(" Network interface type : " + networkInterface.NetworkInterfaceType.ToString());
Debug.Print(" MAC Address : " + BytesToHexString(networkInterface.PhysicalAddress));
Debug.Print(" DHCP enabled : " + networkInterface.IsDhcpEnabled.ToString());
Debug.Print(" Dynamic DNS enabled : " + networkInterface.IsDynamicDnsEnabled.ToString());
Debug.Print(" IP Address : " + networkInterface.IPAddress.ToString());
Debug.Print(" Subnet Mask : " + networkInterface.SubnetMask.ToString());
Debug.Print(" Gateway : " + networkInterface.GatewayAddress.ToString());
foreach (string dnsAddress in networkInterface.DnsAddresses)
{
Debug.Print(" DNS Server : " + dnsAddress.ToString());
}
Timer humidityAndtemperatureUpdates = new Timer(HumidityAndTemperatureTimerProc, null, timerDueAfter, timerPeriod);
Thread.Sleep(Timeout.Infinite);
}
static private void HumidityAndTemperatureTimerProc(object state)
{
led.Write(true);
try
{
double humidity = sensor.Humidity();
Debug.Print(" Humidity " + humidity.ToString("F0") + "%");
adaFruitIoClient.FeedUpdate(group, humidityFeedKey, humidity.ToString("F0"));
}
catch (Exception ex)
{
Debug.Print("Humidifty read+update failed " + ex.Message);
return;
}
try
{
double temperature = sensor.Temperature();
Debug.Print(" Temperature " + temperature.ToString("F1") + "°C");
adaFruitIoClient.FeedUpdate(group, temperatureFeedKey, temperature.ToString("F1"));
}
catch (Exception ex)
{
Debug.Print("Temperature read+update failed " + ex.Message);
return;
}
led.Write(false);
}
private static string BytesToHexString(byte[] bytes)
{
string hexString = string.Empty;
// Create a character array for hexidecimal conversion.
const string hexChars = "0123456789ABCDEF";
// Loop through the bytes.
for (byte b = 0; b < bytes.Length; b++) { if (b > 0)
hexString += "-";
// Grab the top 4 bits and append the hex equivalent to the return string.
hexString += hexChars[bytes[b] >> 4];
// Mask off the upper 4 bits to get the rest of it.
hexString += hexChars[bytes[b] & 0x0F];
}
return hexString;
}
}
}
AdaFruit.IO client.cs, handles feed groups and individual feeds
/*
Copyright ® 2017 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.
http://www.devmobile.co.nz
*/
using System;
using System.IO;
using System.Net;
using System.Text;
using Microsoft.SPOT;
namespace devMobile.IoT.NetMF
{
public class AdaFruitIoClient
{
private const string apiBaseUrlDefault = @"http://IO.adafruit.com/api/v2/";
private string apiBaseUrl = "";
private string userName = "";
private string apiKey = "";
private int httpRequestTimeoutmSec;
private int httpRequestReadWriteTimeoutmSec;
public AdaFruitIoClient(string userName, string apiKey, string apiBaseUrl = apiBaseUrlDefault, int httpRequestTimeoutmSec = 2500, int httpRequestReadWriteTimeoutmSec = 5000)
{
this.apiBaseUrl = apiBaseUrl;
this.userName = userName;
this.apiKey = apiKey;
this.httpRequestReadWriteTimeoutmSec = httpRequestReadWriteTimeoutmSec;
this.httpRequestTimeoutmSec = httpRequestTimeoutmSec;
}
public void FeedUpdate(string group, string feedKey, string value)
{
string feedUrl;
if (group.Trim() == string.Empty)
{
feedUrl = apiBaseUrl + userName + @"/feeds/" + feedKey + @"/data";
}
else
{
feedUrl = apiBaseUrl + userName + @"/feeds/" + group.Trim() + "." + feedKey + @"/data";
}
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(feedUrl);
{
string payload = @"{""value"": """ + value + @"""}";
byte[] buffer = Encoding.UTF8.GetBytes(payload);
DateTime httpRequestedStartedAtUtc = DateTime.UtcNow;
request.Method = "POST";
request.ContentLength = buffer.Length;
request.ContentType = @"application/json";
request.Headers.Add("X-AIO-Key", apiKey);
request.KeepAlive = false;
request.Timeout = this.httpRequestTimeoutmSec;
request.ReadWriteTimeout = this.httpRequestReadWriteTimeoutmSec;
using (Stream stream = request.GetRequestStream())
{
stream.Write(buffer, 0, buffer.Length);
}
using (var response = (HttpWebResponse)request.GetResponse())
{
Debug.Print(" Status: " + response.StatusCode + " : " + response.StatusDescription);
}
TimeSpan duration = DateTime.UtcNow - httpRequestedStartedAtUtc;
Debug.Print(" Duration: " + duration.ToString());
}
}
}
}
My second proof of concept application for the Mikrobus.Net Quail and EthClick uploads temperature and humidity data to Xively every 30 seconds for display and analysis.
Temperature and humidity Xively data stream
The Xively REST API uses an HTTP PUT which initially didn’t work because the payload was not getting attached.
I patched the AssembleRequest method in the EtherClick driver to fix this issue.
private byte[] AssembleRequest()
{
var a = RequestType;
a += " " + Path + " " + Protocol + "\r\nHost: ";
a += Host + "\r\n";
foreach (object aHeader in Headers.Keys)
a += (string)aHeader + ": " + (string)Headers[aHeader] + "\r\n";
a += "\r\n"; // Cache-Control: no-cache\r\n //Accept-Charset: utf-8;\r\n
if (Content != null && Content != string.Empty && (RequestType == "POST" || RequestType == "PUT")) a += Content;
return Encoding.UTF8.GetBytes(a);
}
The code reads the WeatherClick temperature and humidity values then assembles a CSV payload which it uploads with an HTTP PUT
</pre>
public class Program
{
private const string xivelyHost = @"api.xively.com";
private const string xivelyApiKey = @"YourAPIKey";
private const string xivelyFeedId = @"YourFeedID";
public static void Main()
{
WeatherClick weatherClick = new WeatherClick(Hardware.SocketOne, WeatherClick.I2CAddresses.Address0);
weatherClick.SetRecommendedMode(WeatherClick.RecommendedModes.WeatherMonitoring);
EthClick ethClick = new EthClick(Hardware.SocketTwo);
ethClick.Start(ethClick.GenerateUniqueMacAddress("devMobileSoftware"), "QuailDevice");
// Wait for an internet connection
while (true)
{
if (ethClick.ConnectedToInternet)
{
Debug.Print("Connected to Internet");
break;
}
Debug.Print("Waiting on Internet connection");
}
while (true)
{
Debug.Print("T " + weatherClick.ReadTemperature().ToString("F1") + " H " + weatherClick.ReadHumidity().ToString("F1") + " P " + weatherClick.ReadPressure(PressureCompensationModes.Uncompensated).ToString("F1"));
HttpRequest request = new HttpRequest(@"http://" + xivelyHost + @"/v2/feeds/" + xivelyFeedId + @".csv");
request.Host = xivelyHost;
request.RequestType = "PUT";
request.Headers.Add("Content-Type", "text/csv");
request.Headers.Add("X-ApiKey", xivelyApiKey );
request.Content = "OfficeT," + weatherClick.ReadTemperature().ToString("F1") + "\r\n" + "OfficeH," + weatherClick.ReadHumidity().ToString("F1") ;
request.Headers.Add("Content-Length", request.Content.Length.ToString());
var response = request.Send();
if (response != null)
{
Debug.Print("Response: " + response.Message);
}
else
{
Debug.Print("No response");
}
Thread.Sleep(30000);
}
}
}
MikrobustNet Quail with Eth and Weather Clicks
This proof of concept code appears to be reliable and has run for days at a time. The IP stack looks like it needs a bit more work.
public static void Main()
{
EthClick ethClick = new EthClick(Hardware.SocketTwo);
ethClick.Start(ethClick.GenerateUniqueMacAddress("devMobileSoftware"), "QuailDevice");
while (true)
{
if (ethClick.ConnectedToInternet)
{
Debug.Print("Connected to Internet");
break;
}
Debug.Print("Waiting on Internet connection");
}
while (true)
{
var r = new HttpRequest(@&quot;http://api.icndb.com/jokes/random&quot;);
r.Headers.Add("Accept", "*/*");
var response = r.Send();
if (response != null)
{
if (response.Status == "HTTP/1.1 200 OK")
{
Debug.Print(response.Message);
}
}
else
{
Debug.Print("No response");
}
Thread.Sleep(10000);
}
}
The ran first time and returned the following text
7c
{ "type": "success, "value": { "id": 496, "joke": "Chuck Norris went out of an infinite loop.", "categories": ["nerdy"]}}
0
85
{ "type": "success", "value": { "id": 518, "joke": "Chuck Norris doesn't cheat death. He wins fair and square.", "categories": []}}
0
It looks like the HTTP response parsing is not quite right as each message starts with the length of the message in bytes in hex and the terminating “0”.