Интернет вещей

Интеллектуальное термореле на шине сервисов

Клеменс Вастерс

Продукты и технологии:

Visual Studio, Microsoft Azure Service Bus, .NET Micro Framework, .NET Gadgeteer

В статье рассматриваются:

  • проектирование устройства в .NET Gadgeteer;
  • реализация функциональности локального термореле;
  • подготовка устройства;
  • передача событий и получение команд;
  • вопросы безопасности.

Исходный код можно скачать по ссылке.

Вот вам смелое предсказание: подключенные устройства скоро станут крупным бизнесом, и понимание этих устройств будет по-настоящему важным для разработчиков в самой ближайшей перспективе. «Это очевидно», скажете вы. Но я не имею в виду устройства, на которых вы, возможно, читаете эту статью. Я говорю о тех устройствах, которые будут охлаждать вас этим летом, помогать вам стирать одежду и мыть посуду, варить утренний кофе или собирать другие устройства на заводах.

В июньском номере «MSDN Magazine» (msdn.microsoft.com/magazine/jj133825) я высказал ряд соображений и обрисовал архитектуру для управления потоками событий от встраиваемых (и мобильных) устройств и потоками команд, адресованных этим устройствам, на основе Microsoft Azure Service Bus. В этой статье я сделаю еще один шаг и рассмотрю код, который создает и защищает эти потоки событий и команд. Ну а поскольку по-настоящему понять встраиваемые устройства можно, лишь воочию увидев одно из них, я сконструирую такое устройство, а затем подключу его к Microsoft Azure Service Bus, чтобы оно посылало события, связанные с его текущим состоянием, и дистанционно управлялось сообщениями через облако Microsoft Azure.

Еще несколько лет назад конструирование небольшого устройства с блоком питания, микроконтроллером и набором датчиков требовало познаний в проектировании электроники и умения комбинировать такие детали, не говоря уже о навыках работы паяльником. Искренне признаюсь, что лично я совершенно не в ладах с аппаратной частью — причем настолько, что один мой приятель как-то заметил: если бы на наш мир напали роботы пришельцев, он отправил бы меня на передовую и от одного только моего присутствия там их атака закончилась бы грандиозным фейерверком коротких замыканий. Но благодаря появлению платформ для создания прототипов, таких как Arduino/Netduino или .NET Gadgeteer, даже самые неумелые в обращении с паяльником люди теперь могут собрать небольшое полнофункциональное устройство, используя свои навыки в программировании.

Придерживаясь сценария, изложенного в прошлой статье, я сконструирую «кондиционер воздуха» в виде вентилятора, регулируемого термореле (thermostat-controlled fan), где вентилятор является наименее интересной частью с точки зрения подключения. Компоненты для этого проекта основаны на модели .NET Gadgeteer, включая материнскую плату с микроконтроллером, память и самые разнообразные подключаемые модули. В качестве материнской платы для этого проекта взята плата GHI Electronics FEZ Spider со следующими модулями расширения.

  • Компоненты от GHI Electronics:
    • Ethernet J11D Module, обеспечивающий подключение к проводной сети (модуль Wi-Fi имеется);
    • USB Client DP Module, выступающий в роли блока питания и USB-порта;
    • джойстик для прямого управления устройством.
  • Компоненты от Seeed Studio:
    • термодатчик и датчик влажности (гигрометр);
    • реле для включения или отключения вентилятора;
    • OLED-дисплей, отображающий текущее состояние.

Все вместе эти детали стоят около $230. Это явно дороже, чем эквивалентные компоненты, впаянные в печатную плату, но разве я не упоминал, что в таком случае понадобятся навыки обращения с паяльником? Кроме того, этот рынок только начинает развиваться, поэтому в дальнейшем можно ожидать снижения цен.

Чтобы «оживить» эти компоненты, вам потребуются Visual C# 2010 Express (как минимум), .NET Micro Framework SDK и Gadgeteer SDK от GHI Electronics или Seeed. После установки всего этого программного обеспечения процесс разработки пойдет вполне ожидаемо и наглядно — насколько это возможно в Visual Studio (рис. 1).

Проектирование устройства в .NET Gadgeteer
Рис. 1. Проектирование устройства в .NET Gadgeteer

На рис. 1 показана программа .NET Gadgeteer в режиме проектирования в Visual Studio. Поначалу я хотел включить в статью снимок реального устройства, но все снимки лишь подтверждали то, что вы видите на схеме. Оно именно таково, как выглядит.

