June 2017

Volume 32 Number 6

モノのインターネット - Azure IoT Suite を使用した IoT 開発の生産性向上

Dawid Borycki

モノのインターネット (IoT) のソリューションは、リモート テレメトリ デバイス、Web ポータル、クラウド ストレージ、およびリアルタイム処理で構成されます。このように構造が複雑になると、IoT 開発に着手する気が起きないかもしれません。そこで、こうした複雑さを緩和するため、Microsoft Azure IoT Suite には遠隔監視 (Remote Monitoring) と予兆保全 (Predictive Maintenance) という、事前に構成されたソリューションが用意されています。今回は、遠隔監視ソリューションの作成方法を取り上げます。このソリューションでは、Windows 10 IoT Core によって制御されるリモート IoT デバイスからデータを収集して、分析します。今回のデバイス (Raspberry Pi) は、USB カメラで写真を撮影します。次に、この IoT デバイス上で写真の明度を計算してから、その写真をクラウドにストリーミングします。クラウドでは、写真を保管、処理、および表示します (図 1 参照)。さらに、エンド ユーザーは、リモート デバイスを使って取得した情報を確認できるだけでなく、そのデバイスをリモート コントロールできます。今回の説明に使用するソース コードの完成版は、msdn.com/magazine/0617magcode で確認できます。

事前に構成された Azure IoT Suite ソリューション ポータルと、ビデオ ストリームを取得して処理する、リモート ユニバーサル Windows プラットフォーム アプリ
図 1 事前に構成された Azure IoT Suite ソリューション ポータルと、ビデオ ストリームを取得して処理する、リモート ユニバーサル Windows プラットフォーム アプリ

リモート デバイス

Windows 10 IoT Core を使用した Raspberry Pi のプログラミングの基礎については、Frank LaVigne (msdn.com/magazine/mt694090) と Bruno Sonnino (msdn.com/magazine/mt808503) が執筆したコラムで説明されています。LaVigne と Sonnino は、開発環境と IoT ボードのセットアップ方法、デバイス ポータルを使用したブラウザーでの IoT デバイスの構成、Windows 10 IoT Core を使用した GPIO ポートの制御方法を取り上げています。また LaVigne は、コラムの中で、IoT でプログラムを使って、リモート カメラをコントロールできることを述べています。今回はその考え方を進めて、Raspberry Pi をリモート カメラとして使う方法を具体的に説明します。

その目標に向けて、Visual C# のプロジェクト テンプレートから空白のアプリ (ユニバーサル Windows) を選択して、「RemoteCamera」というユニバーサル Windows プラットフォーム (UWP) アプリを作成し、ターゲットと最小 API のバージョンを Windows 10 Anniversary Edition (10.0; ビルド 14393) に設定します。この API バージョンを使用して、メソッドにバインドできるようになります。このバインドにより、ビュー モデルのメソッドと、表示コントロールから発生するイベントを直接結び付けることができます。

<Button x:Name="ButtonPreviewStart"
        Content="Start preview"
        Click="{x:Bind remoteCameraViewModel.PreviewStart}" />

次に、UI を宣言します (図 1 参照)。UI には、[Camera capture] と [Cloud] という 2 つのタブがあります。 最初のタブには、カメラ プレビューの開始/停止、ビデオ ストリームの表示、画像の明度 (ラベルと進行状況バー) の表示に使用するコントロールがあります。2 つ目のタブには、デバイスをクラウドに接続するボタンと、IoT ポータルにデバイスを登録するボタンがあります。[Cloud] タブには、テレメトリ データのストリーミングを有効にできるチェック ボックスもあります。

UI に関連付けるロジックの大半は、RemoteCameraViewModel クラス内に実装します (RemoteCamera プロジェクトのサブフォルダー ViewModels 参照)。このクラスは、UI にバインドするいくつかのプロパティの実装とは別に、ビデオの撮影、写真の処理、およびクラウドの操作を処理します。これらのサブ機能は、CameraCapture、ImageProcessor、CloudHelper というクラスに個別に実装しています。 ここでは CameraCapture と ImageProcessor を簡単に説明し、CloudHelper と関連ヘルパー クラスについては、事前に構成された Azure IoT ソリューションについての説明の中で取り上げます。

