"Internet of Things" (モノのインターネット)

サービス バスを使用したスマート サーモスタット

Clemens Vasters

コード サンプルのダウンロード

大胆に予測すると、インターネット接続型のデバイスは市場で大きな地位を占めようとしており、こうしたデバイスを理解することが開発者にとって近い将来重要になります。「当然だ」と思うかもしれません。ただ、私が言っているのはこの記事を読むのに使っているようなデバイスではなく、この夏部屋を涼しくしたり、服や食器を洗ったり、朝のコーヒーをいれたりするデバイスや、工場の作業場で他のデバイスを取りまとめるようなデバイスです。

MSDN マガジン 6 月号の記事 (msdn.microsoft.com/magazine/jj133819) で、Windows Azure サービス バスを使用して組み込み (およびモバイル) デバイスとの間のイベントとコマンドのフローを制御する方法についての一連の考慮事項を説明し、アーキテクチャの概要を示しました。今回は、これをさらに進め、こうしたイベントとコマンドのフローを作成してセキュリティを確保するコードを説明します。そして、組み込みデバイスについて本当に理解するには実際に何かで確認する必要があるため、ここではデバイスを 1 つ配線して構築し、Windows Azure サービス バスに接続して、現在状態に関するイベントを送信し、Windows Azure クラウド経由のメッセージを使って遠隔操作できるようにします。

ほんの数年前までは、電源や、マイクロコントローラー、一連のセンサーを取り付けた小さなデバイスを組み立てるには、電子ハードウェアの設計と各装置を 1 つにまとめあげる能力をいくらか必要とし、当然ながら、はんだごてもうまく扱えなければなりませんでした。喜んで打ち明けますが、個人的にはハードウェア学科ではかなり苦労しました。友人に言わせれば「宇宙人のロボットが地球を攻撃してきたら、君を前線に送るよ。そうすれば、君はただいるだけで電気ショートの盛大な火花を散らして、襲撃を失敗させるに違いない」んだそうです。しかし、Arduino/Netduino や .NET Gadgeteer などのプロトタイピング プラットフォームが広まったことで、はんだごてを振り回して人やコンピューターに危害を加えるような人でさえ、それまでのプログラミング スキルを活かして、完全に機能する小さなデバイスを組み立てられるようになりました。

前回設計したシナリオにこだわり、今回はサーモスタット制御のファンの形式で「エアコン」を組み立てます。配線の点から最も興味をひかない部分がファンです。このプロジェクトのコンポーネントは .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、および GHI Electronics か Seeed の Gadgeteer SDK が必要になります。いったんこれらをインストールすると、Visual Studio で表示できるようになるため、褒めすぎと思われるかもしれませんが、開発エクスペリエンスが非常にすばらしくかつ視覚的になります (図 1 参照)。

Designing the Device in the .NET Gadgeteer図 1 .NET Gadgeteer でデバイスをデザインする

図 1 に示すのは、Visual Studio における .NET Gadgeteer プログラムのデザイン ビューです。この記事に実際のデバイスの写真を含めることも考えましたが、どんな写真を含めてもこのダイアグラムに勝るものはありません。これがまさにデバイスの概観を表しています。

拡張子 .gadgeteer の付いたファイルには、エディターで表示されたデザインの XML モデルが含まれています。Gadgeteer ツールは、この XML ファイルから、メインボードに接続される各モジュールのラッパーを備えた Program 部分クラスを自動生成します。他の .NET API でよく知られている分離コード モデルと同様、コードは Program クラスのもう 1 つの部分を格納する Program.cs に配置します。

これらのデバイスと共に .NET Micro Framework を使用します。.NET Micro Framework は、処理能力が制限されますが、メモリの少ない小さなデバイス専用に作成された、完全なオープン ソース版の Microsoft .NET Framework です。.NET Micro Framework には .NET Framework で使い慣れた多くのクラスがありますが、全体的なコードのフットプリントを減らすため、ほとんどのクラスはその機能が低減されています。このフレームワークはデバイスのネイティブ ハードウェアの上位に重ねられる 1 つの層です。このようなデバイスは、あらゆるハードウェアを抽象化して処理する OS を備えた汎用コンピューターではありません (実際 OS は存在しません)。したがって、デバイスで使用できるフレームワークのバージョンは、前提条件をサポートするボードのメーカーによって異なります。これは、ハードウェアの詳細がハードウェア自体から切り離され、.NET Framework と同じ高いレベルで扱われる通常の PC のエクスペリエンスとは明らかに大きく異なります。

通常の .NET Framework、および PC プラットフォーム一般とのその他の違いは、PC のバックグラウンドから生じ、これには最初は驚かされます。たとえば、デバイスのボード上にはバッテリがありません。バッテリがないのでバッファ付きクロックがないことになり、デバイスは起動したときに正確な実測時間を把握できません。OS がないので、サードパーティ製の拡張ディスプレイを取り付けても、ディスプレイに文字列を描くためのフォントもありません。文字列を表示するのであれば、フォントを追加しなければなりません。

同様に、デバイスには事前設定された Windows Update が管理する証明書ストアがありません。SSL/TLS 証明書を検証する場合は、デバイスに少なくともルート CA 証明書を配置する必要があり、当然ながら証明書の有効性を確認するために現在時刻を把握する必要もあります。お察しのように、証明書の処理はこれらのデバイスにとってちょっとした障害になり、処理能力、メモリ消費、およびコードのフットプリントの観点で SSL/TLS の暗号化の要求が重要になり、これはすべてのデバイスがサポートしているわけではありません。ただし、セキュリティの重要性は明らかに高まっており、ここでもデバイスはインターネットを介した通信が必要なので、.NET Micro Framework のバージョン 4.2 では処理するのに十分なリソースを持つデバイス向けに SSL/TLS サポートの重要な強化版を提供しています。これに関しては、もう少し後で詳しく説明します。

サーモスタット機能

このサンプルのローカル サーモスタット機能の実装は、非常に簡単です。定期的にセンサーを使って温度と湿度を確認し、温度が特定のしきい値を上回るか下回ったときに、リレー ポートの 1 つに接続されたファンのオン/オフを切り替えます。現在状態が 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 メソッドで設定温度を調節するたびに、Program クラスの settings フィールドに新しい値を格納し、StoreSettings を呼び出します。settings フィールドは ApplicationSettings 型でデバイス コードに含まれ、リセットや電源のオン/オフで失われないように、デバイスが記憶する必要がある情報をすべて格納するシリアル化可能なクラスです。データを永続的に保存するため、.NET Micro Framework はデバイスの不揮発性メモリにストレージ ページを確保し、ExtendedWeakReference クラスでこのストレージへのアクセスを提供します。これは主に容量が足りないメイン メモリからデータをスワップ アウトするためのメカニズムで、ストレージの機能を簡単に増やすことができます。このクラスは、.NET Framework の通常の WeakReference と同様に、オブジェクトへの弱参照を格納しますが、ガベージ コレクションが行われたらデータが破棄されるのではなく、不揮発性ストレージにスワップします。データはメイン メモリからスワップ アウトされるため、ストレージにシリアル化する必要があり、そのため ApplicationSettings クラス (後でプロビジョニングについて説明するときに使用を確認します) をシリアル化できる必要があります。

RecoverOrCreate メソッドを使用してストレージの場所からオブジェクトを回復したり、新しいストレージ スロットを作成するには、一意識別子を指定する必要があります。格納するオブジェクトは 1 つだけなので、固定識別子 (ゼロ) を使用します。いったんオブジェクトが格納され回復されたら、ExtendedWeakReference インスタンスの PushBackIntoRecoveryList メソッドを使用して更新内容をストレージに書き戻す必要があります。このようにして 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();
}

プロビジョニング

最初は、デバイスは "工場出荷" (新規) 状態で、デバイス コードは配置されていますが、まだ初期化されておらず、現在設定は何もありません。設定オブジェクトが null のままで、既定の設定で初期化されたら、この状態が GetSettings メソッドに反映されているのを確認できます。

インターネット インフラストラクチャ (Windows Azure サービス バス) 経由でデバイスが通信できるようにする必要があるため、このインフラストラクチャと通信し、インフラストラクチャにどのリソースと通信するかを伝えるために、デバイスに一連の資格情報を設定する必要があります。工場出荷状態の新しいデバイスを必要なネットワーク構成に設定し、サーバー側の対応するリソースを設定する最初の手順を "プロビジョニング" と呼びます。これについての基本アーキテクチャ モデルは前回の記事で説明しました。

デバイス コードでは、デバイスが適切にプロビジョニングされ、デバイスがネットワークに接続され有効に構成されていない場合に毎回プロビジョニングの手順を開始するように、非常に厳密に作成します。そのため、設定でブール型フラグを確認し、既にプロビジョニングが成功しているかどうかを判断します。フラグが設定されていなければ、Windows Azure でホストされているプロビジョニング サービスの呼び出しを発行します。

