The Things Network(TTN) Friendly Commands
I have built a several Proof of Concept(PoC) applications (Azure IoT Central Basic Telemetry, Basic Commands, and Request Commands) to explore to how an Azure IoT Central integration with TTN could work. This blog post is about how to configure queued and non queued Cloud to Device(C2D) Commands with request parameters so they should work with my TTN Message Queue Telemetry Transport(MQTT) Data API connector.
I have focused on commands with Analog values but the same approach should be valid for other parameter types like Boolean, Date, DateTime, Double, Duration, Enumeration, Float, Geopoint, Vector, Integer, Long, String, and Time.
There was a lot of “trial and error” (26 template versions) required to figure out how to configure commands and queued commands so they can and used in TTN downlink payloads.
{
"end_device_ids": {
"device_id": "dev1",
"application_ids": {
"application_id": "app1"
},
"dev_eui": "4200000000000000",
"join_eui": "4200000000000000",
"dev_addr": "00E6F42A"
},
"correlation_ids": [
"my-correlation-id",
"..."
],
"downlink_ack": {
"session_key_id": "AWnj0318qrtJ7kbudd8Vmw==",
"f_port": 15,
"f_cnt": 11,
"frm_payload": "....",
"decoded_payload":
{
"Value_0":"1.23"
...
}
"confirmed": true,
"priority": "NORMAL",
"correlation_ids": [
"my-correlation-id",
"..."
]
}
}
My Azure IoT Central client application displays the generated message including the decoded payload field which is used by the built in Low Power Protocol(LPP) decoder/encoder and other custom encoders/decoders.
From the “Device Commands” form I can send commands and a queued commands which have float parameters or object parameters which contain one or more float values in a JSON payload.
For commands which call the methodHander which was been registered by calling SetMethodDefaultHandlerAsync the request payload can be JSON or plain text. If the payload is valid JSON it is “grafted”(couldn’t think of a better word) into the decoded_payload field. If the payload is not valid a JSON object with the method name as the “name” and the text payload as the value is added the decoded_payload.
private static async Task<MethodResponse> MethodCallbackDefaultHandler(MethodRequest methodRequest, object userContext)
{
AzureIoTMethodHandlerContext receiveMessageHandlerConext = (AzureIoTMethodHandlerContext)userContext;
Console.WriteLine($"Default handler method {methodRequest.Name} was called.");
Console.WriteLine($"Payload:{methodRequest.DataAsJson}");
Console.WriteLine();
if (string.IsNullOrWhiteSpace(methodRequest.Name))
{
Console.WriteLine($" Method Request Name null or white space");
return new MethodResponse(400);
}
string payloadText = Encoding.UTF8.GetString(methodRequest.Data);
if (string.IsNullOrWhiteSpace(payloadText))
{
Console.WriteLine($" Payload null or white space");
return new MethodResponse(400);
}
// At this point would check to see if Azure DeviceClient is in cache, this is so nasty
if ( String.Compare( methodRequest.Name, "Analog_Output_1", true) ==0 )
{
Console.WriteLine($" Device not found");
return new MethodResponse(UTF8Encoding.UTF8.GetBytes("Device not found"), 404);
}
JObject payload;
if (IsValidJSON(payloadText))
{
payload = JObject.Parse(payloadText);
}
else
{
payload = new JObject
{
{ methodRequest.Name, payloadText }
};
}
string downlinktopic = $"v3/{receiveMessageHandlerConext.ApplicationId}@{receiveMessageHandlerConext.TenantId}/devices/{receiveMessageHandlerConext.DeviceId}/down/push";
DownlinkPayload downlinkPayload = new DownlinkPayload()
{
Downlinks = new List<Downlink>()
{
new Downlink()
{
Confirmed = false,
//PayloadRaw = messageBody,
PayloadDecoded = payload,
Priority = DownlinkPriority.Normal,
Port = 10,
/*
CorrelationIds = new List<string>()
{
methodRequest.LockToken
}
*/
}
}
};
Console.WriteLine($"TTN Topic :{downlinktopic}");
Console.WriteLine($"TTN downlink JSON :{JsonConvert.SerializeObject(downlinkPayload, Formatting.Indented)}");
return new MethodResponse(200);
}
A JSON request payload also supports downlink messages with more that one value.
For queued commands which call the ReceiveMessageHandler which has was registered by calling SetReceiveMessageHandler the request payload is JSON or plain text.
private async static Task ReceiveMessageHandler(Message message, object userContext)
{
AzureIoTMessageHandlerContext receiveMessageHandlerConext = (AzureIoTMessageHandlerContext)userContext;
Console.WriteLine($"ReceiveMessageHandler handler method was called.");
Console.WriteLine($" Message ID:{message.MessageId}");
Console.WriteLine($" Message Schema:{message.MessageSchema}");
Console.WriteLine($" Correlation ID:{message.CorrelationId}");
Console.WriteLine($" Lock Token:{message.LockToken}");
Console.WriteLine($" Component name:{message.ComponentName}");
Console.WriteLine($" To:{message.To}");
Console.WriteLine($" Module ID:{message.ConnectionModuleId}");
Console.WriteLine($" Device ID:{message.ConnectionDeviceId}");
Console.WriteLine($" User ID:{message.UserId}");
Console.WriteLine($" CreatedAt:{message.CreationTimeUtc}");
Console.WriteLine($" EnqueuedAt:{message.EnqueuedTimeUtc}");
Console.WriteLine($" ExpiresAt:{message.ExpiryTimeUtc}");
Console.WriteLine($" Delivery count:{message.DeliveryCount}");
Console.WriteLine($" InputName:{message.InputName}");
Console.WriteLine($" SequenceNumber:{message.SequenceNumber}");
foreach (var property in message.Properties)
{
Console.WriteLine($" Key:{property.Key} Value:{property.Value}");
}
Console.WriteLine($" Content encoding:{message.ContentEncoding}");
Console.WriteLine($" Content type:{message.ContentType}");
string payloadText = Encoding.UTF8.GetString(message.GetBytes());
Console.WriteLine($" Content:{payloadText}");
Console.WriteLine();
if (!message.Properties.ContainsKey("method-name"))
{
await receiveMessageHandlerConext.AzureIoTHubClient.RejectAsync(message);
Console.WriteLine($" Property method-name not found");
return;
}
string methodName = message.Properties["method-name"];
if (string.IsNullOrWhiteSpace( methodName))
{
await receiveMessageHandlerConext.AzureIoTHubClient.RejectAsync(message);
Console.WriteLine($" Property null or white space");
return;
}
if (string.IsNullOrWhiteSpace(payloadText))
{
await receiveMessageHandlerConext.AzureIoTHubClient.RejectAsync(message);
Console.WriteLine($" Payload null or white space");
return;
}
JObject payload;
if (IsValidJSON(payloadText))
{
payload = JObject.Parse(payloadText);
}
else
{
payload = new JObject
{
{ methodName, payloadText }
};
}
string downlinktopic = $"v3/{receiveMessageHandlerConext.ApplicationId}@{receiveMessageHandlerConext.TenantId}/devices/{receiveMessageHandlerConext.DeviceId}/down/push";
DownlinkPayload downlinkPayload = new DownlinkPayload()
{
Downlinks = new List<Downlink>()
{
new Downlink()
{
Confirmed = false,
//PayloadRaw = messageBody,
PayloadDecoded = payload,
Priority = DownlinkPriority.Normal,
Port = 10,
CorrelationIds = new List<string>()
{
message.LockToken
}
}
}
};
Console.WriteLine($"TTN Topic :{downlinktopic}");
Console.WriteLine($"TTN downlink JSON :{JsonConvert.SerializeObject(downlinkPayload, Formatting.Indented)}");
//await receiveMessageHandlerConext.AzureIoTHubClient.AbandonAsync(message); // message retries
//await receiveMessageHandlerConext.AzureIoTHubClient.CompleteAsync(message);
await receiveMessageHandlerConext.AzureIoTHubClient.CompleteAsync(message.LockToken);
//await receiveMessageHandlerConext.AzureIoTHubClient.RejectAsync(message); // message gone no retry
}
When I initiated an Analog queued command the message handler was invoked with the name of the command capability (Analog_Output_2) in a message property called “method-name”. For a typed parameter the message content was a string representation of the value. For an object parameter the payload contains a JSON representation of the request field(s)
A JSON request payload supports downlink message with more that one value.
The choice of Value_0, Value_1 (I think they are float64 type) etc. for the decoded_payload is specified in the LPP downlink decode/encoder source code.
The context information for both comments and queued commands provides additional information required to construct the MQTT topic for publishing the downlink messages.
For queued commands the correlation_id will contain the message.LockToken so that messages can be Abandoned, Completed or Rejected. The MQTT broker publishes a series of topics so the progress of the transmission of downlink message can be monitored.
If the device is not known the Abandon method will be called immediately. For command messages Completed will be called as soon as the message is “sent”
v3/{application id}@{tenant id}/devices/{device id}/down/queued
v3/{application id}@{tenant id}/devices/{device id}/down/sent
v3/{application id}@{tenant id}/devices/{device id}/down/ack
v3/{application id}@{tenant id}/devices/{device id}/down/nack
v3/{application id}@{tenant id}/devices/{device id}/down/failed
For queued messages the point in the delivery process where the Abandoned, Completed and Rejected methods will be called will be configurable.