カメラ撮影

カメラ撮影 (Helpers フォルダーの CameraCapture.cs を参照) は、Windows.Media.Capture.MediaCapture クラスと Windows.UI.Xaml.Controls.CaptureElement の 2 つを土台にビルドしています。 前者のクラスはビデオ撮影に使用し、後者は撮影したビデオ ストリームを表示します。ビデオは Web カメラで撮影するため、Package.appxmanifest で対応するデバイス機能を宣言しておく必要があります。

MediaCapture クラスを初期化するため、InitializeAsync メソッドを呼び出します。最終的には、そのメソッドに MediaCaptureInitializationSettings クラスのインスタンスを渡し、撮影オプションを指定できるようにします。このオプションにより、ビデオとオーディオのどちらをストリーミングするかや、撮影用ハードウェアを選択できます。今回は、既定の Web カメラからビデオのみを撮影します (図 2 参照)。

図 2 カメラ撮影の初期化

public MediaCapture { get; private set; } = new MediaCapture();
public bool IsInitialized { get; private set; } = false;
public async Task Initialize(CaptureElement captureElement)
{
  if (!IsInitialized)
  {
    var settings = new MediaCaptureInitializationSettings()
    {
      StreamingCaptureMode = StreamingCaptureMode.Video
    };
    try
    {
      await MediaCapture.InitializeAsync(settings);
      GetVideoProperties();
      captureElement.Source = MediaCapture;      IsInitialized = true;
    }
    catch (Exception ex)
    {
      Debug.WriteLine(ex.Message);
      IsInitialized = false;
    }
  }
}

次に、CaptureElement クラス インスタンスの Source プロパティを使用して、このオブジェクトを、ビデオ ストリームを表示する MediaCapture コントロールに結び付けます。さらに、ビデオ フレームのサイズを読み取った後に保存する、ヘルパー メソッド GetVideoProperties も呼び出します。この情報は、プレビュー フレームを取得して処理するために後で使用します。最後に、ビデオ撮影を実際に開始および停止するため、MediaCapture クラスの StartPreviewAsync と StopPreviewAsync を呼び出します。CameraCapture では、これらのメソッドと追加ロジックをラップして、初期化とプレビュー状態を確認します。

public async Task Start()
{
  if (IsInitialized)
  {
    if (!IsPreviewActive)
    {
      await MediaCapture.StartPreviewAsync();
      IsPreviewActive = true;
    }
  }
}

アプリを実行して、カメラ撮影を構成する [Start preview] ボタンを押すと、しばらくしてカメラの写真が表示されます。RemoteCamera はユニバーサル アプリなので、開発用 PC、スマートフォン、タブレット、Raspberry Pi を問わず、何も変更しないで配置できます。RemoteCamera アプリを Windows 10 PC でテストする場合、アプリからカメラを使うことが許可されていることを確認する必要があります。この設定は、Windows の [設定]、[プライバシー]、[カメラ] で構成できます。Raspberry Pi でアプリをテストする場合は、比較的低予算で済む Microsoft LifeCam HD-3000 を使います。これは USB Web カメラなので、Raspberry Pi の 4 つの USB 端子のいずれかに接続すると、Windows 10 IoT Core によってカメラが自動検出されます。Windows 10 IoT Core に互換性のあるカメラの完全な一覧については、bit.ly/2p1ZHGD (英語) で確認できます。Web カメラを Raspberry Pi に接続すると、デバイス ポータルの [Devices (デバイス)] タブにカメラが表示されます。

画像プロセッサ

ImageProcessor クラスは、現在のフレームの明度をバックグラウンドで計算します。バックグラウンド操作を実行するため、タスクベースの非同期パターンを使用するスレッドを作成します (図 3 参照)。

図 3 バックグラウンドでの明度の計算

public event EventHandler<ImageProcessorEventArgs> ProcessingDone;
private void InitializeProcessingTask()
{
  processingCancellationTokenSource = new CancellationTokenSource();
  processingTask = new Task(async () =>
  {
    while (!processingCancellationTokenSource.IsCancellationRequested)
    {
      if (IsActive)
      {
        var brightness = await GetBrightness();
        ProcessingDone(this, new ImageProcessorEventArgs(brightness));
        Task.Delay(delay).Wait();
      }
    }
  }, processingCancellationTokenSource.Token);
}

while ループ内では、画像の明度を決めた後、その値を ProcessingDone イベントのリスナーに渡します。このイベントでは、Brightness というパブリック プロパティを 1 つだけもつ ImageProcessorEventArgs クラスのインスタンスが提供されます。この処理は、タスクがキャンセル シグナルを受け取るまで続きます。この画像処理の重要な要素は、GetBrightness メソッドです (図 4 参照)。

図 4 GetBrightness メソッド

private async Task<byte> GetBrightness()
{
  var brightness = new byte();
  if (cameraCapture.IsPreviewActive)
  {
    // Get current preview bitmap
    var previewBitmap = await cameraCapture.GetPreviewBitmap();
    // Get underlying pixel data
    var pixelBuffer = GetPixelBuffer(previewBitmap);
    // Process buffer to determine mean gray value (brightness)
    brightness = CalculateMeanGrayValue(pixelBuffer);
  }
  return brightness;
}

この CameraCapture クラス インスタンスの GetPreviewBitmap を使用して、プレビュー フレームにアクセスしています。GetPreviewBitmap は、MediaCapture クラスの GetPreviewFrameAsync を内部で使用します。GetPreviewFrameAsync には 2 つのバージョンがあります。最初のパラメーターを受け取らないメソッドは、VideoFrame クラスのインスタンスを返します。今回の場合、Direct3DSurface プロパティを読み取ることで、実際のピクセル データを取得できます。もう 1 つのバージョンは、Video­Frame クラスのインスタンスを受け取り、ピクセル データを SoftwareBitmap プロパティにコピーします。ここでは、2 つ目のオプション (CameraCapture クラスの GetPreviewBitmap メソッドを参照) を使用して、SoftwareBitmap クラス インスタンスの CopyToBuffer メソッドによってピクセル データにアクセスします (図 5 参照)。

図 5 ピクセル データへのアクセス

private byte[] GetPixelBuffer(SoftwareBitmap softwareBitmap)
{
  // Ensure bitmap pixel format is Bgra8
  if (softwareBitmap.BitmapPixelFormat != CameraCapture.BitmapPixelFormat)
  {
    SoftwareBitmap.Convert(softwareBitmap, CameraCapture.BitmapPixelFormat);
  }
  // Lock underlying bitmap buffer
  var bitmapBuffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read);
  // Use plane description to determine bitmap height
  // and stride (the actual buffer width)
  var planeDescription = bitmapBuffer.GetPlaneDescription(0);
  var pixelBuffer = new byte[planeDescription.Height * planeDescription.Stride];
  // Copy pixel data to a buffer
  softwareBitmap.CopyToBuffer(pixelBuffer.AsBuffer());
  return pixelBuffer;
}

最初に、ピクセル形式が BGRA8 であることを確認しています。この形式は、画像が 4 つの 8 ビット チャンネルで表現されることを示します。3 つのチャンネルは、青、緑、赤を示し、残りの 1 つのチャネルはアルファ ブレンドまたは透明度を示します。入力ビットマップのピクセル形式が異なる場合、適切な変換を行っています。次に、ピクセル データをバイト配列にコピーします。バイト配列のサイズは、画像のストライドと画像の高さを乗算することで得られます (bit.ly/2om8Ny9、英語)。両方の値は、BitmapPlaneDescription のインスタンスから読み取ります。このインスタンスは、SoftwareBitmap.LockBuffer メソッドが返す BitmapBuffer オブジェクトから取得しています。

ピクセル データを含むバイト配列を用意したら、後はすべてのピクセルの平均値を計算するだけです。そのためには、ピクセル バッファーを反復処理します (図 6 参照)。

図 6 ピクセルの平均値の計算

private byte CalculateMeanGrayValue(byte[] pixelBuffer)
{
  // Loop index increases by four since
  // there are four channels (blue, green, red and alpha).
  // Alpha is ignored for brightness calculation
  const int step = 4;
  double mean = 0.0;
  for (uint i = 0; i < pixelBuffer.Length; i += step)
  {
    mean += GetGrayscaleValue(pixelBuffer, i);
  }
  mean /= (pixelBuffer.Length / step);
  return Convert.ToByte(mean);
}

次に、反復のたびに各色のチャネル値の平均値を求め、ピクセルをグレー スケールに変換しています。

private static byte GetGrayscaleValue(byte[] pixelBuffer, uint startIndex)
{
  var grayValue = (pixelBuffer[startIndex]
    + pixelBuffer[startIndex + 1]
    + pixelBuffer[startIndex + 2]) / 3.0;
  return Convert.ToByte(grayValue);
}

明度は、ProcessingDone イベントを通じてビューに渡します。このイベントは MainPage クラス (MainPage.xaml.cs) で処理しています。このクラスでは、ラベルと進行状況バーで明度を表示します。どちらのコントロールも、RemoteCameraViewModel の Brightness プロパティにバインドします。ProcessingDone は、バックグラウンド スレッドから発生する点に注意してください。そのため、Dispatcher クラスを使用して、UI スレッドを通じて RemoteCameraViewModel.Brightness を変更しています (図 7 参照)。

図 7 Dispatcher クラスによる UI スレッドを通じた RemoteCameraViewModel.Brightness の変更

private async void DisplayBrightness(byte brightness)
{
  if (Dispatcher.HasThreadAccess)
  {
    remoteCameraViewModel.Brightness = brightness;
  }
  else
  {
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
      DisplayBrightness(brightness);
    });
  }
}

遠隔監視ソリューションのプロビジョニング

ソリューションのプロビジョニングは、専用のポータル (azureiotsuite.com) で行うことができます。ログインして Azure サブスクリプションを選択したら、リダイレクトされるページで、四角形で囲まれた [新しいソリューションの作成] をクリックします。すると、2 つの事前に構成されたソリューション (予兆保全と遠隔監視) のいずれかを選択できる Web サイトが表示されます (図 8 参照)。ソリューションを選択すると新しいフォームが表示されます。このフォームでは、ソリューションの名前と Azure リソースの地域を設定できます。今回は、ソリューション名を「RemoteCameraMonitoring」に、地域を米国西部にそれぞれ設定します。

Azure IoT Suite の事前に構成されたソリューション (上) と遠隔監視ソリューションの構成 (下)
図 8 Azure IoT Suite の事前に構成されたソリューション (上) と遠隔監視ソリューションの構成 (下)

遠隔監視ソリューションをプロビジョニングすると、Azure IoT Suite ポータルによって、IoT Hub、Stream Analytics ジョブ、Storage、App Service という複数の Azure リソースが作成されます。 IoT Hub は、クラウドとリモート デバイス間の双方向通信を提供します。リモート デバイスからストリーミングされるデータは Stream Analytics ジョブによって変換されます。その際、通常、不要なデータはフィルターによって除外されます。フィルターで除外した後のデータは、以降の分析のために格納またはリダイレクトされます。最後の App Service は、Web ポータルをホストするために使用されます。

ソリューションのプロビジョニングは、コマンドラインからも行うことができます。そのためには、bit.ly/2osI4RW (英語) からソリューションのソース コードをコピーまたはダウンロードして、bit.ly/2p7MPPc (英語) の指示に従います。または、bit.ly/2nEePNi (英語) に示されているように、ソリューションをローカルでデプロイするオプションもあります。この場合、ソリューション ポータルはローカル コンピューターで実行されるため、Azure App Service は作成されません。このアプローチは、特に、開発とデバッグが目的の場合や、事前に構成されたソリューションを変更する場合に便利です。