プロビジョニング サービスには、デバイスの一意識別子を使用してデバイスの ID を検証する役目があります。識別子は生成時にサービスが管理する許可リストに登録されます。デバイスがアクティブになると、許可リストから除外されます。ただ、今回はシンプルに保つため、許可リストの管理の実装は省略します。

デバイスが登録済みであることが判断されたら、プロビジョニング サービスは、前回の記事で構築したモデルに従い、デバイスを特定のスケール単位と、そのスケール単位内の特定のファンアウト トピックに割り当てます。今回の場合はシンプルに保ち、クラウドからデバイスへのコマンド チャネルとして機能する "devices" (デバイス) という名前の単一の固定トピックと、デバイスの情報を集める "events" (イベント) という名前のトピック用のサブスクリプションを作成します。サブスクリプションを作成してデバイスをトピックと関連付けることに加えて、アクセス制御サービス (Windows Azure Active Directory の機能) でデバイス用のサービス ID を作成し、イベント トピックにメッセージを送信し、デバイス トピックに新しく作成したサブスクリプションからメッセージを受信するのに必要な権限を与えます。デバイスが Windows Azure サービス バスで実行できるのはこれら 2 つの操作のみです。

図 4 に示すのは、プロビジョニング サービスの中心部分です。このサービスは、Windows Azure SDK の一部として、または NuGet を介して提供される Microsoft.ServiceBus.dll コア アセンブリにある Windows Azure サービス バス管理 API (NamespaceManager) に依存しています。サービス バスの認証サンプルの一部として利用可能で、当然ながらこの記事のダウンロード可能なコードにも含まれている、アクセス制御アカウントと許可を管理するためのヘルパー ライブラリにも依存しています。

図 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;
      }
  }
}

サービスは /setup という単一の HTTP リソースで構成され、POST 要求を受け取る Windows Communication Foundation (WCF) Web オペレーション SetupDevice を使用して実装されます。メソッドにはパラメーターがなく、エンティティ ペイロードを返しません。これは、間違いではありません。XML、JSON、またはフォームエンコーディングで HTTP エンティティ本体を使用して要求と応答情報を通信するのではなく、デバイス用に非常にシンプルにし、カスタムの 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
    {
      // Swallow any timer exceptions
    }
    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);
    }
  }
}

プロビジョニングが必要なことを設定が示している場合、NetworkAvailable 関数から PerformProvisioning メソッドが呼び出されます。NetworkAvailable 関数は、ネットワークが有効でデバイスに DHCP を通じて IP アドレスが割り当てられているときに呼び出されます。プロビジョニングが完了したら、設定を使用してトークンのプロバイダーとメッセージング クライアントを構成し、Windows Azure サービス バスと通信します。NTP クライアントの呼び出しにも注目します。NTP は "ネットワーク タイム プロトコル (network time protocol)" の略です。ここではサンプルで SSL 証明書の期限を確認するときに必要な現在時刻を取得するのに、Michael Schwarz が作成したシンプルな BSD ライセンスの NTP クライアントを借用しました。

図 4 に戻ると確認できるように、SetupDevices は許可リストの CheckAllowList でモックチェックを呼び出し、正常に実行されたら CreateServiceIdentity と CreateAndSecureEntities を呼び出します。CreateServiceIdentity メソッドは、アプリケーション用に構成された Windows Azure サービス バスの名前空間に関連付けられたアクセス制御の名前空間により、新しいサービス ID と秘密キーを作成します。CreateAndSecureEntities メソッドは、デバイス トピックのエンティティ用に新しいサブスクリプションを作成して、サブスクリプションの SQL 規則を構成します。規則では、メッセージをトピックに送信し、デバイス アカウント名に設定した DeviceId プロパティを含めて特定のサブスクリプションを対象にするか、true 値のブール型を持つ Broadcast プロパティを含めてすべてのサブスクリプションを対象にできるようにします。サブスクリプションを作成したら、メソッドは GrantSendOnEventTopic メソッドと GrantListenOnDeviceSubscription メソッドを呼び出し、両メソッドはアクセス制御ライブラリを使用して、エンティティに必要な許可を新しいサービス ID に与えます。

これらがすべて正常に完了したら、プロビジョニング操作の結果が、要求に対する HTTP 応答のヘッダーにマッピングされ、OK 状態コードと共に返されます。そして、デバイスは結果を不揮発性メモリに格納し、NetworkProvisioningCompleted のフラグを設定します。

イベントの送信とコマンドの受信

プロビジョニングが完了したら、デバイスは Windows Azure サービス バスのイベント トピックにイベントを送信し、デバイス トピックのサブスクリプションからコマンドを受信できるようになります。ただその前に、セキュリティという重要な問題を取り上げる必要があります。

前述のように、SSL/TLS は小さなデバイスには負荷の高いプロトコル スイートです。つまり、処理能力やメモリの制約のために SSL/TLS をサポートできないか、サポートが制限されるデバイスもあります。実際、この記事の執筆時に使用している .NET Micro Framework 4.1 に基づく GHI Electronics の FEZ Spider メインボードでは、SSL/TLS、したがって HTTPS と名目上は通信できますが、SSL/TLS ファームウェアは Windows Azure サービス バスかアクセス制御サービスが提供する証明書チェーンを処理できないようです。これらのデバイス用のファームウェアを .NET Micro Framework の新しい 4.2 バージョンに更新すると、この特定のデバイスに関してはこれらの制限はなくなりますが、一部のデバイスでは制約のために SSL/TLS を処理できないという問題は根本的に残るため、組み込みデバイスのコミュニティでは大きくなりすぎない適切なプロトコルの選択について活発に議論されています。

そのため、デバイスに適切なアカウントが用意されましたが、HTTPS を使用できないためアクセス制御サービスからトークンを取得できません。Windows Azure サービス バスへのメッセージの送信に関しても同様で、キューとトピックのすべての通信を含め、アクセス トークンを渡すすべての要求で HTTPS が必要です。さらに、このサンプルが運用コードの場合、デバイスに返すときに、当然ながら HTTPS を介してプロビジョニングのエンドポイントを公開し、秘密キーを保護する必要があります。

次に何をすればいいでしょうか。次に必要なことは、究極的には適切にバランスをとることです。これには、経済的な意味も含まれ、特定の種類のデバイスを数百万台も製造するときは数セントの価格差が大きな違いを生みます。デバイスでは要求されたセキュリティ プロトコルを処理できない場合、そのプロトコルがないとどの程度損失があるか、またデバイスに不足している必要な機能とインフラストラクチャの要求の差を埋めるにはどうしたらよいかを検討します。

明らかなのは、暗号化と署名が施されていないデータ ストリームは、盗聴や操作に対してぜい弱だということです。デバイスが報告するのがセンサー データのみのとき、だれかがそのようなネットワーク パスの man-in-the-middle 操作をする可能性があるか、または分析により検出できるかを検討する価値があります。検討次第では、そのままの状態でデータを送信しても問題ない場合があります。コマンドと制御のパスは別の問題で、デバイスの動作がネットワークを介して遠隔操作できるようになったらすぐに、信頼性のために少なくとも署名を使って通信パスを保護しようと考えるでしょう。コマンドと制御の情報を保護する価値は、ユース ケースによって異なります。操作が適切に防止されている場合、サーモスタットの設定温度の設定には、暗号化のための多大な労力を費やす価値はないと考えられるでしょう。

今回のサンプル コードには、デバイスからクラウドへの 2 種類の通信フローが含まれています。1 つは非常に単純化された Windows Azure サービス バス API で、HTTPS を要求し、アクセス制御トークンを取得して Windows Azure サービス バスと直接通信する通常のハンドシェイクを実行します。

2 つ目のパスは、メッセージの送受信に Windows Azure サービス バス HTTP プロトコルの通常の形式を使用しますが、格納している秘密キーを使用してメッセージに HMACSHA256 署名を作成します。これはメッセージの盗聴は保護されませんが、メッセージを操作することは防止し、一意メッセージ ID が含まれる場合、リプレイ攻撃を検出できるようにします。返信も同じキーを使用して署名されます。サービス バスはまだこの認証モデルをサポートしていないため (この記事は、マイクロソフトがこの問題を熱心に検討していることを示していますが)、このパスではプロビジョニング サービスと共に、ホストされているカスタム ゲートウェイ サービスを使用します。カスタム ゲートウェイは署名を確認して削除し、残りのメッセージを Windows Azure サービス バスに渡します。プロトコル変換にカスタム ゲートウェイを使用するのは、一般にクラウド システムを複数の専用のデバイス プロトコルの 1 つと通信できるようにする必要がある場合にも適切なモデルになります。ただし、カスタム ゲートウェイを使用する方法で注意が必要なのは、同時にメッセージを送信するデバイスの数を増やす必要があることで、そのためゲートウェイ層を非常に薄く、ステートレスにすることをお勧めします。

