Acelerómetro+en+Android

toc =Acelerómetro=

Nociones básicas
El acelerómetro es uno de los sensores más habituales en los dispositivos con Android. Nos permite la lectura de la aceleración (en m/s2) que está sufriendo el dispositivo, repartida en tres ejes que forman un sistema de coordenadas definido respecto del dispositivo. Por tanto, cuando se cambia la orientación del dispositivo los ejes no cambian. Lo normal es que el acelerómetro venga instalado de tal forma que el sistema de coordenadas sea igual al de la figura 1, no obstante, el fabricante no está obligado a respetar esta condición.



En la actualidad, los acelerómetros son dispositivos precisos pero no exactos, esto significa que si se deja el dispositivo sobre una superficie estable y se leen sus valores, veremos que oscilan dentro de un intervalo pequeño. Además, les afecta la gravedad, así que siempre habrá una aceleración de 9,8m/s2 en alguna dirección. La utilización del acelerómetro, al igual que cualquier otro sensor en Android, funciona por eventos. En primer lugar debemos realizar un registro de un Listener para el sensor que queremos utilizar (en nuestro caso el acelerómetro) y a continuación sobrecargar un método que es llamado cada vez que se detecta una lectura en el sensor para definir las acciones deseadas.

Estructuras de datos
Para poder hacer uso del acelerómetro en primer lugar es necesario hacer una llamada al servicio del sensor a través de SensorManager solicitando acceso al acelerómetro. Las dos variables usadas, mySM y myAccel, son de tipo SensorManager y List respectivamente: code format="java" mySM   = (SensorManager) getSystemService(SENSOR_SERVICE); myAccel = mySM.getSensorList(Sensor.TYPE_ACCELEROMETER);

code Es importante que en cada momento se utilicen únicamente aquellos recursos que sean necesarios. Por tanto, a la hora de reanudar (o iniciar por primera vez) es necesario iniciar el Listener: code format="java" mySM.registerListener(this, myAccel.get(0), SensorManager.SENSOR_DELAY_GAME); code Y antes de pausar la aplicación es recomendable detenerlo con: code format="java" mySM.unregisterListener(this); code El tercer parámetro del método registerListener ( SENSOR_DELAY_NORMAL en este caso) controla la frecuencia máxima a la que recibimos información del sensor controlando, de este modo, el compromiso entre consumo de recursos y la rapidez de respuesta. Los posibles valores son: Para la captura de los eventos del sensor sobrecargamos el método: code format="java" public void onAccuracyChanged(Sensor sensor, int accuracy) {} code El acceso a los valores leídos del acelerómetro se realizar accediendo al atributo de event: code format="java" event.values[i] code Con el índice 0 para el eje X, 1 para el eje Y y 2 para el eje Z. Por último, es habitual el uso del atributo event.timestamp para tener la opción de descartar un evento si ha trascurrido menos tiempo del deseado desde el anterior. Es otra manera de controlar la frecuencia de funcionamiento del sensor y puede usarse para reducir el consumo de recursos de una aplicación.
 * SENSOR_DELAY_FASTEST: máxima velocidad
 * SENSOR_DELAY_GAME: velocidad óptima para videojuegos
 * SENSOR_DELAY_NORMAL: velocidad óptima para el resto de aplicaciones
 * SENSOR_DELAY_UI: mínima velocidad

=Ejemplos de uso=

Cuestiones Previas
A continuación vamos a describir el funcionamiento de varias aplicaciones sencillas. Estas aplicaciones han sido desarrolladas y probadas para la versión 15 del SDK (Android 4.0.3). No obstante deberían funcionar sin problemas en cualquier dispositivo Android 4.0 o superior. En la sección de capturas de pantalla, se puede ver cómo son las interfaces de las aplicaciones aquí descritas. Además, en la sección de referencias están disponibles los enlaces para descargar los códigos fuente completos.