プロビジョニングが完了すると、そのソリューションを起動できるようになり、ソリューションのポータルが既定のブラウザーに表示されます (前述の図 1 参照)。このポータルにはいくつかタブがあります。今回の目的で注目するのは [Dashboard (ダッシュボード)] と [Devices (デバイス)] という 2 つのタブだけです。[Dashboard (ダッシュボード)] には、リモート デバイスの位置を示す地図と、デバイスがストリーミングするテレメトリ データが表示されます。[Devices (デバイス)] タブには、リモート デバイスの一覧と、それらの状態、機能、および説明が表示されます。既定では、いくつかエミュレーションされたデバイスがあります。そこで、エミュレーションされないハードウェアを新しく登録する方法を見ていくことにしましょう。

デバイスの登録

デバイスを登録するには、ソリューション ポータルの左下隅にある [Add a device (デバイスの追加)] ハイパーリンクをクリックします。次に、シミュレーションされるデバイスまたはカスタム デバイスのいずれかを選択します。後者のオプションを選択して、[Add New (新規追加)] ボタンをクリックします。これで、デバイス ID を定義できるようになります。この値に「RemoteCamera」を設定します。すると、[ADD A CUSTOM DEVICE (カスタム デバイスの追加)] フォームに、デバイスの資格情報が表示されます (図 9 参照)。このフォームは、IoT デバイスを IoT Hub に接続するために後で使用します。

デバイス登録の概要
図 9 デバイス登録の概要

デバイス メタデータとクラウドとの通信

追加したデバイスがデバイスの一覧に表示され、デバイスのメタデータやデバイスの情報を送信できるようになります。デバイスの情報は、リモート デバイスを説明する JSON オブジェクトで構成されます。このオブジェクトは、クラウド エンドポイントにデバイスの機能を伝え、ハードウェアの説明と、デバイスが受け入れるリモート コマンドのリストを含みます。これらのコマンドは、エンド ユーザーが IoT ソリューション ポータルからデバイスに送信できます。RemoteCamera アプリで、デバイスの情報は DeviceInfo クラス (AzureHelpers サブフォルダー) として表現しています。

public class DeviceInfo
{
  public bool IsSimulatedDevice { get; set; }
  public string Version { get; set; }
  public string ObjectType { get; set; }
  public DeviceProperties DeviceProperties { get; set; }
  public Command[] Commands { get; set; }
}

DeviceInfo の最初の 2 つのプロパティは、デバイスがシミュレーションされているかどうかと、DeviceInfo オブジェクトのバージョンを定義します。後ほど取り上げる 3 つ目の ObjectType プロパティには、文字列定数 DeviceInfo を設定しています。この文字列は、クラウド (具体的には Azure の Stream Analytics ジョブ) によって使用され、テレメトリ データからデバイス情報をフィルタリングします。次に、DeviceProperties (AzureHelpers サブフォルダー) は、デバイスを説明するプロパティのコレクション (シリアル番号、メモリ、プラットフォーム、RAM など) を保持しています。最後の Commands プロパティは、デバイスが認識するリモート コマンドのコレクションです。それぞれのコマンドは、名前とパラメーターのリストを指定して定義します。コマンドの名前は Command クラスで、パラメーターのリストは CommandParameter クラスで表現しています (AzureHelpers\Command.cs 参照)。

IoT デバイスと IoT Hub との間の通信を確立するには、Microsoft.Azure.Devices.Client NuGet パッケージを使用します。このパッケージには、メッセージをクラウドとの間で送受信できるようにする DeviceClient クラスがあります。DeviceClient のインスタンスは、静的メソッドの Create または CreateFromConnectionString のいずれかを使用して作成します。今回は前者のオプション (AzureHelpers フォルダーの CloudHelper.cs) を使用します。

public async Task Initialize()
{
  if (!IsInitialized)
  {
    deviceClient = DeviceClient.Create(
      Configuration.Hostname, Configuration.AuthenticationKey());
    await deviceClient.OpenAsync();
    IsInitialized = true;
    BeginRemoteCommandHandling();
  }
}

