cloud-based iot platformmokkas/files/michal_gutowski _s10894_bsc... · having selected microsoft...
TRANSCRIPT
FACULTY OF COMPUTER SCIENCE
DEPARTMENT OF COMPUTER NETWORKS
MOBILE NETWORKING
Michał Gutowskis10894
Cloud-based IoT platform
Bachelor Thesisdr Michał Tomaszewski
Warsaw, September 2017
WYDZIAŁ INFORMATYKI
KATEDRA SIECI KOMPUTEROWYCH
SIECI URZADZEN MOBILNYCH
Michał Gutowskis10894
Platforma IoT oparta na usługachdziałajacych w chmurze
Praca Inzynierskadr Michał Tomaszewski
Warszawa, Wrzesien 2017
Contents
Introduction 1
1 Cloud Infrastructure 31.1 Choosing a Cloud Provider . . . . . . . . . . . . . . . . . . . . . 31.2 Designing Cloud Infrastructure . . . . . . . . . . . . . . . . . . . 5
2 Authentication and Authorization 112.1 IoT Gateway Identity . . . . . . . . . . . . . . . . . . . . . . . . 112.2 Cloud Service Identity . . . . . . . . . . . . . . . . . . . . . . . 182.3 User Authentication & Authorization . . . . . . . . . . . . . . . . 23
2.3.1 Mobile App Backend Authentication . . . . . . . . . . . 232.3.2 Client App Authentication . . . . . . . . . . . . . . . . . 25
3 Cloud Message Routing 283.1 IoT Hub Device-to-Cloud Message Routing . . . . . . . . . . . . 28
3.1.1 Event Processing Service Implementation . . . . . . . . . 313.2 Service Bus Queue Message Routing . . . . . . . . . . . . . . . . 35
3.2.1 WebJob Console Application Implementation . . . . . . . 35
4 Mobile App Backend 414.1 .NET Backend . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424.2 Data Persistence . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5 IoT Gateway 545.1 OS and Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . 555.2 UWP Application . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.2.1 Sensor Drivers . . . . . . . . . . . . . . . . . . . . . . . 605.2.2 Bluetooth Low Energy Connectivity . . . . . . . . . . . . 705.2.3 IoT Hub Connectivity . . . . . . . . . . . . . . . . . . . 78
i
6 Client Application 856.1 User Interface Layout . . . . . . . . . . . . . . . . . . . . . . . . 876.2 Communication With Backend Service . . . . . . . . . . . . . . . 936.3 Push Notification Channel . . . . . . . . . . . . . . . . . . . . . 96
Conclusions 1016.4 Limitations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1016.5 Future Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Bibliography 103
ii
Abstract
The aim of this thesis is to build an IoT platform that enables its usersto receive real-time telemetry data and control Bluetooth Low Energy(BLE) devices connected to the central IoT device. The platform, co-denamed SlickHub, is composed of the IoT gateway, the cloud in-frastructure, and the software necessary to control the platform com-ponents. The IoT gateway enables BLE devices that are not directlyconnected to the Internet to reach cloud infrastructure and allows forcollecting telemetry data through the connected sensors. The teleme-try data includes the measurements of temperature, humidity, and at-mospheric pressure.
The hardware setup consists of the Raspberry Pi 3 Model B (IoT gate-way) and the atmospheric sensors. The IoT gateway is running Win-dows 10 IoT Core operating system with installed Universal WindowsPlatform (UWP) app.
The cloud infrastructure is based on Azure Cloud Services and is thecentral part of the platform responsible for managing the communi-cation between the IoT gateway and the client application running onthe user’s device.
Streszczenie
Celem pracy jest zbudowanie platformy IoT, która pozwala swoimuzytkownikom na odbieranie danych telemetrycznych przesyłanychw czasie rzeczywistym oraz kontrolowanie urzadzen Bluetooth LowEnergy (BLE) podłaczonych do centralnego urzadzenia IoT. Platformeo nazwie kodowej SlickHub współtworza urzadzenie bedace bramaIoT, infrastruktura chmurowa, oraz oprogramowanie niezbedne do za-rzadzania komponentami platformy. Brama IoT pozwala urzadzeniomBLE, które nie sa bezposrednio podłaczone do sieci Internet, na ko-munikacje z infrastruktura chmurowa, a takze odpowiada za zbieraniedanych telemetrycznych z podłaczonych sensorów. W skład danychtelemetrycznych wchodza pomiary temperatury, wilgotnosci powie-trza, oraz cisnienia atmosferycznego.
Konfiguracja sprzetowa składa sie z Raspberry Pi 3 Model B (be-dacego brama IoT) oraz sensorów wykonujacych pomiary atmosfe-ryczne. Brama IoT działa pod kontrola systemu operacyjnego Win-dows 10 IoT Core z zainstalowana aplikacja typu Universal WindowsPlatform (UWP).
Infrastruktura chmurowa bazuje na serwisach chmurowych platformyAzure i jest centralna czescia platformy odpowiedzialna za zarzadza-nie wymiana informacji pomiedzy brama IoT a aplikacja klienckadziałajaca na urzadzeniu uzytkownika.
Introduction
Internet of Things (IoT) can be described as an interconnected ecosystem of com-
puting devices embedded with network connectivity which can collect and ex-
change data. Such data can then be processed and presented to the end-user in
a meaningful way. The concept of IoT dates back to 1982, however due to the
technological limitations the concept was not so popular until the start of the 21st
Century. As of 2014, IoT market has started to rapidly gain traction due to a
convergence and general availability of cloud-enabled technologies. Nonetheless,
there are still many challenges to face among which the most critical ones are
interoperability of individual IoT devices, as well as the privacy and security con-
cerns.
This project aims to resolve the above key concerns by introducing the IoT
solution which can not only provide end-to-end security, but also deliver a cen-
tralized way to control other IoT-capable devices.
One of the most popular IoT applications is home automation. So far, there are
at least couple of different approaches to smart home solutions, and this project
is built around the idea of an IoT gateway. The IoT gateway is a cloud-enabled
device which lets the user connect individual IoT accessories such as smart light
bulbs or thermostats in order to customize and control user’s smart home. If im-
plemented correctly, such a solution can provide a secure connection and wide
product compatibility which I believe is a key to successful home automation.
1
The IoT solution presented in this thesis has been designed to be highly scal-
able, modular, maintainable, and thus easy to extend with new functionalities. The
key design decisions were the selection of hardware and cloud service provider.
It was necessary to ensure that the selected hardware has a built-in support for
Bluetooth Low Energy, which is one of the most popular standards for wireless
communication. It was also crucial to choose the cloud service provider capable
of delivering all the services required for building a scalable and reliable cloud
infrastructure that can handle a large number of IoT devices. As a result, it was
possible to build the IoT platform that can serve as a solid base for numerous IoT
solutions.
2
Chapter 1
Cloud Infrastructure
1.1 Choosing a Cloud Provider
Choosing a cloud provider that can satisfy project requirements is a key to suc-
cessful implementation of a cloud solution. At the time of writing this thesis,
there were four industry-leading providers which, according to Synergy Research
Group, together accounted for more than 50% of the market share in Q4 2016.
Figure 1.1: Industry-Leading Cloud Service Providers
Having in mind the data presented in figure 1.1, it was decided to analyse
IoT solutions offered by Amazon Web Services (AWS), Microsoft Azure, Google
3
Cloud Platform, and IBM Bluemix. At this point it was important to recall all
project requirements that could strongly influence the decision-making process.
cIt has been stated that the application running on IoT gateway should provide
its user with a graphical user interface. During the project development Windows
10 IoT Core was the only OS, targeting embedded devices, offering SDK for de-
veloping apps with rich and touch-enabled GUI. The choice of Windows 10 IoT
Core and Universal Windows Platform indicated that the optimal IoT cloud ser-
vice should deliver SDK for connecting devices controlled with UWP app. While
choosing the cloud provider it was also taken into account the variety of supported
connection protocols, communication patterns, authentication features, and avail-
ability of Mobile backend as a service (MBaaS).
The results of the analysis have shown that all four platforms deliver support for
Hypertext Transfer Protocol (HTTP) and Message Queue Telemetry Transport
(MQTT) protocols, Transport Layer Security (TLS), strong authentication meth-
ods, as well as MBaaS. However, Microsoft Azure turned out to be the only plat-
form that provided services with built-in Advanced Message Queuing Protocol
(AMQP) support and open source subset of the .NET SDK including client SDK
for UWP apps. Greater variety of communication protocols means more flexi-
bility, whereas native C# SDK allows to safe a lot of time both in development
and integration. This makes Microsoft Azure an ideal cloud service provider for
discussed project.
4
1.2 Designing Cloud Infrastructure
Having selected Microsoft Azure as a cloud service provider, it was possible to
move on to designing the cloud infrastructure. Azure offers a wide range of com-
pute options, ranging from virtual machines to specialized cloud services like IoT
Hub, thus providing a great flexibility when it comes to design decisions.
When designing the cloud infrastructure, the starting point was to define the end-
points of the IoT platform and specifying the amount and type of data that will be
sent and received by each endpoint. The platform should enable users to remotely
control Bluetooth Smart IoT accessories via client application installed on user’s
smartphone, tablet, or desktop PC. Based on that, we can conclude that there are
two platform endpoints - client application and IoT gateway. From the cloud per-
spective, client application will authenticate with the cloud, send HTTP requests
to the cloud, and receive notifications from the cloud. IoT gateway will authenti-
cate with the cloud, send telemetry/connectivity data to the cloud and interpret the
commands received from the cloud. To ensure responsiveness, the IoT platform
will need to leverage the real-time data processing.
Figure 1.2: IoT Platform - Endpoints
After establishing the endpoints shown in figure 1.2, it was possible to proceed
with the selection of cloud services. The decision was taken to start with IoT gate-
way and look for the Azure services designed for connecting IoT devices to the
cloud. Azure offers two very powerful event processing services, namely Azure
Event Hub and Azure IoT Hub. Both services enable event and telemetry ingress
5
to the cloud at massive scale, ensuring low latency and high reliability. However,
IoT Hub is equipped with IoT-specific features making it especially suited for
IoT solutions. Unlike Event Hub, IoT Hub enables bi-directional communication
(both device-to-cloud and cloud-to-device), device state information, per-device
identity, file uploads from devices to the cloud, enhanced device protocol support,
and custom message routing rules. Additionally, IoT Hub is optimized to support
millions of simultaneously connected devices, whereas Event Hub can support a
more limited number of concurrent connections (up to 5,000 AMQP connections
[14]). At this point, it has been clear that IoT Hub is the right choice for the IoT
platform.
The next step was to decide which of the supported protocols to use in order to
connect IoT gateway device with IoT Hub cloud service.
Azure IoT Hub natively supports multiple communication protocols including
MQTT, AMQP, and HTTP. Since the IoT gateway is an embedded device with
limited processing power 1, it was decided to opt out the HTTP long polling in fa-
vor of event driven communication. That left a choice between MQTT and AMQP
protocols. While both protocols work on top of TCP/IP protocol suite, they differ
significantly in terms of the utilized communication models.
MQTT is a lightweight messaging protocol based on publish-subscribe model.
It has been designed for resource-constrained devices which use low bandwidth
and/or high latency networks. MQTT introduces the concepts of MQTT client
and MQTT broker. MQTT client is any device that has MQTT library running and
connecting to MQTT broker, whereas MQTT broker is responsible for receiving,
filtering, routing, and sending all messages to the relevant subscribed clients. It
can be said that MQTT broker is a middleware between two MQTT clients. The
main advantages of MQTT are fast communication model, small wire footprint,
1The hardware selection decisions are described in detail in chapter 5.
6
and the relative simplicity of the protocol itself.
AMQP 1.0 is an efficient, binary wire-level, peer-to-peer protocol for reliable mes-
saging. Unlike MQTT, it doesn’t impose a particular structural mode, therefore it
can be used to support different topologies. AMQP has a rich feature set which
includes flexible routing model, reliable message queuing, transactions, and secu-
rity. Its main advantages are efficiency, reliability, and flexibility.
In case of IoT gateway, the extra overhead of AMQP long-lived sessions was not
a great concern and an overarching goal was to ensure secure and reliable con-
nection between the gateway and cloud service. Therefore, it was decided to use
AMQP protocol as a solid base for bi-directional communication between the IoT
gateway and cloud.
While Azure IoT Hub can handle device-to-cloud and cloud-to-device mes-
saging, the inbound messages need to be processed by back-end app in order to
be securely delivered to the authenticated user’s device. For that to happen, the
cloud infrastructure will need to include services responsible for message routing
and message queuing. IoT Hub exposes a built-in service-facing endpoint [18] to
enable other cloud services to read the device-to-cloud messages received by the
hub. This built-in endpoint is compatible with Event Hubs, which support several
mechanisms for reading messages. One of such mechanisms is the Event Hub
Processor hosted in Azure Worker Role. As a matter of fact, IoT Hub message
routing pattern has become so common that in December 2016 Microsoft has de-
cided to add a message routing feature directly to IoT Hub, making it possible to
add custom endpoints and routes. Both mechanisms have advantages and disad-
vantages 2, and the proposed cloud infrastructure assumes the use of any of the two
mechanisms with a preference for IoT Hub message routing whenever possible.
Once the device-to-cloud messages have been correctly filtered and routed,
2Chapter 3 provides a more detailed explanation of both of these mechanisms.
7
they need to be reliably delivered to the back-end app for further processing. In
order to achieve this goal, Microsoft Azure offers a set of specialized queuing
mechanisms. At the time of writing the thesis, there are two types of queue mech-
anisms supported by Microsoft Azure; namely, Storage queues and Service Bus
queues. Based on the recommendations [19] provided by Microsoft, it was de-
cided to utilize the Service Bus queues. This queuing technology will satisfy the
reliability requirement by providing first-in-first-out (FIFO) ordered delivery, au-
tomatic duplicate detection, and at-most-once delivery guarantee. Furthermore,
using Service Bus queues provides an inherent loose coupling between the com-
ponents, because the producers (senders) and consumers (receivers) are not aware
of each other. This allows for the consumer to be upgraded without having any
effect on the producer. It was also decided to set up two Service Bus queues, one
for queuing telemetry data and the other one for queuing connectivity data. With
such a configuration, the messages containing sensor readings will be routed to
telemetry queue and messages containing information about newly connected ac-
cessories will be routed to connectivity queue respectively. This should result in
the higher throughput of the infrastructure, thus increasing the responsiveness.
The very last part of architecting the cloud infrastructure was the selection of
backend services. Microsoft Azure offers several ways to host backend solutions,
therefore before making the decision it was necessary to analyse all the require-
ments for backend functionality.
The backend service should expose REST API with endpoints for authenticating
users, registering IoT gateways at the IoT Hub identity registry, as well as re-
trieving information about and sending commands to the registered IoT gateways.
It should also provide push notifications, integration with social networking ser-
vices, and cloud storage. These requirements suggest the use of a model called
Mobile Backend as a Service (MBaaS). Microsoft Azure App Service platform
8
offers several hosting resources, each of which is dedicated to running a specific
workload. One of the hosting resources is called Azure Mobile App and it is com-
pliant with MBaaS model, which makes it an ideal choice for the infrastructure.
Moving on to the cloud storage solution, it was opted to use Azure SQL Database
in favor of classical relational database ensuring advanced indexing capabilities
and high data integrity. Push notifications will be dispatched to the client applica-
tion via Azure Notification Hub, which delivers multi-platform, scaled-out push
notification engine. Both Azure SQL Database and Azure Notification Hub can
be easily integrated with the Azure Mobile App.
Having chosen the above services, the only remaining problem was to dequeue
the messages arriving at Service Bus queue and route them to the Notification
Hub in order for them to be dispatched to the authenticated users. Unlike IoT
Hub, Azure Mobile App does not provide a built-in message routing capabilities.
It does, however, allow for running background processes by means of Azure We-
bJobs. The idea behind WebJobs is to enable programs or scripts to be hosted and
run alongside the Azure App Service web app. Furthermore, WebJobs are well
suited for real-time processing scenarios as they can be configured to run contin-
uously. From the cloud infrastructure perspective, the mentioned characteristics
make WebJobs an optimal solution for running background tasks that will wire-up
the cloud services on the backend side.
Figure 1.3 presents a complete set of cloud services that constitute the cloud in-
frastructure.
9
Figure 1.3: IoT Platform - Final Cloud Infrastructure
10
Chapter 2
Authentication and Authorization
Without a doubt, IoT security is an issue of high concern. As the number of
connected IoT devices grows, the attack surface increases making the security one
of the biggest challenges in implementing IoT solutions. In order to address this
challenge a special care has been taken regarding the design and implementation
decisions that affect the security of individual components and IoT platform as a
whole. The sections below describe the key aspects of the security mechanisms
used to protect platform resources.
2.1 IoT Gateway Identity
The cloud infrastructure discussed in chapter 1 shows that every IoT gateway
establishes a bi-directional communication with the cloud by connecting to the
Azure IoT Hub. However, before an IoT gateway can connect to the IoT Hub,
it must be authenticated based on credentials that uniquely identify the device
and grant access to a set of per-device resources. To enable per-device identity,
IoT Hub has a built in identity registry [20] - an authentication model for man-
aging per-device credentials. The identity registry stores device identities, au-
thentication keys, and status codes. Every device that is being provisioned must
be registered in the identity registry with a unique device ID. The IoT gateway
11
provisioning process is handled by Azure Mobile App backend service 1, which
exposes REST API for registering new devices with the IoT platform.
Listing 2.1: IoT Gateway Provisioning Endpoint
[HttpPost][Route("")][ResponseType(typeof(BootstrapResponse))]public async Task<IHttpActionResult> Post([FromBody]BootstrapRequest
bootstrapRequest){
var account = await GetAuthorizedUserAccountAsync<MasterAccount>();
string connectionString;
try{
connectionString = await GetDeviceConnectionStringAsync(bootstrapRequest.DeviceId, account);
if (connectionString != null){
return Ok(new BootstrapResponse{
ConnectionString = connectionString});
}}catch (Exception){
return Unauthorized();}
if (account == null){
account = await CreateAuthorizedUserAccountAsync<MasterAccount>();}
connectionString = await CreateDeviceAsync(bootstrapRequest.DeviceId, account);
await UnitOfWork.CompleteAsync();
return Ok(new BootstrapResponse{
ConnectionString = connectionString});
}
...
private async Task<string> CreateDeviceAsync(Guid deviceId, MasterAccount account)
{var device = new Device{
Id = deviceId,Account = account
1The Mobile App backend service is described in detail in chapter 4.
12
};
UnitOfWork.Devices.Add(device);
var iotDevice = await IotHubRegistryService.AddIotDeviceAsync(deviceId);
return iotDevice.GetConnectionString();}
...
The action method responsible for handling IoT gateway provisioning requests
is contained inside the controller decorated with Authorize attribute. This implies
that the provisioning request can only be made by an authenticated user (see sec-
tion 2.3). The CreateDeviceAsync method adds a new record to the Devices table
and delegates a registration process to the AddIotDeviceAsync method exposed
by IotHubRegistryService class. Once the IoT gateway is successfully registered
in the identity registry, the action method returns a connection string, in the format
HostName={iothubname};DeviceId={deviceid};SharedAccessKey={devicekey}, that
enables IoT gateway to establish a connection to the IoT Hub.
Listing 2.2: Registration of IoT Gateway in IoT Hub Identity Registry
public async Task<Device> AddIotDeviceAsync(Guid deviceId){
Device device;
try{
device = await registry.AddDeviceAsync(new Device(deviceId.ToString()));}catch (DeviceAlreadyExistsException){
device = await registry.GetDeviceAsync(deviceId.ToString());}
return device;}
The AddIotDeviceAsync method registers a new device with the IoT Hub using
an instance of RegistryManager class, which is a part of Microsoft.Azure.
Devices NuGet package.
Once the IoT gateway has received the connection string, it is ready to authen-
ticate itself to the IoT Hub. As described in chapter 1, the communication be-
13
tween IoT Hub and IoT gateway is based on AMQP 1.0 messaging protocol.
The IoT Hub supports two authentication mechanisms [16] for AMQP clients;
namely, SASL PLAIN and AMQP Claims-Based-Security. The IoT gateway uses
the latter mechanism (AMQP CBS) to ensure efficient use of network resources
and lower the latency of each device connection. The current implementation
of UWP app responsible for managing IoT gateway uses a DeviceClient class
from Microsoft.Azure.Devices.Client NuGet package for establish-
ing AMQP connection with IoT Hub. However, before June 2016 the Device SDK
for Azure IoT Devices did not support communication over AMQP for UWP apps.
The workaround was to use AMQPNetLite library that provides an AMQP proto-
col stack for multiple .Net frameworks including UWP apps. The downside of us-
ing AMQPNetLite library was that the client component responsible for creating
secure connection to IoT Hub had to be implemented from scratch. Nevertheless,
it was decided to use the code from my AMQPNetLite-based implementation of
DeviceClient class to better explain the process of transmitting SAS tokens as part
of claims-based authentication.
The IoT Hub authenticates a specific device by verifying a token against the secu-
rity credentials stored in the identity registry, but before the token can be transmit-
ted one must be first created. The security token structure supported by IoT Hub
along with the sample snippets for generating the token is described in MSDN
documentation [16].
Listing 2.3: Generating Security Token
private static string CreateSharedAccessSignatureToken(string resourceUri, stringsharedAccessKey, TimeSpan ttl)
{// For more information about generating a SAS token, see// https://azure.microsoft.com/en-us/documentation/articles/service-bus-sas-
overview/
string expiry = (DateTimeOffset.UtcNow + ttl).ToUnixTimeSeconds().ToString();string stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + expiry;
var objMacProv = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.
14
HmacSha256);
var hash = objMacProv.CreateHash(Convert.FromBase64String(sharedAccessKey).AsBuffer());
hash.Append(CryptographicBuffer.ConvertStringToBinary(stringToSign,BinaryStringEncoding.Utf8));
string signature = CryptographicBuffer.EncodeToBase64String(hash.GetValueAndReset());
var sasToken = string.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}",
WebUtility.UrlEncode(resourceUri), WebUtility.UrlEncode(signature), WebUtility.UrlEncode(expiry));
return sasToken;}
The CreateSharedAccessSignatureToken method computes the SAS token from
the specified inputs, which include resource URI ({IoT hub name}.azure-devices
.net/devices/{deviceid}), signing key for the ’{deviceid}’ identity, and a time in-
terval used to calculate expiration time. A signing key used for creating base-
64 encoded signature is a symmetric key stored in the IoT Hub identity reg-
istry and returned as part of IoT gateway connection string. The resulting secu-
rity token has the following format: SharedAccessSignature sr={URL-encoded-
resourceURI}&sig={signature-string}&se={expiry}. It is worth noting that when
using the symmetric key from the identity registry, the ’{policyName}’ (skn) ele-
ment of the token is omitted.
Once the security token has been generated, it is ready to be transmitted to the IoT
Hub for verification. The implementation of the token transmission process fol-
lows the AMQP 1.0 authentication scheme described in the official OASIS stan-
dard specification for AMQP CBS [22].
Listing 2.4: Sending Token to IoT Hubs CBS Node
private static async Task PutCbsTokenAsync(Connection connection, string token,string audience)
{// For more information about an AMQP authentication scheme based on claims-
based security tokens, see// https://www.oasis-open.org/committees/download.php/50506/amqp-cbs-v1%200-
15
wd02%202013-08-12.doc
Session session = new Session(connection);
const string cbsReplyToAddress = "cbs-reply-to";
// Note that we MUST initialize both AMQP links before sending the SAS token;// otherwise the reply from the service will never get delivered.var cbsSender = new SenderLink(session, "cbs-sender", "$cbs");var cbsReceiver = new ReceiverLink(session, cbsReplyToAddress, "$cbs");
// Construct and transfer a "put-token" messagevar request = CreatePutTokenRequestMessage(token, cbsReplyToAddress, audience
);
await cbsSender.SendAsync(request);
// Process a token validation resultvar response = await cbsReceiver.ReceiveAsync();
VerifyPutTokenResponseMessage(response);
// Clean-upawait cbsSender.CloseAsync();await cbsReceiver.CloseAsync();await session.CloseAsync();
}
private static Message CreatePutTokenRequestMessage(string token, string replyTo,string audience)
{var message = new Message(token){
Properties = new Properties{
MessageId = Guid.NewGuid().ToString(),ReplyTo = replyTo
},ApplicationProperties = new ApplicationProperties{
["operation"] = "put-token",["type"] = "azure-devices.net:sastoken",["name"] = audience
}};
return message;}
According to the specification, the token transmission begins by sending a
’put-token’ message to an AMQP endpoint responsible for managing tokens. This
endpoint is referred to as a Claim-Based Security Node (CBS Node). The PutCb-
sTokenAsync method creates a new session using the specified AMQP connection,
opens sender and receiver links to CBS Node with the address ’$cbs’, creates and
16
sends the ’put-token’ message over the sender link, and processes the token val-
idation result received by the receiver link. If the response message contains a
200 OK HTTP response code, then the request is considered successful and the
Client is authorized to access device-facing endpoints exposed by IoT Hub. The
CreatePutTokenRequestMessage method creates a ’put-token’ message according
to the format defined by the AMQP CBS specification [22]. It is important to
note that the CBS is connection-scoped, which means that the links to CBS Node
must be made over the same connection as the links to /devices/deviceid/mes-
sages/events and /devices/deviceid/messages/deviceBound nodes 2 that are being
secured using CBS. It is also important to remember that the SAS tokens grant a
time-bounded access to claimed resources, therefore the Client must send updated
tokens to the CBS Node in order to keep the TCP connection alive.
Listing 2.5: Establishing Connection With Azure IoT Hub
private async Task EstablishConnectionAsync(){
var connState = ConnectionState;
if (connState != ConnectionState.Closed && connState != ConnectionState.Broken &&CbsTokenExpiryTime > DateTime.UtcNow)
{return;
}
connEstablishedEvent.Reset();
ConnectionState = ConnectionState.Connecting;
var createSession = connState == ConnectionState.Closed || connState ==ConnectionState.Broken;
try{
if (createSession){
CreateNewSession();}
CbsTokenExpiryTime = await AuthenticateAsync(session.Connection,DeviceCredentials.HostName, DeviceCredentials.DeviceId,DeviceCredentials.SharedAccessKey);
2The two listed nodes correspond to IoT Hubs device-to-cloud and cloud-to-device endpointsrespectively.
17
// Create the sender and receiver linksvar entity = string.Format("/devices/{0}/messages/events",
DeviceCredentials.DeviceId);
senderLink = new SenderLink(session, "sender-link", entity);
entity = string.Format("/devices/{0}/messages/deviceBound",DeviceCredentials.DeviceId);
receiverLink = new ReceiverLink(session, "receiver-link", entity);}catch (Exception){
ConnectionState = ConnectionState.Broken;throw;
}
ConnectionState = ConnectionState.Open;
// Signal to the waiting threads that the connection has been successfullyestablished
connEstablishedEvent.Set();}
2.2 Cloud Service Identity
While most of the authentication scenarios focus on user-to-service authentica-
tion, it is often necessary for one cloud service to call API of another cloud ser-
vice. Such a scenario is usually referred to as service-to-service authentication.
During the development of cloud message routing 3 it was necessary to solve the
problem of mapping the IoT gateway to associated user and sending push noti-
fications to all devices registered by that user (targeted push). As explained in
chapter 1, the responsibility of sending push notifications has been delegated to
Azure WebJob instances running alongside the Mobile App backend service. In
order to enable targeted push, the WebJob instance has to retrieve a list of user ID
tags for the given IoT gateway ID. The user ID tag consists of _UserId: prefix fol-
lowed by SID of an authenticated user stored in the database. The infrastructure
assumes that Mobile App backend service has the exclusive access to the platform
3Cloud message routing implementation details are covered in chapter 3.
18
database, which means that in order to enable WebJob instances to retrieve user
ID tags the backend service must expose the relevant endpoints. Azure App Ser-
vice offers three authentication methods for handling service-to-service scenarios
[8]; namely Azure Active Directory, client certificates, and basic authentication.
While Azure Active Directory will suit most of the service-to-service authenti-
cation scenarios, in case of WebJob-to-AppService authentication it is enough to
use basic authentication. It is important to remember that the App Service back-
end along with the attached WebJob instances share the same physical resources.
This implies that the backend and WebJob instances can share a secret that will be
securely stored in the server and used to authorize API calls.
App Service issues a JSON web token (JWT) to every client authenticated with
third-party identity provider (see section 2.3). The obtained authentication token
is then included in all requests made to the App Service backend, allowing client
to perform actions that require authenticated-user level permissions. The tokens
issued by App Service are signed with the symmetric security key known as the
signing key. The signing key is stored in WEBSITE_AUTH_SIGNING_KEY envi-
ronment variable. To enable viewing App Service server environment variables,
Azure provides a set of troubleshooting and analysis tools called KUDU.
The KUDU console can be accessed by navigating to:
"https://{app-service-name}.scm.azurewebsites.net".
19
Figure 2.1: KUDU Console - Environment Variables
The environment variables can be accessed by all services hosted by the server,
thus allowing the WebJob console application to retrieve the value of WEBSITE
_AUTH_SIGNING_KEY environment variable to obtain the signing key. The re-
trieved key can then be used to create a valid JSON web token as shown in the
following source code listing.
Listing 2.6: Factory Class Used For Creating JSON Web Token
internal class AuthenticationTokenFactory{
private static readonly TimeSpan DefaultLifetime = TimeSpan.FromHours(1);
private readonly IList<Claim> OutputClaims = new List<Claim> { new Claim(ClaimTypes.Role, "WebJob") };
public AuthenticationTokenFactory(){
var groupSid = CloudConfigurationManager.GetSetting(Constants.Identity.SidSettingKey);
if (String.IsNullOrEmpty(groupSid)){
throw new KeyNotFoundException($"{Constants.Identity.SidSettingKey}setting could not be found.");
}
OutputClaims.Add(new Claim(ClaimTypes.GroupSid, groupSid));}
public string CreateWebToken(){
return CreateWebToken(DefaultLifetime);}
public string CreateWebToken(TimeSpan lifetime){
// Create JSON Web Tokenvar tokenHandler = new JwtSecurityTokenHandler();
20
var ts = DateTime.UtcNow;var hostName = $"https://{EnvironmentUtils.GetHostName()}/";var signingKey = HexStringUtils.ToByteArray(EnvironmentUtils.
GetSigningKey());
var tokenDescriptor = new SecurityTokenDescriptor{
Subject = new ClaimsIdentity(OutputClaims),AppliesToAddress = hostName,TokenIssuerName = hostName,Lifetime = new Lifetime(ts, ts.Add(lifetime)),SigningCredentials = new SigningCredentials(new
InMemorySymmetricSecurityKey(signingKey),SignatureAlgorithms.HmacSha512,DigestAlgorithms.Sha512)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);}
}
The CreateWebToken method generates a JWT token by creating an instance
of the SecurityTokenDescriptor class, populating its properties with the attributes
related to the issued token, and passing that instance as a parameter to the JwtSe-
curityTokenHandler.CreateToken method. The interesting part is the output claims
list that will be included in the token payload. Every token created by the Authen-
ticationTokenFactory class contains two public claims, which specify the role of
the service and the SID for the group to which the service belongs to. The group
SID is the secret shared between the App Service backend and WebJob instances.
The secret is stored as an app setting entry, which means that it can be easily ac-
cessed and managed via Microsoft Azure portal by navigating to the Application
settings blade of the App Service. Having generated the security token, the We-
bJob console application is ready to make authenticated requests to the backend
service.
In order to protect the service-facing endpoints from being accessed by an unau-
thorized entity, the backend app uses authorization filters. The authorization fil-
ter can be created by subclassing the AuthorizeAttribute class and overriding the
OnAuthorization method with a logic that will verify the identity of the client
21
making the request.
Listing 2.7: Service Authorization Filter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]public sealed class TrustedClientAttribute : AuthorizeAttribute{
public override void OnAuthorization(HttpActionContext actionContext){
var principal = (ClaimsPrincipal)actionContext.RequestContext.Principal;
var groupSid = ConfigurationManager.AppSettings[Constants.Identity.SidSettingKey];
if (principal.HasClaim(ClaimTypes.GroupSid, groupSid)){
base.OnAuthorization(actionContext);}else{
HandleUnauthorizedRequest(actionContext);}
}}
The client identity is verified by comparing the value of the group SID claim
with the shared secret. The TrustedClient attribute can be used to add additional
layer of security to service-facing endpoints.
Listing 2.8: Action Method Representing Service-Facing Endpoint For RetrievingUser ID Tags
[HttpGet][TrustedClient][Route("{id:guid}/tags")][ResponseType(typeof(IEnumerable<string>))]public async Task<IHttpActionResult> GetUserNotificationTags(Guid id){
var device = await UnitOfWork.Devices.GetAsync(id);
if (device == null) return NotFound();
var masterAccount = device.Account;
var accounts = new List<Account> { masterAccount };accounts.AddRange(masterAccount.AffiliateAccounts);
var tags = accounts.Select(a => $"{Constants.Services.NotificationHubUserIdTagPrefix}{a.Sid}").ToList();
return Ok(tags);}
22
2.3 User Authentication & Authorization
In order to secure the mobile backend, Azure App Service provides a built-in
Authentication / Authorization feature that enables the backend application to sign
in users via third-party identity providers (IdPs). Such a federated identity model
greatly simplifies the user authentication process, because the application relies
on the identity information provided by the IdP and it doesn’t have to store that
information itself. In order to authenticate by using one of the supported identity
providers, the Mobile App App Service application must first be configured with
the IdP to obtain the trust relationship. The configuration process usually involves
IDs and secrets, which are generated by the IdP and saved on the App Service
application server. The platform’s backend application has been configured to use
Facebook [10], Google [11], and Twitter [12] identity providers. All three identity
providers are officially supported by the App Service and they conform to OAuth
2.0 standard. The relevant configuration instructions for each of the supported
IdPs are documented on MSDN.
2.3.1 Mobile App Backend Authentication
The process of adding authentication to the App Service .NET backend app is rela-
tively simple and involves installing Microsoft.Azure.Mobile.Server.
Authentication NuGet package and registering the OWIN middleware com-
ponent responsible for validating App Service tokens. The authentication com-
ponent can be registered by calling the UseAppServiceAuthentication extension
method in the application configuration method. Alternatively, one can install
Microsoft.Azure.Mobile.Server.Quickstart NuGet package and
call the UseDefaultConfiguration extension method, which will register a whole
set of OWIN middleware components including the authentication component.
23
Listing 2.9: .NET Backend App Authentication Middleware Configuration
public partial class Startup{
public static void ConfigureMobileApp(IAppBuilder app){
HttpConfiguration config = new HttpConfiguration();
// Web API attribute routingconfig.MapHttpAttributeRoutes();
// Enable required server featuresnew MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
...}
...
}
The ConfigureMobileApp method registers the OWIN middleware authentica-
tion component by calling the UseDefaultConfiguration extension method. Apart
from OWIN middlewares, the ConfigureMobileApp method also configures other
application mechanisms discussed in chapter 4.
Once the application has been correctly configured, the Authorize attribute can be
added to any controller or method that requires authentication.
Listing 2.10: Base Controller Class
[Authorize][MobileAppController]public abstract class AppServiceController : ApiController{
...}
The AppServiceController is an abstract class decorated with the Authorize
attribute, which implies that access to the action methods of its subclasses is re-
stricted to authorized clients. Thus, it is a base class for all controllers that require
authentication.
24
2.3.2 Client App Authentication
There are two approaches for adding authentication to the client app. One can ei-
ther use the provider SDK to establish identity used to gain access to App Service
or let the Mobile Apps client SDK sign in users. The former approach usually
results in more consistent sign in experience and provides the client with provider
token, which makes it much easier to consume graph APIs. The latter approach is
based on the mechanism that is often referred to as server-directed flow, because
the management of the process that signs in users is delegated to the server. In this
approach, the Mobile Apps client SDK will open a web view to the selected iden-
tity provider and sign in the user. This time the client only gets the App Service
JSON web token which is automatically attached to all requests to the App Ser-
vice backend. However, it is still possible for the client to access provider-specific
identity information. At this point, it is worth mentioning that the App Service
Authentication / Authorization feature has an advanced capability called the to-
ken store. The token store is responsible for collecting and storing the OAuth
tokens issued by an authorization server [6] of the identity provider during the
user sign in process. In order to enable access to the identity information stored
in the token store, App Service has a built-in /.auth/me endpoint that supports the
authenticated GET requests. The client can take advantage of this endpoint to ob-
tain a richer set of information about the signed in user.
The current implementation of the UWP client app authenticates users using Mo-
bile Apps client SDK [9]. Note that in order to use that SDK, one must first
install Microsoft.Azure.Mobile.Client NuGet package. To keep the
code loosely coupled, clean, and more reusable the authentication logic has been
extracted into a dedicated service. The service can be registered in IoC container
and injected into view models responsible for managing user sign in process.
25
Listing 2.11: Client Authentication With Mobile Apps Client SDK
public class AuthenticationService : IAuthenticationService{
...
private async Task<bool> AuthenticateCoreAsync(PasswordVault vault,MobileServiceAuthenticationProvider provider)
{bool success = true;
try{
// Facebook and Twitter both have a long access token expiration,however in case of Google authentication provider,
// where the token expiration is 1 hour, we need to use additionalparameters to enable the app to refresh the access tokens.
var loginParams = provider == MobileServiceAuthenticationProvider.Google
? new Dictionary<string, string> { { "access_type", "offline" }, { "approval_prompt", "force" } } :null;
await MobileServiceClient.LoginAsync(provider, loginParams);
var credential = new PasswordCredential(provider.ToString(),MobileServiceClient.CurrentUser.UserId,
MobileServiceClient.CurrentUser.MobileServiceAuthenticationToken);
vault.Add(credential);
var localSettings = ApplicationData.Current.LocalSettings;
localSettings.Values[Constants.UserIdentityProviderLSK] = provider.ToString();
}catch (Exception){
await ForgetCurrentUserCoreAsync(vault);success = false;
}
await UpdateCurrentUserAsync();
return success;}
...
}
The AuthenticateCoreAsync method authenticates users by calling the Logi-
nAsync method of Mobile Apps client SDK, which opens a web view to the spec-
ified authentication provider allowing the user to sign in. The AuthenticateCore-
Async method also takes care of securely storing the authentication token on the
26
client device, so that the user can be signed in automatically the next time the app
is launched.
27
Chapter 3
Cloud Message Routing
One of the most common challenges in cloud computing is the exchange of infor-
mation between individual cloud services. Every non-trivial cloud solution will
use a certain set of services that process and exchange information, and the dis-
cussed solution is no different. This chapter will cover the implementation aspects
of the services responsible for message routing.
3.1 IoT Hub Device-to-Cloud Message Routing
Azure IoT Hub is a communication gateway between the cloud and IoT devices.
It provides a bi-directional communication meaning that it is also responsible for
collecting the data sent by the IoT devices (device-to-cloud messages). IoT Hub
supports two distinct mechanisms for routing messages, namely routing rules and
custom event processor. The first mechanism is much easier to implement and
maintain, therefore it was selected as the preferred way for handling simple rout-
ing scenarios.
In order to configure IoT Hub to route messages to the backend processing ser-
vices via Service Bus messaging (such as queues or topics), one can sign in to
Azure portal, navigate to IoT Hub information pane, and set up messaging end-
points and routes.
28
Figure 3.1: IoT Hub - Endpoints Configuration
In chapter 1, it has been said that the cloud infrastructure would utilize Service
Bus queues for delivering messages to the backend processing services. From
figure 3.1 one can see that the Service Bus queue, used for telemetry data, has
been added to IoT Hubs custom routing endpoints. It is important to note that
the slickhub-telemetry-queue-iothub queue has been created specifically for use
with IoT Hub endpoints. There are certain restrictions [18] for custom endpoints,
which must be met in order for the endpoint to function correctly.
Figure 3.2: IoT Hub - Routes Configuration
Once the custom endpoints have been set up correctly, one can proceed to
29
configuring routes. Figure 3.2 shows a single route, named telemetry-data, which
has been configured to route all the messages satisfying the given condition to the
telemetry-queue endpoint. The route conditions use a dedicated query language
[13] for writing query expressions, which are evaluated on the message properties.
For example, the telemetry-data route will be matched against all messages where
’messageType’ property is assigned ’telemetry’ value and which include ’deviceId’
property. Note that the routing rules do not require to write a single line of code,
which means that changes related to the routing logic can be made on the fly
without worrying about time-consuming implementation and deployment details.
While such a code-free solution is easy to maintain, thus simplifying the de-
velopment, it is not suitable for more advanced routing scenarios where additional
control and data processing is required. Fortunately, IoT Hub makes it possible to
use an alternative mechanism which delivers a fine-grained control over the mes-
sage routing logic.
By default, all inbound messages that do not match any routing rules are written
to the built-in service-facing endpoint messages/events. Such messages can then
be read by the Event Hub Processor, which is one of the commonly used mecha-
nisms for reading data from Event Hubs stream. This mechanism is much more
complex to implement and deploy, but it also offers greater control and flexibility.
In order to implement the event processing service Azure Worker Role project
hosted in Azure Cloud Service has been created. The project utilizes Microsoft.
Azure.ServiceBus.EventProcessorHost and WindowsAzure.ServiceBus
NuGet packages. The technical implementation details are depicted in subsection
3.1.1.
30
3.1.1 Event Processing Service Implementation
The main event processing logic is located in the IEventProcessor.ProcessEventsAsync
method, which is a member of IEventProcessor interface and it is called whenever
there are new messages in the Event Hubs stream. The following listing shows the
method implementation.
Listing 3.1: Processing Events From Event Hubs Stream
async Task IEventProcessor.ProcessEventsAsync(PartitionContext context,IEnumerable<EventData> messages)
{foreach (EventData eventData in messages){
await routeManager.RouteAsync(eventData);}
await context.CheckpointAsync();
Trace.TraceInformation("{0} Checkpoint -> Partition: ’{1}’, Offset: ’{2}’",nameof(SlickHubEventProcessor),
context.Lease.PartitionId, context.Lease.Offset);}
Every message polled from the Event Hubs stream is consumed by the Route-
Manager and the progress is saved by PartitionContext. The RouteManager is
responsible for mapping the message type to the registered route. All the routes
can be registered by calling RegisterRoute method on the RouteManager object
as shown below.
Listing 3.2: Route Manager Configuration
routeManager = new RouteManager();
// Register route for telemetry datarouteManager.RegisterRoute(Constants.Messages.TelemetryMessageId, new
TelemetryQueueRoute());
The above code registers an instance of the TelemetryQueueRoute to be matched
against all messages of type ’telemetry’. Once the matching route has been found,
it is used for forwarding the message to the associated target endpoint.
Listing 3.3: Route Manager Event Data Routing Logic
public async Task RouteAsync(EventData eventData)
31
{if (eventData == null){
throw new ArgumentNullException(nameof(eventData));}
string messageType;
if (!eventData.Properties.ContainsKey(MessageTypeKey) ||(messageType = eventData.Properties[MessageTypeKey] as string) == null ||!routes.ContainsKey(messageType))
{return;
}
var routeList = routes[messageType];
foreach (var route in routeList){
await route.ForwardAsync(eventData);}
}
Every route must implement the IRoute interface, which declares a Target
property representing the target endpoint and ForwardAsync method containing
the logic for forwarding the message.
Listing 3.4: IRoute Interface Declaration
public interface IRoute{
ClientEntity Target { get; }
Task<bool> ForwardAsync(EventData eventData);}
Since the cloud infrastructure assumes the use of multiple queues as messaging
endpoints, it was decided to implement a base class for all the routes that use the
queue client object as the target endpoint. With such a design one can avoid
duplicate code making the service easier to maintain.
Listing 3.5: QueueRouteBase Implementation - IRoute Members
public async Task<bool> ForwardAsync(EventData eventData){
if (eventData == null){
throw new ArgumentNullException(nameof(eventData));}
if (!ValidateEventData(eventData)) return false;
32
var message = CreateBrokeredMessage(eventData);
await queueClient.SendAsync(message);
return true;}
public ClientEntity Target => queueClient;
The base implementation provides two overridable methods: ValidateEvent-
Data and CreateBrokeredMessage. The ValidateEventData method is used for
validating the message data and it contains the logic analogous to the routing rule’s
query expression, which has been discussed earlier. The CreateBrokeredMessage
method wraps the message data inside the BrokeredMessage object, which is then
consumed by the queue client object (an instance of the QueueClient class). The
QueueClient class is a part of the WindowsAzure.ServiceBus API.
Listing 3.6: Creating Brokered Message
protected virtual BrokeredMessage CreateBrokeredMessage(EventData eventData){
string messageId = null;
if (eventData.SystemProperties.ContainsKey("message-id")){
messageId = (string)eventData.SystemProperties["message-id"];}
byte[] data = eventData.GetBytes();
var message = new BrokeredMessage(new MemoryStream(data)){
// Each message is stamped with MessageId to enable Service Bus to ensurethat, in the specified
// deduplication time window, no two messages with the same MessageId aredelivered to the receivers.
MessageId = messageId ?? Guid.NewGuid().ToString()};message.Properties[DeviceIdKey] = eventData.Properties[DeviceIdKey];message.Properties[MessageTypeKey] = eventData.Properties[MessageTypeKey];
return message;}
The concrete queue-based route implementation can derive from the QueueR-
outeBase class by supplying a connection string to the base class constructor. The
supplied connection string is used to create the queue client object.
33
Listing 3.7: Telemetry Data Route Definition
public TelemetryQueueRoute() : base(EnvironmentUtils.GetTelemetryQueueConnectionString())
{ }
Finally, the event processor is registered with the host for processing Event
Hubs event data. The code responsible for host creation and event processor reg-
istration is contained in the WorkerRole class that extends the RoleEntryPoint class
and adds functionality to the role instances.
Listing 3.8: Worker Role Entry Point
public override void Run(){
Trace.TraceInformation("{0} Running", nameof(EventProcessorRole));
// Register the Event ProcessoreventProcessorHost.RegisterEventProcessorAsync<SlickHubEventProcessor>().Wait
();
// Keep the Worker Role runningrunCompleteEvent.WaitOne();
}
public override bool OnStart(){
Trace.TraceInformation("{0} Starting", nameof(EventProcessorRole));
// Set the maximum number of concurrent connectionsServicePointManager.DefaultConnectionLimit = 12;
var iotHubConnectionString = EnvironmentUtils.GetIotHubConnectionString();var storageConnectionString = EnvironmentUtils.GetStorageConnectionString();var eventProcessorHostName = RoleEnvironment.CurrentRoleInstance.Id;
eventProcessorHost = new EventProcessorHost(eventProcessorHostName, Constants.Endpoints.IotHubD2CEndpoint,
EventHubConsumerGroup.DefaultGroupName, iotHubConnectionString,storageConnectionString,
Constants.Storage.LeaseContainerName);
bool result = base.OnStart();
return result;}
After the Azure Worker Role project has been deployed to the Azure Cloud
Service, all connection strings stored in the ServiceConfiguration.Cloud.
cscfg file can be easily modified through the Azure Cloud Service Configuration
blade as can be seen in figure 3.3.
34
Figure 3.3: Event Processing Service - Cloud Service Configuration
3.2 Service Bus Queue Message Routing
All messages that arrive at IoT Hub are being routed to the matching Service
Bus queue in order to be delivered to the backend processing services, which in-
clude Azure App Service Mobile App and Azure Notification Hub. Before the
messages can be delivered to those services, they need to be polled from the
Service Bus queue and wrapped into packages supported by each service. The
cloud infrastructure assumes the use of Azure WebJobs, which allow for run-
ning programs and scripts alongside the Mobile App service. In order to sat-
isfy the mentioned assumptions a Console Application has been created. The ap-
plication utilizes the Microsoft.Azure.WebJobs, Microsoft.Azure.
Mobile.Client, Microsoft.Azure.NotificationHubs, WindowsAzure.
ServiceBus, System.IdentityModel.Tokens.Jwt, and Microsoft.
Practices.EnterpriseLibrary.TransientFaultHandlingNuGet
packages. The technical implementation details are depicted in subsection 3.2.1.
3.2.1 WebJob Console Application Implementation
Similarly to the Event Processing Service, the WebJob project has been carefully
crafted to ensure clean architecture. The main logic responsible for polling and
processing queue messages has been located in PushService class. The instances
35
of this class can be created by providing the PushServiceConfiguration object
which stores preconfigured instances of MobileServiceClient and Notification-
HubClient classes (both of which are the part of Azure API). Once initialized,
the PushService can be wired-up to the source Service Bus queue via the connec-
tion string.
Listing 3.9: Push Service Configuration
private PushService CreateAndConfigurePushService(){
var configuration = PushServiceConfiguration.DefaultConfiguration;
var pushService = new PushService(configuration);
// Setup push notifications for telemetry datavar telemetryQueueConnectionString = EnvironmentUtils.
GetTelemetryQueueConnectionString();
var telemetryDataConsumer = new TelemetryDataConsumer(configuration.MobileServiceClient);
pushService.Subscribe(telemetryQueueConnectionString, telemetryDataConsumer);
return pushService;}
The PushService.Subscribe method also allows for specifying an optional mes-
sage data consumer, which will be called every time a new message is being pro-
cessed. In case of telemetry data, it is necessary to update rows in the database
to ensure data integrity between the client application and backend service. The
TelemetryDataConsumer.ProcessDataAsync method contains the logic that invokes
REST API responsible for updating telemetry data for the specified device.
Listing 3.10: Updating Telemetry Data
public override async Task ProcessDataAsync(string deviceId, string data){
if (deviceId == null || data == null) return;
await RequestExecutor.ExecuteAsync(() =>MobileServiceClient.InvokeApiAsync($"v1/devices/{deviceId}/telemetry"
, JToken.FromObject(data)));
Trace.TraceInformation("{0} Telemetry data updated", Assembly.GetEntryAssembly().GetName().Name);
}
36
All data consumer components derive from the DeviceDataConsumerBase ab-
stract class, which implements IDeviceDataConsumer interface and provides its
subclasses with RequestExecutor object that ensures transient fault handling while
executing REST API requests.
The queue message processing is delegated to the OnMessageCallbackAsync call-
back method that is registered with QueueClient.OnMessageAsync method. Every
message is validated, wrapped into notification payload that is sent via Notifica-
tion Hub, and passed to the optional data consumer.
Listing 3.11: Queue Message Processing
private async Task OnMessageCallbackAsync(BrokeredMessage message,IDeviceDataConsumer dataConsumer)
{try{
string messageType, deviceId;
if (!message.TryGetProperty(MessageTypeKey, out messageType) || !message.TryGetProperty(DeviceIdKey, out deviceId))
{await message.DeadLetterAsync();return;
}
var tags = await GetNotificationTagsAsync(deviceId);var data = message.ReadMessageData(Encoding.UTF8);
if (tags != null && data != null){
var payload = new NotificationPayload(messageType, data);
await SendNotificationAsync(payload, tags);
if (dataConsumer != null){
await dataConsumer.ProcessDataAsync(deviceId, data);}
await message.CompleteAsync();}else{
await message.DeadLetterAsync();}
}catch (Exception){
await message.AbandonAsync();}
}
37
The current implementation only supports Windows Push Notification Ser-
vice (WNS) and uses general purpose push notifications called raw notifications.
The notification payload objects are serialized to a JSON string, enclosed in an
instance of the WindowsNotification class, and sent to the authenticated users.
Listing 3.12: Sending Targeted Push Notifications
private async Task SendNotificationAsync(NotificationPayload payloadObject,IEnumerable<string> tags)
{var payload = payloadObject.CreatePayloadString();
// According to MSDN, the string payload of the raw notification MUST besmaller than 5 KB in size.
// For further information, see https://msdn.microsoft.com/en-us/library/windows/apps/jj676791.aspx
// and https://msdn.microsoft.com/en-us/library/windows/apps/hh761463.aspx.if (Encoding.UTF8.GetByteCount(payload) >= 5120){
Trace.TraceWarning("{0} Dropping notification payload - size limit (5KB)exceeded", Assembly.GetEntryAssembly().GetName().Name);
return;}
// Create a payload for raw notificationNotification notification = new WindowsNotification(payload);notification.ContentType = "application/octet-stream";notification.Headers["X-WNS-Type"] = "wns/raw";
var outcome = await NotificationHubClient.SendNotificationAsync(notification,tags);
Trace.TraceInformation("Notification Hub -> Notification State: {0}", outcome.State);
}
Every user authenticated by the Mobile App backend service is automatically
tagged with User ID to enable targeted push notifications [15]. In order to retrieve
those tags, the WebJob application executes REST API request that returns a list
of User ID tags matched with the specified device ID.
Listing 3.13: Retrieving User ID Tags From Mobile App Backend
private async Task<IEnumerable<string>> GetNotificationTagsAsync(string deviceId){
var queryParams = MobileServiceClient.GetApiQueryParameters();
var json = await requestExecutor.ExecuteAsync(() =>MobileServiceClient.InvokeApiAsync($"v1/devices/{deviceId}/
tags", HttpMethod.Get, queryParams))as JArray;
38
return json?.Select(t => t.ToObject<string>());}
The REST API requests are executed by an instance of the RequestExecutor
class to ensure transient fault handling. The retry logic of the RequestExecutor
class is based on the retry mechanisms (RetryPolicy and RetryStrategy) provided
by Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling
API.
Listing 3.14: Transient Fault Handling - Retry Policy Creation
private RetryPolicy<T> CreateRequestRetryPolicy(){
var retryStrategy = new ExponentialBackoff(3, TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(2));
var policy = new RetryPolicy<T>(retryStrategy);
policy.Retrying += (o, args) =>{
Trace.TraceInformation("RequestExecutor Retry Policy -> Count: {0}, Delay: {1}, Exception: {2}",
args.CurrentRetryCount, args.Delay, args.LastException);
retryCallback?.Invoke(args.LastException);};
return policy;}
After the WebJob project has been deployed to the Azure App Service Mobile
App, all connection strings and app settings stored in the app.config file can
be easily modified through the Azure App Service Application Settings blade as
can be seen in figure 3.4.
39
Figure 3.4: App Service - Mobile App & WebJobs Settings
40
Chapter 4
Mobile App Backend
One of the major challenges in developing cloud solutions is to provide a secure
way for the users to interact with cloud services. To address this challenge, the
cloud providers introduced a model called Mobile Backend as a Service (MBaaS).
MBaaS is a backend solution that greatly simplifies the process of scaffolding
RESTful API for multi-platform mobile apps by providing a unified API that
adds an abstraction layer to the server-side infrastructure. In addition, MBaaS
also provides integration with social networks, push notifications, and cloud stor-
age. It can be said that MBaaS acts as a bridge between client app and various
cloud-based services. Microsoft Azure offers MBaaS solution called Azure Mo-
bile App, which delivers all of the aforementioned features. The discussed IoT
platform uses this service to enable a secure bi-directional communication be-
tween the client application and other cloud services that make up the platform.
Azure Mobile App provides a wide range of supported backend platforms for de-
veloping custom back-end logic. At the time of writing, the supported platforms
include ASP.NET, Node.js, PHP, Java, and Python. In order to ensure a com-
mon programming language across all cloud services, it was decided to develop
backend service with C# and ASP.NET.
41
4.1 .NET Backend
The REST API was built based on ASP.NET Web API framework. A special fo-
cus was put on authentication 1, versioning, maintainability, and testability. The
service is hosted within a server compatible with Open Web Interface for .NET
(OWIN). OWIN defines a standard interface that allows for decoupling web appli-
cation from the underlying web server. In addition, OWIN allows to split the web
app infrastructure into separate modules, which can be chained together into a
middleware pipeline. The backend service uses OWIN to register various middle-
ware components responsible for adding default home page, mapping endpoints
to table controllers, integration with Notification Hub, authentication, and custom
API capabilities for controllers used by Azure Mobile App client. The middle-
ware components are registered into the application pipeline by OWIN startup
class, which is connected with the hosting runtime via an OwinStartup attribute.
Listing 4.1: .NET Backend App Configuration
[assembly: OwinStartup(typeof(SlickHubWebApi.Startup))]
public partial class Startup{
public static void ConfigureMobileApp(IAppBuilder app){
HttpConfiguration config = new HttpConfiguration();
// Web API attribute routingconfig.MapHttpAttributeRoutes();
// Enable required server featuresnew MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
var container = CreateContainer();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
// Register the Autofac middleware FIRSTapp.UseAutofacMiddleware(container);
// Use Entity Framework Code First to create database tables
1The backend app authentication is covered in chapter 2.
42
Database.SetInitializer(new MigrateDatabaseToLatestVersion<MobileServiceContext, MigrationsConfiguration>());
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().GetMobileAppSettings();
if (string.IsNullOrEmpty(settings.HostName)){
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions{
// This middleware is intended to be used locally for debugging.By default, HostName will
// only have a value when running in an App Service application.SigningKey = ConfigurationManager.AppSettings[Constants.Identity.
WebsiteAuthSigningKeySettingKey],ValidAudiences = new[] { ConfigurationManager.AppSettings[
Constants.Identity.WebsiteAuthValidAudienceSettingKey] },ValidIssuers = new[] { ConfigurationManager.AppSettings[Constants
.Identity.WebsiteAuthValidIssuerSettingKey] },TokenHandler = config.GetAppServiceTokenHandler()
});}
// Register Web API middlewaresapp.UseAutofacWebApi(config);app.UseWebApi(config);
}
...
}
Apart from registering middleware components that are specific to Azure Mo-
bile App service, the
emphConfigureMobileApp method also configures HTTP request routing, depen-
dency injection (DI) container, and the database initializer.
When designing the software, one of the most important goals is to ensure loosely-
coupled architecture. In order to achieve that goal, the application uses Autofac
IoC container that manages the dependencies between controllers. The use of
DI increases the flexibility and testability of code modules allowing to easily
inject components with mock data. However, in case of backend services, the
well-designed application architecture is not sufficient. Since the backend service
exposes API to be used by the clients, it is also important to introduce API ver-
sioning to make sure that the API can be updated without affecting the existing
functionality. There are three most commonly used ways to accomplish API ver-
43
sioning. One can either specify the version information in the URL, use custom
request headers, or use accept headers. In the discussed project, it was decided
to use versioned URL due to its simplicity and ease of implementation with a use
of attribute routing mechanism. With attribute routing it is possible to include the
version number for each controller in the route prefix specified by the RoutePre-
fix attribute. In such a way, every controller is explicitly decorated with the API
version to which it belongs.
Listing 4.2: Versioned URL With Attribute Routing
[RoutePrefix("api/v1/devices")]public class DevicesController : AppServiceController{
...}
[RoutePrefix("api/v1/accessories")]public class AccessoriesController : AppServiceController{
...}
[RoutePrefix("api/v1/bootstrap")]public class BootstrapController : AppServiceController{
...}
Moving on to the REST API itself, the application exposes a set of endpoints
for registering IoT gateways (see chapter 2), retrieving a list of registered IoT
gateways for an authorized user, sending cloud commands to the selected IoT
accessory, and retrieving the current state of the IoT accessory. In addition, there
is a set of service-facing endpoints used by the WebJob instances as discussed in
chapter 3. All of the controller action methods operate on data transfer objects
(DTOs) that represent the relevant request and response data. It was decided to
use DTOs in order to avoid exposing the entity data that should be hidden from
the client.
Listing 4.3: Action Method Returning DTO With Accessory State
[RoutePrefix("api/v1/accessories")]
44
public class AccessoriesController : AppServiceController{
...
[HttpGet][Route("{id:guid}")][ResponseType(typeof(AccessoryDescriptionResponse))]public async Task<IHttpActionResult> Get(Guid id){
var account = await GetAuthorizedUserAccountAsync<MasterAccount>();
if (account == null) return BadRequest();
var accessory = await UnitOfWork.Accessories.GetAsync(id);
if (accessory == null) return BadRequest();
var device = accessory.Device;
// Check request integrityif (device.Account.Sid != account.Sid && device.Account.Id != account.Id){
return BadRequest();}
return Ok(new AccessoryDescriptionResponse{
Description = accessory.PropertySet});
}}
[DataContract(Name = "AccessoryDescriptionResponse")]public class AccessoryDescriptionResponse{
[DataMember(Name = "description", IsRequired = true)]public string Description { get; set; }
}
The AccessoryDescriptionResponse class represents a DTO that is used by the
AccessoriesController.Get(Guid) action method to return the state of the acces-
sory with the specified identifier. It is worth noting that all data transfer objects
are decorated with DataContract attribute to indicate that they can be serialized
by Web API media-type formatters. The media-type formatters are responsible for
reading CLR objects from an HTTP request body and writing CLR objects into
an HTTP response body. With DTO, it is possible to return only the JSON string
that represents the accessory state without exposing the rest of the data contained
in Accessory entity.
Looking at the above code snippets, one can notice that the controller classes de-
45
rive from AppServiceController class. As mentioned in chapter 2, the AppService-
Controller is an abstract base class for all controllers that require authentication.
Listing 4.4: Methods Accessible by AppServiceController Subclasses
[Authorize][MobileAppController]public abstract class AppServiceController : ApiController{
...
protected async Task<TAccount> GetAuthorizedUserAccountAsync<TAccount>()where TAccount : Account, new()
{var account = await GetAuthorizedUserAccountAsync();
if (account == null) return null;
if (account.GetType() == typeof (AffiliateAccount) && typeof (TAccount)== typeof (MasterAccount))
{return ((AffiliateAccount) account).MasterAccount as TAccount;
}
return account as TAccount;}
protected async Task<Account> GetAuthorizedUserAccountAsync(){
var principal = (ClaimsPrincipal) User;
var id = principal.GetGloballyUniqueUserId();
if (id == null){
throw new InvalidOperationException("Identity does not exist.");}
// Get a hash used for linking a user across two providersvar sid = principal.FindFirst(ClaimTypes.NameIdentifier).Value;
var account = (Account) await GetAccountFromDatabaseAsync<MasterAccount>(id, sid) ??
(Account) await GetAccountFromDatabaseAsync<AffiliateAccount>(id, sid);
return account;}
protected async Task<TAccount> CreateAuthorizedUserAccountAsync<TAccount>()where TAccount : Account, new()
{var principal = (ClaimsPrincipal) User;
var id = principal.GetGloballyUniqueUserId();
if (id == null){
throw new InvalidOperationException("Identity does not exist.");}
46
// Get a hash used for linking a user across two providersvar sid = principal.FindFirst(ClaimTypes.NameIdentifier).Value;
var account = await AddAccountToDatabaseAsync<TAccount>(id, sid);
return account;}
...}
The AppServiceController class provides its subclasses with GetAuthorize-
dUserAccountAsync and GetAuthorizedUserAccountAsync<TAccount> methods,
which retrieve an entity that represents authenticated identity associated with a
client request. The two methods are also used to determine whether an autho-
rized user is actually registered with the platform. The user can be registered by
calling a CreateAuthorizedUserAccountAsync<TAccount> method, which is also
provided by the AppServiceController class. It is worth noting that the user ac-
count is created based on the information contained in a principal associated with
the request. The principal contains the claims generated by App Service Authen-
tication feature. There are two very useful claims that represent the user IDs,
namely sid and stable_sid [7]. The value of the stable_sid claim represents a sta-
ble and unique (when combined with the provider name) user ID, therefore it is
used as a primary key for user account. The value of sid claim is the hash ID
generated based on the user email and it is used for linking user account across
different identity providers.
By extracting the aforementioned methods to a base class it was possible to avoid
code duplication, since vast majority of action methods manipulate data associ-
ated with authorized user.
47
4.2 Data Persistence
One of the most important aspects of MBaaS solution is the integration with cloud
storage. There are two commonly used approaches for setting up a cloud storage,
both of which are supported by Azure. One can either use an Infrastructure as a
Service (IaaS) approach or Platform as a Service (PaaS) approach. To enable the
use of IaaS, Azure provides an option to install and host SQL Server on Azure
Virtual Machines. This approach is optimized for migrating existing SQL Server
applications from on-premises to cloud-based solutions. When compared with
PaaS approach, IaaS provides much greater control over the operating system, the
specific version of the database engine used, updates, and backups. However, a
greater control also requires a lot more operational work during the development
and maintenance of the solution. Such administrative costs can be minimized
to a great extent by using PaaS solutions. Due to the cost savings and perfor-
mance optimization, PaaS is a recommended approach for building new cloud
storage applications. Following from these guidelines, the discussed Azure Mo-
bile App backend service was configured to use a PaaS-based cloud storage solu-
tion, known as Azure SQL Database [17]. The current configuration uses a single
instance of Azure SQL database that is assigned a set of fully-managed compute
and storage resources. Every Azure SQL database instance is hosted on Azure
SQL Database logical server, which allows for configuring firewall rules for the
databases. To obtain a connection to the database, the backend application uses
a connection string, which consists of key/value parameter pairs that specify the
server name, database name, and SQL authentication credentials.
Upon configuring cloud storage, it was possible to design a relational database
for the discussed IoT platform. The database was designed by determining the
entities to be stored in the database and establishing the relationships between
48
those entities. The resulting database model consists of four entities: Master-
Account representing a registered user account, AffiliateAccount representing a
linked user account used to enable multiple users on a single IoT gateway device,
Device representing a registered IoT gateway device, and Accessory representing
an IoT accessory connected to IoT gateway. Figure 4.1 shows a detailed entity
relationship diagram describing the structure of the individual entities and their
relationships to each other. Since the Azure SQL Database service uses Microsoft
SQL Server Engine, the diagram includes the relevant data types and constraints
for entity columns.
Figure 4.1: ERD Physical Model For Microsoft SQL Server
To simplify the tasks related to database creation and updating database schema,
it was decided to use Entity Framework code first migrations. The code first ap-
49
proach allows to create the database based on the POCO classes, which represent
the domain entities. In addition, code first migrations allow to update the database
schema without loss of data.
Listing 4.5: POCO Class For Device Entity
public class Device{
[Key][DatabaseGenerated(DatabaseGeneratedOption.None)]public Guid Id { get; set; }
[MaxLength(255)]public string Name { get; set; }
public string TelemetryData { get; set; }
[MaxLength(128)]public string AccountId { get; set; }
[ForeignKey("AccountId")]public virtual MasterAccount Account { get; set; }
public virtual ICollection<Accessory> Accessories { get; set; } = new HashSet<Accessory>();
}
Apart from code first migrations, it was also decided to use Entity Frame-
work to automate a standard CRUD operations. Since Entity Framework is an
object-relational mapper (ORM), it simplifies the data-access code by mapping
domain-specific objects to relational database objects. In other words, the ORM
allows to perform CRUD operations against the database using strongly typed ob-
jects and queries. In this way, it solves the object-relational impedance mismatch
problem and enables the developer to write more maintainable and extendable
code. However, despite its undoubtful advantages, the Entity Framework has a
disadvantage of generating SQL queries, which are less efficient than their raw
SQL equivalents. It is therefore important to design the application architecture
that will allow to easily switch to raw SQL queries in case the performance of
ORM will become a bottleneck. In order to achieve such a flexible architecture,
it was decided to use repository and unit of work patterns to add an additional
abstraction layer between data access layer and business logic layer. By imple-
50
menting these patterns it was possible to insulate the business logic from changes
in the data access logic.
Listing 4.6: Repository Class For MasterAccount & AffiliateAccount Entities
public sealed class AccountRepository : RepositoryBase<Account, string>,IAccountRepository
{...
public async Task<TAccount> GetAsync<TAccount>(string id) where TAccount :Account
{if (id == null){
throw new ArgumentNullException(nameof(id));}
var account = await InternalContext.Set<TAccount>().FindAsync(id);
return account;}
public async Task<TAccount> GetBySidAsync<TAccount>(string sid) whereTAccount : Account
{if (sid == null){
throw new ArgumentNullException(nameof(sid));}
var account = await InternalContext.Set<TAccount>().FirstOrDefaultAsync(a=> a.Sid == sid);
return account;}
public void Add<TAccount>(TAccount account) where TAccount : Account{
if (account == null){
throw new ArgumentNullException(nameof(account));}
InternalContext.Set<TAccount>().Add(account);}
public void Remove<TAccount>(TAccount account) where TAccount : Account{
if (account == null){
throw new ArgumentNullException(nameof(account));}
InternalContext.Set<TAccount>().Remove(account);}
...}
51
Every repository class implements an interface, which defines a contract used
to manipulate the entity data. The application controllers are only aware of the
contract and not the concrete implementation, thus it is possible to modify a spe-
cific implementation without affecting the dependent code or inject a mock imple-
mentation to perform unit tests. Since the current implementation of the backend
application utilizes the Entity Framework every repository class requires a refer-
ence to the database context. To avoid undesired partial updates, it is necessary
to use a single, shared instance of database context for all repositories. Such a
behavior was achieved by creating a unit of work class, which provides a Com-
pleteAsync method for coordinating the database updates. The unit of work class
also provides a set of read-only properties to enable controllers to access the repos-
itories.
Listing 4.7: Unit of Work Class
public class UnitOfWork : IUnitOfWork{
private readonly MobileServiceContext _context;
...
public async Task<int> CompleteAsync(){
return await _context.SaveChangesAsync();}
public IAccountRepository Accounts => _accounts ?? (_accounts = newAccountRepository(_context));
public IDeviceRepository Devices => _devices ?? (_devices = newDeviceRepository(_context));
public IAccessoryRepository Accessories => _accessories ?? (_accessories =new AccessoryRepository(_context));
...}
To ensure loose coupling the unit of work class is registered with the depen-
dency injection container. The registered implementation of IUnitOfWork inter-
face can be injected into the constructors of the dependent controllers, thus im-
proving the testability of the code.
52
Listing 4.8: Registration of IUnitOfWork Implementation With DI Container
public partial class Startup{
...
private static IContainer CreateContainer(){
var builder = new ContainerBuilder();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
RegisterDependencies(builder);
var container = builder.Build();
return container;}
private static void RegisterDependencies(ContainerBuilder builder){
// Register unit of workbuilder.RegisterType<UnitOfWork>().As<IUnitOfWork>();
...}
}
53
Chapter 5
IoT Gateway
While the concept of IoT brings a large set of new possibilities, it also introduces
non-trivial security challenges related to data exchange. Let’s imagine a smart
home in which we have smart LED lighting system, air conditioner, thermostat,
automated window blinds, and a couple of smart plugs. Now let’s say that we
would like to be able to remotely control all of our smart devices from anywhere
in the world, provided there’s an Internet connection available. The trivial solution
would be to connect individual IoT devices to the mobile backend service that
would handle the communication between user’s mobile device and IoT device.
Such an architecture implies that the number of registered IoT devices would be
directly proportional to the number of IoT devices owned by the users, which
would result in serious security and scalability problems. To solve these problems,
IoT industry has introduced the concept of IoT gateway. An IoT gateway can be
thought of as a device that manages the local ecosystem of IoT accessories. The
gateway device is responsible for security tasks and protocol translation enabling
the devices without direct access to the Internet (such as Bluetooth devices) to
reach cloud services.
54
5.1 OS and Hardware
On 29 July 2015, Microsoft released Windows 10 operating system. Along with
the baseline Windows 10 editions, Microsoft also released the device-specific edi-
tions such as Windows 10 IoT Core, which was designed specifically for use in
small-footprint devices and IoT scenarios. From an IoT developer’s point of view,
Windows 10 IoT Core is particularly interesting due to the fact that it has a built-
in support for the Universal Windows Platform (UWP) apps running on top of
WinRT stack and it is the first OS in Windows embedded OS family that is avail-
able for free. These two factors make Windows 10 IoT Core an optimal choice for
IoT gateway solution, as the UWP makes it possible to build an app with a pow-
erful set of features and modern UI design, while keeping costs low. We should
not forget, however, that due to the lightweight nature of embedded operating sys-
tems, Windows 10 IoT Core also has some limitations that the developer must be
aware of. One of the major limitations is a lack of Win32 desktop shell, which
means that there is no support for Win32 GUI apps. Yet another limitation is that
Windows 10 IoT Core only allows one foreground app to run at any given time. As
an example, the above-mentioned limitations imply that there is no way to open a
new window with embedded web view to sign in user using server-directed flow.
It is therefore important to take these limitations into consideration when design-
ing UWP app targeting IoT device family.
Having selected Windows 10 IoT Core as an OS for the IoT gateway, it was possi-
ble to move on to hardware selection. The first thing was to choose a single-board
computer (SBC). At the time of writing, Windows 10 IoT Core includes support
for Raspberry Pi 2/3 Model B, MinnowBoard MAX, Qualcomm DragonBoard
410c, and Intel Joule. Among the supported SBCs, Raspberry Pi is the most af-
fordable unit equipped with all the necessary features for creating IoT gateway.
55
Since the Raspberry Pi 2 Model B was already owned, the initial plan was to
reuse it for making the gateway device. The only problem with RPi 2 Model B is
the lack of built-in Bluetooth Low Energy support, therefore it was necessary to
use an external BLE module. Such a module can be easily connected using Gen-
eral Purpose Input Output (GPIO) pins of the RPi. The first attempt was Adafruit
Bluefruit LE UART Friend module [2]. The module uses the Nordic nRF51822
SoC with custom firmware that includes a support for AT commands. Unfortu-
nately, the Adafruit module can only operate in peripheral (GATT Server) mode,
which means that there is no way for this module to detect other BLE peripherals
in range. In order to discover other BLE devices and send connection requests,
the IoT gateway must operate in GATT Client mode. The Adafruit module does
not satisfy this requirement, thus there was a need to look for an alternative unit.
The second attempt was a HM-11 BLE module [4], which uses Texas Instru-
ments CC2540 SoC with custom firmware that also includes a support for AT
commands. The module supports both GATT Server and GATT Client opera-
tional modes, which can be switched using AT+ROLE AT command. While the
HM-11 module can scan for other BLE devices and send connection request to
the selected device, it has a limit of only one BLE connection at a time. After
the HM-11 is connected to another BLE device, the module stops responding to
AT commands and acts as a serial port emulator allowing for data transmission
only. Such a drawback is also unacceptable for the IoT gateway device, as the
gateway should allow users to connect and control multiple BLE accessories. The
two failed attempts of using the external BLE module were convincing enough
to switch to Raspberry Pi 3, which has a built-in Bluetooth support without the
above-mentioned limitations. An additional advantage of using RPi 3 is that it is
possible to use the UWP Bluetooth API for managing BLE connectivity, which
allows to save a lot of development time and write more maintainable code. Apart
56
from Bluetooth support, the RPi 3 also includes a built-in WiFi module, which
means that it has a greater potential for adding new connectivity capabilities.
Upon selecting the SBC that satisfies the Bluetooth connectivity requirements, it
was possible to move on to selecting the sensors for collecting telemetry data. In
order to collect the temperature and humidity readings the decision was taken to
use the popular AM2302 DHT22 digital sensor module. For collecting the atmo-
spheric pressure readings the high precision BMP180 digital barometric pressure
sensor module was selected. The two selected modules use 1-Wire and I2C serial
communication protocols respectively. Both protocols are supported by RPi 3 and
Windows 10 IoT Core.
Figure 5.1: IoT Gateway Wiring
5.2 UWP Application
While designing a UWP app for IoT gateway, one of the main goals was to cre-
ate a solid code base for future app development. It is crucial to carefully craft
the scalable application architecture that will allow for adding new features while
57
keeping the code base manageable. In order to achieve that it was decided to use
a Prism framework [23] for UWP. The Prism framework provides an implementa-
tion of various design patterns including Model-View-ViewModel (MVVM), De-
pendency Injection, and Event Aggregator. In addition, Prism provides platform-
specific services responsible for navigation and session state management.
The current app implementation supports collecting and sending telemetry data
(see subsections 5.2.1 and 5.2.3), controlling BLE smart bulb (see subsection
5.2.2), and parsing cloud commands to GATT transactions (see subsection 5.2.3).
To enable access to APIs required to implement the aforementioned functionali-
ties it was necessary to declare a relevant capabilities in app’s package manifest.
Listing 5.1: App Capability Declarations
<?xml version="1.0" encoding="utf-8"?><Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"xmlns:iot="http://schemas.microsoft.com/appx/manifest/iot/windows10"IgnorableNamespaces="uap mp iot">
...
<Dependencies><TargetDeviceFamily Name="Windows.IoT" MinVersion="10.0.0.0"
MaxVersionTested="10.0.0.0" /></Dependencies>
...
<Capabilities><Capability Name="internetClient" /><iot:Capability Name="systemManagement" /><iot:Capability Name="lowLevelDevices" /><DeviceCapability Name="bluetooth" />
</Capabilities></Package>
58
The app’s user interface consists of a single dashboard screen (see figure
5.2), which displays the name of IoT gateway, the current readings from sen-
sor modules, and network connectivity status. The application also includes the
developer tools (see figure 5.3) that provide system information, a list of con-
nected BLE accessories, and a list of log messages. The app UI was created in
XAML and the data binding was used to link the data with UI controls. In ad-
dition, the Microsoft.Toolkit.Uwp.UI, Microsoft.Toolkit.Uwp.
UI.Controls, and Microsoft.Xaml.Behaviors.Uwp.ManagedNuGet
packages were used in order to simplify the UI development.
Figure 5.2: IoT Gateway App Dashboard
59
Figure 5.3: IoT Gateway App Developer Tools
5.2.1 Sensor Drivers
In order to get the sensor readings, it is necessary to use a dedicated sensor drivers.
In case of Linux-based operating systems there are multiple implementations of
Linux drivers for both DHT22 and BMP180 sensor modules, which are free to
download and easy to use. In case of Windows 10 IoT Core, however, the sensor
driver availability is a lot more problematic. While developing the IoT gateway, it
was not possible to find the UWP-compatible drivers for the selected modules and
so it was necessary to implement the required drivers from scratch. In order to do
so, the decision was made to use C++/CX 1 to create a Windows Runtime com-
ponent that can be used by a UWP app. To access device GPIO, it was necessary
to use Windows IoT Extension SDK. It is worth noting that it is also possible to
create a managed Windows Runtime component with C# programming language,
but to ensure optimal performance of GPIO operations it is recommended to use
1C++/CX is a set of language extensions for C++ compilers introduced by Microsoft to enablethe creation of Windows apps and WinRT components.
60
native code.
Moving on to the drivers implementation, it started off by creating the driver for
DHT22 sensor module (also known as AM2302). The driver has been imple-
mented based on the official AM2302 datasheet [3].
Listing 5.2: DHT22 - Interface Declaration
public ref struct DhtReading sealed{
private:const double _temperature;const double _humidity;unsigned _retryCount;
DhtReading(double temperature, double humidity): _temperature(temperature), _humidity(humidity), _retryCount(0)
{ }internal:
static DhtReading^ CreateFromBitset(std::bitset<40>* bits);public:
property double Temperature {double get() {
return _temperature;}
}property double Humidity {
double get() {return _humidity;
}}property uint32 RetryCount {
uint32 get() {return _retryCount;
}internal:
void set(uint32 value) {_retryCount = value;
}}
};
public ref class Dht22 sealed{
public:virtual ~Dht22();
static Windows::Foundation::IAsyncOperation<Dht22^>^ InitAsync(intdataPinNumber);
Windows::Foundation::IAsyncOperation<DhtReading^>^ TryGetReadingAsync();Windows::Foundation::IAsyncOperation<DhtReading^>^ TryGetReadingAsync(
uint32 maxRetryCount);
property Windows::Devices::Gpio::GpioPin^ DataPin {Windows::Devices::Gpio::GpioPin^ get() {
return _pin;}
61
}private:
enum DHT22 { MCU_START_SIGNAL_MILLIS = 18, DEFAULT_MAX_RETRY_COUNT = 20};
Windows::Devices::Gpio::GpioPin^ _pin;Windows::Devices::Gpio::GpioPinDriveMode inputReadMode;
Dht22(Windows::Devices::Gpio::GpioController^ controller, intdataPinNumber);
inline HRESULT WaitForSensorResponseSignal() const;internal:
DhtReading^ InternalGetReading() const;};
The AM2302 module uses the 1-Wire protocol to communicate with the mi-
crocontroller unit (MCU). It is worth noting, however, that the module is not com-
patible with Dallas 1-Wire bus in that it does not have a unique 64-bit address, thus
each module needs its own single bi-directional digital I/O line to the MCU. The
bits that the AM2302 module sends to MCU are encoded as pulse widths. To
begin the communication, the MCU sends a start signal by pulling the data-bus
low for at least 18ms. Upon receiving the start signal, AM2302 sends a response
signal followed by the 40-bit data that reflect the relative humidity and tempera-
ture. The first 16 bits represent the relative humidity (RH) data, the next 16 bits
represent the temperature (T) data, and the last 8 bits represent the check sum. To
enable the use of 1-Wire interface, the Windows IoT Extension SDK provides the
GpioController and GpioPin classes.
Listing 5.3: DHT22 - Getting Sensor Reading
DhtReading ^ Dht22::InternalGetReading() const{
DhtReading^ reading = nullptr;
LARGE_INTEGER pcf; // a performance-counter frequency (in counts per second)QueryPerformanceFrequency(&pcf);
// A ’0’ has a pulse time of 76us, while a ’1’ has a pulse time of 120us -// 110us is chosen as a reasonable threshold used to determine whether a bit
is a ’0’ or a ’1’.// We convert the value to QPC units for later use.const unsigned int oneThreshold = static_cast<unsigned int>(110LL * pcf.
QuadPart / 1000000LL);
// Set pin as output
62
this->_pin->SetDriveMode(GpioPinDriveMode::Output);
// Data-bus’s free status is high voltage level - latch low value onto pin// to begin communication.this->_pin->Write(GpioPinValue::Low);
// Send start signal and keep this signal at least 18 msSleep(MCU_START_SIGNAL_MILLIS);
// Latch high value onto pinthis->_pin->Write(GpioPinValue::High);
// Set pin back to inputthis->_pin->SetDriveMode(this->inputReadMode);
HRESULT hr = this->WaitForSensorResponseSignal();
if (hr != S_OK){
return reading;}
bitset<40> bits; // a buffer for the 40-bit reading
GpioPinValue prevValue = this->_pin->Read(), currValue;LARGE_INTEGER prevTime = { 0 }, currTime;
const ULONG readingTimeoutMillis = 10;ULONGLONG endTickCount = GetTickCount64() + readingTimeoutMillis;
// Capture every falling edge until all bits are received or timeout occurs.for (unsigned int i = 0; i < (bits.size() + 1);){
if (GetTickCount64() > endTickCount){
return reading;}
currValue = this->_pin->Read();
if ((prevValue == GpioPinValue::High) && (currValue == GpioPinValue::Low))
{QueryPerformanceCounter(&currTime);
if (i != 0){
unsigned int difference = static_cast<unsigned int>(currTime.QuadPart - prevTime.QuadPart);
bits[bits.size() - i] = difference > oneThreshold;}
prevTime = currTime;++i;
}
prevValue = currValue;}
reading = DhtReading::CreateFromBitset(&bits);
63
return reading;}
DhtReading ^ DhtReading::CreateFromBitset(bitset<40>* bits){
unsigned long long value = bits->to_ullong();
unsigned int checksum = ((value >> 32) & 0xFF) + ((value >> 24) & 0xFF) +((value >> 16) & 0xFF) + ((value >> 8) & 0xFF);
if ((checksum & 0xFF) != (value & 0xFF)) // checksum mismatch{
return nullptr;}
double temp, hum;
// AM2303 has a humidity resolution of 0.1% RHhum = ((value >> 24) & 0xFFFF) * 0.1;
// AM2303 has a temperature resolution of 0.1 Celsiustemp = ((value >> 8) & 0x7FFF) * 0.1;
if ((value >> 8) & 0x8000) temp *= -1;
return ref new DhtReading(temp, hum);}
Moving on to the second of two sensors, the driver for BMP180 sensor module
has been implemented based on the information provided in the official BMP180
datasheet [5].
Listing 5.4: BMP180 - Interface Declaration
public enum class BmpMode { UltraLowPower = 0, Standard = 1, HighRes = 2,UltraHighRes = 3 };
public ref struct BmpReading sealed{
private:const double _temperature;const unsigned _pressure;
internal:BmpReading(double temperature, unsigned pressure)
: _temperature(temperature), _pressure(pressure){ }
public:property double Temperature {
double get() {return _temperature;
}}property unsigned Pressure {
unsigned get() {return _pressure;
}}
};
64
public ref class Bmp180 sealed{
public:virtual ~Bmp180();
static Windows::Foundation::IAsyncOperation<Bmp180^>^ InitAsync(BmpModemode);
Windows::Foundation::IAsyncOperation<double>^ ReadTemperatureAsync();Windows::Foundation::IAsyncOperation<unsigned>^ ReadPressureAsync();Windows::Foundation::IAsyncOperation<double>^ ReadAltitudeAsync(unsigned
seaLevelPressure);Windows::Foundation::IAsyncOperation<BmpReading^>^ GetReadingAsync();
property BmpMode SamplingMode {BmpMode get() {
return _oss;}
}private:
enum BMP180 {ADDRESS = 0x77, ADDRESS_READ = 0xEF, ADDRESS_WRITE = 0xEE,WRITE_REG = 0xF4, READ_REG = 0xF6, READ_TEMP_CMD = 0x2E,
READ_PRESSURE_CMD = 0x34};enum BMP180_CAL {
AC1 = 0xAA, AC2 = 0xAC, AC3 = 0xAE, AC4 = 0xB0, AC5 = 0xB2, AC6 = 0xB4,
B1 = 0xB6, B2 = 0xB8, MB = 0xBA, MC = 0xBC, MD = 0xBE};
Windows::Devices::I2c::I2cDevice^ i2cDevice;BmpMode _oss;
short ac1, ac2, ac3;unsigned short ac4, ac5, ac6;short b1, b2, mb, mc, md;
Bmp180(Windows::Devices::I2c::I2cController^ controller, BmpMode mode);
double ReadTemperatureCore(int ut) const;unsigned ReadPressureCore(int ut, int up) const;double ReadAltitudeCore(unsigned pressure, unsigned seaLevelPressure)
const;void ReadCalibrationData();int ReadUTemperature() const;int ReadUPressure() const;inline int ReadData(Platform::Array<unsigned char>^ writeBuffer, Platform
::Array<unsigned char>^ readBuffer) const;};
Unlike DHT22 sensor module, the BMP180 communicates with the MCU via
I2C bus. Prior to reading the temperature and pressure measurements, the driver
needs to be initialized with the calibration data that can be read out from the
BMP180 E2PROM via the I2C interface. The calibration data consists of a set of
65
11 parameters {ac1, ac2, ac3, ac4, ac5, ac6, b1, b2, mb, mc, md}. Once the driver
has been initialized, the calibration data is used for calculating the temperature in
°C and pressure in hPa. To enable the use of I2C bus, the Windows IoT Extension
SDK provides the I2cController and I2cDevice classes.
Listing 5.5: BMP180 - Getting Sensor Reading
IAsyncOperation<BmpReading^>^ Bmp180::GetReadingAsync(){
return create_async([this]() -> BmpReading^{
int ut, up;unsigned pressure;double temperature;
ut = ReadUTemperature();up = ReadUPressure();
pressure = ReadPressureCore(ut, up);temperature = ReadTemperatureCore(ut);
BmpReading^ reading = ref new BmpReading(temperature, pressure);
return reading;});
}
double Bmp180::ReadTemperatureCore(int ut) const{
double t;int x1, x2, b5;
x1 = ((ut - ac6) * ac5) >> 15; // X1=(UT-AC6)*AC5/2^15x2 = (mc << 11) / (x1 + md); // X2=MC*2^11/(X1+MD)b5 = x1 + x2; // B5=X1+X2
t = (b5 + 8) >> 4; // T=(B5+8)/2^4t *= 0.1;
return t;}
unsigned Bmp180::ReadPressureCore(int ut, int up) const{
int p;int b3, b5, b6, x1, x2, x3;unsigned b4, b7;
x1 = ((ut - ac6) * ac5) >> 15; // X1=(UT-AC6)*AC5/2^15x2 = (mc << 11) / (x1 + md); // X2=MC*2^11/(X1+MD)b5 = x1 + x2; // B5=X1+X2
b6 = b5 - 4000; // B6=B5-4000x1 = (b2 * ((b6 * b6) >> 12)) >> 11; // X1=(B2*(B6*B6/2^12))/2^11x2 = (ac2 * b6) >> 11; // X2=AC2*B6/2^11x3 = x1 + x2; // X3=X1+X2b3 = ((((ac1 << 2) + x3) << (int)_oss) + 2) >> 2; // (((AC1*4+X3)<<oss)+2)/4
66
x1 = (ac3 * b6) >> 13; // X1=AC3*B6/2^13x2 = (b1 * ((b6 * b6) >> 12)) >> 16; // X2=(B1*(B6*B6/2^12))/2^16x3 = ((x1 + x2) + 2) >> 2; // X3=((X1+X2)+2)/2^2b4 = (ac4 * (unsigned)(x3 + 32768)) >> 15; // B4=AC4*(unsigned long)(X3
+32768)/2^15b7 = ((unsigned)up - b3) * (50000U >> (int)_oss); // B7=((unsigned long)UP-B3
)*(50000>>oss)
if (b7 < 0x80000000) {p = (b7 << 1) / b4; // p=(B7*2)/B4
}else {
p = (b7 / b4) << 1; // p=(B7/B4)*2}
x1 = (p >> 8) * (p >> 8); // X1=(p/2^8)*(p/2^8)x1 = (x1 * 3038) >> 16; // X1=(X1*3038)/2^16x2 = (-7357 * p) >> 16; // X2=(-7357*p)/2^16
p += (x1 + x2 + 3791) >> 4; // p=p+(X1+X2+3791)/2^4
return (unsigned)p;}
int Bmp180::ReadUTemperature() const{
Array<unsigned char>^ writeBuffer = ref new Array<unsigned char>(2);Array<unsigned char>^ readBuffer = ref new Array<unsigned char>(2);
writeBuffer[0] = WRITE_REG;writeBuffer[1] = READ_TEMP_CMD;
i2cDevice->Write(writeBuffer);
Sleep(5); // wait at least 4.5ms
writeBuffer = ref new Array<unsigned char>(1);writeBuffer[0] = READ_REG;
int ut = ReadData(writeBuffer, readBuffer);
return ut;}
int Bmp180::ReadUPressure() const{
Array<unsigned char>^ writeBuffer = ref new Array<unsigned char>(2);Array<unsigned char>^ readBuffer = ref new Array<unsigned char>(3);
writeBuffer[0] = WRITE_REG;writeBuffer[1] = READ_PRESSURE_CMD + ((int)_oss << 6);
i2cDevice->Write(writeBuffer);
// According to the data sheet, the wait time for pressure readings is// dependent on the oversampling setting.Sleep(2 + (3 << (int)_oss));
writeBuffer = ref new Array<unsigned char>(1);
writeBuffer[0] = READ_REG;
67
int up = ReadData(writeBuffer, readBuffer) >> (8 - (int)_oss); // 16 to 19bits
return up;}
inline int Bmp180::ReadData(Array<unsigned char>^ writeBuffer, Array<unsignedchar>^ readBuffer) const
{i2cDevice->WriteRead(writeBuffer, readBuffer);
unsigned char* data = readBuffer->Data;int count = readBuffer->Length - 1, ret = 0;
for (int i = 0; i <= count; i++, data++){
ret |= (*data) << (8 * (count - i));}
return ret;}
Having created the Windows Runtime component with sensor drivers, it was
possible to reference it from the main UWP application project and use the Dht22
and Bmp180 classes for collecting sensor readings. The actual telemetry data
(a.k.a. sensor readings) is collected through TelemetrySender class of which in-
stance is registered as an IoT Hub message sender (see subsection 5.2.3). The
TelemetrySender class creates a periodic timer, which invokes the specified call-
back each time the sensing period elapses. The class is also responsible for de-
tecting anomalous DHT22 sensor readings to ensure the stable and reliable data.
Listing 5.6: Class Responsible For Collecting Sensor Readings
public class TelemetrySender : IotHubMessageSender<AtmosphereTelemetryData>,ITelemetrySender
{...
public void Start(){
Stop(); // stop all running timers first
timer = ThreadPoolTimer.CreatePeriodicTimer(OnGetReading, TimeSpan.FromMilliseconds(SensingPeriod));
}
...
private async Task InitializeAsync(){
68
SensorState = SensorState.Initializing;
try{
dht22 = await Dht22.InitAsync(Dht22PinNumber);
bmp180 = await Bmp180.InitAsync(BmpMode.UltraHighRes);}catch (Exception){
SensorState = SensorState.NotAvailable;return;
}
await StabilizeReadingsAsync();}
...
private async void OnGetReading(ThreadPoolTimer sender){
if (SensorState != SensorState.Ready && SensorState != SensorState.Unstable)
{return;
}
var dhtRd = await dht22.TryGetReadingAsync();var bmpRd = await bmp180.GetReadingAsync();
var isAnomalous = false;var dhtOk = dhtRd != null && CompareExchangeDhtReading(ref dhtReading,
dhtRd, out isAnomalous);var bmpOk = bmpRd != null && CompareExchangeBmpReading(ref bmpReading,
bmpRd);
if (dhtReading != null && bmpReading != null && (dhtOk || bmpOk) && !isAnomalous)
{var avgTemp = Math.Round((dhtReading.Temperature + bmpReading.
Temperature) / 2, 1);
var data = new AtmosphereTelemetryData(avgTemp, dhtReading.Humidity,bmpReading.Pressure / 100d);
await Logger.Instance.LogAsync($"{nameof(TelemetrySender)}", $"Temperature: {data.Temperature}\u00B0C | Humidity: {data.Humidity}% | " + $"Pressure: {data.Pressure}hPa");
OnDataChanged(data);OnSendMessage(data, DeviceMessageType.Telemetry, useRetryPolicy: true
);
SensorState = SensorState.Ready;}else if (isAnomalous){
if (SensorState == SensorState.Unstable){
await StabilizeReadingsAsync();}else
69
{SensorState = SensorState.Unstable;
}}
}
...}
5.2.2 Bluetooth Low Energy Connectivity
Bluetooth Low Energy (BLE) is a lightweight subset of classic Bluetooth. It was
officially introduced in 2010 as a part of the Bluetooth Core Specification Ver-
sion 4.0 maintained and developed by the Bluetooth Special Interest Group (SIG).
The BLE is characterized by an entirely new protocol stack optimized for low
power, low latency, and low throughput applications. The standard is widely used
for IoT services due to the low energy consumption and relatively low manu-
facturing costs. In order to add BLE connectivity to the IoT gateway, the UWP
Bluetooth API was used. The process of obtaining the connection between the
peripheral (IoT accessory) and the central device (IoT gateway) begins with de-
vice discovery, which is done through the Generic Access Profile (GAP) protocol
[1]. The Windows API provides a DeviceWatcher class, which can be used for
finding BLE devices. To obtain an instance of DeviceWatcher for BLE devices,
one must call the DeviceInformation.CreateWatcher factory method with the ap-
propriate Advanced Query Syntax (AQS) string filter. The AQS filter for BLE
devices has the following form: "System.Devices.Aep.ProtocolId:
=bb7bb05e-5972-42b5-94fc-76eaa7084d49". After obtaining a pre-
configured instance of DeviceWatcher class, we can register the event handlers to
receive notifications whenever the devices are added, removed, or changed. In or-
der to simplify this process, a BluetoothLEScanner class was created that exposes
a simple interface for BLE scanning.
70
Listing 5.7: Scanning For BLE Devices
public sealed class BluetoothLEScanner{
private DeviceWatcher deviceWatcher;private List<DeviceInformation> scanResults;private ScanResultsChangedCallback scanResultsChangedCallback;
public void StartScan(ScanResultsChangedCallback callback){
if (callback == null){
throw new ArgumentNullException(nameof(callback));}
StopScan();
scanResults = new List<DeviceInformation>();scanResultsChangedCallback = callback;
deviceWatcher = DeviceWatcherHelper.CreateBluetoothLEDeviceWatcher();
deviceWatcher.Added += OnDeviceAdded;deviceWatcher.Updated += OnDeviceUpdated;deviceWatcher.Removed += OnDeviceRemoved;
deviceWatcher.Start();}
public void StopScan(){
if (deviceWatcher == null) return;
deviceWatcher.Added -= OnDeviceAdded;deviceWatcher.Updated -= OnDeviceUpdated;deviceWatcher.Removed -= OnDeviceRemoved;
if (deviceWatcher.Status == DeviceWatcherStatus.Started ||deviceWatcher.Status == DeviceWatcherStatus.EnumerationCompleted)
{deviceWatcher.Stop();
}
deviceWatcher = null;
scanResults.Clear();scanResults = null;scanResultsChangedCallback = null;
}
private void OnScanResultsChanged(){
scanResultsChangedCallback?.Invoke(scanResults);}
private void OnDeviceAdded(DeviceWatcher sender, DeviceInformation deviceInfo)
{scanResults.Add(deviceInfo);
OnScanResultsChanged();
71
}
private void OnDeviceUpdated(DeviceWatcher sender, DeviceInformationUpdatedeviceInfoUpdate)
{var deviceInfo = scanResults.FirstOrDefault(res => res.Id ==
deviceInfoUpdate.Id);
if (deviceInfo == null) return;
deviceInfo.Update(deviceInfoUpdate);
OnScanResultsChanged();}
private void OnDeviceRemoved(DeviceWatcher sender, DeviceInformationUpdatedeviceInfoUpdate)
{var deviceInfo = scanResults.FirstOrDefault(res => res.Id ==
deviceInfoUpdate.Id);
if (deviceInfo == null) return;
scanResults.Remove(deviceInfo);
OnScanResultsChanged();}
}
public delegate void ScanResultsChangedCallback(IList<DeviceInformation>scanResults);
The BluetoothLEScanner class is used by the BluetoothAccessoriesService
class, which represents a service responsible for managing a list of connected
BLE accessories. The service contains a callback for handling BLE scan results.
Upon receiving the scan results, the service attempts to connect to the detected
device provided that it belongs to a set of supported BLE accessories. Please note
that the current implementation only supports the Magic Blue Smart Bulb, but it
can be easily modified to support other BLE accessories.
Listing 5.8: Class Managing Connected BLE Devices
public class BluetoothAccessoriesService : IBluetoothAccessoriesService{
...
public BluetoothAccessoriesService(IEventAggregator eventAggregator){
EventAggregator = eventAggregator;
EventAggregator.GetEvent<PubSubEvent<LedLightCommand>>().Subscribe(HandleLedLightCommand);
}
72
public void StartDiscovery(){
StopDiscovery();
scanner = new BluetoothLEScanner();
scanner.StartScan(OnScanResultsChanged);}
public void StopDiscovery(){
if (scanner == null) return;
scanner.StopScan();scanner = null;
}
private async void OnScanResultsChanged(IList<DeviceInformation> scanResults){
if (scanResults.Count == 0){
_connectedAccessories.ForEach(a => a.Dispose());_connectedAccessories.Clear();
OnConnectedAccessoriesChanged();}else{
if (_connectedAccessories.Any(a => a is MagicBlueSmartBulb)) return;
var deviceInfo = scanResults.FirstOrDefault(MagicBlueSmartBulb.CanCreateInstance);
if (deviceInfo == null) return;
try{
var smartBulb = await MagicBlueSmartBulb.CreateInstanceAsync(deviceInfo);
if (smartBulb != null){
_connectedAccessories.Add(smartBulb);
OnConnectedAccessoriesChanged();
await Logger.Instance.LogAsync(nameof(BluetoothAccessoriesService),
$"Successfully connected BLEaccessory {{{deviceInfo.Id}}}");
}}catch (Exception ex){
await Logger.Instance.LogAsync(nameof(BluetoothAccessoriesService), ex.Message);
}}
}
73
...
private void HandleLedLightCommand(LedLightCommand command){
foreach (var accessory in _connectedAccessories.Where(a => a is IHandle<LedLightCommand>))
{((IHandle<LedLightCommand>)accessory).Handle(command);
}}
...}
Most of the Bluetooth Low Energy peripherals use a custom set of services and
characteristics. Therefore, each BLE accessory should be treated individually. For
demonstration purposes a MagicBlueSmartBulb class was created. This class im-
plements the GATT profile of Magic Blue UU E27 Bulb, which can be discovered
by reverse engineering [24] the GATT services and characteristics used to control
the BLE peripheral. In case of Magic Blue Smart Bulb there is only one writable
characteristic 0xFFE9 contained in the service 0xFFE5.
Listing 5.9: Magic Blue Smart Bulb GATT Profile Implementation
public sealed class MagicBlueSmartBulb : BluetoothAccessory, IHandle<LedLightCommand>
{private const string DeviceName = "LEDBLE-786009A5";
private static readonly Guid CmdServiceUuid = Guid.Parse("0000ffe5-0000-1000-8000-00805f9b34fb");
private static readonly Guid CmdCharacteristicUuid = Guid.Parse("0000ffe9-0000-1000-8000-00805f9b34fb");
private const byte MagicNumber = 0x56;
...
public static bool CanCreateInstance(DeviceInformation deviceInformation){
if (deviceInformation == null){
throw new ArgumentNullException(nameof(deviceInformation));}
return deviceInformation.Name == DeviceName &&deviceInformation.Kind == DeviceInformationKind.
AssociationEndpoint;}
public static async Task<MagicBlueSmartBulb> CreateInstanceAsync(DeviceInformation deviceInformation)
{
74
var instance = new MagicBlueSmartBulb(deviceInformation);
bool paired = await instance.TryPairAsync();
if (paired){
instance.StartGattServicesWatcher();}
return paired ? instance : null;}
public async Task<bool> TrySetColorAsync(byte red, byte green, byte blue){
byte[] payload = { MagicNumber, red, green, blue, 0x00, 0xF0, 0xAA };
return await TrySetPropertyAsync(CmdServiceUuid, CmdCharacteristicUuid,payload);
}
public async Task<bool> TrySetWarmLightAsync(byte intensity){
byte[] payload = { MagicNumber, 0x00, 0x00, 0x00, intensity, 0x0F, 0xAA};
return await TrySetPropertyAsync(CmdServiceUuid, CmdCharacteristicUuid,payload);
}
...
public async void Handle(LedLightCommand command){
var intensity = (byte)(0xFF * command.Description.Intensity);
await TrySetWarmLightAsync(intensity);}
}
The MagicBlueSmartBulb class exposes methods for setting the color and light
intensity of the light bulb. These methods are specific to the Magic Blue Smart
Bulb, however there is a certain set of operations that is common to all BLE ac-
cessories. Such operations include pairing with the device, watching for GATT
services, and writing data to / reading data from the characteristic. To avoid dupli-
cate code a BluetoothAccessory abstract base class was created that contains the
methods for performing operations common to all BLE accessories.
Listing 5.10: Base Class For Bluetooth Low Energy Accessories
public abstract class BluetoothAccessory : IDisposable{
...
75
protected BluetoothAccessory(DeviceInformation deviceInformation){
if (deviceInformation == null){
throw new ArgumentNullException(nameof(deviceInformation));}
DeviceInformation = deviceInformation;SupportedPairingCeremonies = DevicePairingKinds.ConfirmOnly;
}
...
protected virtual async Task<bool> TryPairAsync(){
var pairing = DeviceInformation.Pairing;
if (!pairing.IsPaired && !pairing.CanPair){
return false;}
if (pairing.IsPaired) return true;
var customPairing = pairing.Custom;
customPairing.PairingRequested += OnPairingRequested;
var result = await customPairing.PairAsync(SupportedPairingCeremonies);
customPairing.PairingRequested -= OnPairingRequested;
if (result.Status != DevicePairingResultStatus.Paired){
return false;}
return true;}
protected void StartGattServicesWatcher(){
var serviceUuids = GetGattServiceUuidList();
if (serviceUuids == null || serviceUuids.Count == 0){
throw new InvalidOperationException("GATT service UUID list must notbe null or empty.");
}
StopGattServicesWatcher();
servicesWatcher = DeviceWatcherHelper.CreateGattDeviceServicesWatcher(serviceUuids);
servicesWatcher.Added += OnServiceAdded;servicesWatcher.Updated += OnServiceUpdated;servicesWatcher.Removed += OnServiceRemoved;
servicesWatcher.Start();}
76
...
protected GattCharacteristic GetWritableGattCharacteristic(Guid serviceUuid,Guid characteristicUuid)
{var service = GattServices.FirstOrDefault(s => s.Uuid == serviceUuid);
var characteristics = service?.GetCharacteristics(characteristicUuid);
var characteristic =characteristics?.FirstOrDefault(
c => c.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Write));
return characteristic;}
protected async Task<bool> WriteValueToCharacteristicAsync(GattCharacteristiccharacteristic, byte[] payload)
{if (characteristic == null){
throw new ArgumentNullException(nameof(characteristic));}if (payload == null){
throw new ArgumentNullException(nameof(payload));}if (!characteristic.CharacteristicProperties.HasFlag(
GattCharacteristicProperties.Write)){
throw new ArgumentException($"The specified {nameof(characteristic)}is not writable.", nameof(characteristic));
}
GattCommunicationStatus status;
using (var writer = new DataWriter()){
writer.WriteBytes(payload);
status = await characteristic.WriteValueAsync(writer.DetachBuffer());}
return status == GattCommunicationStatus.Success;}
protected async Task<bool> TrySetPropertyAsync(Guid serviceUuid, GuidcharacteristicUuid, byte[] payload)
{var characteristic = GetWritableGattCharacteristic(serviceUuid,
characteristicUuid);
if (characteristic == null) return false;
bool result = await WriteValueToCharacteristicAsync(characteristic,payload);
return result;}
...
77
}
5.2.3 IoT Hub Connectivity
The IoT gateway UWP app communicates with the cloud by establishing a con-
nection to the Azure IoT Hub. The connection is initiated by IotHubService.
OpenConnectionAsync method that is called on application startup. The Open-
ConnectionAsync method initializes and configures the components managing the
flow of information between the IoT gateway and IoT Hub service. Those compo-
nents include the telemetry sender for collecting telemetry data, cloud command
receiver for handling cloud commands, and connection manager for coordinating
the flow of IoT Hub messages.
Listing 5.11: Initiating Connection With Azure IoT Hub
public class IotHubService : IIotHubService{
...
public async Task OpenConnectionAsync(){
await Task.Run(async () =>{
InternalTelemetrySender = await IotHub.Senders.TelemetrySender.CreateAsync();
InternalCloudCommandReceiver = new CloudCommandReceiver(EventAggregator);
var deviceClient = DeviceClient.CreateFromConnectionString(Constants.IotHubDeviceConnectionString, TransportType.Amqp);
connectionManager = await IotHubConnectionManager.CreateAsync(deviceClient);
connectionManager.RegisterMessageSender(InternalTelemetrySender);connectionManager.RegisterMessageReceiver(
InternalCloudCommandReceiver, CloudMessageType.Command);
InternalTelemetrySender.Start();});
}
...}
The IotHubConnectionManager class represents a connection manager, which
coordinates the cloud message flow using the concept of message senders and
78
message receivers. The message senders publish the messages that are to be send
to the cloud, whereas the message receivers handle the data contained in the mes-
sages received form the cloud. Once initialized, both the senders and receivers
can be registered with the connection manager that handles all the complexity
related to message filtering, data conversion, and transient fault handling. Such
a model ensures loose-coupling between the individual components while being
highly flexible and extensible.
To establish a secure connection to the IoT Hub service 2, the IotHubConnec-
tionManager class uses a DeviceClient class from the Microsoft.Azure.
Devices.Client NuGet package. The DeviceClient class is also used to re-
ceive cloud messages in a message loop and send the messages published by the
message senders. To avoid blocking the main application thread, the message loop
runs in a separate thread.
Listing 5.12: Class Managing IoT Hub Message Flow
public sealed class IotHubConnectionManager : IDisposable{
...
private IotHubConnectionManager(DeviceClient deviceClient){
this.deviceClient = deviceClient;
senders = new List<IIotHubMessageSender>();receivers = new ConcurrentDictionary<string, List<IIotHubMessageReceiver
>>();
cloudMsgLoopTask = StartCloudMessageLoopAsync();}
...
private Task StartCloudMessageLoopAsync(){
if (cts != null && !cts.IsCancellationRequested){
return Task.CompletedTask;}
cts = new CancellationTokenSource();
var token = cts.Token;
2The IoT gateway authentication is discussed in detail in chapter 2.
79
return Task.Factory.StartNew(() => CloudMessageLoopAsync(token), token);}
...
private async void CloudMessageLoopAsync(CancellationToken token){
Message cloudMessage = null;
while (!token.IsCancellationRequested){
try{
var retryPolicy = CreateOperationRetryPolicy();
await retryPolicy.ExecuteAsync(async () => cloudMessage = awaitInternalReceiveMessageAsync(), token);
}catch (Exception ex){
await Logger.Instance.LogAsync($"{nameof(IotHubConnectionManager)}", ex.ToString());
continue;}
if (cloudMessage == null) continue;
await ProcessCloudMessageAsync(cloudMessage);}
}
private async Task ProcessCloudMessageAsync(Message message){
string messageBody = null, messageTypeString = null;
if (message.Properties.ContainsKey("messageType")){
messageTypeString = message.Properties["messageType"];}
var messageType = CloudMessageType.TryParse(messageTypeString);
if (messageType == null){
await deviceClient.RejectAsync(message);return;
}
var data = message.GetBytes();
if (data != null){
messageBody = Encoding.UTF8.GetString(data);}
var args = new MessageReceivedEventArgs(messageBody ?? "", messageType);
await deviceClient.CompleteAsync(message);
NotifyMessageReceivers(args);
80
}
private void NotifyMessageReceivers(MessageReceivedEventArgs args){
List<IIotHubMessageReceiver> rcvs;
// Notify the receivers that were registered to handle the specificmessage type
receivers.TryGetValue(args.MessageType.Id, out rcvs);
if (rcvs != null){
foreach (var receiver in rcvs){
receiver.OnMessageReceived(args);}
}
// Notify the receivers that were registered to handle all message typesreceivers.TryGetValue("", out rcvs);
if (rcvs == null) return;
foreach (var receiver in rcvs){
receiver.OnMessageReceived(args);}
}
...
private async void OnSendMessage(IIotHubMessageSender sender,SendMessageEventArgs e)
{var message = CreateAmqpMessage(e.MessageBody, e.MessageType.Id);
Interlocked.Exchange(ref sendMessageId, message.MessageId);
await Task.Factory.StartNew(async () =>{
try{
await Logger.Instance.LogAsync($"{nameof(IotHubConnectionManager)}", $"Sending message {{{message.MessageId}}}");
if (e.UseRetryPolicy){
var retryPolicy = CreateOperationRetryPolicy();
await retryPolicy.ExecuteAsync(() => InternalSendMessageAsync(message));
}else{
await InternalSendMessageAsync(message);}
await Logger.Instance.LogAsync($"{nameof(IotHubConnectionManager)}", $"Message has been sent {{{message.MessageId}}}");
}catch (Exception ex){
81
await Logger.Instance.LogAsync($"{nameof(IotHubConnectionManager)}", ex.ToString());
}});
}
...}
All message senders registered with an instance of the IotHubConnectionMan-
ager class must implement the IIotHubMessageSender interface. The interface
defines a simple event that is used to notify the connection manager about the
message that is to be sent. Most of the message senders will operate on data types
other than JSON string. To simplify the process of serializing data object to JSON
string, an IotHubMessageSender generic abstract base class was created that hides
the complexity of this process.
Listing 5.13: IoT Hub Message Sender Interface & Base Implementation
public interface IIotHubMessageSender{
event SendMessageEventHandler SendMessage;}
public abstract class IotHubMessageSender<TData> : IIotHubMessageSender whereTData : class
{protected virtual void OnSendMessage(TData data, DeviceMessageType
messageType, bool useRetryPolicy = false){
using (var stream = new MemoryStream()){
var serializer = new DataContractJsonSerializer(typeof(TData));
serializer.WriteObject(stream, data);
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream)){
OnSendMessage(reader.ReadToEnd() ?? "", messageType,useRetryPolicy);
}}
}
private void OnSendMessage(string messageData, DeviceMessageType messageType,bool useRetryPolicy)
{var args = new SendMessageEventArgs(messageData, messageType,
useRetryPolicy);
SendMessage?.Invoke(this, args);
82
}
public event SendMessageEventHandler SendMessage;}
Similarly to message senders, all message receivers registered with an instance
of the IotHubConnectionManager class must implement the IIotHubMessageRe-
ceiver interface. This time the interface defines a callback method that is called
by the connection manager to notify the message receiver about the received mes-
sage. Most of the message receivers will operate on data types other than JSON
string. To simplify the process of deserializing JSON string to data object, an Io-
tHubMessageReceiver generic abstract base class was created that hides the com-
plexity of this process.
Listing 5.14: IoT Hub Message Receiver Interface & Base Implementation
public interface IIotHubMessageReceiver{
void OnMessageReceived(MessageReceivedEventArgs args);}
public abstract class IotHubMessageReceiver<TData> : IIotHubMessageReceiver whereTData : class
{public void OnMessageReceived(MessageReceivedEventArgs args){
TData data = args.MessageBody as TData;
if (data == null){
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(args.MessageBody)))
{var serializer = new DataContractJsonSerializer(typeof(TData));
data = serializer.ReadObject(stream) as TData;}
}
OnMessageReceivedCore(data, args.MessageType);}
protected virtual void OnMessageReceivedCore(TData data, CloudMessageTypemessageType)
{ }}
In order to parse the cloud messages to commands that can be consumed by
objects representing BLE accessories, a CloudCommandReceiver class was cre-
83
ated. This class parses the message data to command object and uses the event
aggregator to notify the BluetoothAccessoriesService (see subsection 5.2.2 about
the received command.
Listing 5.15: Parsing & Handling Cloud Commands
public class CloudCommandReceiver : IotHubMessageReceiver<string>,ICloudCommandReceiver
{public event TypedEventHandler<ICloudCommandReceiver, ICloudCommand>
CommandReceived;
...
protected override async void OnMessageReceivedCore(string data,CloudMessageType messageType)
{ICloudCommand cloudCommand;
if (LedLightCommand.TryParse(data, out cloudCommand)){
EventAggregator.GetEvent<PubSubEvent<LedLightCommand>>().Publish((LedLightCommand)cloudCommand);
}
if (cloudCommand == null) return;
CommandReceived?.Invoke(this, cloudCommand);
await Logger.Instance.LogAsync($"{nameof(CloudCommandReceiver)}", "Command has been received");
}
...}
84
Chapter 6
Client Application
The final part of the discussed IoT platform is the client application of which
purpose is to allow user to control the IoT gateway together with the connected
IoT accessories. The client application is based on the Universal Windows Plat-
form (UWP) and it was written in C# and XAML. In order to achieve a highly
scalable and maintainable code, the application architecture complies with the
MVVM pattern and utilizes the dependency injection mechanism. Moreover, the
application skeleton was built with components provided by the Prism framework
[23] for UWP. Following from these architectural design decisions, the applica-
tion components can be divided into three main groups: the models containing
the business logic, the views defining the layout of the displayed content, and the
view models containing the view logic and wiring-up the views with models. The
view model binds to the view using a data binding mechanism for UWP apps and
manipulates the models in response to commands triggered by view. Even though
the MVVM pattern provides a clear separation of concerns, as the application
grows, the view models can become large and difficult to maintain. It was there-
fore decided to introduce the services, which encapsulate the application logic
that would otherwise be contained inside the view models. Such services can be
responsible for user authentication, push notifications, background task registra-
85
tion, internet connectivity information, and other common application logic. The
discussed application registers the services with the IoC container provided by the
Prism framework, thus enabling the loose coupling between the services and view
models.
Since the application requires an Internet access and uses the background tasks
to perform certain operations, it was necessary to declare the Internet (Client)
capability along with the background tasks declaration in app’s package manifest.
Listing 6.1: App Capability & Background Task Declarations
<?xml version="1.0" encoding="utf-8"?><Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"IgnorableNamespaces="uap mp">
...
<Dependencies><TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0"
MaxVersionTested="10.0.0.0" /></Dependencies>
...
<Applications><Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="
SlickHubRemote.App">...
<Extensions><Extension Category="windows.backgroundTasks" EntryPoint="
SlickHubRemote.BackgroundTasks.PushNotificationTask"><BackgroundTasks><Task Type="pushNotification" />
</BackgroundTasks></Extension>
</Extensions></Application>
</Applications><Capabilities><Capability Name="internetClient" />
</Capabilities></Package>
86
6.1 User Interface Layout
Since the UWP apps can run on a wide range of Windows 10 devices, one of
the main challenges was to design and implement the UI that would adapt to
different screen sizes. The XAML layout system provides three main approaches
for creating a responsive UI. The first approach is called a fluid layout and it
assumes the use of layout properties and panels to reposition and scale the content.
The second approach, an adaptive layout, is based on the visual states, which
define a set of values applied to layout properties whenever the visual state is
activated by the state trigger. Finally, there is a tailored layout approach, which
allows to create the UI layout optimized for a specific device family by either using
custom device family trigger or by creating per-device family XAML views. Due
to major differences in UI design principles between mobile and desktop devices
it was decided to define separate XAML views tailored to mobile devices. Such an
approach allowed to create a responsive UI optimized for different device families
while keeping the XAML code clean and simple.
As an example, listing 6.2 shows the UI definition for dashboard view that is
displayed on non-mobile devices. The resulting view can be seen on figure 6.1.
Listing 6.2: Client App Dashboard - UI Definition
<base:TopViewx:Class="SlickHubRemote.Views.Dashboard.DashboardView"...prism:ViewModelLocator.AutoWireViewModel="True">
<base:TopView.Transitions><TransitionCollection>
<NavigationThemeTransition><NavigationThemeTransition.DefaultNavigationTransitionInfo>
<DrillInNavigationTransitionInfo /></NavigationThemeTransition.DefaultNavigationTransitionInfo>
</NavigationThemeTransition></TransitionCollection>
</base:TopView.Transitions>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<controls:HamburgerMenu x:Name="HamburgerMenu"DisplayMode="CompactOverlay"
87
PanePlacement="Left"Foreground="{StaticResource
AppThemeBaseMediumBrush}"PaneBackground="{StaticResource
AppThemeAltBaseHighBrush}"CompactPaneLength="48"OpenPaneLength="240"HamburgerHeight="48"ItemTemplate="{StaticResource
HamburgerMenuGlyphItemTemplate}"OptionsItemTemplate="{StaticResource
HamburgerMenuCommandGlyphItemTemplate}"Loaded="HamburgerMenu_OnLoaded"ItemClick="HamburgerMenu_OnItemClick">
<controls:HamburgerMenu.ItemsSource><controls:HamburgerMenuItemCollection>
<controls:HamburgerMenuGlyphItem Label="Home"Glyph="{StaticResource
HomeGlyph}"TargetPageType="
children:DashboardHomeChildView" />
<controls:HamburgerMenuGlyphItem Label="Accessories"Glyph="{StaticResource
DevicesGlyph}"TargetPageType="
children:DashboardAccessoriesChildView" />
</controls:HamburgerMenuItemCollection></controls:HamburgerMenu.ItemsSource>
<controls:HamburgerMenu.OptionsItemsSource><controls:HamburgerMenuItemCollection>
<menuItems:HamburgerMenuCommandGlyphItem Label="Settings"Glyph="{
StaticResourceSettingsGlyph}"
Command="{x:BindViewModel.GoToSettingsCommand}" />
<menuItems:HamburgerMenuCommandGlyphItem Label="Switch Hub"Glyph="{
StaticResourceSwitchGlyph}"
Command="{x:BindViewModel.GoToSwitchHubCommand}" />
<menuItems:HamburgerMenuCommandGlyphItem Label="Sign Out"Glyph="{
StaticResourceSwitchUserGlyph}"
Command="{x:BindViewModel.LogoutCommand}"/>
</controls:HamburgerMenuItemCollection></controls:HamburgerMenu.OptionsItemsSource>
88
<controls:HamburgerMenu.Content>
<Frame x:Name="ChildViewFrame" />
</controls:HamburgerMenu.Content>
</controls:HamburgerMenu>
</Grid>
</base:TopView>
89
Figure 6.1: Client App Dashboard
90
Unlike the standard Page files (XAML and code-behind), the XAML views
are decorated with qualifier string to denote the device family to which the UI
definition contained in the XAML view applies. The qualifier string can be either
added to the XAML view file name or used as a name for the folder containing
the XAML view file. Following from these requirements, the XAML view files
containing UI definition for mobile device family were placed in a folder named
DeviceFamily-Mobile. Listing 6.3 shows the UI definition for dashboard view
tailored to mobile devices. The resulting dashboard view for mobile devices can
be seen on figure 6.2.
Listing 6.3: Client App Dashboard - UI Definition For Mobile Device Family
<base:TopViewx:Class="SlickHubRemote.Views.Dashboard.DashboardView"...prism:ViewModelLocator.AutoWireViewModel="True">
<base:TopView.Transitions><TransitionCollection>
<NavigationThemeTransition><NavigationThemeTransition.DefaultNavigationTransitionInfo>
<DrillInNavigationTransitionInfo /></NavigationThemeTransition.DefaultNavigationTransitionInfo>
</NavigationThemeTransition></TransitionCollection>
</base:TopView.Transitions>
<Pivot Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"HeaderTemplate="{StaticResource PivotHeaderGlyphItemTemplate}">
<PivotItem>
<PivotItem.Header><headerItems:PivotHeaderGlyphItem Label="Home" Glyph="{
StaticResource HomeGlyph}" /></PivotItem.Header>
<components:DashboardHomeComponentView base:ViewBase.ViewModel="{x:Bind ViewModel.DashboardHomeComponent}" />
</PivotItem>
<PivotItem>
<PivotItem.Header><headerItems:PivotHeaderGlyphItem Label="Accessories" Glyph="{
StaticResource DevicesGlyph}" /></PivotItem.Header>
<components:DashboardAccessoriesComponentView base:ViewBase.ViewModel="{x:Bind ViewModel.DashboardAccessoriesComponent}" />
91
</PivotItem>
</Pivot>
<base:TopView.BottomAppBar><CommandBar Foreground="{StaticResource AppThemeBaseMediumBrush}"
Background="{StaticResource AppThemeAltBaseHighBrush}"><CommandBar.SecondaryCommands>
<AppBarButton Label="Settings"Command="{x:Bind ViewModel.GoToSettingsCommand}"Foreground="{StaticResource AppThemeBaseMediumBrush
}" /><AppBarButton Label="Switch Hub"
Command="{x:Bind ViewModel.GoToSwitchHubCommand}"Foreground="{StaticResource AppThemeBaseMediumBrush
}" /><AppBarButton Label="Sign Out"
Command="{x:Bind ViewModel.LogoutCommand}"Foreground="{StaticResource AppThemeBaseMediumBrush
}" /></CommandBar.SecondaryCommands>
</CommandBar></base:TopView.BottomAppBar>
</base:TopView>
Figure 6.2: Client App Dashboard Tailored To Mobile Devices
92
6.2 Communication With Backend Service
To enable Windows and Xamarin client apps to access the Azure Mobile Apps
backend service, Microsoft has released a managed client library. The library is
distributed as Microsoft.Azure.Mobile.Client NuGet package and it
provides a MobileServiceClient class that enables access to various backend ser-
vice resources, including authentication endpoints 1, custom endpoints, and push
notifications. To ensure compatibility with dependency injection mechanisms, the
library also provides an IMobileServiceClient interface that describes the methods
and properties implemented by MobileServiceClient class. In this way, the client
object can be initialized during the application startup and registered with IoC
container, thus allowing it to be reused by all components that require an access
to the backend service.
Listing 6.4: Initialization of MobileServiceClient Object
public sealed partial class App : PrismUnityApplication{
...
protected override void ConfigureContainer(){
base.ConfigureContainer();
RegisterServices();RegisterComponents();
}
...
private void RegisterServices(){
var mobileService = new MobileServiceClient(new Uri(@"https://slickhub.azurewebsites.net/", UriKind.Absolute));
Container.RegisterInstance<IMobileServiceClient>(mobileService, newContainerControlledLifetimeManager());
...}
...}
1The client app authentication is covered in chapter 2.
93
As can be seen in listing 6.4, an instance of the MobileServiceClient class (a
client object) is initialized with the mobile service URL. The initialized client ob-
ject exposes several InvokeApiAsync method overloads, which can be used to call a
custom REST API. The overloads were implemented to run various requests, thus
the InvokeApiAsync method allows to specify the endpoint path, HTTP method,
optional request body and request headers, as well as optional query string param-
eters. Such a method implementation provides a thorough dose of flexibility at the
expense of lower maintainability. To overcome this issue, it was decided to intro-
duce an additional abstraction layer between application code and backend API
calls by creating a service that encapsulates the InvokeApiAsync method calls. The
service hides the request-specific information and exposes a simple interface that
allows to work with well-defined set of data types.
Listing 6.5: REST API Client Service
public class SlickHubClientService : ISlickHubClientService{
...
public async Task<AccessoryDescriptionResponse<TDescription>>GetAccessoryDescription<TDescription>(AccessoryDescriptionRequest request) where TDescription : class
{if (request == null){
throw new ArgumentNullException(nameof(request));}
var queryParams = MobileServiceClient.GetApiQueryParameters();
var json = await MobileServiceClient.InvokeApiAsync($"v1/accessories/{request.AccessoryId}", HttpMethod.Get, queryParams);
return json?.ToObject<AccessoryDescriptionResponse<TDescription>>();}
public async Task PostDeviceCommandAsync(CommandRequest request){
if (request == null){
throw new ArgumentNullException(nameof(request));}
var json = JObject.FromObject(request);
await MobileServiceClient.InvokeApiAsync($"v1/devices/commands", json);}
94
...}
The SlickHubClientService class is registered with the IoC container and in-
jected into all view models that require a communication with the backend ser-
vice. As an example, the SmartBulbSettingsComponentViewModel class uses the
injected instance of SlickHubClientService to send a command informing about
the change in light intensity of a smart bulb. It is worth noting that the meth-
ods specified by the ISlickHubClientService interface do not require the caller to
specify the endpoint path and HTTP method to be used - it is enough to pass an
instance of the request object containing the data to be sent.
Listing 6.6: Sending Led Light Command
public class SmartBulbSettingsComponentViewModel :AccessorySettingsComponentViewModelBase
{...
public SmartBulbSettingsComponentViewModel(ISlickHubClientServiceslickHubClientService, LedLightDescription description)
: base(slickHubClientService){
// Initialize bindable propertiesIntensity = _intensityValue = description?.Intensity * 100 ?? 0d;
}
...
private async void SendLightIntensityCommand(){
if (AccessoryId == Guid.Empty) return;
var request = new CommandRequest{
AccessoryId = AccessoryId,Command = new LedLightCommand{
Description = new LedLightDescription(Intensity * 0.01)}.ToString()
};
await SlickHubClientService.PostDeviceCommandAsync(request);}
...}
95
6.3 Push Notification Channel
As explained in chapter 3, the telemetry data collected by the IoT gateway are
transmitted to the client app using push notifications. The UWP app can receive
push notifications by means of the notification channel URI issued by the Win-
dows Push Notification Service (WNS) [21]. The WNS is the notification ser-
vice for Windows devices, which generates and manages the channel URIs used
to uniquely identify the registered users. The application requests a notification
channel by calling a CreatePushNotificationChannelForApplicationAsync static
method provided by the PushNotificationChannelManager class. The obtained
channel can then be registered with Azure Notification Hub service by retrieving
a push object from the MobileServiceClient instance and calling the RegisterAsync
method on that object. The registered channel URI is then used by the Azure No-
tification Hub service to send an HTTP POST request containing the notification
payload, which in turn will be delivered to the client app via WNS. Since the
backend service uses a user ID tag to send targeted push notifications (see chapter
3), the application must ensure that the channel URI is registered after the user au-
thentication process is complete in order for this tag to be included in the channel
registration metadata.
Listing 6.7: Requesting & Registering Push Notification Channel URI
public sealed class PushNotificationService : IPushNotificationService{
...
public async Task<PushNotificationChannel> RequestNotificationChannelAsync(){
PushNotificationChannel channel = null;
var retryCount = 3;
do{
try{
channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
96
break;}catch (Exception){
// Could not create a channel. Use 10-second delay between eachunsuccessful attempt.
await Task.Delay(RetryDelay);}
} while (--retryCount > 0);
if (channel != null){
await MobileServiceClient.GetPush().RegisterAsync(channel.Uri);}
return channel;}
...}
The logic responsible for requesting and registering push notification chan-
nel URI has been extracted into a PushNotificationService class, which serves as
a service that can be injected into other application components. The PushNo-
tificationService class exposes a RequestNotificationChannelAsync asynchronous
method that returns a notification channel object. The callers of this method can
use the returned notification channel object to subscribe to a notification delivery
event and intercept the push notifications while the app is running.
Listing 6.8: Receiving Push Notifications Through Notification Delivery Event
public sealed class DashboardViewModel : ViewModelBase{
...
private async Task RegisterPushNotificationHandlerAsync(){
if (PushNotificationService == null) return;
try{
channel = await PushNotificationService.RequestNotificationChannelAsync();
if (channel != null){
channel.PushNotificationReceived += OnPushNotification;}
}catch (Exception){
// ignore}
97
}
...
private async void OnPushNotification(PushNotificationChannel sender,PushNotificationReceivedEventArgs e)
{if (e.NotificationType == PushNotificationType.Raw && e.RawNotification
!= null){
try{
var payload = NotificationPayload.CreateFromJson(e.RawNotification.Content);
var data = TelemetryData.LoadFromNotificationPayload<AtmosphereTelemetryData>(payload);
if (data != null){
await Execute.OnUIThreadAsync(() =>{
DashboardHomeComponent.UpdateAtmosphereReadings(data);});
ApplicationData.Current.LocalSettings.Values[Constants.UserHubDataLSK] = e.RawNotification.Content;
}}catch (Exception){
// ignored}
}
e.Cancel = true;}
...}
The DashboardViewModel class subscribes to the notification delivery event
after the user navigates to the application dashboard screen. The OnPushNotifica-
tion event handler processes the received notifications by deserializing a notifica-
tion payload into telemetry data, which is then displayed on the dashboard Home
tab. It is important to note that the notification delivery event only allows to inter-
cept the push notifications while the app is running in the foreground. However,
because the platform uses raw notifications to send telemetry data, the background
running app can still receive those notifications by registering a background task
that gets triggered whenever a new raw notification arrives.
98
Listing 6.9: Receiving Push Notifications Through Background Task
public sealed class PushNotificationTask : IBackgroundTask{
...
public void Run(IBackgroundTaskInstance taskInstance){
deferral = taskInstance.GetDeferral();
var notification = (RawNotification) taskInstance.TriggerDetails;
if (!string.IsNullOrEmpty(notification.Content)){
ApplicationData.Current.LocalSettings.Values[Constants.UserHubDataLSK] = notification.Content;
}
deferral.Complete();}
...}
The PushNotificationTask class implements an IBackgroundTask interface, which
provides a Run method. The Run method is an entry point to the background task,
thus it must be implemented by all tasks that are to be registered with the system.
The discussed implementation of the Run method saves the notification payload
in the local application settings, so that the next time the app is launched the user
is presented with the most up to date telemetry data. In order for a PushNotifi-
cationTask background task to be invoked in response to the receipt of the raw
notification, the background task was registered with a PushNotificationTrigger
task event trigger.
Listing 6.10: Background Task Registration
public sealed partial class App : PrismUnityApplication{
...
private async Task RegisterBackgroundTasksAsync(){
if (BackgroundTaskService == null){
Logger.Log("Failed to register background tasks", Category.Warn,Priority.High);
return;}
var status = await BackgroundExecutionManager.RequestAccessAsync();
99
// Ensure that app has been granted lock screen accessif (status != BackgroundAccessStatus.DeniedByUser && status !=
BackgroundAccessStatus.DeniedBySystemPolicy){
var pushTrigger = new PushNotificationTrigger();
BackgroundTaskService.RegisterBackgroundTask<PushNotificationTask>(pushTrigger);
}}
...}
100
Conclusions
The project discussed in this thesis addressed the problems related to security and
interoperability in IoT solutions. It has been presented how the selected Azure
cloud services can be connected together to form a scalable IoT platform that
provides secure and reliable communication between IoT accessories, gateways,
and client application. The main focus has been put on the choice of messaging
protocols, the authentication mechanisms, as well as the device and user iden-
tity management. In addition, the thesis stressed the importance of architectural
design in software engineering by showing the practical implications of various
design patterns and their positive impact on maintainability and extensibility of
the code.
6.4 Limitations
Even though the main project goals have been achieved, there still exist areas
for continued development. In the discussed implementation it has been shown
how the real-time telemetry data is sent from the IoT gateway device to the client
application. However, the presented solution does not include a mechanism for
communicating the connection status of the BLE accessories. The implementa-
tion of such a mechanism requires modifications in all parts of the platform, but
the general implementation scheme is identical to the implementation of telemetry
data mechanism. The discussed solution is also lacking the IoT gateway activa-
101
tion mechanism. The backend API exposes the endpoint for provisioning the IoT
gateway device, but there is no way for the gateway to authenticate itself to the
backend service in order to use that endpoint. The authentication issue could be
resolved by transferring the user credentials from the client application to the IoT
gateway via BLE connection. Unfortunately, at the time of project implementa-
tion, the Bluetooth API for UWP apps did not support GATT Server role, and
therefore it was impossible to complete the implementation of activation mecha-
nism within the timeframe available.
6.5 Future Work
Apart from the aforementioned limitations of the presented implementation, the
discussed project has a great potential for further improvements. One of the ideas
is to use the Azure Cognitive Services in order to extend the client application
with a speech recognition feature, that would enable the IoT gateway to be con-
trolled with voice commands. The client application could also be improved by
adding the feature that would enable the user to manage multiple IoT gateways
and to share the selected IoT gateways with other users. It is worth noting that
this feature is already partially supported by the backend service, thus the imple-
mentation would mainly involve changes in the client application code.
The discussed project could also be used as a foundation for researching the pos-
sibility of remote management of GATT profiles with the idea that compatibility
with unknown BLE devices will be added without making any changes to the
IoT gateway app. The proposed research could involve the comparison of BLE
with other wireless communication protocols such as ZigBee or Z-Wave, and the
discussion on interoperability that can be achieved with a use of each of the pro-
tocols.
102
Bibliography
[1] Introduction to bluetooth low energy. https://learn.adafruit.com/introduction-to-bluetooth-low-energy, 2014.
[2] Introducing the adafruit bluefruit le uart friend. https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-uart-friend,2015.
[3] Am2303 product manual. http://akizukidenshi.com/download/ds/aosong/AM2302.pdf, 2013.
[4] Hm bluetooth module datasheet. http://fab.cba.mit.edu/classes/863.15/doc/tutorials/programming/bluetooth/bluetooth40_en.pdf, 2014.
[5] Bmp180 digital pressure sensor. https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BMP180-DS000-121.pdf,2015.
[6] E. D. Hardt. The OAuth 2.0 Authorization Framework. RFC 6749, Internet Engi-neering Task Force (IETF), October 2012.
[7] Understanding user ids. https://github.com/Azure/azure-mobile-apps-net-server/wiki/Understanding-User-Ids, 2016.
[8] Authentication and authorization in azure app service. https://github.com/Microsoft/azure-docs/blob/master/articles/app-service/app-service-authentication-overview.md, 2017.
[9] Add authentication to your windows app. https://docs.microsoft.com/en-us/azure/app-service-mobile/app-service-mobile-windows-store-dotnet-get-started-users,2016.
[10] How to configure your app service application to use facebook login. https://docs.microsoft.com/en-us/azure/app-service-mobile/
103
app-service-mobile-how-to-configure-facebook-authentication,2016.
[11] How to configure your app service application to use google login. https://docs.microsoft.com/en-us/azure/app-service-mobile/app-service-mobile-how-to-configure-google-authentication,2016.
[12] How to configure your app service application to use twitter login. https://docs.microsoft.com/en-us/azure/app-service-mobile/app-service-mobile-how-to-configure-twitter-authentication,2016.
[13] Reference - iot hub query language for device twins and jobs.https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language, 2016.
[14] Service bus quotas. https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-quotas, 2016.
[15] Azure notification hubs. https://docs.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-overview, 2017.
[16] Control access to iot hub. https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-security, 2017.
[17] Create an azure sql database in the azure portal. https://docs.microsoft.com/en-us/azure/sql-database/sql-database-get-started-portal, 2017.
[18] Reference - iot hub endpoints. https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-endpoints, 2017.
[19] Storage queues and service bus queues - compared and contrasted. https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-azure-and-service-bus-queues-compared-contrasted,2017.
[20] Understand identity registry in your iot hub. https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry, 2017.
[21] Windows push notification services (wns) overview. https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-windows-push-notification-services--wns--overview,2017.
104
[22] Advanced message queuing protocol (amqp) claims-based security.http://docs.oasis-open.org/amqp/amqp-cbs/v1.0/csd01/amqp-cbs-v1.0-csd01.doc, 2013.
[23] Prism. https://github.com/PrismLibrary/Prism, 2015.
[24] U. Shaked. Reverse engineering a bluetoothlightbulb. https://medium.com/@urish/reverse-engineering-a-bluetooth-lightbulb-56580fcb7546,2016.
105