Construir un robot con Windows 10 IoT Core

Rover

Durante la keynote del evento Windows 10: Hel10 World íbamos a demostrar todas las novedades que teníamos en Windows 10 para desarrolladores, como las aplicaciones universales, continuum, la seguridad en aplicaciones móviles y también las posibilidades que nos brinda Windows 10 IoT Core, la versión de Windows 10 para makers que permite desplegar las mismas aplicaciones universales que desarrollamos para los otros dispositivos en una Raspberry Pi 2,  en una MinnowBoard Max o en una DragonBoard 410c. Además, podemos aprovechar las características especiales de estos dispositivos , como el bus GPIO que nos permite conectar todo tipo de sensores, controladores y un largo etcétera de componentes electrónicos tanto analógicos como digitales.

La construcción del robot

Dentro del mundo de IoT puedes hacer una demo trivial como encender y apagar un led, incluso de forma remota utilizando IoT Hub, pero encontramos que es mucho más divertido ensamblar y desarrollar un robot. Teníamos pocos días para hacerlo así que nos pusimos manos a la obra. Como ya anunciamos en el Build 2014, tenemos un acuerdo con https://hackster.io, donde encontraréis todo tipo de ejemplos hechos por y para makers, con instrucciones paso a paso de gran cantidad de proyectos de IoT. En https://microsoft.hackster.io tenemos tanto los ejemplos oficiales como los que ha realizado la comunidad. Buscando un poco dentro de los proyectos encontramos el ejemplo de un Rover, un robot muy sencillo que consta del control de dos motores y un sensor de distancia por ultrasonidos. Es un ejemplo básico que nos puede servir para empezar y luego ir ampliando con nuestras propias ideas.

Las instrucciones de ensamblaje eran claras, pero había algunos componentes que eran difíciles de encontrar en España y comprándolos fuera tardaban demasiado en llegar, así que tuvimos que improvisar y en lugar del conversor DC/DC y el paquete de pilas adicional para alimentar la Raspberry Pi 2, nuestra compañera Yolanda nos aconsejó que utilizáramos una batería portátil (una PowerBank) para alimentarla, con lo que simplificamos bastante el diseño. Además, el chasis que compramos era un poco más ancho que el original, lo que nos permitió poner las pilas para los motores en la parte inferior para tener mejor acceso.

Una vez ensamblado el robot de forma parecida al tutorial, pero con algunas modificaciones personales como hace todo buen maker Sonrisa, hubo que ponerse manos a la obra con el código. En principio es muy sencillo de desplegar, basta clonar el repositorio de github en https://github.com/peejster/Rover y desplegarlo desde Visual Studio en una Raspberry Pi 2 con Windows 10 IoT Core. Al desplegar el código vimos que la precisión de la medición era de unos 17 centímetros, así que busqué cómo de mejorar la forma de medir el tiempo transcurrido con más precisión.

Midiendo la distancia los microsegundos son importantes

Cuando usamos sensores digitales, muchas veces necesitaremos precisión de microsegundos para operar con ellos. En el caso del sensor de distancia por ultrasonidos adquiere mucho más sentido porque necesitamos medir cuánto tiempo tarda un sonido en rebotar y volver al sensor para medir la distancia, pero ¿cómo podemos tener precisión de microsegundos en C#?

La primera opción es crear una librería en C++ y encapsularla en un Windows Component para usarla desde C#, pero, aunque en muchos foros os dirán que en C# no tenemos precisión suficiente, no es del todo verdad pues en este tipo de dispositivos se está realizando una compilación nativa y con la última versión de Windows 10 IoT core hemos optimizado muchísimo la comunicación con GPIO. Lo único que pasa es que cuando se interrumpa la ejecución de nuestro código por alguna razón, como puede ser el GC, debemos tener en cuenta esas situaciones excepcionales y repetir la medición en caso de que no funcione. Este mismo truco lo podemos utilizar para leer un sensor de tipo one-wire como el DHT22.

En el código del Rover tenemos dos puntos donde necesitaremos medir en microsegundos:

  • Necesitamos enviar una pulso de 10 µs al puerto de “trigger”.
  • Medimos el tiempo que tarda el sonido en salir del sensor y volver hasta él. El sonido viaja a unos 343.2 metros por segundo (depende de las condiciones atmosféricas), o lo que es lo mismo, tarda unos 29 microsegundos en recorrer un centímetro, entonces, para poder medir con precisión de centímetros tenemos que ser capaces de medir con una precisión de al menos 58  µs.

En el primer caso, necesitamos hacer una pequeña pausa entre que activamos el trigger y lo desactivamos. Esto lo conseguiremos realizando un Task.Delay síncrono utilizando la sobrecarga que nos permite pasar un TimeSpan, que podemos generar a partir del número de ticks. Para calcular los microsegundos, tenemos que 1 Tick son 100 nanosegundos, así que tenemos 10 ticks cada microsegundo. Por lo tanto tenemos que hacer una pausa de 100 ticks:

 gpioPinTrig.Write(GpioPinValue.High);
Task.Delay(TimeSpan.FromTicks(100)).Wait();
gpioPinTrig.Write(GpioPinValue.Low);

Una vez hemos solicitado el pulso, tenemos que esperar a que el pin donde está el Echo cambie, para evitar fallos utilizamos la clase SpinWait que nos permite establecer una condición y un timeout, de forma que si no hemos podido leer en el tiempo esperado podamos repetir la lectura:

 SpinWait.SpinUntil(() => {
   return gpioPinEcho.Read() != GpioPinValue.Low;
   }, timeoutInMilliseconds)

