Uno de los primeros pasos que queremos dar cuando comenzamos a experimentar con Internet of Things es el de integrar nuestro primer sensor para leer datos de nuestro entorno.
Para este artículo nos hemos decidido por Raspberry Pi 2 para integrar nuestro primer sensor, un medidor de distancias por ultrasonidos.
Dado que este artículo requiere de componentes hardware, proporcionamos la lista simplificada de ellos a continuación:
- Raspberry Pi 2
- Ultrasonic Sensor
- Cables Jumper Hembra-Hembra
2. Principios físicos de funcionamiento
Antes de proceder a conectar nuestro sensor a nuestra Raspberry, vamos a repasar los conceptos básicos de funcionamiento de este tipo de sensores.
Como se ve en la imagen a continuación, un sensor de distancia por ultrasonidos está compuesto por un emisor y un receptor de ultrasonidos, que actúan bajo el principio de eco de una onda de sonido por encima del espectro audible del sonido.
Como podemos ver en el diagrama, el módulo emite un pulso, una onda de ultrasonidos que atraviesa el espacio hasta que se encuentra con un objeto, entonces rebota sobre el mismo y el receptor recoge dicha onda en una fracción de tiempo medible.
Basándonos en un dato que conocemos de antemano, la velocidad del sonido, podemos usar el tiempo que tarda, concretamente la mitad del tiempo que necesita el pulso para alcanzar al objeto sobre el que colisiona y retornar al sensor.
El sensor por ultrasonidos habitualmente se compone de cuatro pines, alimentación, tierra, ping y echo.
Los dos primero sirven para alimentar el módulo, mientras que el pin de ping se encarga de emitir el pulso que usaremos para calcular la distancia y el pin de echo se encargará de recibir el pulso una vez regrese al módulo.
A continuación una ilustración sobre el funcionamiento del módulo:
3. Conexión a nuestra Rasberry Pi
A modo de referencia os dejamos el diagrama de conexiones para los distintos puertos que nos provee la Raspberry Pi 2/3, que siempre es adecuado tener a mano para no equivocarnos a la hora de realizar las conexiones de entre los distintos pines.
Únicamente serán necesarios dos puertos de entrada-salida digitales de propósito general o GPIO, uno para el ping y otro para el echo.
Para nuestro ejemplo haremos uso de los pines que se detallan a continuación:
- Trigger/Ping –> GPIO 5
- Echo –> GPIO 6
- Vcc –> (2) 5V PWR
- Gnd –> (6) GND
4. Código de la aplicación
En el ejemplo usado para poder ilustrar el uso de sensores con Microsoft IoT Core y aplicaciones universales Windows UWP nos hemos decidido por una aplicación universal con interfaz de usuario compuesta únicamente por una pequeña etiqueta de texto que nos proporcione la distancia medida en centímetros a la que se encuentra el objeto contra el que colisiona.
Vamos a comenzar con la creación de la aplicación en Visual Studio 2015.
1 |
File -> New -> Project -> Installed -> Visual C# -> Windows -> Universal -> Blank App (Universal Windows) |
Acto seguido procedemos a añadir las extensiones de Windows para IoT de las aplicaciones universales
1 |
Universal Windows -> Extensions |
En la página principal de la aplicación añadimos un Textblock, alineado en el centro de la pantalla donde mostraremos los valores leídos al usuario.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<Page x:Class="DistanceMeasureApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:DistanceMeasureApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <TextBlock x:Name="distanceLabel" Text="" Foreground="Black" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </Page> |
En el código de la página procederemos a inicializar una nueva instancia de la clase del módulo de ultrasonidos, y crearemos un DispatcherTimer con un intervalo de 100 milisegundos que nos permita leer y actualizar los valores obtenidos desde el sensor en la interfaz de usuario.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public sealed partial class MainPage : Page { private readonly DistanceSensor _distance = new DistanceSensor((int)GpioPins.GpioPin_5, (int)GpioPins.GpioPin_6); private readonly DispatcherTimer _timer; public MainPage() { this.InitializeComponent(); Loaded += MainPage_Loaded; _timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromMilliseconds(100); _timer.Tick += _timer_Tick; } private void _timer_Tick(object sender, object e) { distanceLabel.Text = String.Format("Distance: {0} cm", ((long)_distance.GetDistance()).ToString()); } private void MainPage_Loaded(object sender, RoutedEventArgs e) { _timer.Start(); } } |
Como podemos ver el código hace referencia a dos tipos que hasta ahora no hemos presentado.
Por un lado, se encuentra un tipo Enum llamado GpioPins, que hemos añadido a este ejemplo para simplificar el uso de los diferentes puertos GPIO disponibles en Raspberry.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
public enum GpioPins : int { /// <summary> /// Raspberry Pi 2 - Header Pin Number : 29 /// </summary> GpioPin_5 = 5, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 31 /// </summary> GpioPin_6 = 6, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 32 /// </summary> GpioPin_12 = 12, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 33 /// </summary> GpioPin_13 = 13, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 36 /// </summary> GpioPin_16 = 16, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 12 /// </summary> GpioPin_18 = 18, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 15 /// </summary> GpioPin_22 = 22, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 16 /// </summary> GpioPin_23 = 23, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 18 /// </summary> GpioPin_24 = 24, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 22 /// </summary> GpioPin_25 = 25, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 37 /// </summary> GpioPin_26 = 26, /// <summary> /// Raspberry Pi 2 - Header Pin Number : 13 /// </summary> GpioPin_27 = 27 } |
Este enumerado nos permite relacionar los puertos físicos de la placa con los puertos que usaremos el código de manera muy legible, que evitará en gran medida las posibles confusiones a la hora de realizar las conexiones físicas con los cables hembra-hembra.
Por otro lado nos encontramos con la clase DistanceSensor, que implementa la lógica necesaria para llevar a cabo las mediciones de los pulsos.
Esta clase está basada en la implementación de GHI Electronics para Compact Framework 4.2 de su módulo de sensor de ultrasonidos para placas de desarrollo Gadgeteer, adaptándola a las Apis disponibles en UWP.
Esta clase define una serie de variables auxiliares:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
private const int MIN_DISTANCE = 5; private const int MAX_DISTANCE = 400; private const int MaxFlag = -1; private const int MinFlag = -2; private readonly int TicksPerMicrosecond = (int)(TimeSpan.TicksPerMillisecond / 1000); GpioController gpio = GpioController.GetDefault(); GpioPin Trigger; GpioPin Echo; /// <summary> /// Number of errors that can be accumulated in the GetDistanceInCentimeters function before the function returns an error value; /// </summary> public int AcceptableErrorRate = 10; /// <summary> /// Error that will be returned if the sensor fails to get an accurate reading after the AcceptableErrorRate has been reached. /// </summary> public readonly int SENSOR_ERROR = -1; |
Por un lado se definen los distintos puertos a usar, trigger y echo, a través de del controlador de puertos GPIO de Windows Iot Core.
Por otro lado, se definen dos constantes que representan los valores mínimos y máximos que el sensor es capaz de leer.
A primera vista puede llamarnos la atención el hecho de que la distancia mínima sea de 5 cm, esto se debe al rango de sensibilidad de estos módulos y a la presencia en todos ellos de lo que se conoce como punto ciego, un área o región ciega en la que el sensor no es capaz de realizar la medición, esto se ilustra a continuación.
En esta clase se define el método público que usaremos para obtener la distancia, que nos permite realizar una medida única y devolver el valor de la medición realizada, o por otro lado realizar un número de medidas secuenciales y devolver la media de todas las mediciones realizadas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public long GetDistance(int numMeasurements = 1) { long measuredValue = 0; long measuredAverage = 0; int errorCount = 0; for (int i = 0; i < numMeasurements; i++) { measuredValue = GetDistanceHelper(); if (measuredValue != MaxFlag || measuredValue != MinFlag) { measuredAverage += measuredValue; } else { errorCount++; i--; if (errorCount > AcceptableErrorRate) { return SENSOR_ERROR; } } } measuredAverage /= numMeasurements; return measuredAverage; } |
Este método principal hace uso de un método auxiliar que es el que se encarga de realizar los cálculos de tiempo entre la emisión del pulso y su recepción y su transformación a centímetros.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
private long GetDistanceHelper() { var mre = new ManualResetEventSlim(false); long start = 0; long microseconds = 0; long time = 0; long distance = 0; var sw = new Stopwatch(); //Send a 10µs pulse to start the measurement Trigger.Write(GpioPinValue.High); mre.Wait(TimeSpan.FromTicks(100)); Trigger.Write(GpioPinValue.Low); int error = 0; while (Echo.Read() != GpioPinValue.High) { error++; if (error > 1000) break; mre.Wait(TimeSpan.FromTicks(0)); } start = DateTime.Now.Ticks; sw.Start(); while (Echo.Read() == GpioPinValue.High) mre.Wait(TimeSpan.FromTicks(0)); time = (DateTime.Now.Ticks - start); sw.Stop(); microseconds = sw.Elapsed.Ticks / TicksPerMicrosecond; distance = (microseconds / 58); distance += 2; if (distance < MAX_DISTANCE) { if (distance >= MIN_DISTANCE) return distance; else return MinFlag; } else { return MaxFlag; } } |
Los valores usados en este método no son arbitrarios, son los resultantes de la fórmula que detallamos a continuación:
El sonido viaja a aproximadamente 340 metros por segundo.
Esto corresponde a unos 29.412μs (microsegundos) por centímetro.
Para medir la distancia el sonido ha viajado utilizamos la fórmula:
Distancia = (Tiempo x velocidad del sonido) / 2.
El «2» está en la fórmula, porque el sonido tiene que viajar de ida y vuelta.
En primer lugar, el sonido viaja desde el sensor, y luego rebota de una superficie y vuelve de nuevo al mismo.
La manera fácil de leer la distancia en centímetros es utilizar la fórmula:
centímetros = ((microsegundos / 2) / 29).
Por ejemplo, si se lleva a 100μs (microsegundos) para el sonido ultrasónico de recuperarse, entonces la distancia es ((100/2) / 29) centímetros o alrededor de 1,7 centímetros.
5. Conclusiones
Como hemos podido ver en este artículo, hacer uso de distintos sensores que nos permitan recoger datos de nuestro entorno en una aplicación de Windows Universal es extremadamente sencillo.
Basta con conocer el uso de cada uno de los sensores que queramos integrar y aprovechar la ventaja de todos los conocimientos que ya tenemos sobre aplicaciones XAML+C#.
6. Referencias
A continuación, se detallan las distintas referencias usadas en este artículo:
Código completo en GitHub
https://github.com/cristianmp/IoTCore
GHI Distance_US3 Gadgeteer Module Driver
GpioPins Enum
Develop Windows IoT Applications
https://developer.microsoft.com/en-us/windows/iot
Deja un comentario