Файл с расширением .gadgeteer содержит XML-модель, которая визуализируется в редакторе. Используя этот XML-файл, инструментарий Gadgeteer автоматически генерирует частичный класс Program с оболочками для каждого модуля, подключенного к материнской плате. Ваш код помещается в Program.cs, содержащий другую часть класса Program, — по аналогии с моделью отделенного кода, знакомой вам по другим .NET API.

С этими устройствами используется инфраструктура .NET Micro Framework. Это версия Microsoft .NET Framework с полностью открытым исходным кодом, созданная специально для малых устройств с ограниченными вычислительными ресурсами и небольшими объемами памяти. В .NET Micro Framework присутствуют многие привычные классы .NET Framework, но большинство из них имеют урезанную функциональность, чтобы максимально уменьшить объем памяти, занимаемой кодом. Поскольку эта инфраструктура является уровнем над аппаратным обеспечением устройства, а это устройство не относится к универсальным компьютерам с операционными системами, обрабатывающими все уровни абстракции аппаратного обеспечения (здесь вообще нет операционной системы), версия инфраструктуры, которую вы можете применять с устройством, зависит от производителя материнской платы и реализованной им поддержки, которая, очевидно, сильно отличается от той, что есть в обычных ПК.

Есть еще несколько отличий по сравнению с обычной .NET Framework и платформы ПК в целом, которые поначалу сильно удивляют. Например, у данного устройства нет набортного аккумулятора. Отсутствие аккумулятора означает, что в устройстве нет часов, поэтому устройство не имеет ни малейшего понятия о текущем времени, когда оно пробуждается. Из-за отсутствия ОС и того факта, что дисплей является устройством расширения, вы лишены встроенных шрифтов, с помощью которых вы могли бы выводить строки на дисплее. Если вам нужно вывести строку, сначала добавьте шрифт для этого.

Аналогично в устройстве нет предварительно заполняемого и впоследствии обновляемого с помощью Windows Update хранилища сертификатов. Если вам нужно проверять сертификаты SSL/TLS, вам придется развернуть на устройстве, как минимум, корневые сертификаты CA — и, конечно, вам также потребуется знать текущее время, чтобы проверять допустимость сертификатов. Как вы, вероятно, догадались, обработка сертификатов на этих устройствах — дело весьма непростое, а требования к шифрованию SSL/TLS настолько велики в отношении вычислительных ресурсов, использования памяти и объема кода, что далеко не все устройства способны поддерживать их. Однако, поскольку безопасность явно становится все более важным фактором даже в этой сфере и поскольку устройствам необходимо взаимодействовать через Интернет, в .NET Micro Framework версии 4.2 в поддержку SSL/TLS были внесены значительные усовершенствования для устройств, обладающих достаточными для обработки SSL/TLS ресурсами. Подробнее эту тему мы обсудим позже.

Функциональность термореле

Реализация функциональности локального термореле для этого примера весьма прямолинейна. Я буду проверять температуру и влажность по расписанию, используя датчик, и включать или выключать вентилятор, подсоединенный через один из релейных портов, когда температура будет расти выше или падать ниже определенного порога. Текущее состояние отображается на OLED-дисплее, а джойстик позволяет вручную регулировать целевую температуру.

При запуске устройства я буду подключать события к таймеру для получения показаний температуры и приема событий от джойстика. При нажатии кнопки джойстика я буду приостанавливать таймер, проверять целевую температуру согласно позиции джойстика, немедленно запрашивать новые данные по температуре от датчика и возобновлять таймер. По окончании считывания температуры датчик генерирует событие TemperatureHumidityMeasurementComplete. Я буду сохранять текущие показания и регулировать состояние реле, чтобы при необходимости включать вентилятор. Такова основная логика термореле, и ее часть показана на рис. 2.

Рис. 2. Считывание показаний температуры и влажности

void WireEvents()
{
  this.InitializeTemperatureSensor();
  this.InitializeJoystick();
}
void InitializeTemperatureSensor()
{
  this.temperatureCheckTimer = new Timer(5000);
  this.temperatureCheckTimer.Tick += (t) =>
    this.temperatureHumidity.RequestMeasurement();
  this.temperatureCheckTimer.Start();
    this.temperatureHumidity.MeasurementComplete 
    += this.TemperatureHumidityMeasurementComplete;
}
void InitializeJoystick()
{
  this.joystick.JoystickPressed += this.JoystickPressed;
}
void JoystickPressed(Joystick sender, Joystick.JoystickState state)
{
  this.temperatureCheckTimer.Stop();
  var jStick = this.joystick.GetJoystickPostion();
  if (jStick.Y < .3 || jStick.X < .3)
  {
    settings.TargetTemperature -= .5;
    StoreSettings(settings);
  }
  else if (jStick.Y > .7 || jStick.X > .7)
  {
    settings.TargetTemperature += .5;
    StoreSettings(settings);
  }
  this.RedrawDisplay();
  this.temperatureHumidity.RequestMeasurement();
  this.temperatureCheckTimer.Start();
}
void TemperatureHumidityMeasurementComplete(TemperatureHumidity sender, 
  double temperature, double relativeHumidity)
{
  var targetTemp = settings.TargetTemperature;
  this.lastTemperatureReading = temperature;
  this.lastHumidityReading = relativeHumidity;
  this.relays.Relay1 = (lastTemperatureReading > targetTemp);
  this.RedrawDisplay();
}