DeviceClient.Create メソッドには、IoT Hub のホスト名とデバイスの資格情報 (ID とキー) を提供する必要があります。これらの値は、デバイスのプロビジョニング中にソリューション ポータルから取得します (図 9 参照)。RemoteCamera アプリでは、静的クラス Configuration に、ホスト名、デバイス ID、およびキーを格納します。

public static class Configuration
{
  public static string Hostname { get; } = "<iot-hub-name>.azure-devices.net";
  public static string DeviceId { get; } = "RemoteCamera";
  public static string DeviceKey { get; } = "<your_key>";
  public static DeviceAuthenticationWithRegistrySymmetricKey AuthenticationKey()
  {
    return new DeviceAuthenticationWithRegistrySymmetricKey(DeviceId, DeviceKey);
  }
}

また、Configuration クラスは、静的メソッド AuthenticationKey を実装します。このメソッドは、デバイスの資格情報を DeviceAuthenticationWithRegistrySymmetricKey クラスのインスタンスにラップします。これを使用して、DeviceClient クラスのインスタンス作成を簡略化します。

接続を行ったら、DeviceInfo を送信するだけです (図 10 参照)。

図 10 デバイス情報の送信

public async Task SendDeviceInfo()
{
  var deviceInfo = new DeviceInfo()
  {
    IsSimulatedDevice = false,
    ObjectType = "DeviceInfo",
    Version = "1.0",
    DeviceProperties = new DeviceProperties(Configuration.DeviceId),
    // Commands collection
    Commands = new Command[]
    {
      CommandHelper.CreateCameraPreviewStatusCommand()
    }
  };
  await SendMessage(deviceInfo);
}

RemoteCamera アプリは、実際のハードウェアを説明するデバイス情報を送信するため、IsSimulatedDevice を false に設定します。前述のように、ObjectType を DeviceInfo に設定します。また、Version プロパティを 1.0 に設定します。DeviceProperties プロパティには、大半が静的文字列で構成される属性値を使用します (DeviceProperties クラスの SetDefaultValues 参照)。さらに、カメラ プレビューのリモート コントロールを有効にする、1 つのリモート コマンド「Update camera preview」も定義します。このコマンドには、1 つのブール値パラメーター IsPreviewActive があります。このパラメーターは、カメラ プレビューを開始するか停止するかを指定します (AzureHelpers フォルダーの CommandHelper.cs ファイル参照)。

実際にデータをクラウドに送信するには、SendMessage メソッドを実装します。

private async Task SendMessage(Object message)
{
  var serializedMessage = MessageHelper.Serialize(message);
  await deviceClient.SendEventAsync(serializedMessage);
}

基本的には、C# オブジェクトを、JSON 形式のオブジェクトを含むバイト配列にシリアル化する必要があります (AzureHelpers サブフォルダーの静的クラス MessageHelper 参照)。

public static Message Serialize(object obj)
{
  ArgumentCheck.IsNull(obj, "obj");
  var jsonData = JsonConvert.SerializeObject(obj);
  return new Message(Encoding.UTF8.GetBytes(jsonData));
}

その後、シリアル化後の配列を Message クラスにラップします。このクラスは、DeviceClient クラス インスタンスの SendEventAsync メソッドを使用して、クラウドに送信します。Message クラスは、追加のプロパティを使って未加工のデータを補完するオブジェクト (転送される JSON オブジェクト) です。この追加プロパティは、デバイスと IoT Hub 間で送信されるメッセージの追跡に使用します。

RemoteCamera アプリでは、[Cloud] タブの 2 つのボタンで、クラウドとの接続の確立とデバイス情報の送信を起動できます。 この 2 つのボタンは、[Connect and initialize(接続と初期化)] と [Send device info (デバイス情報の送信)] です。最初のボタンのクリック イベント ハンドラーは RemoteCameraViewModel の Connect メソッドにバインドします。

public async Task Connect()
{
  await CloudHelper.Initialize();
  IsConnected = true;
}

2 つ目のボタンのクリック イベント ハンドラーは CloudHelper クラス インスタンスの SendDeviceInfo メソッドに結び付けます。このメソッドについては、前半で説明しました。

クラウドへの接続が完了したら、テレメトリ データの送信も開始できます。これは、デバイス情報の送信とほぼ同じです。つまり、テレメトリ オブジェクトを渡して SendMessage メソッドを使用します。今回のテレメトリ オブジェクトは Telemetry­Data クラスのインスタンスで、Brightness プロパティを 1 つだけ備えています。以下に、クラウドへのテレメトリ データ送信の完全な例を示します。これは CloudHelper クラスの SendBrightness メソッドに実装しています。

public async void SendBrightness(byte brightness)
{
  if (IsInitialized)
  {
    // Construct TelemetryData
    var telemetryData = new TelemetryData()
    {
      Brightness = brightness
    };
    // Serialize TelemetryData and send it to the cloud
    await SendMessage(telemetryData);
  }
}

SendBrightness は、ImageProcessor によって計算した明度を取得した直後に呼び出します。これは ProcessingDone イベント ハンドラーで実行されます。

private void ImageProcessor_ProcessingDone(object sender, ImageProcessorEventArgs e)
{
  // Update display through dispatcher
  DisplayBrightness(e.Brightness);
  // Send telemetry
  if (remoteCameraViewModel.IsTelemetryActive)
  {
    remoteCameraViewModel.CloudHelper.SendBrightness(e.Brightness);
  }
}

したがって、この時点で RemoteCamera アプリを実行してプレビューを開始し、クラウドに接続すると、対応するグラフに明度の値が表示されます (図 1 参照)。事前に構成されたソリューションのシミュレーション デバイスは、気温と湿度をテレメトリ データとして送信するだけですが、他の値を送信することもできます。今回送信する明度は、適切なグラフに自動的に表示されます。

リモート コマンドの処理

CloudHelper クラスは、クラウドから受信したリモート コマンドを処理するメソッドも実装します。ImageProcessor の場合と同様、コマンドはバックグラウンドで処理します (CloudHelper クラスの BeginRemoteCommandHandling)。ここでも、タスクベースの非同期パターンを使用します。

private void BeginRemoteCommandHandling()
{
  Task.Run(async () =>
  {
    while (true)
    {
      var message = await deviceClient.ReceiveAsync();
      if (message != null)
      {
        await HandleIncomingMessage(message);
      }
    }
  });
}

このメソッドの役割は、クラウドのエンドポイントから受信したメッセージを連続分析するタスクを作成することです。リモート メッセージを受信するには、DeviceClient クラスの ReceiveAsync メソッドを呼び出します。ReceiveAsync は、Message クラスのインスタンスを返します。このインスタンスを使用して、JSON 形式のリモート コマンド データを含む未加工のバイト配列を取得します。次に、この配列を RemoteCommand オブジェクトにシリアル化解除します (AzureHelpers フォルダーの RemoteCommand.cs 参照)。これは、MessageHelper クラス (AzureHelpers サブフォルダー) に実装しています。

public static RemoteCommand Deserialize(Message message)
{
  ArgumentCheck.IsNull(message, "message");
  var jsonData = Encoding.UTF8.GetString(message.GetBytes());
  return JsonConvert.DeserializeObject<RemoteCommand>(
    jsonData);
}

RemoteCommand にはいくつかプロパティがありますが、通常、コマンドの名前とパラメーターを含む、name と parameters の 2 つのみを使用します。RemoteCamera アプリでは、これらの値を使用して、想定どおりのコマンドが受信されたかどうかを判断します (図 11 参照)。想定どおりのコマンドを受信した場合、UpdateCameraPreviewCommandReceived イベントを発生させてリスナーに情報を渡した後、DeviceClient クラスの CompleteAsync メソッドを使用して、コマンドを受け入れることをクラウドに伝えます。コマンドを認識できない場合は、RejectAsync メソッドを使用して、そのコマンドを拒否します。

