Sensores del dispositivo: SensorManager, frecuencia y filtrado
El subsistema de sensores de Android en profundidad: sensores físicos, sintéticos y de despertar; el ciclo de registro y la reutilización del array de valores; qué significan de verdad las constantes de retardo y el agrupamiento por latencia máxima; y los filtros que separan la señal del ruido, del sesgo y de la deriva en acelerómetro, giroscopio, luz y proximidad.
La ubicación responde a dónde; los sensores responden a cómo. Un acelerómetro no mide movimiento, mide fuerza específica y siempre está midiendo la gravedad. Un giroscopio no mide orientación, mide velocidad angular y su integral se desvía sin remedio. Un sensor de proximidad rara vez mide distancia: emite un binario disfrazado de centímetros. Todo el trabajo serio con sensores consiste en saber qué magnitud física entrega cada uno, con qué ruido, con qué sesgo y a qué coste, y en construir encima el filtro que convierte esa señal cruda en la afirmación que el producto necesita hacer.
- Distinguir sensores físicos, sintéticos y de despertar, y elegir el adecuado para cada necesidad.
- Registrar y liberar oyentes con corrección, entendiendo la reutilización del array de valores.
- Traducir las constantes de retardo y la latencia máxima de informe en frecuencia y consumo reales.
- Aplicar filtros de paso bajo, paso alto y complementario para separar señal, sesgo y deriva.
La taxonomía que decide el diseño
El SensorManager expone un catálogo heterogéneo que conviene clasificar en tres ejes antes de escribir una línea.
El primer eje separa lo físico de lo sintético. El acelerómetro, el giroscopio, el magnetómetro, el sensor de luz y el de proximidad corresponden a piezas de silicio reales. Junto a ellos, la plataforma publica sensores sintéticos que no existen como hardware sino como fusión: la gravedad y la aceleración lineal descomponen la lectura del acelerómetro en su componente estática y su componente de movimiento; el vector de rotación combina acelerómetro, giroscopio y magnetómetro en una orientación absoluta estable; el paso y el conteo de pasos incorporan un clasificador entero. Usar el sintético cuando existe es casi siempre correcto, porque su fusión está implementada en el coprocesador y calibrada por el fabricante, con un resultado mejor y más barato que cualquier reimplementación en la aplicación.
El segundo eje es el modo de reporte. Los sensores continuos entregan a la frecuencia solicitada. Los de cambio, como la luz o la proximidad, entregan solo cuando el valor varía, lo que hace absurdo razonar sobre ellos en términos de frecuencia. Los de disparo único, como la detección de movimiento significativo, se desregistran solos al disparar.
El tercer eje, el más olvidado, es si el sensor despierta o no al procesador. Los sensores de despertar sacan al dispositivo del reposo profundo para entregar; los normales acumulan en el búfer del coprocesador y entregan cuando la CPU ya esté despierta por otra razón. Para el registro en segundo plano de una actividad prolongada, elegir el sensor sin despertar con un búfer amplio es la diferencia entre consumir microamperios y consumir miliamperios.
El objeto SensorEvent que llega al oyente y su array values pertenecen al sistema y se reutilizan en la siguiente entrega. Guardar una referencia a ese array —meterlo en una lista, pasarlo a otro hilo, retenerlo en un flujo— produce un fallo especialmente cruel: los datos históricos mutan solos y la corrupción aparece de forma no determinista bajo carga. La regla es copiar siempre lo que se vaya a conservar. Y para la marca temporal hay que usar el campo timestamp del evento, expresado en nanosegundos del reloj monótono, no la hora del sistema: es la única base fiable para calcular el intervalo entre muestras y para integrar correctamente.
Registro, frecuencia y agrupamiento
El registro pide un tipo, una frecuencia deseada y, opcionalmente, una latencia máxima de informe. Las constantes clásicas de retardo son solo atajos: SENSOR_DELAY_NORMAL ronda los cinco hercios y sirve para cambios de orientación de la interfaz, SENSOR_DELAY_UI los quince, SENSOR_DELAY_GAME los cincuenta y SENSOR_DELAY_FASTEST entrega tan rápido como el hardware permita, lo que en un terminal moderno puede superar los cuatrocientos hercios y saturar el hilo receptor con facilidad.
Todas ellas son peticiones, no garantías: la frecuencia real queda acotada por el hardware y por lo que el sistema conceda, y varias aplicaciones pidiendo el mismo sensor comparten la tasa más alta solicitada. Para control fino conviene expresar el periodo directamente en microsegundos.
private val gestor = getSystemService(SensorManager::class.java)
private val acelerometro = gestor.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
override fun onResume() {
super.onResume()
gestor.registerListener(
oyente,
acelerometro,
20_000, // periodo deseado en microsegundos, unos 50 Hz
5_000_000, // latencia maxima de informe: agrupa hasta 5 segundos
)
}
override fun onPause() {
super.onPause()
gestor.unregisterListener(oyente)
}
La latencia máxima de informe es al mundo de los sensores lo que el retardo máximo era al de la ubicación: la palanca energética de mayor retorno. Autoriza al coprocesador a acumular muestras en su búfer de hardware y entregarlas en bloque, de forma que quinientas muestras cuestan un despertar en lugar de quinientos. La capacidad del búfer es finita y consultable mediante fifoMaxEventCount, y cuando se desborda las muestras más antiguas se pierden, así que la latencia solicitada debe ser coherente con la frecuencia y con el tamaño real disponible.
El desregistro en onPause no es higiene opcional. Un oyente vivo a cincuenta hercios con la pantalla apagada impide el reposo del procesador y vacía la batería en horas, sin que ninguna herramienta lo señale como fuga.
Antes de registrar nada hay que aceptar que ningún sensor está garantizado. getDefaultSensor devuelve nulo cuando el tipo no existe en el terminal, y el giroscopio, el magnetómetro o el barómetro faltan con frecuencia en la gama de entrada. Cada sensor disponible declara además sus propias capacidades, que conviene inspeccionar en lugar de asumir: la resolución mínima distinguible, el rango máximo por encima del cual satura, el consumo declarado en miliamperios y el tamaño del búfer de hardware.
fun describir(sensor: Sensor?): String = when (sensor) {
null -> "no disponible en este dispositivo"
else -> buildString {
append("rango ").append(sensor.maximumRange)
append(", resolucion ").append(sensor.resolution)
append(", consumo ").append(sensor.power).append(" mA")
append(", fifo ").append(sensor.fifoMaxEventCount)
append(", periodo minimo ").append(sensor.minDelay).append(" us")
}
}
Ese minDelay es el límite físico real: pedir por debajo de él no produce error, simplemente no se cumple. Y un minDelay de cero identifica a un sensor de cambio, para el que hablar de frecuencia carece de sentido.
flowchart TD
A[sensor fisico] --> B[coprocesador de bajo consumo]
B --> C{latencia maxima de informe}
C -- cero --> D[entrega inmediata y despertares frecuentes]
C -- alta --> E[buffer FIFO y entrega en bloque]
E --> F[un solo despertar por lote]
D --> G[SensorEventListener]
F --> G
style E fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111bFiltrado: separar la señal del ruido, del sesgo y de la deriva
Ninguna lectura cruda de un sensor inercial es utilizable tal cual. Hay tres contaminaciones distintas y cada una exige un tratamiento propio.
El ruido de alta frecuencia se combate con un filtro de paso bajo, que en su forma exponencial es una línea de código y una constante de suavizado. Cuanto mayor el suavizado, más estable la salida y más retardo introduce, un compromiso que hay que elegir a conciencia según se prefiera reposo visual o respuesta rápida.
La componente estática es el problema inverso. El acelerómetro mide siempre la gravedad sumada al movimiento, de modo que un dispositivo en reposo sobre la mesa devuelve un módulo cercano a nueve coma ocho. Para detectar movimiento hay que restar esa componente, y eso es exactamente un filtro de paso alto: estimar la gravedad con un paso bajo agresivo y restarla de la lectura.
private val gravedad = FloatArray(3)
private val lineal = FloatArray(3)
private const val ALFA = 0.85f
override fun onSensorChanged(evento: SensorEvent) {
for (i in 0..2) {
gravedad[i] = ALFA * gravedad[i] + (1 - ALFA) * evento.values[i]
lineal[i] = evento.values[i] - gravedad[i]
}
val magnitud = sqrt(lineal[0].pow(2) + lineal[1].pow(2) + lineal[2].pow(2))
procesar(magnitud, evento.timestamp)
}
Hay un detalle de marcos de referencia que arruina implementaciones enteras y rara vez se documenta bien. Los ejes de los sensores están anclados al dispositivo en su orientación natural, que en una tableta es apaisada y en un teléfono vertical, y no rotan cuando gira la interfaz. Cualquier cálculo que traduzca sensores a la pantalla debe consultar la rotación actual de la ventana y remapear los ejes con remapCoordinateSystem antes de interpretar nada; omitirlo produce un código que funciona en vertical y confunde ejes en apaisado, un fallo que en pruebas manuales se atribuye casi siempre al filtro y casi nunca al marco de referencia.
La tercera contaminación, la deriva, es la más traicionera porque no se ve en una muestra sino en la acumulación. El giroscopio entrega velocidad angular con un sesgo pequeño pero sistemático; al integrar para obtener ángulo, ese sesgo se convierte en un error que crece de forma lineal con el tiempo, y en decenas de segundos la orientación estimada deja de tener relación con la real. La solución clásica es el filtro complementario: confiar en el giroscopio a corto plazo, donde es preciso y suave, y corregirlo continuamente con el acelerómetro y el magnetómetro, que son ruidosos pero no derivan porque están anclados a referencias físicas absolutas. En la práctica, para orientación conviene usar directamente el vector de rotación con getRotationMatrix y getOrientation, que ya implementa esa fusión con calidad de fabricante.
Acelerómetro
Fuerza específica en tres ejes, gravedad incluida. Ruidoso y sin deriva. Base de la detección de movimiento, golpes y pasos.
Giroscopio
Velocidad angular. Suave y preciso a corto plazo, con deriva acumulativa al integrar. Nunca usar solo para orientación.
Luz
Iluminancia en lux, sensor de cambio. Escala logarítmica y muy ruidosa: exige histéresis para no oscilar entre modos.
Proximidad
En la mayoría de terminales es binario cerca o lejos. Comparar contra maximumRange, jamás asumir centímetros útiles.
Cualquier decisión que compare una lectura contra un umbral fijo oscilará cuando el valor real se sitúe cerca de ese umbral, y en un sensor ruidoso eso ocurre constantemente. El sensor de luz es el caso arquetípico: una aplicación que cambia a tema oscuro por debajo de diez lux parpadeará sin descanso en una habitación con quince. La solución es doble: dos umbrales separados para entrar y para salir del estado, y una confirmación temporal que exija que la condición se sostenga durante un intervalo mínimo antes de actuar. La misma disciplina se aplica a la detección de agitado, a la de caída y a cualquier clasificador binario construido sobre una magnitud continua.
El error conceptual que arruina la mayoría de las integraciones de sensores es tratarlos como fuentes de verdad en lugar de como instrumentos de medida. Un instrumento tiene ruido, sesgo, deriva, saturación, resolución finita y una calibración que puede perderse; ignorar esas propiedades no las elimina, solo traslada su efecto a la capa de negocio, donde aparecen como errores inexplicables. El desarrollador experto razona en tres capas separadas y no las mezcla nunca. La primera es la adquisición, cuya única responsabilidad es obtener muestras con su marca temporal monótona correcta y a una frecuencia justificada. La segunda es el acondicionamiento, donde viven los filtros: paso bajo contra el ruido, paso alto contra la componente estática, fusión y complementario contra la deriva, histéresis contra la oscilación. Solo la tercera capa, alimentada por una señal ya limpia y con su incertidumbre acotada, tiene derecho a afirmar que el usuario ha dado un paso, ha girado el dispositivo o se lo ha acercado a la oreja. Cuando esas tres capas se colapsan en un solo oyente lleno de condicionales sobre valores crudos, el resultado es un código que funciona en el terminal del desarrollador y falla en la mitad del parque, porque cada fabricante calibra distinto, cada modelo tiene una frecuencia máxima distinta y cada búfer una capacidad distinta. Y hay una última asimetría que conviene tener presente: la frecuencia se pide, no se obtiene. Todo el diseño debe tolerar recibir la mitad de las muestras esperadas, o recibirlas en bloques de trescientas tras cinco segundos de silencio. Un algoritmo que asume cadencia uniforme es un algoritmo que aún no ha visto un dispositivo real en reposo profundo.
- Registra el acelerómetro a cincuenta hercios y grafica el módulo crudo, el filtrado por paso bajo y la aceleración lineal en la misma escala.
- Integra la velocidad angular del giroscopio durante dos minutos con el dispositivo inmóvil y mide cuántos grados de deriva acumula.
- Compara tu filtro complementario contra el vector de rotación del sistema en una rotación controlada de noventa grados.
- Mide con
fifoMaxEventCountel búfer real de tu terminal y comprueba qué latencia máxima de informe puedes pedir sin perder muestras. - Implementa un detector de agitado con doble umbral y confirmación temporal, y cuenta los falsos positivos frente a la versión con un solo umbral.