Cuando el pin Echo está en modo High, tenemos que medir el tiempo que se mantiene en ese estado: ese es el tiempo que ha tardado la señal en ir y volver. La propiedad Elapsed.TotalSeconds de la clase StopWatch nos proporciona el número de segundos con decimales, así, multiplicado por la mitad de la velocidad del sonido en cm/s nos dará los centímetros que ha recorrido. Para evitar problemas de precisión está todo el código en modo síncrono en un bucle, así que encapsulamos el código dentro en una tarea para poder ejecutar el código sin bloquear el interfaz de usuario. El código completo de los cálculos es el que sigue:

 public async Task GetDistanceInCmAsync(int timeoutInMilliseconds)
{
    return await Task.Run(() =>
    {
        double distance = double.MaxValue;
        // turn on the pulse
        gpioPinTrig.Write(GpioPinValue.High);
        Task.Delay(TimeSpan.FromTicks(100)).Wait();
        gpioPinTrig.Write(GpioPinValue.Low);

        if (SpinWait.SpinUntil(() => { return gpioPinEcho.Read() != GpioPinValue.Low; }, timeoutInMilliseconds))
        {
            var stopwatch = Stopwatch.StartNew();
            while (stopwatch.ElapsedMilliseconds < timeoutInMilliseconds && gpioPinEcho.Read() == GpioPinValue.High)
            {
                distance = stopwatch.Elapsed.TotalSeconds * 17150;
            }
            stopwatch.Stop();
            return distance;
        }
        throw new TimeoutException("Could not read from sensor");
    });
}

Control de los motores

El código de control de las ruedas es mucho más sencillo, simplemente ponemos a High o Low un pin que le indica la dirección al motor por medio del controlador, en este ejemplo no se utiliza PWM para no complicar demasiado el robot, pues necesitaríamos además instalar fotosensores para comprobar la velocidad de las ruedas. Por ahora es bastante sencillo, activamos los motores en una dirección u otra según queramos ir hacia adelante o girar, es decir, no paramos nunca los motores. Para ir hacia adelante:

 public void MoveForward()
{
    _motorGpioPinA.Write(GpioPinValue.Low);
    _motorGpioPinB.Write(GpioPinValue.High);
}

En este ejemplo escribimos un valores contrarios al motor izquierdo y al derecho, pues tenemos la conexión cruzada. Cuando construyas tu robot puedes ajustar estos valores o bien cruzar una de las conexiones del motor al controlador.

Interfaz de usuario para el robot

Para mostrar el resultado en pantalla podemos crear un interfaz de usuario, pues la Raspberry Pi 2 tiene salida HDMI y la podemos aprovechar. No siempre vamos a tener acceso a un Visual Studio conectándose de forma remota al robot, así que hemos creado una interfaz de usuario muy sencilla para ver si los sensores están funcionando bien.

Al ser una aplicación universal, la forma de desarrollar la interfaz de usuario es la misma que en cualquier otra aplicación universal, sea para tableta, escritorio, teléfono u otro dispositivo. Creamos una interfaz con una imagen que nos indica en qué dirección está rodando el Rover y un indicador de la distancia a la que se encuentra el siguiente obstáculo. También tenemos una pequeña ventana de log para ver si ha ocurrido alguna excepción o error.

image

Qué necesito

En los proyectos de IoT y de Makers la lista de la compra va a ser un poco larga, pero no tienen por qué ser especialmente caro, de hecho espero que Yolanda nos cuente un día cómo hizo su propio robot recortando unas piezas de cartón y reaprovechando algunos materiales que podemos encontrar en casa. Como yo no tengo tanta maña ni imaginación, me fui de compras:

  • Piezas del robot: tenéis una lista de la compra en la página del Rover, aunque como van a una tienda online de USA, también podéis encontrar las piezas en tiendas de electrónica en España como por ejemplo en Conectrol.
  • Materiales adicionales: un juego de destornilladores, alicates y pinzas os irá bien para montar todo el proyecto, además de unas gomas elásticas para aguantar temporalmente los elementos como la Raspberry Pi 2 y el controlador de los motores. También es recomendable tener la Rpi2 protegida dentro de una caja.
  • Código fuente: el código original está en GitHub, os recomiendo hacer un fork y colaborar con el proyecto.
  • Software: tendremos que preparar nuestro PC con la última versión de Visual Studio 2015 Update 1 (la versión Community Edition es gratuita) e instalar algunas cosas más para poder grabar una tarjeta SD con Windows 10 IoT Core. Tienes unas instrucciones completas en https://ms-iot.github.io/content/GetStarted.htm

Agradecimientos

Aunque este artículo lo haya escrito yo y haya hecho la demo públicamente, la construcción del robot no habría sido posible sin los consejos de mis compañeros Yolanda Azcunaga que me dio muchos consejos prácticos, Jorge López que se recorrió todas las tiendas de electrónica de Madrid para conseguir las piezas a tiempo, Beatriz Roces que estuvo ensamblando las piezas y el circuito y todo el resto del equipo que nos apoyó de una manera u otra, en definitiva, ¡un verdadero trabajo en equipo!

Ahora te toca a ti.

Juan Manuel Servera

@jmservera

Developer eXperience