Всякий раз, когда я регулирую целевую температуру в методе JoystickPressed, я сохраняю новое значение в поле settings класса Program и вызываю StoreSettings. Поле settings имеет тип ApplicationSettings — это сериализуемый класс в коде устройства, который сохраняет все, что устройству нужно запоминать между сбросами состояния и выключениями электропитания. Для постоянного хранения данных .NET Micro Framework резервирует некоторые страницы хранилища в энергонезависимой памяти устройства и предоставляет доступ к этому хранилищу через класс ExtendedWeakReference. Вряд ли это покажется вам интуитивно понятным, пока вы не осознаете, что это основной механизм переброса данных из основной памяти в случае ее нехватки и он тем более удобен, что заодно является хранилищем. Данный класс хранит слабые ссылки на объекты — так же, как это делает обычный WeakReference в .NET Framework, но переводит данные в энергонезависимую память, а не отбрасывает их, когда срабатывает сборщик мусора. Поскольку данные сбрасываются из основной памяти, для сохранения их нужно сериализовать, и это объясняет, почему класс ApplicationSettings должен быть сериализуемым (использование этого класса вы увидите позднее, когда мы будем обсуждать подготовку устройства).

Восстановление объекта из этого хранилища или создание нового слота в хранилище с помощью метода RecoverOrCreate требует указания уникального идентификатора. У меня сохраняется всего один объект, поэтому я буду использовать фиксированный идентификатор (ноль). После того как объект сохранен и один раз восстановлен, любые его обновления нужно принудительно записывать в хранилище, вызывая метод PushBackIntoRecoveryList экземпляра ExtendedWeakReference, и именно это я делаю в StoreSettings для передачи изменений, как показано на рис. 3.

Рис. 3. Обновление сохраненных данных

static ApplicationSettings GetSettings()
{
  var data = ExtendedWeakReference.RecoverOrCreate(
    typeof(ApplicationSettings),
    0,
    ExtendedWeakReference.c_SurviveBoot | 
    ExtendedWeakReference.c_SurvivePowerdown);
  var settings = data.Target as ApplicationSettings;
  if (settings == null)
  {
    data.Target = settings = ApplicationSettings.Defaults;
  }
  return settings;
}
static void StoreSettings(ApplicationSettings settings)
{
  var data = ExtendedWeakReference.RecoverOrCreate(
    typeof(ApplicationSettings),
    0,
    ExtendedWeakReference.c_SurviveBoot | 
    ExtendedWeakReference.c_SurvivePowerdown);
  data.Target = settings;
  data.PushBackIntoRecoverList();
}

Подготовка

Изначально устройство находится в состоянии «factory new» (заводские настройки): код устройства развернут, но само устройство не инициализировано, и поэтому у него нет текущих настроек. Вы можете увидеть это состояние — оно отражается метод GetSettings, когда объект settings все еще пуст и поэтому инициализируется настройками по умолчанию.

Поскольку я хочу, чтобы это устройство взаимодействовало через инфраструктуру Microsoft Azure Service Bus, мне нужно снабдить устройство набором удостоверений, необходимых для общения с ней, а также указать устройству, с какими ресурсами ему следует взаимодействовать. Первый шаг в настройке нового устройства под требуемую сетевую конфигурацию и соответствующих ресурсов на серверной стороне называется подготовкой (provisioning); базовую архитектурную модель этого процесса я обсуждал в прошлой статье.

В коде устройства я сделаю так, чтобы инициировать операции подготовки всякий раз, когда оно подключается к сети и не имеет должной конфигурации. Для этого я использую булев флаг в параметрах, который сообщает мне, все ли в порядке. Если этот флаг не установлен, я вызываю сервис подготовки, размещенный в Microsoft Azure.

Сервис подготовки отвечает за проверку идентификации устройства по его уникальному идентификатору, который был зарегистрирован в списке, поддерживаемом этим сервисом. При активации устройства этот идентификатор удаляется из списка. Однако, чтобы не усложнять код для этой статьи, я опущу реализацию управления списком разрешенных устройств (allow-list).

