hardware 2018 · un par de fines de semana · ~10 €

Head tracker casero con ESP8266: alternativa al TrackIR por menos de 10€

Un TrackIR cuesta entre 130 y 180€. Este head tracker casero con ESP8266 cuesta menos de 10€, funciona via WiFi, y lleva giróscopo, acelerómetro y magnetómetro de 9 ejes. Se conecta a OpenTrack, que es el software estándar para head tracking en simuladores, y tiene hasta caja impresa en 3D para montarlo encima de unos cascos. Lo hice para jugar al IL-2 Sturmovik. Lo llamo Pakutracker.

Head tracker casero con ESP8266 montado encima de unos auriculares gaming
El Pakutracker montado encima de unos cascos. La caja impresa en 3D va sujeta a la diadema con un clip impreso también.

01 — ¿Qué es un head tracker y para qué sirve?

Un head tracker es un dispositivo que detecta la orientación de tu cabeza en tiempo real y la traduce en movimiento de cámara dentro del simulador. Cuando giras la cabeza a la derecha para mirar el motor, la cámara del avión gira a la derecha. Cuando inclinas la cabeza hacia abajo para mirar el tablero, la cámara baja. Es una de esas cosas que, una vez que lo pruebas, no puedes volver a jugar sin él.

En un simulador de vuelo como IL-2 Sturmovik, el head tracking cambia completamente la experiencia. Pierdes de vista a un bandido por el ángulo, giras la cabeza y ahí está. Estás buscando la pista de aterrizaje, ladeas la cabeza y la encuentras. En combate, es la diferencia entre ver lo que pasa y adivinar lo que pasa.

El producto comercial de referencia es el TrackIR de NaturalPoint, que funciona muy bien y lleva décadas en el mercado. El problema es que cuesta entre 130 y 180€, y hay que tener también un clip con LEDs IR para la gorra o los cascos. Precio respetable para lo que es.

La alternativa es hacer un head tracker casero con ESP8266 y un módulo IMU de 9 ejes. El IMU (Inertial Measurement Unit) lleva acelerómetro, giróscopo y magnetómetro integrados. El ESP8266 lee esos datos, calcula la orientación de la cabeza mediante fusión de sensores, y manda los ángulos por UDP a OpenTrack, que es el software open source que hace de interfaz con el simulador. Sin LEDs IR, sin cámara, sin nada. Solo WiFi.

02 — Lo que necesitas para hacer un head tracker casero con ESP8266

La lista de la compra es cortísima. Todo lo que necesitas está en el sidebar. Lo interesante es que hay dos variantes de IMU que funcionan perfectamente con el mismo código:

Opción A — GY-91 (MPU-9250): Un solo chip con acelerómetro, giróscopo y magnetómetro de 9 ejes. Es el que uso habitualmente. Los clones chinos rondan los 3-4€ en AliExpress. Cuidado: hay muchos módulos GY-91 falsos con un MPU-6500 dentro que no tiene magnetómetro real. Si no te aparece el magnetómetro en el monitor serial, es ese.

Opción B — GY-86 (MPU-6050 + HMC5883L): Dos chips en la misma placa: el MPU-6050 hace de acelerómetro y giróscopo, y el HMC5883L es el magnetómetro. Más antiguo pero más fiable que los GY-91 baratos. Alrededor de 3€.

Ambas opciones se conectan igual (I2C) y funcionan con el mismo código. La diferencia la configuras en RTIMUSettings.h, que es donde le dices a la librería qué IMU tiene conectada. Si no tienes magnetómetro (o el tuyo es falso), el tracker sigue funcionando en modo 6DOF: yaw/pitch/roll con giróscopo y acelerómetro, sin corrección de deriva en el yaw. Funciona, pero el yaw se va derivando lentamente. Con el magnetómetro, se autocorrige usando el campo magnético de la Tierra.

Para los cables, cuatro o cinco dupont hembra-hembra son suficientes. La caja en 3D está en Thingiverse y entra bien encima de unos cascos con diadema.

03 — Esquema de conexiones del head tracker casero ESP8266

Tanto el GY-91 como el GY-86 se conectan por I2C, que son dos hilos de señal (SDA y SCL) más alimentación. El Wemos D1 Mini tiene el I2C en los pines D1 (SCL) y D2 (SDA). El pin de interrupción del IMU va al D7 (GPIO13).

Opción A — Conexiones con GY-91 (MPU-9250):

Head tracker casero ESP8266 — conexiones Wemos D1 Mini con GY-91 (MPU-9250) HEAD TRACKER CASERO ESP8266 — GY-91 (MPU-9250) WEMOS D1 MINI ESP8266 3V3 GND D1 / SCL D2 / SDA D7 / INT GY-91 MPU-9250 VCC GND SCL SDA INT 3.3V GND SCL (I2C) SDA (I2C) INT
Conexiones del head tracker casero ESP8266 con GY-91 (MPU-9250). Alimentación a 3.3V — no conectes a 5V o te cepillas el IMU.

