Junho de 2017

Volume 32 - Número 6

Internet das Coisas - Use o Azure IoT Suite para aumentar o desenvolvimento da IoT

Por Dawid Borycki

Uma solução de Internet das Coisas (IoT) abrange dispositivos de telemetria remota, um portal Web, armazenamento em nuvem e processamento em tempo real. Uma estrutura com esta complexidade pode fazer com que você hesite em iniciar um desenvolvimento de IoT. Para facilitar as coisas, o Microsoft Azure IoT Suite fornece duas soluções pré-configuradas: monitoramento remoto e manutenção preditiva. Aqui, explicarei como criar uma solução de monitoramento remoto, que coletará e analisará dados de um dispositivo de IoT remoto controlado pelo Windows 10 IoT Core. Este dispositivo, o Raspberry Pi, vai adquirir imagens de uma câmera USB. Posteriormente, o brilho da imagem será calculado no dispositivo IoT e, depois, transmitido para a nuvem, onde será armazenada, processada e exibida (consulte a Figura 1). Além disso, o usuário final não só será capaz de ver as informações adquiridas com o dispositivo remoto, como também poderá controlar esse dispositivo remotamente. Você pode encontrar o código-fonte completo que dá suporte a esta discussão em msdn.com/magazine/0617magcode.

Um portal de soluções do Azure IoT Suite e o aplicativo remoto da Plataforma Universal do Windows, que adquire e processa a transmissão de vídeo
Figura 1 Um portal de soluções do Azure IoT Suite e o aplicativo remoto da Plataforma Universal do Windows, que adquire e processa a transmissão de vídeo

Dispositivo remoto

As noções básicas de programação do Raspberry Pi com o Windows 10 IoT Core já foram discutidas nesta revista por Frank LaVigne (msdn.com/magazine/mt694090) and Bruno Sonnino (msdn.com/magazine/mt808503). LaVigne e Sonnino mostraram como configurar o ambiente de desenvolvimento e o painel de IoT, como configurar a unidade de IoT em seu navegador usando o Portal de dispositivos e como controlar as portas GPIO com o Windows 10 IoT Core. Além disso, LaVigne mencionou neste artigo que a IoT pode ser usada para programar e controlar câmeras remotas. Aqui, desenvolvo este pensamento e mostro de forma explícita como transformar o Raspberry Pi em um dispositivo desse tipo.

Para isso, eu crio o aplicativo RemoteCamera da Plataforma Universal do Windows (UWP) usando o modelo do projeto em Visual C# do Aplicativo em Branco (Windows Universal) e, em seguida, defino as versões de API de destino e mínimas para a Edição de Aniversário do Windows 10 (10.0; Build 14393). Eu uso esta versão da API para permitir a associação de métodos, que me permite ligar diretamente métodos do modelo de exibição com eventos acionados por controles visuais:

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

Em seguida, declaro a interface do usuário como mostrado na Figura 1. Há duas guias: Captura com câmera e Nuvem. A primeira contém controle para iniciar e parar a versão prévia da câmera, exibindo a transmissão do vídeo e apresentando o brilho da imagem (um rótulo e uma barra de progresso). A segunda guia contém dois botões, que você pode usar para conectar um dispositivo à nuvem e registrar um dispositivo com o portal de IoT. A guia Nuvem também tem uma caixa de seleção que permite que você transmita dados de telemetria.

A maioria da lógica associada com a interface de usuário é implementada na classe RemoteCameraViewModel (consulte a subpasta ViewModels do projeto RemoteCamera). Esta classe, para além de implementar algumas propriedades ligadas à interface do usuário, trata da aquisição de vídeo, processamento de imagem e interação de nuvem. Essas subfuncionalidades são implementadas em classes separadas: CameraCapture, ImageProcessor e CloudHelper, respectivamente. Discutirei brevemente sobre as classes CameraCapture e ImageProcessor, enquanto CloudHelper e classes auxiliares relacionadas serão descritas mais tarde no contexto da solução pré-configurada da IoT do Azure.

