Junio de 2017
Volumen 32, número 6
Internet de las cosas: use el Conjunto de aplicaciones de IoT de Azure para impulsar el desarrollo de IoT
Por Dawid Borycki
Una solución de Internet de las cosas (IoT) incluye dispositivos de telemetría remotos, un portal web, almacenamiento en nube y procesamiento en tiempo real. Una estructura tan compleja puede hacer que sea reacio a iniciar el desarrollo de IoT. Para facilitar las cosas, el Conjunto de aplicaciones de IoT de Microsoft Azure proporciona dos soluciones preconfiguradas: supervisión remota y mantenimiento predictivo. Aquí, le indicaré cómo crear una solución de supervisión remota, que recopilará y analizará los datos de un dispositivo IoT remoto controlado por Windows 10 IoT Core. Este dispositivo, Raspberry Pi, adquirirá las imágenes de una cámara USB. Posteriormente, el brillo de la imagen se calculará sobre el dispositivo IoT y se transmitirá a la nube, donde se almacenará, procesará y mostrará (consulte la Figura 1). Además, el usuario final no solo podrá ver la información adquirida con el dispositivo remoto, sino que también controlará ese dispositivo de forma remota. Encontrará el código fuente completo en que se basa esta discusión en msdn.com/magazine/0617magcode.
Figura 1 Portal de solución del Conjunto de aplicaciones de IoT de Azure preconfigurado y aplicación remota para la Plataforma universal de Windows, que adquiere y procesa la transmisión de vídeo
Dispositivo remoto
En esta publicación, Frank LaVigne (msdn.com/magazine/mt694090) y Bruno Sonnino (msdn.com/magazine/mt808503) ya trataron los conceptos básicos de programación de Raspberry Pi con Windows 10 IoT Core. LaVigne y Sonnino mostraron cómo configurar el entorno de desarrollo y el panel de IoT, cómo configurar la unidad de IoT en el explorador mediante Device Portal y cómo controlar los puertos GPIO con Windows 10 IoT Core. Además, LaVigne mencionó en su artículo que IoT se puede usar para programar y controlar cámaras remotas. Aquí, desarrollo esta idea y muestro de manera explícita cómo convertir Raspberry Pi en un dispositivo de este tipo.
Con esta finalidad, creo la aplicación RemoteCamera para la Plataforma universal de Windows (UWP) mediante la plantilla de proyecto Aplicación vacía (Windows universal) de Visual C# y, después, establezco las versiones de API de destino y mínima en Windows 10 Anniversary Edition (10.0; compilación 14393). Uso esta versión de API para permitir el enlace a métodos, lo que me permite conectar directamente los métodos del modelo de vista que desencadenan los controles visuales:
<Button x:Name="ButtonPreviewStart"
Content="Start preview"
Click="{x:Bind remoteCameraViewModel.PreviewStart}" />
A continuación, declaro la interfaz de usuario tal como se muestra en la Figura 1. Existen dos pestañas: Captura de cámara y Nube. La primera contiene controles para iniciar y detener la vista previa de cámara, mostrar la transmisión de vídeo y presentar el brillo de la imagen (una etiqueta y una barra de progreso). La segunda pestaña contiene dos botones, que puede usar para conectar un dispositivo a la nube y para registrar un dispositivo en el portal de IoT. La pestaña Nube también incluye una casilla que permite habilitar el streaming de datos de telemetría.
La mayor parte de la lógica asociada a la interfaz de usuario se implementa dentro de la clase RemoteCameraViewModel (consulte la subcarpeta ViewModels del proyecto RemoteCamera). Esta clase, aparte de implementar algunas propiedades enlazadas a la interfaz de usuario, controla la adquisición de vídeo, el procesamiento de imágenes y la interacción de la nube. Estas funcionalidades secundarias se implementan en clases diferentes: CameraCapture, ImageProcessor y CloudHelper, respectivamente. En unos instantes, explicaré las clases CameraCapture e ImageProcessor, mientras que la clase CloudHelper y las clases auxiliares relacionadas se describirán más adelante en el contexto de la solución Azure IoT preconfigurada.
Captura de cámara
Captura de cámara (consulte CameraCapture.cs en la carpeta Helpers) se basa en dos elementos: la clase Windows.Media.Capture.MediaCapture y Windows.UI.Xaml.Controls.CaptureElement. La primera se usa para adquirir vídeo, mientras que la segunda muestra la transmisión de vídeo adquirida. El vídeo se adquiere con una cámara web, por lo que debo declarar la funcionalidad del dispositivo correspondiente en Package.appxmanifest.
Para inicializar la clase MediaCapture, se invoca el método InitializeAsync. Finalmente, se pasa a ese método una instancia de la clase MediaCaptureInitializationSettings, que permite especificar las opciones de captura. Puede elegir entre el streaming de vídeo o audio y seleccionar el hardware de captura. Aquí, solo adquiero vídeo de la cámara web predeterminada (consulte la Figura 2).
Figura 2 Inicialización de la captura de cámara
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;
}
}
}
A continuación, con la propiedad Source de la instancia de clase CaptureElement, conecto este objeto al control MediaCapture para mostrar la transmisión de vídeo. También invoco el método auxiliar GetVideoProperties, que lee y posteriormente almacena el tamaño de un fotograma de vídeo. Más tarde, uso esta información para obtener el fotograma de vista previa para el procesamiento. Finalmente, para iniciar y detener realmente la adquisición de vídeo, invoco StartPreviewAsync y StopPreviewAsync de la clase MediaCapture. En la clase CameraCapture, encapsulé esos métodos con lógica adicional, y comprobé la inicialización y el estado de vista previa:
public async Task Start()
{
if (IsInitialized)
{
if (!IsPreviewActive)
{
await MediaCapture.StartPreviewAsync();
IsPreviewActive = true;
}
}
}
Al ejecutar la aplicación, puede pulsar el botón de vista previa Iniciar, que configura la adquisición de la cámara, y verá la imagen de la cámara al cabo de un rato. Observe que la aplicación RemoteCamera es universal, de modo que puede implementarse sin realizar ningún cambio en el equipo de desarrollo, un smartphone, una tableta o el dispositivo Raspberry Pi. Si prueba la aplicación RemoteCamera en su equipo con Windows 10, deberá asegurarse de que las aplicaciones tengan permiso para usar la cámara. Esta opción se configura mediante la aplicación Configuración (Privacidad/Cámara). Para probar la aplicación con el dispositivo Raspberry Pi, usaré una Microsoft Life Cam HD-3000 de bajo presupuesto. Se trata de una cámara web USB, de modo que Windows 10 IoT Core la detecta automáticamente cuando la conecto a uno de los cuatro puertos USB del dispositivo Raspberry Pi. Encontrará una lista completa de las cámaras compatibles con Windows 10 IoT Core en bit.ly/2p1ZHGD. Cuando conecte la cámara web al dispositivo Rasbperry Pi, aparecerá debajo de la pestaña Dispositivos de Device Portal.
Procesador de imágenes
La clase ImageProcessor calcula el brillo del fotograma actual en el fondo. Para realizar la operación en segundo plano, creo un subproceso con el modelo asincrónico basado en tareas, como se muestra en la Figura 3.
Figura 3 Cálculo del brillo del fondo
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);
}
Dentro del bucle while, determino el brillo de la imagen y, luego, paso este valor a los agentes de escucha del evento ProcessingDone. Este evento se aprovisiona con una instancia de la clase ImageProcessorEventArgs, que solo tiene una propiedad pública, Brightness. El procesamiento se ejecuta hasta que la tarea recibe una señal de cancelación. El elemento clave del procesamiento de imágenes es el método GetBrightness, que se muestra en la Figura 4.
Figura 4 El 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;
}
Accedo al fotograma de vista previa mediante el método GetPreviewBitmap de la instancia de clase CameraCapture. Internamente, GetPreviewBitmap usa el método GetPreviewFrameAsync de la clase MediaCapture. Existen dos versiones de GetPreviewFrameAsync. La primera, un método sin parámetros, devuelve una instancia de la clase VideoFrame. En este caso, puede obtener los datos de píxel reales mediante la lectura de la propiedad Direct3DSurface. La segunda versión acepta una instancia de la clase VideoFrame y copia los datos de píxel en su propiedad SoftwareBitmap. Aquí, uso la segunda opción (consulte el método GetPreviewBitmap de la clase CameraCapture) y, después, accedo a los datos de píxel a través del método CopyToBuffer de la instancia de clase SoftwareBitmap que se muestra en la Figura 5.
Figura 5 Acceso a los datos de píxel
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;
}
En primer lugar, compruebo que el formato de píxel es BGRA8, lo que significa que la imagen se representa con cuatro canales de 8 bits: tres de color azul, verde y rojo, y uno de alfa o transparencia. Si el mapa de bits de entrada tiene un formato de píxel diferente, realizo la conversión pertinente. A continuación, copio los datos de píxel en la matriz de bytes, cuyo tamaño viene determinado por la altura de la imagen multiplicada por la densidad de píxeles de la imagen (bit.ly/2om8Ny9). Leo ambos valores de una instancia de BitmapPlaneDescription, que obtengo del objeto BitmapBuffer, devuelto por el método SoftwareBitmap.LockBuffer.
Una vez proporcionada la matriz de bytes con los datos de píxel, todo lo que tengo que hacer es calcular el valor medio de todos los píxeles. Por tanto, itero el búfer de píxeles (consulte la Figura 6).
Figura 6 Cálculo del valor medio de los píxeles
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);
}
A continuación, en cada iteración, convierto un píxel determinado a la escala de grises con el promedio de los valores de cada canal de color:
private static byte GetGrayscaleValue(byte[] pixelBuffer, uint startIndex)
{
var grayValue = (pixelBuffer[startIndex]
+ pixelBuffer[startIndex + 1]
+ pixelBuffer[startIndex + 2]) / 3.0;
return Convert.ToByte(grayValue);
}
La propiedad Brightness se pasa a la vista a través del evento ProcessingDone. Este evento se controla en la clase MainPage (MainPage.xaml.cs), donde muestro el brillo en la etiqueta y una barra de progreso. Ambos controles están enlazados a la propiedad Brightness de la clase RemoteCameraViewModel. Observe que el evento ProcessingDone se genera en el subproceso en segundo plano. Por consiguiente, modifico RemoteCameraViewModel.Brightness a través del subproceso de interfaz de usuario mediante la clase Dispatcher, como se muestra en la Figura 7.
Figura 7 Modificación de RemoteCameraViewModel.Brightness a través del subproceso de interfaz de usuario mediante la clase Dispatcher
private async void DisplayBrightness(byte brightness)
{
if (Dispatcher.HasThreadAccess)
{
remoteCameraViewModel.Brightness = brightness;
}
else
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
DisplayBrightness(brightness);
});
}
}
Aprovisionamiento de la solución de supervisión remota
Para aprovisionar una solución, puede usar un portal dedicado en azureiotsuite.com. Después de iniciar sesión y de elegir su suscripción de Azure, se le redirigirá a una página donde deberá hacer clic en el cuadro "Crear una nueva solución". Se abrirá un sitio web donde podrá elegir una de dos soluciones preconfiguradas: mantenimiento predictivo o supervisión remota (consulte la Figura 8). Después de elegir la solución, se mostrará otro formulario que permite establecer el nombre y la región de la solución para los recursos de Azure. Aquí, establezco el nombre y la región de la solución en RemoteCameraMonitoring y West US, respectivamente.
Figura 8 Soluciones preconfiguradas del Conjunto de aplicaciones de IoT de Azure (arriba) y configuración de la solución de supervisión remota (abajo)
Al aprovisionar la solución de supervisión remota, el portal del Conjunto de aplicaciones de IoT de Azure crea varios recursos de Azure: IoT Hub, trabajos de Stream Analytics, Storage y App Service. IoT Hub ofrece comunicación bidireccional entre la nube y los dispositivos remotos. Los datos transmitidos por dispositivos remotos los transforma un trabajo de Stream Analytics, que suele filtrar los datos innecesarios. Los datos filtrados se almacenan o se dirigen para realizar más análisis. Finalmente, se usa App Service para hospedar el portal web.
El aprovisionamiento de soluciones también se puede realizar desde la línea de comandos. Para ello, puede clonar o descargar el código fuente de la solución de bit.ly/2osI4RW y, después, seguir la instrucción de bit.ly/2p7MPPc. También tiene la opción de implementar una solución localmente, como se muestra en bit.ly/2nEePNi. En este caso, no se crea ningún servicio de Azure App Service porque el portal de soluciones se ejecuta en una máquina local. Un enfoque de este tipo puede resultar especialmente útil para el desarrollo o la depuración, o si quiere modificar una solución preconfigurada.
Una vez finalizado el aprovisionamiento, puede iniciar la solución. El portal se mostrará en el explorador predeterminado (vuelva a consultar la Figura 1). Este portal presenta algunas pestañas. Para la finalidad de este artículo, limitaré mi atención solo a dos de ellas: Panel y Dispositivos. La pestaña Panel muestra el mapa con los dispositivos remotos y los datos de telemetría que estos transmiten mediante streaming. La pestaña Dispositivos muestra una lista de los dispositivos remotos, incluido el estado, las funcionalidades y una descripción. De manera predeterminada, existen varios dispositivos emulados. Permítame explicar como registrar el nuevo hardware no emulado.
Registro de un dispositivo
Para registrar un dispositivo, se usa el portal de soluciones, donde se hace clic en el hipervínculo Agregar un dispositivo ubicado en la esquina inferior izquierda. A continuación, puede elegir entre un dispositivo simulado o personalizado. Elija la segunda opción y haga clic en el botón Agregar nuevo. Ahora, puede definir el id. de dispositivo. Establezco este valor en RemoteCamera. Posteriormente, el formulario ADD A CUSTOM DEVICE (AGREGAR UN DISPOSITIVO PERSONALIZADO) muestra las credenciales del dispositivo (consulte la Figura 9), que usaré más adelante para conectar mi dispositivo IoT a IoT Hub.
Figura 9 Resumen del registro de dispositivos
Metadatos del dispositivo y comunicación con la nube
El dispositivo que agregue aparecerá en la lista de dispositivos y, a continuación, podrá enviar los metadatos o la información del dispositivo. La información del dispositivo incluye un objeto JSON que describe el dispositivo remoto. Este objeto indica al punto de conexión de nube las funcionalidades del dispositivo, además de incluir una descripción del hardware y la lista de comandos remotos que acepta el dispositivo. El usuario final puede enviar estos comandos al dispositivo a través del portal de soluciones de IoT. En la aplicación RemoteCamera, la información del dispositivo se representa como la clase DeviceInfo (en la subcarpeta 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; }
}
Las primeras dos propiedades de DeviceInfo especifican si un dispositivo es simulado y definen la versión del objeto DeviceInfo. Como podrá ver más adelante, la tercera propiedad, ObjectType, está establecida en una constante de cadena, DeviceInfo. Esta cadena se usa en la nube, especialmente en el trabajo de Azure Stream Analytics, para filtrar la información del dispositivo de los datos de telemetría. Luego, DeviceProperties (consulte la subcarpeta AzureHelpers) contiene una colección de propiedades (como número de serie, memoria, plataforma y RAM) que describen el dispositivo. Por último, la propiedad Commands es una colección de comandos remotos que reconoce el dispositivo. Para definir cada comando, se especifican su nombre y una lista de parámetros, que se representan mediante las clases Command y CommandParameter, respectivamente (consulte AzureHelpers\Command.cs).
Para establecer la comunicación entre el dispositivo IoT e IoT Hub, se usa el paquete Microsoft.Azure.Devices.Client NuGet. Este paquete proporciona la clase DeviceClient, que se usa para enviar y recibir mensajes a y desde la nube. Puede crear una instancia de DeviceClient mediante los métodos estáticos Create o CreateFromConnectionString. Aquí, uso la primera opción (CloudHelper.cs en la carpeta AzureHelpers):
public async Task Initialize()
{
if (!IsInitialized)
{
deviceClient = DeviceClient.Create(
Configuration.Hostname, Configuration.AuthenticationKey());
await deviceClient.OpenAsync();
IsInitialized = true;
BeginRemoteCommandHandling();
}
}
Como puede ver, el método DeviceClient.Create requiere que proporcione el nombre de host de IoT Hub y las credenciales del dispositivo (el identificador y la clave). Estos valores se obtienen del portal de soluciones durante el aprovisionamiento del dispositivo (vuelva a consultar la Figura 9). En la aplicación RemoteCamera, almaceno el nombre de host, el id. de dispositivo y la clave en la clase 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);
}
}
Además, la clase Configuration implementa un método estático AuthenticationKey, que encapsula las credenciales del dispositivo en una instancia de la clase DeviceAuthenticationWithRegistrySymmetricKey. Uso esto para simplificar la creación de la instancia de clase DeviceClient.
Una vez establecida la conexión, todo lo que debe hacer es enviar el objeto DeviceInfo, como se muestra en la Figura 10.
Figura 10 Envío de información del 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);
}
La aplicación RemoteCamera envía la información del dispositivo, que describe el hardware real, de modo que la propiedad IsSimulatedDevice está establecida en false. Como mencioné anteriormente, ObjectType está establecido en DeviceInfo. Además, establezco la propiedad Version en 1.0. Para DeviceProperties, uso valores arbitrarios, que están formados principalmente por cadenas estáticas (consulte el método SetDefaultValues de la clase DeviceProperties). También defino un comando remoto, Update camera preview (Actualizar vista previa de cámara), que permite el control remoto de la vista previa de cámara. Este comando presenta un parámetro booleano, IsPreviewActive, que especifica si la vista previa de cámara se debe iniciar o detener (consulte el archivo CommandHelper.cs de la carpeta AzureHelpers).
Para enviar datos realmente a la nube, implemento el método SendMessage:
private async Task SendMessage(Object message)
{
var serializedMessage = MessageHelper.Serialize(message);
await deviceClient.SendEventAsync(serializedMessage);
}
Básicamente, debe serializar su objeto de C# en una matriz de bytes que contenga objetos con formato JSON (consulte la clase estática MessageHelper de la subcarpeta AzureHelpers):
public static Message Serialize(object obj)
{
ArgumentCheck.IsNull(obj, "obj");
var jsonData = JsonConvert.SerializeObject(obj);
return new Message(Encoding.UTF8.GetBytes(jsonData));
}
A continuación, debe encapsular la matriz resultante en la clase Message, que envía a la nube con el método SendEventAsync de la instancia de clase DeviceClient. La clase Message es el objeto, que complementa los datos sin formato (un objeto JSON que se está transfiriendo) con propiedades adicionales. Estas propiedades se usan para realizar el seguimiento de los mensajes que se envían entre los dispositivos e IoT Hub.
En la aplicación RemoteCamera, el establecimiento de una conexión con la nube y el envío de la información del dispositivo se desencadenan mediante dos botones ubicados en la pestaña Nube: Conecte, inicialice y envíe la información del dispositivo. El controlador de eventos de clic del primer botón está enlazado al método Connect de la clase RemoteCameraViewModel:
public async Task Connect()
{
await CloudHelper.Initialize();
IsConnected = true;
}
El controlador de eventos de clic del segundo botón está conectado con el método SendDeviceInfo de la instancia de clase CloudHelper. Expliqué este método anteriormente.
Tras conectarse a la nube, también puede empezar a enviar datos de telemetría, de manera muy parecida a cuando envió la información del dispositivo. Es decir, puede usar el método SendMessage al que pasa un objeto de telemetría. Aquí, este objeto, una instancia de la clase TelemetryData, solo tiene una propiedad, Brightness. A continuación, se muestra un ejemplo completo de envío de datos de telemetría a la nube, implementado en el método SendBrightness de la clase 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 se invoca justo después de obtener el brillo, calculado mediante ImageProcessor. Esto se lleva a cabo en el controlador 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);
}
}
Así, si ahora ejecuta la aplicación RemoteCamera, inicia la vista previa y se conecta a la nube, observará que esos valores de brillo se muestran en el gráfico correspondiente, tal como se mostraba en la Figura 1. Observe que, aunque los dispositivos simulados de la solución preconfigurada se dedican al envío de la temperatura y la humedad como datos de telemetría, también puede enviar otros valores. Aquí, envío el brillo, que se muestra automáticamente en el gráfico adecuado.
Control de comandos remotos
La clase CloudHelper también implementa métodos para controlar los comandos remotos que se reciben de la nube. De manera similar, como en el caso de ImageProcessor, controlo los comandos en segundo plano (BeginRemoteCommandHandling de la clase CloudHelper). De nuevo, uso el modelo asincrónico basado en tareas:
private void BeginRemoteCommandHandling()
{
Task.Run(async () =>
{
while (true)
{
var message = await deviceClient.ReceiveAsync();
if (message != null)
{
await HandleIncomingMessage(message);
}
}
});
}
Este método es responsable de crear una tarea que analiza de manera continua los mensajes recibidos del punto de conexión de nube. Para recibir un mensaje remoto, se invoca el método ReceiveAsync de la clase DeviceClient. ReceiveAsync devuelve una instancia de la clase Message, que se usa para obtener una matriz de bytes sin formato que contiene los datos de comandos remotos con formato JSON. A continuación, se deserializa esta matriz en el objeto RemoteCommand (consulte RemoteCommand.cs de la carpeta AzureHelpers), que se implementa en la clase MessageHelper (la subcarpeta AzureHelpers):
public static RemoteCommand Deserialize(Message message)
{
ArgumentCheck.IsNull(message, "message");
var jsonData = Encoding.UTF8.GetString(message.GetBytes());
return JsonConvert.DeserializeObject<RemoteCommand>(
jsonData);
}
El objeto RemoteCommand presenta varias propiedades, pero solo se suelen usar dos: name y parameters, que contienen el nombre y los parámetros del comando. En la aplicación RemoteCamera, uso estos valores para determinar si se recibió un comando previsto (consulte la Figura 11). En ese caso, genero el evento UpdateCameraPreviewCommandReceived para pasar esa información a los agentes de escucha y, luego, informo a la nube que el comando se aceptó con el método CompleteAsync de la clase DeviceClient. Si el comando no se reconoce, lo rechazo mediante el método RejectAsync.
Figura 11 Deserialización y análisis de mensajes remotos
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));
}
}
El evento UpdateCameraPreviewCommandReceived se controla en la clase MainPage. En función del valor de parámetro que obtengo del comando remoto, detengo o inicio la vista previa de la cámara local. De nuevo, la operación se envía al subproceso de interfaz de usuario, como se muestra en la Figura 12.
Figura 12 Actualización de la vista previa de la cámara
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);
});
}
}
Envío de comandos desde la nube
Finalmente, muestro que el portal de soluciones se puede usar para controlar de forma remota el dispositivo IoT. Para ello, se usa la pestaña Dispositivos, donde debe buscar y hacer clic en su dispositivo (consulte la Figura 13).
Figura 13 Pestaña Dispositivos del portal de IoT con los detalles de RemoteCamera
Activa un panel de detalles del dispositivo, donde puede hacer clic en el hipervínculo Comandos. A continuación, verá otro formulario que le permite elegir y enviar un comando remoto. El diseño particular de este formulario depende el comando que se elige. Aquí, solo tengo un comando con un único parámetro, por lo que solo hay una casilla. Si desmarca esta casilla y envía el comando, la aplicación RemoteCamera detendrá la vista previa. Todos los comandos que envía, junto con su estado, aparecen en el historial de comandos, como se muestra en la Figura 14.
Figura 14 Formulario para enviar comandos remotos al dispositivo IoT
Resumen
Mostré cómo configurar la solución del Conjunto de aplicaciones de IoT de Azure de supervisión remota preconfigurada. Esta solución recopila y muestra información sobre las imágenes adquiridas con la cámara web conectada al dispositivo remoto, además de permitir controlar de forma remota el dispositivo IoT. El código fuente complementario se puede ejecutar en cualquier dispositivo UWP, por lo que no es realmente necesario implementarlo en Raspberry Pi. Este artículo, junto con la documentación en línea de la solución de supervisión remota y su código de origen, le ayudará a poner en marcha el desarrollo de IoT completo para la supervisión remota práctica. Como puede observar, con Windows 10 IoT Core, puede ir mucho más allá del simple parpadeo de LED.
Dawid Borycki es ingeniero de software e investigador biomédico, autor y conferenciante. Le encanta aprender nuevas tecnologías para probar y crear prototipos de software.
Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Rachel Appel