Opción B — Conexiones con GY-86 (MPU-6050 + HMC5883L):

Head tracker casero ESP8266 — conexiones Wemos D1 Mini con GY-86 (MPU-6050 + HMC5883L) HEAD TRACKER CASERO ESP8266 — GY-86 (MPU-6050 + HMC5883L) WEMOS D1 MINI ESP8266 3V3 GND D1 / SCL D2 / SDA D7 / INT GY-86 MPU-6050 + HMC5883L VCC_IN GND SCL SDA INT 3.3V GND SCL (I2C) SDA (I2C) INT
Conexiones con GY-86. El pin se llama VCC_IN en algunas versiones de la placa. Las conexiones son idénticas al GY-91.
Interior del head tracker casero ESP8266: Wemos D1 Mini y módulo IMU conectados en la caja abierta
Interior real del Pakutracker. El Wemos D1 Mini arriba, el módulo IMU abajo a la izquierda, y los cables dupont haciendo el I2C.

Tabla rápida por si prefieres tenerlo escrito:

Wemos D1 Mini    →    IMU (GY-91 o GY-86)
─────────────────────────────────────────
3V3              →    VCC (3.3V — NO 5V)
GND              →    GND
D1 (GPIO5)       →    SCL
D2 (GPIO4)       →    SDA
D7 (GPIO13)      →    INT

04 — El código

El firmware hace tres cosas: se conecta al WiFi, lee el IMU en un bucle, y manda los ángulos a OpenTrack por UDP. El paquete UDP son 48 bytes exactos: 6 valores double (8 bytes cada uno) en el orden que espera OpenTrack — X, Y, Z, yaw, pitch, roll. OpenTrack lo llama protocolo "UDP over Network" y lo escucha en el puerto 4242 por defecto.

Lo más interesante es la fusión de sensores. El giróscopo da orientación muy precisa a corto plazo pero deriva con el tiempo. El acelerómetro no deriva pero es ruidoso y solo es fiable cuando la placa no está acelerando (es decir, cuando está quieta). El magnetómetro da la referencia absoluta del norte para el yaw. La librería RTIMULib fusiona los tres usando un filtro RTQF (Real-Time Quaternion Fusion) y el parámetro setSlerpPower(0.04) controla cuánto peso le das al acelerómetro/magnetómetro frente al giróscopo — 0 = todo giróscopo, 1 = todo acelerómetro. En torno a 0.02-0.04 va muy bien.

Antes de compilar, abre RTIMUSettings.h y busca la línea que define qué IMU tienes. Por defecto suele venir con MPU-9250 activado. Si usas GY-86 (MPU-6050 + HMC5883L), cambia la define correspondiente.

Las tres constantes que tienes que editar en el sketch:

const char* ssid      = "TU_RED_WIFI";
const char* password  = "TU_PASSWORD_WIFI";
const char* servidorUDP = "192.168.X.X";  // IP de tu PC con OpenTrack

El código completo:

#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
#include "ESP8266WiFi.h"
#include <WiFiUdp.h>
#include "Wire.h"
#include "I2Cdev.h"
#include "RTIMUSettings.h"   // Configura aquí el IMU
#include "RTIMU.h"
#include "RTFusionRTQF.h"
#include "CalLib.h"
#include <EEPROM.h>

const char* ssid        = "TU_RED_WIFI";
const char* password    = "TU_PASSWORD_WIFI";
const char* servidorUDP = "192.168.X.X";   // ← IP de tu PC con OpenTrack
int intPin = 7;                            // D7 (GPIO13) en Wemos D1 Mini
const int localPort = 4242;               // puerto UDP de OpenTrack

WiFiUDP UDP;

struct stPosicion {
  double x;
  double y;
  double z;
};
stPosicion posicion;

RTIMU *imu;
RTFusionRTQF fusion;
RTIMUSettings settings;
#define   DISPLAY_INTERVAL  50
#define   SERIAL_PORT_SPEED 115200
unsigned long lastDisplay;
unsigned long lastRate;
int sampleCount;