Captura com Câmera

A captura com câmera (veja CameraCapture.cs na pasta Auxiliares) é compilada em cima de dois elementos: Classe Windows.Media.Capture.MediaCapture e Windows.UI.Xaml.Controls.CaptureElement. O último é usado para adquirir vídeo, enquanto o último exibe a transmissão de vídeo adquirida. Eu adquiro o vídeo com uma webcam, por isso eu tenho que declarar a funcionalidade do dispositivo correspondente no Package.appxmanifest.

Para inicializar a classe MediaCapture, você invoca o método InitializeAsync. Por fim, você pode passar para esse método uma instância da classe MediaCaptureInitializationSettings, que permite que você especifique as opções de captura. Você pode escolher entre vídeo de transmissão ou áudio e selecionar o hardware de captura. Aqui, eu adquiro somente vídeos da webcam padrão (veja a Figura 2).

Figura 2 Inicialização de captura de câmera

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

Em seguida, usando a propriedade Fonte da instância de classe CaptureElement, eu conecto este objeto com o controle MediaCapture para exibir a transmissão de vídeo. Eu também invoco o método auxiliar GetVideoProperties, que lê e depois armazena o tamanho de um quadro de vídeo. Eu uso esta informação mais tarde para obter o quadro de prévia para processamento. Por fim, para realmente iniciar e parar a aquisição de vídeo, eu invoco StartPreviewAsync e StopPreviewAsync da classe MediaCapture. Na CameraCapture, eu encapsulei esses métodos com lógica adicional, verificando a inicialização e o estado de visualização:

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

Ao executar o aplicativo, você pode pressionar o botão de visualização Iniciar, que configura a aquisição da câmera, e você verá a imagem da câmera pouco tempo depois. Repare que o aplicativo RemoteCamera é universal, por isso, ele pode ser implantado sem qualquer alteração no seu PC, smartphone, tablet ou Raspberry Pi de desenvolvimento. Se você testar o aplicativo RemoteCamera com seu PC Windows 10, você precisa ter certeza de que os aplicativos tem permissão para usar sua câmera. Você configura isto com o aplicativo Configurações (Privacidade/Câmera). Para testar o aplicativo com o Raspberry Pi eu uso uma Microsoft Life Cam HD-3000 de baixo custo. Esta é uma webcam USB, por isso, é automaticamente detectada pelo Windows 10 IoT Core depois que eu o conectei a uma das quatro portas USB do Raspberry Pi. Você encontrará uma lista completa de câmeras compatíveis com o 10 IoT Core em bit.ly/2p1ZHGD. Quando você conecta sua webcam ao Raspberry Pi, ele aparecerá na guia Dispositivos do Portal de Dispositivos.

Processador de imagem

A classe do ImageProcessor calcula o brilho do quadro atual na tela de fundo. Para realizar a operação de tela de fundo, eu crio um thread com o padrão assíncrono baseado em tarefas, como mostrado na Figura 3.

Figura 3 Calculando o Brilho na tela de fundo

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

No loop while, determino o brilho da imagem e, em seguida, passo este valor a ouvintes do evento ProcessingDone. Este evento é alimentado com uma instância da classe ImageProcessorEventArgs, que tem apenas uma propriedade pública, o Brilho. O processamento é executado até que a tarefa recebe um sinal de cancelamento. O elemento chave do processamento da imagem é o método GetBrightness, mostrado na Figura 4.

Figura 4 O método 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;
}