Comenzamos describiendo cómo se implementa una aplicación que se limita a leer la información del acelerómetro y mostrarla en pantalla. El primer paso consiste en crear una clase que implemente SensorEventListener y añadirle dos atributos relativos a los sensores: code format="java" public class Main extends Activity implements SensorEventListener { private SensorManager sm; private Sensor accel; […] code El siguiente paso consiste en definir los métodos del ciclo de vida de la aplicación. Este paso es de gran importancia, porque se ha de controlar qué pasa con el sensor cuando la aplicación es iniciada, minimizada o cerrada. Hay que tener presente que, una vez iniciada la escucha al sensor, esta permanecerá activa preguntando frecuentemente al acelerómetro aunque se haya minimizado la aplicación, consumiendo tanto batería como memoria. Por ello, si no se necesita este procesamiento de fondo, es recomendable cerrar la conexión. Cuando se arranca una aplicación Android, se ejecutan por este orden los siguiente métodos: onCreate, onStart y onResume; si la aplicación es pausada se ejecutará onPause y si después se detiene, se ejecutará onStop; desde ambos estados siempre se puede volver a ejecutando, y en tal caso, se ejecutará onResume. //Más información en: Ciclo de vida en Android//

Por ello, como mínimo, se han de definir los métodos onCreate, onResume y onPause que, aparte de realizar las tareas que requiramos, llamarán a su superclase. La configuración más básica es la siguiente: code format="java" protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //Nombre de tu layout XML sm = (SensorManager) getSystemService(SENSOR_SERVICE); accel = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); }
 * onCreate**: Aquí se indicará el tipo de sensor que se va a escuchar. En este caso en vez de usar getSensorList, se ha optado por usar getDefaultSensor. La diferencia es que el segundo devuelve el sensor por defecto para el tipo solicitado mientras que el primer método devuelve una lista con todos los sensores del tipo indicado.

code code format="java" protected void onResume{ super.onResume; sm.registerListener(this, accel, SensorManager.SENSOR_DELAY_GAME); } code **onPause**: En este método se desconectará el Listener en caso de no necesitarlo mientras la aplicación no esté en primer plano. code format="java" protected void onPause{ sm.unregisterListener(this); super.onPause; } code Además, por implementar SensorEventListener, necesitamos definir dos métodos más: code format="java" public void onAccuracyChanged(Sensor sensor, int accuracy) {} code code format="java" public void onSensorChanged(SensorEvent event) { ((TextView) findViewById(R.id.xTextField)).setText(event.values[0]); ((TextView) findViewById(R.id.yTextField)).setText(event.values[1]); ((TextView) findViewById(R.id.zTextField)).setText(event.values[2]); }
 * onResume**: Donde se registrará el Listener para empezar a recibir los datos del acelerómetro inmediatamente.
 * onAccuracyChanged**: Método que se ejecutará cuando se modifique la precisión del sensor. En principio, no necesitamos realizar ninguna acción.
 * onSensorChanged**: Se ejecuta cuando se detectan cambios en el sensor, aquí es donde se incluirá la lógica que realizará el tratamiento de la información recibida del sensor.

code

Lectura de los valores del acelerómetro
En la siguiente aplicación aplicaremos lo aprendido en el apartado anterior para deducir la posición del dispositivo y para detectar cuando se está agitando el dispositivo. La interfaz es muy sencilla, con campos de texto para mostrar información y una barra de progreso que se crece cada vez que se detecta una agitación. Respecto a los métodos, principalmente se han modificado dos. En **onCreate** se han añadido la inicialización de los atributos. code format="java" sonido = MediaPlayer.create(this, R.raw.touch); cont  = 0; mAccel = 0; last_update = 0; curX = curY = curZ = 0; mAccelLast    = SensorManager.GRAVITY_EARTH; mAccelCurrent = SensorManager.GRAVITY_EARTH; history_size  = 50; //Tamaño del vector umbral_shake  = 7;  //Valor de gravedad para considerar una agitación umbral_cont   = 4;  //nº de agitaciones necesarias para que sea un shake umbral_pos    = 7f; //Valor necesario para detectar que el disp está en una posición

//Ponemos el vector a cero for(int i=0; i<history_size; i++) mov_history.add(0f);

code En **onSensorChanged** lo que hace, a grandes rasgos, es tomar los valores del sensor, calcular la aceleración global y almacenarla en el vector que funciona a modo de buffer con history_size posiciones de memoria. A continuación, se recorre el buffer buscando el número de veces que la aceleración ha superado un umbral y se contabiliza. Como lo que queremos detectar es agitación, contaremos las veces que es superado el umbral de forma positiva y negativa. Finalmente si se superan los dos umbrales (positivo y negativo) el número de veces indicado, se considera detectado un shake, se emite un sonido y la barra de progreso crece. Por otra parte, se aprovecha que se conocen los valores del acelerómetro para detectar de forma simple en qué posición está el dispositivo. code format="java" public void onSensorChanged(SensorEvent event) { synchronized (this) { TextView textPantalla = ((TextView) findViewById(R.id.texto)); //Monitoreamos la frecuencia de muestreo del acelerometro current_time   = event.timestamp; time_difference = current_time - last_update; if(time_difference>10E8 && cont!=0){ last_update = current_time; System.out.println("Frequency: "+cont+"Hz"); cont=0; }       cont++;

//Obtenemos las lecturas de la aceleracion separada por ejes curX = event.values[0]; curY = event.values[1]; curZ = event.values[2]; //Comparamos la lectura de aceleracion anterior con la actual para detectar //movimiento de aceleracion/desaceleracion. Como aceleracion se obtiene una //medida global que es la raíz cuadrada de la suma de los cuadrados de la       //aceleracion en cada eje mAccelLast   = mAccelCurrent; mAccelCurrent = (float) Math.sqrt((double) (curX*curX + curY*curY + curZ*curZ)); delta        = mAccelCurrent - mAccelLast; mAccel       = mAccel * 0.9f + delta; mAccelLast   = mAccelCurrent; //Almacenamos en una cola las history_size ultimas lecturas para la deteccion //del gesto shake mov_history.add(mAccel); mov_history.remove(0);

//Contamos las lecturas mayores y menores que umbral_shake y -umbral_shake //en mov_history contp=contn=0; for(int i=0; iumbral_shake) contp++; if(mov_history.get(i)<-umbral_shake) contn++; }

//Si hemos contado los suficientes registros por encima del valor umbral de       //aceleracion en mov_history entonces hemos detectado un SHAKE if(contp>umbral_cont && contn>umbral_cont){ textPantalla.setText("Movimiento Shake Detectado"); sonido.start; System.out.println("SHAKE DETECTADO"); tmp=((ProgressBar) findViewById(R.id.progressBar1)).getProgress; ((ProgressBar) findViewById(R.id.progressBar1)).setProgress(tmp+5);

for (int i=0; i umbral_pos) textPantalla.setText("Posición: Horizontal inverso"); if(curY<-umbral_pos) textPantalla.setText("Posición: Vertical inverso"); if(curY> umbral_pos) textPantalla.setText("Posición: Vertical"); if(curZ<-umbral_pos) textPantalla.setText("Posición: Bocabajo"); if(curZ> umbral_pos) textPantalla.setText("Posición: Bocarriba");

//Lectura actual del acelerometro ((TextView) findViewById(R.id.txtAccX)).setText("Valor de X: " + curX); ((TextView) findViewById(R.id.txtAccY)).setText("Valor de Y: " + curY); ((TextView) findViewById(R.id.txtAccZ)).setText("Valor de Z: " + curZ); } }

code

Memorización y reconocimiento de gestos
A continuación vamos a explicar cómo reconocer un patrón gestual complejo, para ello, lo que haremos será crear una aplicación que, tras pulsar un botón, memorice los valores del acelerómetro hasta que el usuario decida parar. Tras esto, el usuario deberá pulsar otro botón que activará el sistema de reconocimiento. Cada vez que el usuario repita el gesto memorizado, será reconocido y se emitirá un sonido para indicarlo. Primero crearemos una interfaz básica con 4 botones y varios campos de texto para mostrar información. La función de los botones será: A continuación, crearemos la activity que implemente la interfaz SensorEventListener en el fichero java. Como esto ya se ha explicado antes, aquí pondremos los detalles a destacar. Declaramos los atributos de la clase. Para memorizar el gesto usaremos un ArrayList y para reconocer gestos usaremos una cola (ArrayDeque) a modo de buffer cuyo tamaño será el mismo que el ArrayList anterior. De esta forma siempre mantenemos en memoria las últimas X capturas realizadas al sensor. code format="java" //Memoria para el gesto private ArrayList> datos = new ArrayList>; //Buffer private Queue> buffer   = new ArrayDeque>; private int estado; //Controla el estado del programa: //(memorizando o detectando movimiento)
 * **Start**.- Inicia la memorización del gesto.
 * **Stop**.- Detiene la memorización del gesto.
 * **Iniciar/Detener Detector**.- Botón de dos estados, que iniciará el detector de gestos cuando esté pulsado y lo detendrá cuando no lo esté.
 * **Mostrar datos por consola**.- Botón que lanzará información de depurado por la consola de Eclipse.

code El registro del Listener del acelerómetro lo vamos a controlar con los botones definidos, de esta forma, permanece desconectado mientras no se esté grabando ningún gesto. Los métodos de los botones quedan de la siguiente forma: code format="java" //Botón start public void startMemorizarMovimiento(View v){ estado = 0; datos.clear; sm.registerListener(this, accel, SensorManager.SENSOR_DELAY_GAME); }

//Botón stop public void stopMemorizarMovimiento(View v){ sm.unregisterListener(this); }

//Botón Iniciar/detener detector public void reconocerMovimiento(View v){ //Iniciar reconocimiento if(((CompoundButton) v).isChecked && datos.size>0){ estado = 1; buffer.clear; sm.registerListener(this, accel, SensorManager.SENSOR_DELAY_GAME); }else{ //detener reconocimiento estado = 0; sm.unregisterListener(this); } }

code Para terminar con el apartado de eventos, modificaremos el método **onResume** puesto que ya no va a controlar el registro del Listener, y el método **onSensorChanged** para que realice las operaciones que deseamos. Este método hará una llamada sincronizada al método que corresponda según el botón pulsado (controlado por el atributo estado). El motivo de usar sincronización es para evitar posibles fallos en la lectura concurrente del sensor. code format="java" protected void onResume{ super.onResume; }

public void onSensorChanged(SensorEvent event) { synchronized (this) { //Si se ha pulsado memorizar movimiento... if(estado==0) memorizarMovimiento(event); //Si se ha pulsado detectar movimiento... else detectarMovimiento(event); } }

code Pasamos a describir los métodos encargados de guardar y detectar los gestos. El primero, **memorizarMovimiento**, se lanza cuando se está memorizando un gesto y su función consiste en almacenar los valores del acelerómetro. code format="java" private void memorizarMovimiento(SensorEvent event){ //Array para amacenar las tres coordenadas del acelerómetro ArrayList coords = new ArrayList(3);

//Redondeamos a 3 decimales para bajar sensibilidad coords.add((float) (Math.round(event.values[0]*1000.0)/1000.0)); coords.add((float) (Math.round(event.values[1]*1000.0)/1000.0)); coords.add((float) (Math.round(event.values[2]*1000.0)/1000.0)); //Actualizamos valores en pantalla ((TextView) findViewById(R.id.xCoordText)).setText(getString(R.string.coord_x_) + coords.get(0)); ((TextView) findViewById(R.id.yCoordText)).setText(getString(R.string.coord_y_) + coords.get(1)); ((TextView) findViewById(R.id.zCoordText)).setText(getString(R.string.coord_z_) + coords.get(2)); datos.add(coords); } code code format="java" private void detectarMovimiento(SensorEvent event){ //Array para amacenar las tres coordenadas del acelerómetro ArrayList coords = new ArrayList(3);
 * detectarMovimiento** se lanza cuando se está reconociendo un gesto e inserta en la cola los valores del acelerómetro. Hasta que no llena la cola, no trata de reconocer el gesto.

//Redondeamos a 3 decimales coords.add((float) (Math.round(event.values[0]*1000.0)/1000.0)); coords.add((float) (Math.round(event.values[1]*1000.0)/1000.0)); coords.add((float) (Math.round(event.values[2]*1000.0)/1000.0));

//Llenamos el buffer y lo mantenemos al mismo tamaño que el gesto memorizado if(buffer.size==datos.size){ buffer.poll; buffer.add(coords);

//Reconocemos el gesto según el método implementado reconocimientoDifCuadrados; }else buffer.add(coords); }

code code format="java" private void reconocimientoDifCuadrados{ float sumaX, sumaY, sumaZ; float umbral1=500, umbral2=1000; int i;   Iterator> coordsIt; ArrayList coordsBuffer = new ArrayList(3);
 * reconocimientoDifCuadrados** compara posición a posición el ArrayList que tiene el gesto memorizado (datos) con la cola actual (buffer). Para ello realiza la sumatoria de la diferencia de cuadrados por cada coordenada de forma independiente y si todas las sumas están por debajo de los umbrales establecidos, se acepta que el movimiento realizado es el mismo (o parecido) al memorizado y se reproduce un sonido. Hay que tener en cuenta que esta parte requiere algo de trabajo por parte del programador puesto que es necesario ajustar apropiadamente los valores de los umbrales para reducir en la medida de lo posible la detección de falsos positivos (gestos realizados por el usuario que no son iguales al memorizado pero que tienen el mismo valor tras realizar los cálculos y por tanto se detectan como gestos válidos).

coordsIt = buffer.iterator; i = 0; sumaX = sumaY = sumaZ = 0;

//Recorremos el buffer y calculamos las diferencias por coordenadas while(coordsIt.hasNext){ coordsBuffer = coordsIt.next; sumaX += Math.pow((datos.get(i).get(0)-coordsBuffer.get(0)), 2); sumaY += Math.pow((datos.get(i).get(1)-coordsBuffer.get(1)), 2); sumaZ += Math.pow((datos.get(i).get(2)-coordsBuffer.get(2)), 2); i++; }

//Si no se supera el umbral, se acepta el gesto realizado. //Reproducimos sonido y vaciamos el buffer if(sumaX<umbral1 && sumaY<umbral1 && sumaZ<umbral1 &&(sumaX+sumaY+sumaZ)<umbral2){ sonido.start; buffer.clear; }   //Actualizamos en pantalla los valores de las sumas ((TextView) findViewById(R.id.xDiffText)).setText(getString(R.string.diff_x_) + sumaX); ((TextView) findViewById(R.id.yDiffText)).setText(getString(R.string.diff_y_) + sumaY); ((TextView) findViewById(R.id.zDiffText)).setText(getString(R.string.diff_z_) + sumaZ); ((TextView) findViewById(R.id.sumDiffText)).setText(getString(R.string.suma_diff_) + (sumaX+sumaY+sumaZ)); }

code = = =Capturas de pantalla de las aplicaciones=





=Referencias=