Как только устройство считается разрешенным, сервис подготовки в соответствии с моделью, изложенной в предыдущей статье, приписывает устройство к конкретной единице масштаба и к конкретному Fan-Out Topic внутри этой единицы. В этом примере я не буду ничего усложнять и создам подписку для фиксированного Topic с именем «devices», который выступает в роли канала команд от облака к устройству, и для Topic с именем «events», собирающего информацию о событиях от устройств. В дополнение к созданию подписки и сопоставлению устройства с Topic «devices» я также создам идентификацию устройства для сервиса в Access Control Service (функция Microsoft Azure Active Directory) и выдам этой идентификации права, необходимые для отправки сообщений в Topic «events» и приема сообщений от только что созданной подписки на Topic «devices». Устройство может выполнять в Microsoft Azure Service Bus только эти две операции — и ничего больше.

Базовая часть сервиса подготовки показана на рис. 4. Сервис зависит от API управления в Microsoft Azure Service Bus (NamespaceManager), который находится в базовой сборке Microsoft.ServiceBus.dll — она поставляется как часть Microsoft Azure SDK или через NuGet. Сервис также полагается на вспомогательную библиотеку для управления разрешениями в учетных записях. Эта библиотека является частью примера Authorization для Service Bus, и, конечно, она тоже включена в пакет исходного кода для этой статьи.

Рис. 4. Сервис подготовки

namespace BackendWebRole
{
  using System;
  using System.Configuration;
  using System.Linq;
  using System.Net;
  using System.ServiceModel;
  using System.ServiceModel.Web;
  using Microsoft.ServiceBus;
  using Microsoft.ServiceBus.AccessControlExtensions;
  using Microsoft.ServiceBus.Messaging;
  [ServiceContract(Namespace = "")]
  public class ProvisioningService
  {
    const string DevicesTopicPath = "devices";
    const string EventsTopicPath = "events";
    static readonly AccessControlSettings AccessControlSettings;
    static readonly string ManagementKey;
    static readonly string NamespaceName;
    static Random rnd = new Random();
      static ProvisioningService()
      {
        NamespaceName = ConfigurationManager.AppSettings["serviceBusNamespace"];
        ManagementKey = ConfigurationManager.AppSettings["managementKey"];
        AccessControlSettings = new AccessControlSettings(
          NamespaceName, ManagementKey);
      }
      [OperationContract, WebInvoke(Method = "POST", UriTemplate = "/setup")]
      public void SetupDevice()
      {
        var rcx = WebOperationContext.Current.OutgoingResponse;
        var qcx = WebOperationContext.Current.IncomingRequest;
        var id = qcx.Headers["P-DeviceId"];
        if (this.CheckAllowList(id))
        {
          try
          {
            var deviceConfig = new DeviceConfig();
            CreateServiceIdentity(ref deviceConfig);
            CreateAndSecureEntities(ref deviceConfig);
            rcx.Headers["P-DeviceAccount"] = deviceConfig.DeviceAccount;
            rcx.Headers["P-DeviceKey"] = deviceConfig.DeviceKey;
            rcx.Headers["P-DeviceSubscriptionUri"] =
              deviceConfig.DeviceSubscriptionUri;
            rcx.Headers["P-EventSubmissionUri"] = deviceConfig.EventSubmissionUri;
            rcx.StatusCode = HttpStatusCode.OK;
            rcx.SuppressEntityBody = true;
          }
          catch (Exception)
          {
            rcx.StatusCode = HttpStatusCode.InternalServerError;
            rcx.SuppressEntityBody = true;
          }
        }
        else
        {
          rcx.StatusCode = HttpStatusCode.Forbidden;
          rcx.SuppressEntityBody = true;
        }
      }
      static void CreateAndSecureEntities(ref DeviceConfig deviceConfig)
      {
        var namespaceUri = ServiceBusEnvironment.CreateServiceUri(
          Uri.UriSchemeHttps, NamespaceName, string.Empty);
        var nsMgr = new NamespaceManager(namespaceUri,
          TokenProvider.CreateSharedSecretTokenProvider("owner", ManagementKey));
        var ruleDescription = new SqlFilter(
          string.Format("DeviceId='{0}' OR Broadcast=true",
            deviceConfig.DeviceAccount));
        var subscription = nsMgr.CreateSubscription(
          DevicesTopicPath, deviceConfig.DeviceAccount, ruleDescription);
        deviceConfig.EventSubmissionUri = new Uri(
          namespaceUri, EventsTopicPath).AbsoluteUri;
        deviceConfig.DeviceSubscriptionUri =
          new Uri(namespaceUri,
            SubscriptionClient.FormatSubscriptionPath(
              subscription.TopicPath,
              subscription.Name)).AbsoluteUri;
        GrantSendOnEventTopic(deviceConfig);
        GrantListenOnDeviceSubscription(deviceConfig);
      }
      static void GrantSendOnEventTopic(DeviceConfig deviceConfig)
      {
        var settings = new AccessControlSettings(NamespaceName, ManagementKey);
        var topicUri = ServiceBusEnvironment.CreateServiceUri(
          Uri.UriSchemeHttp, NamespaceName, EventsTopicPath);
        var list = NamespaceAccessControl.GetAccessControlList(topicUri, settings);
        var identityReference =
          IdentityReference.CreateServiceIdentityReference(
            deviceConfig.DeviceAccount);
        var existing = list.FirstOrDefault((r) =>
          r.Condition.Equals(identityReference) &&
          r.Right.Equals(ServiceBusRight.Send));
        if (existing == null)
        {
          list.AddRule(identityReference, ServiceBusRight.Send);
          list.SaveChanges();
        }
      }
      static void GrantListenOnDeviceSubscription(DeviceConfig deviceConfig)
      {
        var settings = new AccessControlSettings(NamespaceName, ManagementKey);
        var subscriptionUri = ServiceBusEnvironment.CreateServiceUri(
          Uri.UriSchemeHttp,
          NamespaceName,
          SubscriptionClient.FormatSubscriptionPath(
            DevicesTopicPath, deviceConfig.DeviceAccount));
        var list = NamespaceAccessControl.GetAccessControlList(
          subscriptionUri, settings);
        var identityReference = IdentityReference.CreateServiceIdentityReference(
          deviceConfig.DeviceAccount);
        var existing = list.FirstOrDefault((r) =>
          r.Condition.Equals(identityReference) &&
          r.Right.Equals(ServiceBusRight.Listen));
        if (existing == null)
        {
          list.AddRule(identityReference, ServiceBusRight.Listen);
          list.SaveChanges();
        }
      }
      static void CreateServiceIdentity(ref DeviceConfig deviceConfig)
      {
        var name = Guid.NewGuid().ToString("N");
        var identity =
          AccessControlServiceIdentity.Create(AccessControlSettings, name);
        identity.Save();
        deviceConfig.DeviceAccount = identity.Name;
        deviceConfig.DeviceKey = identity.GetKeyAsBase64();
      }
        bool CheckAllowList(string id)
      {
        return true;
      }
  }
}