Eu acesso o quadro de visualização usando o GetPreviewBitmap da instância de classe CameraCapture. Internamente, GetPreviewBitmap usa GetPreviewFrameAsync da classe MediaCapture. Existem duas versões do GetPreviewFrameAsync. A primeira, o método sem parâmetro, retorna uma instância da classe VideoFrame. Neste caso, você pode obter os dados de pixel lendo a propriedade Direct3DSurface. A segunda versão aceita uma instância da classe Video­Frame e copia os dados do pixel na sua propriedade SoftwareBitmap. Aqui, eu uso a segunda opção (veja o método GetPreviewBitmap da classe CameraCapture) e então acesse os dados do pixel pelo método CopyToBuffer da instância de classe SoftwareBitmap mostrada na Figura 5.

Figura 5 Acessando os dados de pixel

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

Primeiro, verifico se o formato de pixel é BGRA8, que significa que a imagem é representada com quatro canais de 8 bits: três para as cores azul, verde e vermelha, e um para alfa ou transparência. Se o bitmap de entrada tiver um formato de pixel diferente, eu realizo uma conversão apropriada. Em seguida, eu copio os dados do pixel para a matriz de bytes, cujo tamanho é determinado pela altura da imagem multiplicada pelo stride da imagem (bit.ly/2om8Ny9). Eu leio ambos os valores de uma instância do BitmapPlaneDescription, que eu obtenho do objeto BitmapBuffer, retornado pelo método SoftwareBitmap.LockBuffer.

Dada a matriz de bytes com dados de pixel, tudo o que eu preciso fazer é calcular o valor médio de todos os pixels. Por isso, eu itero sobre o buffer de pixel (veja a Figura 6).

Figura 6 Calculando o valor médio dos pixels

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

Em seguida, em cada iteração, eu converto um determinado pixel para a escala de cinza com a média dos valores de cada canal de cor:

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

O Brilho é passado para a exibição através do evento ProcessingDone. Este evento é tratado na classe MainPage (MainPage.xaml.cs), na qual eu exibo o brilho no rótulo e na barra de progresso. Os dois controles estão limitados à propriedade Brilho do RemoteCameraViewModel. Observe que o ProcessingDone é gerado a partir do thread da tela de fundo. Consequentemente, eu modifico o RemoteCameraViewModel.Brightness pela interface de usuário usando a classe Dispatcher, como mostrado na Figura 7.

Figura 7 Modificando RemoteCameraViewModel.Brightness através do thread da interface de usuário usando a classe Dispatcher

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

Provisionamento da solução de monitoramento remoto

Para provisionar uma solução, você pode usar um portal dedicado em azureiotsuite.com. Depois de fazer logon e escolher sua assinatura do Azure, você será redirecionado para uma página em que deve pressionar o retângulo “Criar uma nova solução”. Isso abrirá um site da Web no qual você pode escolher uma das soluções pré-configuradas: manutenção preditiva ou monitoramento remoto (veja a Figura 8). Depois de escolher a solução, outro formulário aparece, permitindo que você ajuste o nome da solução e a região dos recursos Azure. aqui, eu defino o nome da solução e da região para RemoteCameraMonitoring e Oeste dos EUA, respectivamente.

Soluções pré-configuradas do Azure IoT Suite (Na parte superior) e Configuração do Remote Monitoring Solution (Na parte inferior)
Figura 8 Soluções pré-configuradas do Azure IoT Suite (Na parte superior) e Configuração do Remote Monitoring Solution (Na parte inferior)

Ao provisionar a solução de monitoramento remota, o portal do Azure IoT Suite cria vários recursos do Azure: Hub IoT, trabalhos do Stream Analytics, Armazenamento e Serviço de Aplicativo. O Hub IoT fornece comunicação bidirecional entre a nuvem e os dispositivos remotos. Os dados transmitidos por dispositivos remotos são transformados por um trabalho de Stream Analytics, que normalmente filtra os dados desnecessários. Os dados filtrados são armazenados ou direcionados para serem melhor analisados. Por fim, o Serviço de Aplicativo é usado para hospedar o portal Web.

O provisionamento de soluções também pode ser realizado a partir da linha de comando. Para isso, você pode clonar ou fazer download do código-fonte da solução de bit.ly/2osI4RW, e depois seguis as instruções em bit.ly/2p7MPPc. Você também tem a opção de implantar uma solução localmente, como mostrado em bit.ly/2nEePNi. Neste caso, nenhum Serviço de Aplicativo do Azure é criado, pois o portal de soluções é executado em uma máquina local. Tal abordagem pode ser especialmente útil para desenvolver e depurar ou para quando você desejar modificar uma solução pré-configurada.

Quando o provisionamento estiver terminado, você pode iniciar a solução, e seu portal aparecerá no navegador padrão (consulte novamente a Figura 1). Este portal tem algumas guias. Para o objetivo deste artigo, eu limitarei minha atenção a apenas duas delas: painel e dispositivos. O painel exibe o mapa com os dispositivos remotos e os dados de telemetria que eles transmitem. A guia Dispositivos mostra uma lista dos dispositivos remotos, incluindo seus status, recursos e uma descrição. Por padrão, existem diversos dispositivos emulados. Deixe-me explicar como registrar o hardware novo e não emulado.

Registrando um dispositivo

Para registrar um dispositivo você usa o portal de soluções, no qual você pressiona o hiperlink Adicionar um dispositivo no canto inferior esquerdo. Em seguida, você pode escolher entre um dispositivo simulado ou personalizado. Escolha a segunda opção e pressione o botão Adicionar Novo. Agora você pode definir a ID do dispositivo. Eu defino este valor para RemoteCamera. Em seguida, o formulário ADICIONAR UM NOVO DISPOSITIVO exibe as credenciais do dispositivo (ver a Figura 9), que eu usarei mais tarde para conectar meu dispositivo de IoT ao Hub IoT.

Resumo do registro de dispositivos
Figura 9 Resumo do registro de dispositivos

Metadados do dispositivo e comunicação com a nuvem

O dispositivo que você adicionar aparecerá na lista de dispositivos e, em seguida, você poderá enviar os metadados ou as informações do dispositivo. As informações do dispositivo incluem um objeto JSON que descreve o dispositivo remoto. este objeto diz ao ponto de extremidade da nuvem as funcionalidade do seu dispositivo, e inclui uma descrição de hardware e a lista de comandos remotos aceitos pelo seu dispositivo. Esses comandos podem ser enviados pelo usuário final para o seu dispositivo pelo portal de soluções da IoT. No aplicativo RemoteCamera, as informações do dispositivo é representada como a classe DeviceInfo (na subpasta 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; }
}

As primeiras duas propriedades do DeviceInfo especificam se um dispositivo é simulado e define a versão do objeto DeviceInfo. Como você poderá ver mais tarde, a terceira propriedade, ObjectType, é definida para uma constante de cadeia de caracteres, DeviceInfo. Esta cadeia de caracteres é usada pela nuvem, especificamente pelo trabalho do Azure Stream Analytics, para filtrar informações do dispositivo dos dados de telemetria. Em seguida, DeviceProperties (veja na subpasta AzureHelpers) contém uma coleção de propriedades (como número serial, memória, plataforma, RAM) que descreve seu dispositivo. Por fim, a propriedade Comandos é uma coleção de comandos remotos reconhecidos pelo seu dispositivo. Você define cada comando especificando seu nome e uma lista de parâmetros, que são representados pelas classes Command e CommandParameter, respectivamente (veja AzureHelpers\Command.cs).

Para estabelecer comunicação entre seu dispositivo de IoT e o Hub IoT, você usa o pacote NuGet Microsoft.Azure.Devices.Client. Este pacote fornece a classe DeviceClient, que você usa para enviar e receber mensagens para e da nuvem. Você pode criar uma instância do DeviceClient os métodos estáticos Create ou CreateFromConnectionString. Aqui, eu uso a primeira opção (CloudHelper.cs na pasta AzureHelpers):

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

Como você pode ver, o método DeviceClient.Create exige que você forneça o hostname do Hub IoT e as credenciais do dispositivo (o identificador e a chave). Você obtém esses valores do portal de soluções durante o provisionamento do dispositivo (consulte novamente a Figura 9). No aplicativo RemoteCamera, eu armazeno o hostname, id do dispositivo e a chave na classe estática Configuration:

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

Além disso, a classe Configuração implementa um método estático AuthenticationKey, que encapsula credenciais do dispositivo em uma instância da classe DeviceAuthenticationWithRegistrySymmetricKey. Eu uso isso para simplificar a criação de uma instância de classe DeviceClient.

Quando a conexão é feita, tudo o que você precisa fazer é enviar o DeviceInfo, como mostrado na Figura 10.

Figura 10 Enviando informações do dispositivo

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

O aplicativo RemoteCamera envia as informações do aplicativo, que descreve o hardware real, por isso, a propriedade IsSimulatedDevice é definida como falsa. Como mencionei anteriormente, ObjectType é definido como DeviceInfo. Além disso, eu defino a propriedade da versão como 1.0. Eu uso valores arbitrários para DeviceProperties, que principalmente consistem em cadeias de caracteres estáticas (consulte o método SetDefaultValues da classe DeviceProperties). Eu também defino um comando remoto, atualização de visualização de câmera, que habilita o controle remoto da visualização da câmera. Este comando tem um comando booliano, IsPreviewActive, que especifica se a visualização da câmera deve ser iniciada ou parada (consulte o arquivo CommandHelper.cs na pasta AzureHelpers).

Para realmente enviar dados para a nuvem, eu implemento o método SendMessage:

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

Basicamente, você precisa serializar seu objeto C# para uma matriz de bytes que contenha objetos formatados JSON (consulte a classe estática MessageHelper da subpasta AzureHelpers):

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

Em seguida, você encapsula a matriz resultante na classe Message, que você envia para a nuvem com o método SendEventAsync da instância de classe DeviceClient. A classe Message é o objeto, que suplementa dados brutos (um objeto JSON sendo transferido) com propriedades adicionais. Essas propriedades são usadas para rastrear mensagens sendo enviadas entre dispositivos e o Hub IoT.

No aplicativo RemoteCamera, as ações de estabelecer uma conexão com a nuvem e enviar informações do dispositivo são disparadas por dois botões localizados na guia Nuvem: Conecte e inicialize e envie informações do dispositivo. O clique no manipulador de eventos do primeiro botão é limitado ao método Connect do RemoteCameraViewModel:

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

O clique no manipulador de eventos do segundo botão é conectado com o método SendDeviceInfo da instância de classe CloudHelper. Este método foi discutido anteriormente.

Quando você se conecta à nuvem, você também pode começar a enviar dados de telemetria, na mesma forma que quando envia informações do dispositivo. Ou seja, você pode usar o método SendMessage para o qual você passa um objeto de telemetria. Aqui, este objeto, uma instância da classe Telemetry­Data, tem apenas uma única propriedade, Brilho. O seguinte mostra um exemplo completo de envio de dados de telemetria para a nuvem, implementado no método SendBrightness da classe CloudHelper:

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 é invocado logo após a obtenção do brilho, calculado pelo ImageProcessor. Isso é realizado no manipulador de eventos 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);
  }
}

Por isso, se você agora executa o aplicativo RemoteCamera, inicie a visualização e conecte-se à nuvem. Você verá que os valores de brilho são exibidos no gráfico correspondente, como mostrado na Figura 1. Observe que, embora os dispositivos simulados da solução pré-configurada são dedicados para enviar temperatura e umidade como dados de telemetria, você também pode enviar outros valores. Aqui, estou enviando brilho, que é automaticamente exibido no gráfico apropriado.

Manipulando comandos remotos

A classe CloudHelper também implementa métodos para manipular comandos remotos recebidos da nuvem. De forma semelhante, como no caso do ImageProcessor, eu manipulo comandos na tela de fundo (BeginRemoteCommandHandling da classe CloudHelper). Da mesma forma, eu uso o padrão assíncrono baseado em tarefa:

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

Este método é responsável por criar uma tarefa que analisa continuamente mensagens recebidas do ponto de extremidade da nuvem. Para receber uma mensagem remota, você invoca o método ReceiveAsync da classe DeviceClient. ReceiveAsync retorna uma instância da classe Message, que você usa para obter uma matriz de bytes brutos contendo dados de comando remoto formatados para JSON. Então, você desserializa esta matriz para o objeto RemoteCommand (consulte RemoteCommand.cs na pasta AzureHelpers), que é implementada na classe MessageHelper (a subpasta AzureHelpers):

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

O RemoteCommand tem várias propriedades, mas você normalmente só usa duas: nome e parâmetros, que contêm o nome do comando e os parâmetros do comando. No aplicativo RemoteCamera, eu uso esses valores para determinar se um comando esperado foi recebido (consulte a Figura 11). Se foi o caso, eu gero o evento UpdateCameraPreviewCommandReceived para passar essa informação aos ouvintes, e informo a nuvem que o comando foi aceito usando o método CompleteAsync da classe DeviceClient. Se o comando não for reconhecido, eu o rejeito usando o método RejectAsync.

Figura 11 Mensagens remotas de desserialização e análise

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

O evento UpdateCameraPreviewCommandReceived é manipulado na classe MainPage. Dependendo do valor do parâmetro que eu obtenho do comando remoto, ou eu paro ou inicio a visualização da câmera local. Esta operação é novamente enviada para o thread da interface do usuário, como mostrada na Figura 12.

Figura 12 Atualizando a visualização da câmera

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

Enviando comandos da nuvem

Por fim, eu mostro que o portal de solução pode ser usado para controlar remotamente seu dispositivo de IoT. Para isso, você usa a guia Dispositivos, na qual você precisa encontrar e depois clicar em seu dispositivo (consulte a Figura 13).

A guia Dispositivos do portal de IoT mostrando detalhes do RemoteCamera
Figura 13 A guia Dispositivos do portal de IoT mostrando detalhes do RemoteCamera

Isso ativa um painel de detalhes do dispositivo, no qual você deve clicar no hiperlink Comandos. Você então verá outro formulário que permite que você escolha e envie um comando remoto. O layout específico desse formulário depende do comando que você escolhe. Aqui, eu tenho somente um comando de parâmetro único, por isso só existe uma caixa de seleção. Se você desmarcar esta caixa de seleção e enviar o comando, o aplicativo RemoteCamera vai interromper a visualização. Todos os comandos que você envia, junto com os seus status, aparecem no histórico de comandos, como mostrado na Figura 14.

Um formulário para enviar comandos remotos ao dispositivo de IoT
Figura 14 Um formulário para enviar comandos remotos ao dispositivo de IoT

Conclusão

Mostrei como configurar o monitoramento remoto pré-configurado do Azure IoT Suite. Esta solução coleta e exibe informações sobre imagens adquiridas com a webcam anexada ao dispositivo remoto, e também lhe permite controlar remotamente o dispositivo de IoT. O código-fonte complementar pode ser executado em qualquer dispositivo da Plataforma Universal do Windows, por isso, você não precisa realmente implantá-lo no Raspberry Pi. Este artigo, junto com a documentação online da solução de monitoramento remoto e de seu código-fonte, ajudará você a impulsionar o desenvolvimento integral da IoT para um monitoramento remoto prático. Como você pode ver, com o Windows 10 IoT Core, é possível ir muito além de um simples LED piscando.


Dawid Borycki é engenheiro de software, pesquisador em biomédica, autor e conferencista. Ele gosta de aprender novas tecnologias para experimentação de software e protótipos.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Rachel Appel