図 11 リモート メッセージのシリアル化解除と解析

private async Task HandleIncomingMessage(Message message)
{
  try
  {
    // Deserialize message to remote command
    var remoteCommand = MessageHelper.Deserialize(message);
    // Parse command
    ParseCommand(remoteCommand);
    // Send confirmation to the cloud
    await deviceClient.CompleteAsync(message);
  }
  catch (Exception ex)
  {
    Debug.WriteLine(ex.Message);
    // Reject message, if it was not parsed correctly
    await deviceClient.RejectAsync(message);
  }
}
private void ParseCommand(RemoteCommand remoteCommand)
{
  // Verify remote command name
  if (string.Compare(remoteCommand.Name,
    CommandHelper.CameraPreviewCommandName) == 0)
  {
    // Raise an event, when the valid command was received
    UpdateCameraPreviewCommandReceived(this,
      new UpdateCameraPreviewCommandEventArgs(
      remoteCommand.Parameters.IsPreviewActive));
  }
}

UpdateCameraPreviewCommandReceived イベントは MainPage クラスで処理します。リモート コマンドから取得するパラメーター値に応じて、ローカル カメラのプレビューを停止または開始します。この操作を、UI スレッドに再度ディスパッチします (図 12 参照)。

図 12 カメラのプレビューの更新

private async void CloudHelper_UpdateCameraPreviewCommandReceived(
  object sender, UpdateCameraPreviewCommandEventArgs e)
{
  if (Dispatcher.HasThreadAccess)
  {
    if (e.IsPreviewActive)
    {
      await remoteCameraViewModel.PreviewStart();
    }
    else
    {
      await remoteCameraViewModel.PreviewStop();
    }
  }
  else
  {
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
      CloudHelper_UpdateCameraPreviewCommandReceived(sender, e);
    });
  }
}

クラウドからのコマンドの送信

最後に、ソリューション ポータルを使用した IoT デバイスのリモート コントロールを取り上げます。このためには、[Devices (デバイス)] タブで、使用するデバイスを見つけてクリックします (図 13 参照)。

RemoteCamera の詳細を示す IoT Portal の [Devices (デバイス)] タブ
図 13 RemoteCamera の詳細を示す IoT Portal の [Devices (デバイス)] タブ

これにより、デバイスの詳細を表示するペインがアクティブになります。ここで、[Commands (コマンド)] ハイパーリンクをクリックします。すると、リモート コマンドを選択して送信できるもう 1 つのフォームが表示されます。このフォームの細かいレイアウトは、選択するコマンドによって異なります。今回はパラメーターが 1 つしかないコマンドなので、チェック ボックスが 1 つだけ表示されます。このチェック ボックスをオフにしてコマンドを送信すると、RemoteCamera アプリがプレビューを停止します。送信するすべてのコマンドが、コマンド履歴にその状態と共に表示されます (図 14 参照)。

IoT デバイスにコマンドを送信するフォーム
図 14 IoT デバイスにコマンドを送信するフォーム

まとめ

今回は Azure IoT Suite の事前に構成された遠隔監視 ソリューションのセットアップ方法を取り上げました。このソリューションは、リモート デバイスに装着された Web カメラで撮影した画像の情報を収集、表示します。また、IoT デバイスをリモート コントロールすることもできます。付属のソース コードはすべての UWP デバイスで動作可能なため、コードを実際に Raspberry Pi に配置する必要はありません。今回の説明と、遠隔監視ソリューションのオンライン ドキュメンテーションおよびソース コードは、実践的な遠隔監視に関する包括的な IoT 開発を速やかに始める役に立ちます。Windows 10 IoT Core を使用すれば、単純な LEDの点滅よりもさらに複雑なことを実現できます。


Dawid Borycki はソフトウェア エンジニアであり、生物医学研究者、作家、および壇上演説家でもあります。彼はソフトウェアの実験とプロトタイプ開発のための新しいテクノロジの学習を楽しんでいます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Rachel Appel に心より感謝いたします。