2 つのパスの違いは、結局のところプロビジョニング サービスが HTTP URI と HTTPS URI のどちらを使用するかの違いです。デバイス コードでは、この違いは TokenProvider によって処理されます。HTTPS の場合、デバイスは Windows Azure サービス バスと直接通信しますが、HTTP の場合は、プロビジョニング サービスがカスタム ゲートウェイと通信します。ここでは HTTP の場合、デバイスは保護されていないインターネット通信パスで、秘密キーを公開することなくプロビジョニングされることを想定しています。言い換えると、プロビジョニング サービスは Windows Azure ではなく、ファクトリで実行されます。

デバイス コードは Windows Azure サービス バスで、イベントの送信とコマンドの受信の 2 つの操作を行います。ここでは新しい温度情報が作成されたら 1 分に 1 回イベントを送信し、同時に保留中のコマンドを取得し、実行します。このため、図 2 で示した TemperatureHumidityMeasurementComplete メソッドを修正し、SendEvent と ProcessCommands の呼び出しを追加して 1 分に 1 回処理されるようにします (図 6 参照)。

図 6 イベントの送信とコマンドの受信

void TemperatureHumidityMeasurementComplete(TemperatureHumidity sender,
  double temperature, double relativeHumidity)
{
  [...] (see Figure 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 メソッドはメッセージング クライアントを使用し、ネットワーク接続が有効になったら初期化されます。メッセージング クライアントは、サービス バスのキュー、トピック、およびサブスクリプションとメッセージを送受信できる Windows Azure サービス バス API の小型版です。ProcessCommands メソッドは、同じクライアントを使用してデバイスのサブスクリプションからコマンドを取得し、処理します。今のところ、デバイスが把握するのは、数値として設定する絶対温度 (摂氏) を示す Parameter を指定した SetTemperature コマンドのみです。ProcessCommands は、Windows Azure サービス バス サブスクリプションからのメッセージの受信に TimeSpan.Zero タイムアウトを指定し、メッセージが到着するのを待機しないことに注意します。利用できるメッセージがある場合に限りメッセージを取得し、それ以外の場合はすぐにバックオフします。これにより、カスタム ゲートウェイのトラフィックが少なくなり (HTTP を使用し、メッセージがある場合)、デバイスで受信ループを開き続ける必要がなくなります。代わりに、待機時間が長くなります。最悪の場合、コマンドの待機時間が 1 分になります。これが問題なら、タイムアウトを引き延ばしてポーリングを長くし (ライブラリでサポートされています)、それによってコマンドの待機時間を数ミリ秒にします。

対応するサーバー側は、メッセージをトピックに配置してすべての登録デバイスにイベントの受信とコマンドの送信を実行するため、Windows Azure サービス バス API の通常の規則に単に従います。これは、ダウンロード可能なサンプル コードに含まれているので、ここではコードを割愛します。

まとめ

この "Internet of Things" (モノのインターネット) のシリーズの目的は、インターネット接続デバイスのプロトタイピングと開発のために私たちがマイクロソフトで取り組んでいる技術についての知見を提供することです。また、Windows Azure サービス バスなどのクラウド技術と StreamInsight などの分析技術を使ってインターネット接続デバイスとのデータ フローの管理に役立て、大規模なクラウド アーキテクチャを作成して膨大な数のデバイスを処理し、そこから情報を収集して取り込む方法を示したいと考えています。

その過程で、ホーム ネットワークに配置して他のネットワークから遠隔操作できる組み込みデバイスを組み立てました。あえて言うと、これは非常にすばらしいデバイスになりました。

私たちはまだ、初めの 1 歩を踏み出したばかりです。世界中のマイクロソフトの顧客との会話の中で、インターネットに接続されたカスタム構築のデバイスの大きな波が到来しているのを感じています。これは、こうしたデバイスと接続し、画期的な方法で他のインターネットに接続された資産と組み合わせるクラウド サービスを模索している .NET の開発者や革新的な企業にとって大きな機会になります。何ができるのかを見てみましょう。

Clemens Vasters は、Windows Azure サービス バス チームの主な技術リーダーです。Vasters は、最も初期のインキュベーションの段階からサービス バス チームに参加しており、プッシュ通知および Web やデバイスへの大規模なシグナル化を含む Windows Azure サービス バスの技術機能のロードマップに取り組んでいます。カンファレンスにも頻繁に登壇しており、アーキテクチャのコースウェアも数多く執筆しています。ツイッター (twitter.com/clemensv、英語) で彼をフォローしてください。

この記事のレビューに協力してくれた技術スタッフの Elio DamaggioTodd Holmquist-SutherlandAbhishek LalZach LibbyColin Miller、および Lorenzo Tessiore に心より感謝いたします。