void setup()
{
  Serial.begin(SERIAL_PORT_SPEED);
  delay(100);
  Serial.println("Pakutracker v.1.1");
  Serial.println("------------------");
  delay(100);
  int errcode;

  Wire.begin();   // I2C: SDA=D2 (GPIO4), SCL=D1 (GPIO5)
  delay(100);
  pinMode(intPin, INPUT);
  digitalWrite(intPin, LOW);
  delay(100);

  Serial.print("Conectando a ");
  Serial.println(ssid);
  delay(100);
  IPAddress none(0, 0, 0, 0);
  WiFi.disconnect();
  delay(100);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi conectada");

  UDP.begin(localPort);
  Serial.print("IP local: ");
  Serial.println(WiFi.localIP());

  // Creamos y arrancamos el IMU según la config de RTIMUSettings.h
  imu = RTIMU::createIMU(&settings);
  Serial.print("Iniciando IMU: ");
  Serial.println(imu->IMUName());
  if ((errcode = imu->IMUInit()) < 0) {
    Serial.print("Error al iniciar el IMU: ");
    Serial.println(errcode);
  }

  if (imu->getCalibrationValid())
    Serial.println("Usando magnetómetro calibrado");
  else
    Serial.println("Sin calibración de magnetómetro");

  lastDisplay = lastRate = millis();
  sampleCount = 0;

  // Parámetros de fusión de sensores
  // setSlerpPower: 0 = todo giróscopo (deriva), 1 = todo acelerómetro (ruido)
  // 0.02-0.04 es el punto dulce
  fusion.setSlerpPower(0.04);
  fusion.setGyroEnable(true);
  fusion.setAccelEnable(true);
  fusion.setCompassEnable(true);
}

void loop()
{
  unsigned long millisActuales = millis();
  int loopCount = 1;

  while (imu->IMURead()) {
    if (++loopCount >= 10) continue;
    fusion.newIMUData(
      imu->getGyro(),
      imu->getAccel(),
      imu->getCompass(),
      imu->getTimestamp()
    );
    sampleCount++;

    if ((millisActuales - lastRate) >= 1000) {
      sampleCount = 0;
      lastRate = millisActuales;
    }

    if ((millisActuales - lastDisplay) >= DISPLAY_INTERVAL) {
      lastDisplay = millisActuales;

      RTVector3 pose = fusion.getFusionPose();
      Serial.print("roll:");  Serial.print(pose.x() * RTMATH_RAD_TO_DEGREE);
      Serial.print(" pitch:"); Serial.print(pose.y() * RTMATH_RAD_TO_DEGREE);
      Serial.print(" yaw:");  Serial.println(pose.z() * RTMATH_RAD_TO_DEGREE);

      // Posición estimada por integración del acelerómetro (deriva mucho)
      RTVector3 acc = imu->getAccel();
      posicion.x += acc.x();
      posicion.y += acc.y();
      posicion.z += acc.z();

      // Paquete para OpenTrack: 6 doubles = 48 bytes exactos
      // Orden: x, y, z, yaw, pitch, roll
      struct paqueteOpenTrack {
        double x = 0, y = 0, z = 0;
        double yaw = 0, pitch = 0, roll = 0;
      };
      paqueteOpenTrack envio;

      envio.x     = posicion.x;
      envio.y     = posicion.y;
      envio.z     = posicion.z;
      envio.yaw   = pose.z() * RTMATH_RAD_TO_DEGREE;
      envio.pitch = pose.x() * RTMATH_RAD_TO_DEGREE;
      envio.roll  = pose.y() * RTMATH_RAD_TO_DEGREE;

      UDP.beginPacket(servidorUDP, localPort);
      UDP.write((byte *)&envio, sizeof envio);
      UDP.endPacket();
    }
  }
}

Una nota sobre la posición X/Y/Z: el código la calcula acumulando la aceleración del IMU a lo largo del tiempo. En teoría, integrando dos veces la aceleración obtendrías posición. En la práctica, el error se acumula tan rápido que en cuestión de segundos el valor es basura. Para head tracking en simuladores, lo que importa son los ángulos (yaw, pitch, roll), no la posición lineal. La posición la manda el código porque el protocolo de OpenTrack la espera, pero en los simuladores generalmente no se usa.

05 — Configurar OpenTrack para el head tracker casero con ESP8266

OpenTrack es gratuito y open source. Lo descargas desde su repositorio de GitHub. Una vez instalado:

OpenTrack configurado para head tracker casero con ESP8266: ventana UDP, filtros y curvas de mapping
Todo lo que necesitas configurar en OpenTrack: ventana principal con UDP over network seleccionado, el diálogo de UDP con el puerto 4242, la configuración de freetrack para IL-2, los filtros de suavizado, y las curvas de mapping para yaw, pitch y roll.
  1. Input: Selecciona UDP over network. Puerto 4242. No hace falta cambiar nada más.
  2. Output: Para IL-2 Sturmovik usa freetrack 2.0 Enhanced (lo ves en la captura). Para DCS, Elite Dangerous, etc., hay plugins específicos en el desplegable de Output.
  3. Arranca el Wemos primero, espera a que el serial diga "WiFi conectada" y luego dale a Start en OpenTrack. Si el tracker está mandando datos, verás moverse la calavera en la ventana principal.