Сервис состоит из единственного HTTP-ресурса с именем «/setup», реализованного с применением WCF-операции SetupDevice, которая принимает POST-запросы. Вы заметите, что этот метод не имеет параметров и ничего не возвращает. Это не случайно. Вместо использования HTTP entity-body в XML, JSON или кодирования формы для передачи информации о запросе и ответе я предельно упрощаю операции для устройства и помещаю полезные данные в собственные HTTP-заголовки. Это исключает необходимость в специфическом средстве разбора для анализа полезных данных и позволяет использовать минимум памяти для кода. HTTP-клиенту известно, как разбирать эти заголовки, и для того, что мне нужно здесь делать, этого более чем достаточно.

Соответствующий код устройства, вызывающий HTTP-ресурс, показан на рис. 5, и трудно вообразить, как сделать этот вызов еще проще. Идентификатор устройства посылается в одном из заголовков и в них же возвращаются параметры конфигурации после подготовки. Никакой возни с потоками данных, никакого разбора — только простые пары «ключ-значение», изначально понятные HTTP-клиенту.

Рис. 5. Конфигурирование устройства

bool PerformProvisioning()
{
  [ ... display status ... ]
  try
  {
    var wr = WebRequest.Create(
      "http://cvdevices.cloudapp.net/Provisioning.svc/setup");
    wr.Method = "POST";
    wr.ContentLength = 0;
    wr.Headers.Add("P-DeviceId", this.deviceId);
    using (var wq = (HttpWebResponse)wr.GetResponse())
    {
      if (wq.StatusCode == HttpStatusCode.OK)
      {
        settings.DeviceAccount = wq.Headers["P-DeviceAccount"];
        settings.DeviceKey = wq.Headers["P-DeviceKey"];
        settings.DeviceSubscriptionUri = new Uri(
          wq.Headers["P-DeviceSubscriptionUri"]);
        settings.EventSubmissionUri = new Uri(
          wq.Headers["P-EventSubmissionUri"]);
        settings.NetworkProvisioningCompleted = true;
        StoreSettings(settings);
        return true;
      }
    }
  }
  catch (Exception e)
  {
    return false;
  }
  return false;
}
void NetworkAvailable(Module.NetworkModule sender,
  Module.NetworkModule.NetworkState state)
{
  ConvertBase64.ToBase64String(ethernet.NetworkSettings.PhysicalAddress);
  if (state == Module.NetworkModule.NetworkState.Up)
  {
    try
    {
      Utility.SetLocalTime(NtpClient.GetNetworkTime());
    }
    catch
    {
      // Глотаем любые исключения таймера
    }
    if (!settings.NetworkProvisioningCompleted)
    {
      if (!this.PerformProvisioning())
      {
        return;
      }
    }
    if (settings.NetworkProvisioningCompleted)
    {
      this.tokenProvider = new TokenProvider(
        settings.DeviceAccount, settings.DeviceKey);
      this.messagingClient = new MessagingClient(
        settings.EventSubmissionUri, tokenProvider);
    }
  }
}

Если параметры указывают на необходимость подготовки, вызывается метод Perform Provisioning из функции NetworkAvailable, которая запускается в том случае, когда подключается сеть и устройству назначается IP-адрес через DHCP. По завершении подготовки параметры используются для конфигурирования провайдера маркеров и коммуникационного клиента с целью взаимодействия с Microsoft Azure Service Bus. Кроме того, вы заметите, что вызывается NTP-клиент (Network Time Protocol). Я позаимствовал простой NTP-клиент с лицензией BSD, написанный Майклом Шварцем (Michael Schwarz), чтобы получать в своем примере текущее время — оно нужно, если вы собираетесь проверять срок действия сертификатов SSL.

Как видно на рис. 4, SetupDevices вызывает имитационную проверку по списку CheckAllowList и, если проверка заканчивается успешно, вызывает CreateServiceIdentity и CreateAndSecureEntities. Метод CreateServiceIdentity создает новую идентификацию сервиса наряду с секретным ключом в пространстве имен Access Control, сопоставленном с пространством имен Microsoft Azure Service Bus, которое сконфигурировано для данного приложения. Метод CreateAndSecureEntities создает новую подписку для сущности в Topic «devices» и настраивает ее с помощью SQL-правила, позволяющего посылать сообщения в этот Topic для адресации либо конкретной подписке (для этого указывается свойство DeviceId, и ему присваивается имя учетной записи устройства), либо всем подпискам (включением булева свойства Broadcast со значением true). После того как подписка создана, этот метод вызывает методы GrantSendOnEventTopic и GrantListenOnDeviceSubscription, которые выдают требуемые разрешения для доступа к сущностям новой идентификации сервиса, используя библиотеку Access Control.

После успешного выполнения всех действий результаты операций подготовки сопоставляются с заголовками в HTTP-ответе на запрос и возвращаются с кодом состояния OK, а устройство сохраняет эти результаты в энергонезависимой памяти и устанавливает флаг для NetworkProvisioningCompleted.

Отправка событий и прием команд

По окончании подготовки устройство готово посылать события в Topic «events» в Microsoft Azure Service Bus и принимать команды от своей подписки на Topic «devices». Но сначала нам нужно обсудить важный вопрос: безопасность.

Как уже упоминалось, SSL/TLS — набор протоколов, которые дорого обходятся малым устройствам. То есть некоторые устройства либо вообще никогда не смогут поддерживать SSL/TLS, либо смогут, но с серьезными ограничениями из-за малых вычислительных мощностей и памяти. По сути, хотя на момент написания этой статьи используемая мной материнская плата FEZ Spider от GHI Electronics, основанная на .NET Micro Framework 4.1, номинально может взаимодействовать по SSL/TLS, а значит и по HTTPS, ее прошивка с поддержкой SSL/TLS явно не в состоянии справиться с цепочкой сертификатов, передаваемой ей от Microsoft Azure Service Bus или сервиса Access Control Service (ACS). Так как прошивка этих устройств обновлена до .NET Micro Framework версии 4.2, для данного конкретного устройства эти ограничения сняты, но проблема того, что некоторые устройства попросту слишком ограничены в своих возможностях, чтобы работать с SSL/TLS, никуда не исчезла, и в сообществе разработчиков встраиваемых устройств сейчас ведется активная дискуссия по выбору подходящего протокола, который не накладывает такой тяжелой нагрузки на устройства.

Таким образом, хотя устройство теперь имеет нужную учетную запись, оно не может получить маркер от ACS, так как для этого необходимо использовать HTTPS. То же самое верно и в отношении отправки сообщения в Microsoft Azure Service Bus, которая требует применения HTTPS для всех запросов, требующих передачи маркера доступа, в том числе для всех взаимодействий с Queues и Topics. Более того, если бы этот пример был производственным кодом, мне, разумеется, пришлось бы предоставлять конечную точку сервиса подготовки через HTTPS, чтобы защитить секретный ключ, возвращаемый устройству.

И что теперь? Ну, «и что теперь» в конечном счете сводится к допустимым компромиссам, и одним из факторов обязательно являются деньги: в производстве увеличение цены на несколько центов выливается в миллионы при изготовлении какого-либо устройства. Если устройство не способно обрабатывать требуемый протокол безопасности, возникают следующие вопросы: какой ущерб возможен без этого протокола и как закрыть пробел между отсутствием нужных средств на устройстве и требованиями инфраструктуры.

Главное, что должно быть ясно, — любой незашифрованный и неподписанный поток данных может быть перехвачен и изменен. Когда устройство сообщает лишь данные от датчика, стоит поразмыслить, а есть ли смысл кому-то стороннему возиться с этими данными и можно ли обнаруживать попытки модификации чисто аналитическими методами. В ряде случаев вы придете к заключению, что открытая отправка таких данных вполне приемлема. Команды и коммуникационные пути — дело другое; если поведение устройства можно дистанционно контролировать по сети, вряд ли вам понравится, что коммуникационный путь не защищен хотя бы сигнатурой для проверки целостности. Значимость защиты команд и управления зависит от сценария применения. Если возможность сторонних манипуляций ограничена, то при установке целевой температуры для термореле вряд ли стоит прикладывать какие-то особые усилия в шифровании.

Примеры кода, включенные в эту статью, также содержат две вариации коммуникационного потока от устройства в облако. Первая — это сильно упрощенный Microsoft Azure Service Bus API, который требует применения HTTPS, регулярно запрашивает маркер Access Control и напрямую взаимодействует с Microsoft Azure Service Bus.

Вторая вариация использует ту же универсальную форму HTTP-протокола для передачи и приема сообщений через Microsoft Azure Service Bus, но создает в сообщении сигнатуру HMACSHA256 с помощью хранимого в ней секретного ключа. Это не защитит сообщение от перехвата, но исключит манипуляции над ним и позволит обнаруживать атаки с воспроизведением (replay attacks) при условии включения уникального идентификатора сообщения. Ответы будут подписываться тем же ключом. Поскольку Service Bus пока не поддерживает эту модель аутентификации (хотя Microsoft активно размышляет над этой задачей), в данной вариации используется собственный шлюзовой сервис, размещенный вместе с сервисом подготовки. Собственный шлюз проверяет и отделяет сигнатуру, а затем передает само сообщение в Microsoft Azure Service Bus. Применение собственного шлюза для трансляции протоколов — в целом также подходящая модель, если вам нужно, чтобы облачная система «говорила» на одном из множества закрытых протоколов, понятных тому или иному устройству. Но при таком подходе всегда помните, что шлюз должен масштабироваться до числа устройств, одновременно посылающих сообщения, поэтому крайне желательно, чтобы шлюзовой уровень был очень тонким и не использовал состояния.

Различия между этими двумя вариациями в конечном счете сводятся к тому, какими URI оперирует сервис подготовки — для HTTP или HTTPS. В коде устройства эта разница обрабатывается TokenProvider. В случае HTTPS устройства напрямую обращаются к Microsoft Azure Service Bus, тогда как в случае HTTP сервис подготовки взаимодействует со шлюзовым уровнем. Здесь используется допущение, которое заключается в том, что в случае HTTP устройства подготавливаются заранее без передачи секретного ключа по незащищенному коммуникационному каналу через Интернет. Иначе говоря, сервис подготовки выполняется на заводе, а не в Microsoft Azure.

Код устройства взаимодействует с Microsoft Azure Service Bus в двух случаях: при отправке событий и получении команд. Я буду отправлять события каждую минуту после задания новой температуры и в этот момент буду принимать любые ожидающие команды и выполнять их. Для этого я модифицирую метод TemperatureHumidityMeasurementComplete, показанный на рис. 2, и добавлю вызовы SendEvent и ProcessCommands для обработки каждую минуту, как представлено на рис. 6.

Рис. 6. Отправка событий и прием команд

void TemperatureHumidityMeasurementComplete(TemperatureHumidity sender,
  double temperature, double relativeHumidity)
{
  [...] (см. рис. 2)
  if (settings.NetworkProvisioningCompleted &&
    DateTime.UtcNow - settings.LastServerUpdate >
      TimeSpan.FromTicks(TimeSpan.TicksPerMinute))
  {
    settings.LastServerUpdate = DateTime.UtcNow;
    SendEvent(this.lastTemperatureReading, this.lastHumidityReading);
    ProcessCommands();
  }
}
void SendEvent(double d, double lastHumidityReading1)
{
  try
  {
    messagingClient.Send(new SimpleMessage()
      {
        Properties = {
          {"Temperature",d},
          {"Humidity", lastHumidityReading1},
          {"DeviceId", settings.DeviceAccount}
        }
      });
  }
  catch (Exception e)
  {
    Debug.Print(ethernet.ToString());
  }
}
void ProcessCommands()
{
  SimpleMessage cmd = null;
  try
  {
    do
    {
      cmd = messagingClient.Receive(TimeSpan.Zero, ReceiveMode.ReceiveAndDelete);
      if (cmd != null && cmd.Properties.Contains("Command"))
      {
        var commandType = (string)cmd.Properties["Command"];
        switch (commandType)
        {
          case "SetTemperature":
            if (cmd.Properties.Contains("Parameter"))
            {
              this.settings.TargetTemperature =
                double.Parse((string)cmd.Properties["Parameter"]);
              this.RedrawDisplay();
              this.temperatureHumidity.RequestMeasurement();
              StoreSettings(this.settings);
            }
            break;
        }
      }
    }
    while (cmd != null);
  }
  catch (Exception e)
  {
    Debug.Print(e.ToString());
  }
}

Метод SendEvent использует коммуникационный клиент, который инициализируется, как только становится доступным сетевое соединение. Этот клиент — крошечная версия Microsoft Azure Service Bus API, способного отправлять сообщения в Service Bus Queues, Topics и Subscriptions и принимать их оттуда. Метод ProcessCommands работает с тем же клиентом для получения команд от подписки для устройства и их обработки. На данный момент устройство понимает только команду SetTemperature с параметром, указывающим абсолютную температуру в виде числового значения (кстати, в градусах Цельсия). Заметьте, что ProcessCommands задает период ожидания TimeSpan.Zero для приема сообщений от подписки в Microsoft Azure Service Bus, указывая, что он не станет ожидать прибытия сообщений. Я хочу выполнять операцию получения сообщения, только если оно имеется, а в ином случае немедленно прерывать ее. Это уменьшает трафик шлюзового уровня (если используется HTTP и таковой уровень есть) и не требует оставлять открытым цикл приема сообщений в устройстве. Недостаток этого подхода — возможность задержки. В худшем случае команды будут приниматься с задержкой в одну минуту. Если это создает проблему, используйте период ожидания, отличный от нулевого; это позволяет свести задержку в приеме команд до нескольких миллисекунд.

Соответствующий код на серверной стороне для приема событий и отправки команд всем зарегистрированным устройствам передачей сообщений в Topic просто следует всем правилам Microsoft Azure Service Bus API и является частью примеров кода, которые вы можете скачать, поэтому здесь я опускаю этот код.

Заключение

Цель этой серии статей об Интернете вещей — дать вам некоторое представление о технологиях, над которыми мы работаем в Microsoft, чтобы вы могли создавать прототипы подключенных устройств и вести разработки для них. Мы также хотели показать, как облачные технологии вроде Microsoft Azure Service Bus и технологии для аналитики, такие как StreamInsight, могут помочь вам в управлении потоками данных между подключенными устройствами и вашими сервисами. Попутно мы показали, как можно создавать крупномасштабные облачные архитектуры для обработки огромных количеств устройств и как агрегировать и использовать информацию от них.

Я также сконструировал встраиваемое устройство, которое можно разместить в любой домашней сети и дистанционно управлять им из любой другой сети, что само по себе весьма захватывающе.

Я считаю, что мы пока находимся в самом начале большого пути. Общаясь с клиентами Microsoft со всего мира, я вижу, что на подходе большая волна подключенных и самостоятельно создаваемых устройств. Это открывает колоссальные возможности для .NET-разработчиков и инновационных компаний в подключении этих устройств к облаку и объединении их с другими подключенными устройствами.


Клеменс Вастерс (Clemens Vasters)главный технический руководитель группы Microsoft Azure Service Bus. Работает в этой группе с самого начала ее деятельности и занимается «дорожной картой» технических средств для Microsoft Azure Service Bus. Часто выступает на конференциях, автор учебно-методической литературы по архитектуре. Следите за его заметками в twitter.com/clemensv.

Выражаю благодарность за рецензирование статьи экспертам Элио Дамаггио (Elio Damaggio), Тодду Холмквисту-Сазерленду (Todd Holmquist-Sutherland), Абишеку Лалу (Abhishek Lal), Заху Либби (Zach Libby), Колину Миллеру (Colin Miller) и Лоренцо Тессиоре (Lorenzo Tessiore).