El ajuste más importante es la curva de sensibilidad del yaw. Con el Pakutracker, el rango real del IMU es de ±180 grados, pero en el simulador probablemente no quieras un mapeo 1:1 — a medida que giras más, que la cámara vaya más rápido (curva no lineal). OpenTrack tiene un editor de curvas muy cómodo para esto.

Si el yaw deriva lentamente hacia un lado, hay dos causas: el magnetómetro no está calibrado, o hay campos magnéticos cerca (el ordenador, cables con corriente). Calibrar el magnetómetro es importante: la librería RTIMULib tiene soporte para guardarlo en EEPROM.

06 — La caja impresa en 3D

Caja impresa en 3D del head tracker casero con ESP8266: render de las tres piezas
Las tres piezas del Pakutracker: la caja principal, la tapa con el nombre grabado y la tapa trasera. PLA estándar, capa 0.2mm.
Base de la caja del head tracker casero ESP8266 vista en el slicer 3D
La base vista en el software de modelado. El hueco de la derecha es para que el USB del Wemos quede accesible sin abrir la caja.

La caja está diseñada para ir encima de la diadema de unos auriculares, con el IMU apuntando hacia arriba para que los ejes sean los naturales (yaw = izquierda/derecha, pitch = arriba/abajo, roll = ladearse). Los ficheros STL y el modelo original están disponibles en Thingiverse:

thingiverse.com/thing:3262096

Cualquier impresora con 0.2mm de capa y PLA estándar funciona perfectamente. No es una pieza que aguante esfuerzos mecánicos — es básicamente un sujetacables con tapa.

07 — Cómo hacer un head tracker casero con ESP8266: paso a paso

Todo lo que necesitas desde cero hasta tener la cabeza trackeada:

  1. Imprime la caja. Descarga los STL de Thingiverse y mándalo a imprimir. Si no tienes impresora 3D, cualquier servicio de impresión online lo hace por un par de euros.
  2. Compra los componentes. Wemos D1 Mini (~3€), GY-91 o GY-86 (~3-4€), 5 cables dupont hembra-hembra (~1€). Todo en AliExpress o Amazon. Ojo con los GY-91 falsos — si en el serial no aparece el magnetómetro, te han colado un MPU-6500.
  3. Instala Arduino IDE y añade el soporte para ESP8266: en Preferences, añade la URL http://arduino.esp8266.com/stable/package_esp8266com_index.json al gestor de placas. Luego en Boards Manager instala "esp8266 by ESP8266 Community".
  4. Descarga RTIMULib. Busca "RTIMULib" en el gestor de librerías de Arduino IDE. Instala la versión de Richards-Tech.
  5. Configura RTIMUSettings.h para tu IMU. Por defecto viene con el MPU-9250 activo. Si usas GY-86 (MPU-6050), descomenta la define del MPU-6050 y comenta la del 9250.
  6. Edita las tres constantes al principio del sketch: tu SSID WiFi, tu contraseña, y la IP del PC donde corre OpenTrack (la IP local, la que ves con ipconfig en Windows — algo del tipo 192.168.1.X).
  7. Flashea el Wemos. Conecta el Wemos por USB, selecciona "LOLIN(WEMOS) D1 R2 & mini" como placa, selecciona el puerto COM correcto, y dale a Upload. Abre el monitor serial a 115200 baudios para ver que conecta y que el IMU arranca.
  8. Instala OpenTrack y configura Input: UDP over network, puerto 4242. Output: el plugin de tu simulador.
  9. Arranca en orden: primero el Wemos (espera a "WiFi conectada" en el serial), luego Start en OpenTrack. Mueve la cabeza y comprueba que la calavera se mueve.
  10. Calibra el magnetómetro para que el yaw no derive. RTIMULib tiene instrucciones en su repositorio — básicamente giras el IMU en todas las direcciones durante unos segundos mientras el sketch de calibración está corriendo, y lo guarda en EEPROM.
  11. Ajusta las curvas en OpenTrack hasta que se sienta natural. Cada uno tiene sus preferencias — empieza con curvas suaves y ve subiendo la sensibilidad del yaw poco a poco.
  12. Mete todo en la caja, engánchala a la diadema de los cascos, y a volar.

En cuanto al comportamiento en vuelo: el pitch y el roll van muy bien desde el principio. El yaw es el más delicado porque depende del magnetómetro, que a su vez depende de cuánto hierro y cuántos campos eléctricos tenga cerca. Si juegas en un escritorio con el PC al lado, es normal que haya algo de deriva en el yaw. Con la calibración bien hecha y setSlerpPower en torno a 0.02-0.04, es perfectamente usable.

08 — Enlaces y recursos

Software:

Librerías Arduino:

Hardware y modelos 3D: