Android + Processing + Bluetooth

A finales de 2009 los creadores de Processing anunciaron que iban a dar soporte a la plataforma de Android. La verdad es que fue una noticia muy buena, ya que el famoso entorno de creación de gráficos animados iba a poder ejecutar sketchs dentro de móviles que tuviesen a Android como sistema operativo. Pero no se han quedado ahí y además admite la posibilidad de usar las características embebidas en los dispositivos móviles como el GPS, la brújula electrónica, enviar y recibir sms, etc y por supuesto bluetooth.

Lo que explicaré en este artículo son los pasos que he seguido para crear una conexión desde mi teléfono Android con un dispositivo bluetooth y poder enviarle información a la par que recibirla dibujando los resultados de una forma gráfica con Processing para Android.

Ya en su momento me dediqué a programar en symbian algo parecido:

Sin embargo finalmente lo deseché porque la plataforma de symbian tiene ya poco futuro y porque las APIS eran confusas, mal explicadas y no se puede acceder a todos los elementos de hardware con ellas.

Por otra parte no hay que compararlo con Amarino, ya que Amarino es una aplicación ya hecha para Android con una interfaz definida y está pensado para usarse con Arduino, sin embargo aquí vamos a programar nuestra propia aplicación con su propia interfaz para cualquier dispositivo que tenga un controlador bluetooth.

Dicho esto lo primero es necesario instalar todo el entorno de desarrollo, aquí viene bien explicado en castellano por lo que no considero necesario añadir más.

Antes de empezar, sería interesante que echarais un vistazo a dos páginas para entender mejor lo que viene a continuación. Por una parte la API de Android para bluetooth para comprender qué posibilidades nos ofrece. Por otra parte unos conceptos y un ejemplo de cómo usar Android y gestionar el hardware con Processing. A los que hayáis programado en Processing puede resultar extraño que además de poder usar las funciones básicas del entorno, se puedan incluir librerias y código de Java y la API Android. Esto es debido a que tanto Android como Processing tienen una fuerte vinculación con el lenguaje de programación Java y su unión ha creado una suerte de mescolanza que reúne lo mejor de ambos mundos.

El objetivo es que podamos crear una interfaz completa para que nos muestre los dispositivos bluetooth que hay alrededor, a continuación podamos elegir uno de ellos y finalmente conectarnos a este para enviar y recibir datos. En este caso en concreto voy a conectar mi móvil HTC Wildfire S con una placa Arduino a través de un módulo bluetooth. La placa Arduino enviará a mi móvil cada segundo un valor comprendido entre 0 y 39 y este lo mostrará en su pantalla. En el móvil habrá dibujado un botón que cuando se pulse enviará un 0 a la placa Arduino para encender o apagar el led del pin 13.

Este es el código fuente del sketch para la placa Arduino:

boolean estado;
 
void setup()
{
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW);
  randomSeed(analogRead(0));
  estado = false;
}
 
void loop()
{
    delay(1000);
    Serial.write(random(40));
    while(Serial.available() > 0)
    {
      Serial.read();
      estado = !estado;
      digitalWrite(13, estado);
    }
}

Ahora la parte de Android y Processing. A partir de aquí iré explicando los conceptos a medida que publico el código fuente para comentar los aspectos que considero que son más relevantes y finalmente pondré el código fuente integro.

Para poder programar el bluetooth de nuestro dispositivo basado en Android junto con Processing empezaremos por crear un sketch, daremos los permisos BLUETOOTH y BLUETOOTH_ADMIN en el menú Android/Sketch Permissions e importaremos las clases necesarias para que todo funcione:

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import java.util.ArrayList;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;

Hay algunas librerías que, como veremos más adelante, podemos prescindir de ellas dependiendo de lo que queremos hacer y lo bien o mal que nos funcione.

A continuación vamos a añadir los métodos típicos de un sketch de Processing: setup y draw:

void setup() {
  size(320,480);
  frameRate(25);
  f1 = createFont("Arial",20,true);
  f2 = createFont("Arial",15,true);
  stroke(255);
}
 
void draw() {
  switch(estado)
  {
    case 0:
      listaDispositivos("BUSCANDO DISPOSITIVOS", color(255, 0, 0));
      break;
    case 1:
      listaDispositivos("ELIJA DISPOSITIVO", color(0, 255, 0));
      break;
    case 2:
      conectaDispositivo();
      break;
    case 3:
      muestraDatos();
      break;
    case 4:
      muestraError();
      break;
  }
}

En el método setup forzamos a que la resolución de la pantalla sea de 320 píxeles de ancho x 480 píxeles de alto (HVGA), ya que es una configuración muy extendida dentro del mundo de los móviles. Establecemos que la frecuencia de refresco sea de 25 fps. Creamos dos tipos de fuentes con las que dibujar después los textos que queramos.

En el método draw vamos a simular varias ventanas dependiendo del estado en el que se encuentre nuestra aplicación en cada momento. Así nada más empezar se muestra la ventana de búsqueda de dispositivos, a continuación se ofrece al usuario que seleccione uno de los dispositivos mostrados:

Después se intenta conectar con el dispositivo elegido y finalmente si tiene éxito se muestra la ventana con los datos enviados por Arduino y con el botón que mencionaba antes:

Si algo falla se muestra el error en una ventana:

Un sketch de Proccesing para Arduino se trata como una Activity, por eso se puede inicializar todo en el evento OnStart cuando es llamado al arrancar la aplicación (OnCreate no es llamado).

void onStart()
{
  super.onStart();
  println("onStart");
  adaptador = BluetoothAdapter.getDefaultAdapter();
  if (adaptador != null)
  {
    if (!adaptador.isEnabled())
    {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    else
    {
      empieza();
    }
  }
}

En el evento OnStart primero debemos llamar al método onStart de la clase padre para inicializar correctamente. Después recuperamos el adaptador bluetooth del móvil y comprobamos si está activo. Si está activo empezamos a mostrar la lista de dispositivos. Si no lo está, lanzamos una petición para activarlo. En esta petición Android nos mostrará un mensaje similar a este en el móvil para activar el bluetooth:

Pulsemos la opción que pulsemos, se llamará al evento onActivityResult:

void onActivityResult (int requestCode, int resultCode, Intent data)
{
  println("onActivityResult");
  if(resultCode == RESULT_OK)
  {
    println("RESULT_OK");
    empieza();
  }
  else
  {
    println("RESULT_CANCELED");
    estado = 4;
    error = "No se ha activado el bluetooth";
  }
}

Si hemos pulsado Si entonces empezaremos a mostrar la lista de dispositivos, si hemos pulsado No iremos directamente a la pantalla de error.

Cuando se abandona la aplicación se llama al evento onStop (onDestroy no es llamado)  y aquí se pueden liberar los recursos que hayamos utilizado antes de terminar definitivamente.

void onStop()
{
  println("onStop");
  /*
  if(registrado)
  {
    unregisterReceiver(receptor);
  }
  */
 
  if(socket != null)
  {
    try
    {
      socket.close();
    }
    catch(IOException ex)
    {
      println(ex);
    }
  }
  super.onStop();
}

En el evento OnStop vamos a hacer dos cosas, aunque una de ellas está comentada por una razón que explicaré más adelante. Lo que se ve es que se cierra un socket que no es otra cosa que la conexión que hayamos establecido con nuestro dispositivo para poder liberar los recursos asociados a este. Finalmente se llama al método onStop de la clase padre para cerrar correctamente la aplicación.

El método empieza sirve para rellenar la lista de dispositivos bluetooth que más tarde mostraremos al usuario.

void empieza()
{
    dispositivos = new ArrayList();
    /*
    registerReceiver(receptor, new IntentFilter(BluetoothDevice.ACTION_FOUND));
    registerReceiver(receptor, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED));
    registerReceiver(receptor, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
    registrado = true;
    adaptador.startDiscovery();
    */
    for (BluetoothDevice dispositivo : adaptador.getBondedDevices())
    {
        dispositivos.add(dispositivo);
    }
    estado = 1;
}

Inicializamos un array que rellenaremos de objetos BluetoothDevice. Aquí comento una problemática que me he encontrado: Puedes incluir los dispositivos que hay alrededor del móvil o bien incluir los que previamente se hayan emparejado. La parte que está comentada (y por tanto la del método onStop que desregistra el Receiver) se usa para lanzar un proceso de búsqueda de dispositivos bluetooth que estén cerca de nuestro móvil: se registra el empiece, cada dispositivo que se encuentre y el final de la búsqueda. Para manejar estos eventos se usa una clase del tipo BroadcastReceiver ya inicializada:

BroadcastReceiver receptor = new BroadcastReceiver()
{
    public void onReceive(Context context, Intent intent)
    {
        println("onReceive");
        String accion = intent.getAction();
 
        if (BluetoothDevice.ACTION_FOUND.equals(accion))
        {
            BluetoothDevice dispositivo = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            println(dispositivo.getName() + " " + dispositivo.getAddress());
            dispositivos.add(dispositivo);
        }
        else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(accion))
        {
          estado = 0;
          println("Empieza búsqueda");
        }
        else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(accion))
        {
          estado = 1;
          println("Termina búsqueda");
        }
    }
};

Cuando se empieza la búsqueda se encuentra en el estado 0 para mostrar la lista de dispositivos a medida que van apareciendo. Por cada dispositivo bluetooth que se encuentre se añade al array de dispositivos bluetooth, Cuando termina la búsqueda se pasa al estado 1 para que el usuario seleccione el dispositivo al que quiere conectarse.

El hacerlo de esta forma provoca que más adelante cuando se conecte al dispositivo bluetooth se tenga que mostrar una ventana preguntando por un código de emparejamiento para vincular el móvil con este.El problema es que esa ventana no se visualiza cuando están los gráficos mostrándose y el proceso de conexión dará finalmente un error. Por eso la otra alternativa que he encontrado es emparejar el dispositivo bluetooth a mano con el móvil y rellenar la lista de dispositivos con los que ya están vinculados en el móvil, así no pedirá el código de emparejamiento y se conectará con el dispositivo bluetooth directamente.

En varias ocasiones hay que capturar donde pulsa el dedo del usuario para saber qué acción quiere realizar. Esto se consigue con el evento mouseReleased:

void mouseReleased()
{
  switch(estado)
  {
    case 0:
      /*
      if(registrado)
      {
        adaptador.cancelDiscovery();
      }
      */
      break;
    case 1:
      compruebaEleccion();
      break;
    case 3:
      compruebaBoton();
      break;
  }
}

Depende del estado en el que encontremos hará una acción diferente. Cuando estamos en el estado 0 y si se usara la búsqueda de dispositivos bluetooth (pero no es así por los motivos que he comentado) al pulsar en la pantalla cancelaría la búsqueda y lanzaría un ACTION_DISCOVERY_FINISHED al BroadCastReceiver visto anteriormente. Si estamos en el estado 1 se buscaría qué dispositivo bluetooth de la lista ha pulsado el usuario para conectarse a el. Si estamos en el estado 3 se comprueba si se ha pulsado el botón que envía un byte al dispositivo bluetooth.

El método que comprueba que elección ha hecho el usuario en el estado 1 es compruebaEleccion:

void compruebaEleccion()
{
  int elegido = (mouseY - 50) / 55;
  if(elegido < dispositivos.size())
  {
    dispositivo = (BluetoothDevice) dispositivos.get(elegido);
    println(dispositivo.getName());
    estado = 2;
  }
}

Con una sencilla fórmula matemática se puede sacar el dispositivo bluetooth que el usuario ha elegido en base a en qué parte de la pantalla ha pulsado. Después se pasa al estado 2 (Conexión al dispositivo bluetooth).

La lista de dispostivos bluetooth se hace con el método listaDispositivos:

void listaDispositivos(String texto, color c)
{
  background(0);
  textFont(f1);
  fill(c);
  text(texto,0, 20);
  if(dispositivos != null)
  {
    for(int indice = 0; indice < dispositivos.size(); indice++)
    {
      BluetoothDevice dispositivo = (BluetoothDevice) dispositivos.get(indice);
      fill(255,255,0);
      int posicion = 50 + (indice * 55);
      if(dispositivo.getName() != null)
      {
        text(dispositivo.getName(),0, posicion);
      }
      fill(180,180,255);
      text(dispositivo.getAddress(),0, posicion + 20);
      fill(255);
      line(0, posicion + 30, 319, posicion + 30);
    }
  }
}

Se recorre el array de dispositivos bluetooth mostrando su nombre y su dirección MAC. Es usada tanto en el estado 0 (listado de dispositivos bluetooth a medida que van apareciendo en la búsqueda) como en el estado 1 (selección del dispositivo bluetooth al que conectarse).

La conexión con el dispositivo bluetooth se hace con el método conectaDispositivo:

void conectaDispositivo()
{
  try
  {
    socket = dispositivo.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
    /*
    Method m = dispositivo.getClass().getMethod("createRfcommSocket", new Class[] { int.class });
    socket = (BluetoothSocket) m.invoke(dispositivo, 1);
    */
    socket.connect();
    ins = socket.getInputStream();
    ons = socket.getOutputStream();
    estado = 3;
  }
  catch(Exception ex)
  {
    estado = 4;
    error = ex.toString();
    println(error);
  }
}

Aquí ocurre un caso especial. Para conectar con el dispositivo bluetooth se debe crear un socket con el dispositivo y después hacer la conexión propiamente dicha con el método connect. Sin embargo dependiendo de con qué dispositivo se conecte puede dar problemas el método que se use para obtener ese socket. La versión normal es la que está en el metodo y se supone que es la estándar, pero a mi me dio problemas al conectar al blueooth de un PC y tuve que usar las dos líneas que aparecen comentadas . Sin embargo con el módulo bluetooth de Sure la línea que aparece actualmente ha funcionado sin problemas. Como estamos creando una conexión SPP (como si fuese un puerto serie virtual) a través del protocolo RFCOMM, debemos obtener el socket que conecta con el servicio 00001101-0000-1000-8000-00805F9B34FB, que es el identificador único de servicio para SPP en el estandar Bluetooth. El método connect es bloqueante por lo que dejará la aplicación paralizada hasta que se conecte realmente o surja un error. Google dice que se debe usar un thread para separar el hilo de ejecución principal con el de la conexión, pero creo que está solución es más sencilla. Una vez se ha conectado se pueden recuperar el stream de entrada y el de salida para recibir y enviar datos respectivamente con el dispositivo bluetooth. A continuación se pasa al estado 3 (Visualización de datos e interacción con el botón). Si hubiese algún error se pasaría al estado 4 mostrando qué lo ha provocado.

El método que muestra los datos enviados desde el dispositivo bluetooth es muestraDatos:

void muestraDatos()
{
  try
  {
    while(ins.available() > 0)
    {
      valor = (byte)ins.read();
    }
  }
  catch(Exception ex)
  {
    estado = 4;
    error = ex.toString();
    println(error);
  }
  background(0);
  fill(255);
  text(valor, width / 2, height / 2);
  stroke(255, 255, 0);
  fill(255, 0, 0);
  rect(120, 400, 80, 40);
  fill(255, 255, 0);
  text("Botón", 135, 425);
}

Simplemente comprueba si se ha recibido algún byte y lo muestra. También muestra el botón ficticio que puede pulsar el usuario.

El método que se ejecuta cuando el usuario pulsa el botón es compruebaBoton:

void compruebaBoton()
{
  if(mouseX > 120 && mouseX < 200 && mouseY > 400 && mouseY < 440)
  {
    try
    {
        ons.write(0);
    }
    catch(Exception ex)
    {
      estado = 4;
      error = ex.toString();
      println(error);
    }
  }
}

Si se comprueba que el usuario ha pulsado en el área del botón se envía un byte con valor 0 al dispositivo bluetooth, que actualmente lo que hace es encender o apagar el led de la placa Arduino. Si hubiese algún error se pasaría al estado 4 mostrando qué lo ha provocado.

Finalmente el método que muestra los errores es muestraError:

void muestraError()
{
  background(255, 0, 0);
  fill(255, 255, 0);
  textFont(f2);
  textAlign(CENTER);
  translate(width / 2, height / 2);
  rotate(3 * PI / 2);
  text(error, 0, 0);
}

Simplemente muestra el texto del error de forma apaisada para mostrar cadenas largas.

Dejo el código fuente entero:

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import java.util.ArrayList;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
 
private static final int REQUEST_ENABLE_BT = 3;
ArrayList dispositivos;
BluetoothAdapter adaptador;
BluetoothDevice dispositivo;
BluetoothSocket socket;
InputStream ins;
OutputStream ons;
boolean registrado = false;
PFont f1;
PFont f2;
int estado;
String error;
byte valor;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
BroadcastReceiver receptor = new BroadcastReceiver()
{
    public void onReceive(Context context, Intent intent)
    {
        println("onReceive");
        String accion = intent.getAction();
 
        if (BluetoothDevice.ACTION_FOUND.equals(accion))
        {
            BluetoothDevice dispositivo = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            println(dispositivo.getName() + " " + dispositivo.getAddress());
            dispositivos.add(dispositivo);
        }
        else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(accion))
        {
          estado = 0;
          println("Empieza búsqueda");
        }
        else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(accion))
        {
          estado = 1;
          println("Termina búsqueda");
        }
 
    }
};
 
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
  //size(320,480);
  frameRate(25);
  f1 = createFont("Arial",20,true);
  f2 = createFont("Arial",15,true);
  stroke(255);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void draw() {
  switch(estado)
  {
    case 0:
      listaDispositivos("BUSCANDO DISPOSITIVOS", color(255, 0, 0));
      break;
    case 1:
      listaDispositivos("ELIJA DISPOSITIVO", color(0, 255, 0));
      break;
    case 2:
      conectaDispositivo();
      break;
    case 3:
      muestraDatos();
      break;
    case 4:
      muestraError();
      break;
  }
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void onStart()
{
  super.onStart();
  println("onStart");
  adaptador = BluetoothAdapter.getDefaultAdapter();
  if (adaptador != null)
  {
    if (!adaptador.isEnabled())
    {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    else
    {
      empieza();
    }
  }
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void onStop()
{
  println("onStop");
  /*
  if(registrado)
  {
    unregisterReceiver(receptor);
  }
  */
 
  if(socket != null)
  {
    try
    {
      socket.close();
    }
    catch(IOException ex)
    {
      println(ex);
    }
  }
  super.onStop();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void onActivityResult (int requestCode, int resultCode, Intent data)
{
  println("onActivityResult");
  if(resultCode == RESULT_OK)
  {
    println("RESULT_OK");
    empieza();
  }
  else
  {
    println("RESULT_CANCELED");
    estado = 4;
    error = "No se ha activado el bluetooth";
  }
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void mouseReleased()
{
  switch(estado)
  {
    case 0:
      /*
      if(registrado)
      {
        adaptador.cancelDiscovery();
      }
      */
      break;
    case 1:
      compruebaEleccion();
      break;
    case 3:
      compruebaBoton();
      break;
  }
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void empieza()
{
    dispositivos = new ArrayList();
    /*
    registerReceiver(receptor, new IntentFilter(BluetoothDevice.ACTION_FOUND));
    registerReceiver(receptor, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED));
    registerReceiver(receptor, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
    registrado = true;
    adaptador.startDiscovery();
    */
    for (BluetoothDevice dispositivo : adaptador.getBondedDevices())
    {
        dispositivos.add(dispositivo);
    }
    estado = 1;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void listaDispositivos(String texto, color c)
{
  background(0);
  textFont(f1);
  fill(c);
  text(texto,0, 20);
  if(dispositivos != null)
  {
    for(int indice = 0; indice < dispositivos.size(); indice++)
    {
      BluetoothDevice dispositivo = (BluetoothDevice) dispositivos.get(indice);
      fill(255,255,0);
      int posicion = 50 + (indice * 55);
      if(dispositivo.getName() != null)
      {
        text(dispositivo.getName(),0, posicion);
      }
      fill(180,180,255);
      text(dispositivo.getAddress(),0, posicion + 20);
      fill(255);
      line(0, posicion + 30, 319, posicion + 30);
    }
  }
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void compruebaEleccion()
{
  int elegido = (mouseY - 50) / 55;
  if(elegido < dispositivos.size())   
  {     
    dispositivo = (BluetoothDevice) dispositivos.get(elegido);     
    println(dispositivo.getName());     
    estado = 2;   
  } 
} 
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 
void conectaDispositivo() 
{   
  try   
  {     
    socket = dispositivo.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
    /*     
      Method m = dispositivo.getClass().getMethod("createRfcommSocket", new Class[] { int.class });     
      socket = (BluetoothSocket) m.invoke(dispositivo, 1);             
    */     
    socket.connect();     
    ins = socket.getInputStream();     
    ons = socket.getOutputStream();     
    estado = 3;   
  }   
  catch(Exception ex)   
  {     
    estado = 4;     
    error = ex.toString();     
    println(error);   
  } 
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 
void muestraDatos() 
{   
  try
  {     
    while(ins.available() > 0)
    {
      valor = (byte)ins.read();
    }
  }
  catch(Exception ex)
  {
    estado = 4;
    error = ex.toString();
    println(error);
  }
  background(0);
  fill(255);
  text(valor, width / 2, height / 2);
  stroke(255, 255, 0);
  fill(255, 0, 0);
  rect(120, 400, 80, 40);
  fill(255, 255, 0);
  text("Botón", 135, 425);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void compruebaBoton()
{
  if(mouseX > 120 && mouseX < 200 && mouseY > 400 && mouseY < 440)
  {
    try
    {
        ons.write(0);
    }
    catch(Exception ex)
    {
      estado = 4;
      error = ex.toString();
      println(error);
    }
  }
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void muestraError()
{
  background(255, 0, 0);
  fill(255, 255, 0);
  textFont(f2);
  textAlign(CENTER);
  translate(width / 2, height / 2);
  rotate(3 * PI / 2);
  text(error, 0, 0);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Licencia Creative Commons
Android + Processing + Bluetooth por SISTEMAS O.R.P, a excepción del contenido de terceros y de que se indique lo contrario, se encuentra bajo una Licencia Creative Commons Attribution 4.0 International Licencia.

303 pensamientos en “Android + Processing + Bluetooth

  1. Pingback: Torno corta cinta | Viverón

  2. CARLOS

    Quiero saber si me puedes ayudar con algo de tu código, yo sé que han pasado varios años desde que lo publicaste, en estos momento lo necesito, pero al copiar el código, processing me genera un error en con el socket del método conectaDispositivo(), el error es cannot find symbol y no se qué pasa.

    Responder
      1. CARLOS

        gracias por responder, y si tengo Processing en modo android y tengo el instalado el API 10 instalado, al comentar la linea del problema
        “socket=dispositivo.createRfcommSocketToServiceRecord(UUID.fromString(“00001101-0000-1000-8000-00805F9B34FB”));” si deja compilar bien, que me recomiendas.

        Responder
        1. Oscar

          Vale, ahora que me has dado más información sobre en qué línea te ha dado el error, ¿has leído los comentarios de este artículo? ahí está la solución, más concretamente en el del 10 de Enero de 2013 a las 13:16

          Responder
  3. infoe

    Hola. Necesito hacer un driver USB-Serie,o un COM virtual, o algo que me permitaconectar un adaptador serie-usb a una tablet. El driver original (PL2303.ko) no funciona. ¿Con esto se puede hacer?. Gracias. Saludos.

    Responder
  4. Andrea

    Hola!
    Muy interesante este foro, muchas gracias…
    Copié el código en Arduino y Processing y corre sin errores, sin embargo una vez aparece el botón “BOTÓN” lo presiono y no pasa nada, no se prende el LED :(. Ya comprobé el pin digital 13 por aparte y funciona correctamente.
    Que podría ser?

    Responder
    1. Oscar Autor

      Vaya, pues si llegas a ese punto es que todo ha ido bien (elección del módulo, muestra de la pantalla con el botón, etc). ¿Puede ser que los pines de TX y RX del módulo bluetooth estén al revés con respecto a arduino? tendría que ser el TX del módulo al RX del arduino y el RX del módulo al TX del arduino.

      Responder
      1. Andrea

        Hola Oscar!
        Muchas gracias por tu respuesta, tras haber intentado varias cosas, utlice el codigo de Javier el cual es el tuyo con unas pequeñas modificaciones, y me funciono :), gracias.
        Tengo otra pregunta, programe un nuevo boton, para manipular otro Led (Pin 12), sin embargo, no importa el boton que oprima siempre prende el Pin 13.

        Codigo arduino:

        boolean estado;
        byte a = 0;
        void setup()
        {
        Serial.begin(9600);
        pinMode(12, OUTPUT);
        digitalWrite(12, LOW);
        randomSeed(analogRead(0));
        estado = false;
        estado1 = false;
        }
        void loop()
        {
        delay(100);
        Serial.write(random(40));
        while(Serial.available() > 0)
        {
        a = Serial.read();
        if (a == 0)
        {
        estado = !estado;
        digitalWrite(12, estado);
        }
        if (a == 1)
        {
        estado1 = !estado1;
        digitalWrite(13, estado1);
        }
        }
        }

        Codigo Processing (parte modificada):
        void compruebaBoton()
        {
        if( mouseY 640)
        {
        try
        {
        ons.write(1);
        }
        catch(Exception ex)
        {
        estado = 4;
        error = ex.toString();
        println(error);
        }
        }
        }

        Lo que quiero hacer es que si oprimo cualquier punto sobre la mitad superior de la pantalla prendo un led (13), y si oprimo un punto sobre la mitad inferior de la pantalla prendo el otro led (12).
        ¿Es correcto el código que estoy implementando para lo que intento hacer?
        Hice una prueba aparte, con solo 1 botón, cambie solamente el pin 13 por el 12. Y no funciona, no importa que pin modifique en el código de arduino, siempre se prende solo el 13.

        Nuevamente muchas gracias por el tutorial y foro, me han sido de mucha ayuda.

        Responder
        1. Andrea

          Ya resolví el problema, el código esta correcto, el problema era el siguiente: tenia que desconectar el dispositivo bluetooth de la tarjeta arduino para que el programa pudiese ser quemado correctamente en la tarjeta. Gracias :). Excelente tutorial!!

          Responder
  5. miguel

    hola me gustaría saber si cuando tu pones la función de recibir el dato del dispositvo bluetooth y lo muestras en tu celular tu instrucción
    valor = (byte)ins.read(); declaras tu valor como byte y la siguiente no la entiendo por ejemplo si yo mando una cadena de caracteres de mi dispositivo bluetooth como declararía mis variables, mi problema es el siguiente hice un programa en arduino y lo enlazo con processing para graficar temperatura y humedad relativo, por medio del puerto serial, arduino manda una cadena de caracteres separados por comas los datos de temperatura y humedad teniendo un punto como terminación de datos, processing ve esto los separa y los grafica, ejemplo 30,40. como tendría que declarar mis variables de valor y leer los datos para que lleguen completos y pueda procesarlos, gracias

    Responder
    1. Oscar Autor

      Puedes ir convirtiendo los enteros que devuelve read en carácteres (chat) y anexarlos a una variable tipo String. Cuando el programa vea que llega el carácter retorno de carro entonces es momento de procesar los datos de la variable String y volver a dejarla vacía.

      Responder
  6. miguel

    la aplicación se baja correctamente a mi celular sin errores pero a la hora de correrla se queda con la pantalla negra y letras verdes que dice}
    elija dispositivo pero no se ve nada mas, me podrias orientes a lo que pudiera ser gracias

    Responder
      1. miguel

        hola oscar gracias por responder, mira apenas comienzo con processing android blue2, baje algunas aplicaciones donde me enciende el blue2 de mi móvil y busca las disponibles en el área y si funcionan pero quise probar la tuya y me pregunta si quiero habilitar mi blue2 y si lo hace pero se queda en elija dispositivo y no funciona nada mas se queda como bloqueada la función, nho muestra las disponibles tampoco, solo habilita mi blue2 y manda ese mensaje, gracias

        Responder
  7. Fabián

    Hola, primero agradecer el esfuerzo de Oscar y demas personas que a partir de el colaboran en enseñarnos algo muy trabajado.
    Les cuento que hace bien poco descubri Arduino, y he podido seguir todos los pasos para crear sketch diversos y mezclarlos para crear otros o agregarles cosas, lo cual me ha entusiasmado mas aun.
    Comunicarme por BT tampoco es problema mediante el entorno arduino, pero en este ejercicio con android me ha pasado de todo, hace tres dias que leo y he solucionado un error para pasar a otro, el caso es que al querer correrlo en el simulador he tenido como 5 errores distintos hasta que este ultimo:
    Build Failed
    C:Program FilesAndroidandroid-sdktoolsantbuild.xml:680: The following error occurred while execute this line:
    C:Program FilesAndroidandroid-sdktoolsantbuild.xml:693: Error running javac.exe compiler
    Yo no se si soy muy torpe para aprender esto, si ya me tare por leer demasiado en tan poco, pero el asunto es que no encuentro la solucion, asi que disculpadme si mi pedido de ayuda es un poco estupido y la solucion la tengo delante, pero les aseguro que he leido hasta el artazgo.
    Necesito ayuda, por favor.

    Responder
  8. miguel

    hola oscar una pregunta respecto al problema que me encontré, tu programa detecta cualquier blue2 incluso los blue2 de los otros móviles si están activados o solo detecta blue2 externos, disculpa por mi pregunta.

    Responder
    1. Oscar Autor

      Muestra los dispositivos bluetooh que haya emparejado previamente al móvil. En la imagen puedes ver un Parrot CK3100 que es un manos libres para el coche, en el momento de hacer el artículo no estaba cerca de mi coche ni estaba arrancado, pero como ya estaba emparejado hace tiempo en mi móvil aparecía en la lista.

      Responder
  9. Fabián

    Hola Oscar, gracias por contestar, tengo Windows 8 con Java SE version 7 actualizacion 45. Los scratch en modo Java se ejecutan, cuando paso a Android sale en un renglon marron: Error from inside the Androids tool, check the console.
    Luego en la parte de abajo lo que escribi en el mensaje anterior. Lo peor fue cuando tenia instalado el Android SDK r.23, los errores eran varios, luego instale el r.18, para actualizar solo la API 10 y el Google USB driver. Ya que lei en varios lugares que Processing no funcionaba con las ultimas r.
    En fin en Android SDK manager tengo, A. SDK tools rev. 20; platform tools en 12; SDK platform API10 rev2; Googles Apis 10, rev. 2 y el driver.
    Ultimo, segun tengo entendido JSE 7 u45 incluye JDK mas JRE, estoy equivocado y debo descargarlos aparte.
    De nuevo gracias por tu tiempo. Es muy amable de tu parte.

    Responder
  10. miguel

    hola oscar ya ando aquí de enfadoso otra vez, mira ya he activado varios blue2 móviles pero no los detecta solo se queda en elija dispositivo y no muestra nada

    Responder
    1. Oscar Autor

      Cuando dices que has activado te refieres a que has emparejado? En Ajustes/Bluetooth/Dispositivos Sincronizados te aparece una lista de los que has emparejado? Si es así tendría que salirte la misma lista en la pantalla de “Elija Dispositivo”

      Responder
  11. miguel

    hola oscar y nuevamente gracias te lo agradezco, efectivamente cuando yo en mi móvil me voy a ajustes/blue2/dispositivos sincronizados me aparece la lista de los dispositivos detectados en la función del móvil, ahora bien, cuando ejecuto la aplicación de tu programa no aparecen en ella, solo se queda en elija dispositivo y no las muestra, es lo único que me hace falta para acceder a tu aplicación y probar tu programa para hacer pruebas con los datos que necesito enviar del blue2 externo a mi móvil y procesarlos, gracias nuevamente

    Responder
    1. Oscar Autor

      Prueba a cambiar la función listaDispositivos por esta y dime que número aparece a la derecha del texto:

      void listaDispositivos(String texto, color c)
      {
      background(0);
      textFont(f1);
      fill(c);

      if(dispositivos != null)
      {
      text(texto + " " + dispositivos.size(), 0, 20);
      for(int indice = 0; indice < dispositivos.size(); indice++)
      {
      BluetoothDevice dispositivo = (BluetoothDevice) dispositivos.get(indice);
      fill(255,255,0);
      int posicion = 50 + (indice * 55);
      if(dispositivo.getName() != null)
      {
      text(dispositivo.getName(),0, posicion);
      }
      fill(180,180,255);
      text(dispositivo.getAddress(),0, posicion + 20);
      fill(255);
      line(0, posicion + 30, 319, posicion + 30);
      }
      }
      else
      {
      text(texto + " 0",0, 20);
      }
      }

      Responder
  12. miguel

    Hola oscar ya probe la rutina que me pasaste y shgue igual no detecta y dice
    Elija dispositivo y enseguida el 0 y en el bluetooth d mi movil
    Si aparecen

    Responder
  13. luis

    hola
    estoy teniendo un error en la linea

    socket = dispositivo.createRfcommSocketToServiceRecord(UUID.fromString(“00001101-0000-1000-8000-00805F9B34FB”));

    la consola me dice,

    cannot find symbol

    ayudaaa!!
    mucgas gracias

    Responder
  14. miguel

    lo voy a probar oscar para ver si es mi móvil, lo raro es que he bajado otras aplicaciones parecidas a mi móvil de processing android bluetooth y si las detecta, voy a probar y estare por aquí para decirte los resultados, muchas gracias oscar, oscar el numero 0 que aparece a la derecha significa que no encontró dispositivos para la coneccion o que significa

    Responder
    1. Oscar Autor

      Me gustaría ver alguna de ellas, sobre todo las de processing, ¿podrías pasarme los enlaces?. El 0 indica que algo ha ido mal en el método empieza.

      Responder
  15. miguel

    una pregunta oscar en este enlace que te mande en Android/Processing Sketch 5: ConnectBluetooth hay una instrucción
    if(discoveredDeviceName.equals(“SeeedBTSlave”)){ donde el programa processing android le da el nombre del dispositivo blue2 con el cual se tiene que emparejar ahora bien en el sketch de abajo ARDUINO Sketch 1: Bluetooth Pair and Connect tiene el nombre que se le dio al dispositivo blue2 esto es para que se conecte automatico con nuestro android cuando lo encuentra, y no se conecte con otro aunque los detecte, te pregunto es Ahora si yo cambio en mi programa processing android el nombre del dispositvo por algún otro que detecte se conectara mi móvil con otro móvil si pongo si nombre en el programa processing android, gracias oscar, porque este programa los probe y si me da todos los dispositivos blue2 que se encuentran alrededor junto con sus nombres, pero vi que como tiene otro nombre con el que supuestamente se tiene que enlazasr no se enlaza con ninguno, poco a poco trato de entender todo este funcionamiento

    Responder
  16. Miguel Carrasco

    Excelente trabajo Oscar!!, lo probé con éxito en una placa Arduino compatible (elecrow.com) y una antena Bluetoth RN42-I/RM tipo Xbee. Respecto al código comenté tu línea ” socket = dispositivo.createRfcommSocketToServiceRecord(…” y descomenté las dos que seguían. De esta forma me funcionó perfecto y sin errores. A propósito, mi celular tiene una pantalla más chica y por ello no veía el botón. Sería una buena idea dejar la posición del botón relativa al tamaño de la pantalla. Me tomó un buen tiempo en entender porqué no veía el botón hasta que entendí que lo dejaste en una posición fija. Mil Gracias nuevamente por este excelente tutorial!!

    Responder
  17. miguel

    oscar o alguien que me ayude volvi a instalar el processing y el SDK para anddroid y a la hora que querer cargar el programa a mi móvil me sale que error a la hora de cargar que tiene un error el las tolos, dice exactamente error from inside the android tolos,check the console alguien me podría ayudar con eso que hago para que funcione bien

    Responder
  18. Pingback: Torno corta cinta | Viverón

  19. Pedro

    Buenas Oscar, estoy probando tu programa para una conexion con pic, y me corre en el emulador de android pero en el momento que inicia la aplicacion me salta el mensaje de Elija dispositivos y no me sale ninguno.
    Y probe si era problema del emulador y lo instale en el movil y se me instalo bien pero en el momento de arrancar la aplicacion me salta un error diciendo que la aplicacion se cerro de forma imprevista. Queria saber si me podias hechar una mano, muchas gracias.

    Responder
  20. Pedro

    No no con otros moviles no e probado pero se supone que la aplicacion en el emulador tendria que funcionar, esque en el emulador tampoco me encuentra ningun bluetooth.
    Y aparte una cosa para sacar el apk de la aplicacion tengo que hacer que funcione la apliacion en el emulador y de ai sacarla verdad o ai otras formas??

    Responder
    1. Oscar Autor

      Según la documentación de google acerca del emulador de Android: http://developer.android.com/tools/devices/emulator.html#limitations dice expresamente No support for Bluetooth, así que por eso no te funciona en el emulador.

      Por otra parte que no te funcione en ninguno de los dos móviles que has probado me resulta raro, ya que si miras los comentarios a todo el mundo que lo ha probado le arranca la aplicación (independientemente de si luego conectan por bluetooth o no). Mira el comentario de Cesar Arellano que es el único que comenta que le pasa algo parecido, pero sólo cuando cambia de orientación el dispositivo.

      Responder
  21. Pedro

    E estado probando si podia ser por lo de la pantalla, pero aun asi no es por eso.
    Dos errores que me da aunque me funciona en el emulador son estos:

    Emulator process exited with status 1.
    onStart
    Inefficient font rendering: use createFont() with a TTF/OTF instead of loadFont().

    Y cuando cojo el apk y lo paso al movil me pasa lo mismo aunque en el emulador me vaya.
    Quiero saber si podia ser por la version de android si es demasiado nueva en comparacion con la que uso en el emulador.
    El unico cambio que hago en tu programa es que no utilizo el socket que utilizas, pongo las lineas que hay debajo comentadas para que me arranque.

    Responder
    1. Oscar Autor

      En principio parecen más dos warnings que dos errores.

      En cuanto a lo del socket, ese código sólo se ejecuta una vez que has seleccionado el dispositivo, pero por lo que cuentas ni siquieras llegas a la pantalla de selección, por lo que nunca se ejecuta ese código.

      No se, sólo se me ocurre que pruebes el código de ejemplo http://blog.bricogeek.com/noticias/tutoriales/como-programar-para-android-con-processing/ a ver si te funciona en el móvil las aplicaciones de processing.

      Responder
  22. Pedro

    Si estos ejemplos los e probado y me funcionan en el movil, la cosa esque el programa de aqui nose porque me da ese error no lo entiendo.
    Ya que hago lo mismo que con los del ejemplo, por eso se me hace raro que no me funcione. Alguna cosa mas que se te pueda ocurrir, esque e probado alguna cosa mas pero me sigue dando el mismo error.

    Responder
    1. Oscar Autor

      Prueba en el método onStart a borrar todo y a poner esto:

      super.onStart();
      estado = 4;
      error = "Se lee este mensaje?";

      Instalalo en tu móvil y ejecútalo. ¿Ves una pantalla roja con el texto Se lee este mensaje?

      Responder
    1. Oscar Autor

      Ok

      Ahora quita todo en el método onStart y pon esto:

      super.onStart();
      println("onStart");
      adaptador = BluetoothAdapter.getDefaultAdapter();
      if (adaptador != null)
      {
      if (!adaptador.isEnabled())
      {
      Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
      startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
      }
      else
      {
      estado = 4;
      error = "Se lee este mensaje?";
      }
      }

      Responder
  23. Pedro

    Esta vez no me funciona se me cierra de imprevisto como me pasaba antes y en el emulador en vez de leerse el mensaje, me sale arriba a la derecha “Buscando Dispositivos”.
    ¿Esto puede ser por el modelo del movil o por la version de android?

    Responder
  24. Pedro

    Muchas gracias e conseguido que me vaya, ya que el problema esque tenia que actualizar el software de mi movil.
    Muchas gracias por todo, funciona con el programa original que esta en la pagina lo unico que e cambiado es lo del socket pero va bien lo unico que tengo que regular es la dimension de pantalla ya que me sale muy pequeño pero eso es cambiando los parametros y ya esta.
    Muchas gracias por todo me has ayudado mucho.

    Responder
  25. Pedro

    Buenas de nuevo, una pregunta, estoy teniendo problemas al enviar un valor flotante desde un PIC16F876A.
    La cosa esque necesito que me llegue valores con decimales de 0 a 30V y ya e cambiado que el valor me llegue como un float en vez de como un byte y aun asi no me llegan bien los datos al programa y cuando lo miro con el bt terminal si que me llegan bien los datos.
    Se te ocurre alguna idea de si puede ser del processing o del programa en C.
    En el programa en C lo envio asi:
    printf(“%f “,c);
    c es el la variable flotante.

    Responder
  26. Oscar Autor

    Quiero decir que ins.read() devuelve sólo un byte, y ahora que lo miro la función printf que usas en el pic devuelve un número variable de bytes en forma de cadena: No es lo mismo 4,37 (4 bytes) que 2567,5683 (9 bytes).

    Responder
  27. Pedro

    Perdona pero en ese comentario es para el tema de horas minutos y segundos y lo que estoy intentando hacer esque me envie un dato en decimal que va variando en valor de un voltaje que meto en mi microcontrolador, entonces como puedo variar el muestra datos, porque en el tema de processing llevo poco tiempo.

    Responder
  28. Pedro

    Esque aparte no entiendo lo que me quieres decir refiriendote a ese comentario, porque nose como ponerlo y por lo que me parece a mi es de horas minutos y segundos o eso me parece y nose donde ponerlo en el processing ya que yo quiero que siempre me multiplique por el mismo valor.

    Responder
  29. Oscar Autor

    Cuando se es programador hay que extrapolar los ejemplos. En lo que te menciono se trata de un unsigned long (la función millis de Arduino) que se convierten en 4 bytes para enviarse por el puerto serie uno por uno y desde processing recuperar esos 4 bytes para transformarlos otra vez en un unsigned long, para tu caso sería con un float.

    Haciendo yo por ti una búsqueda por Internet me he encontrado con esto que te servirá para hacer esas conversiones:

    Para el PIC:

    http://www.todopic.com.ar/foros/index.php?topic=23144.0

    Para processing:

    http://stackoverflow.com/questions/14308746/how-to-convert-from-a-float-to-4-bytes-in-java

    Responder
  30. angel

    hola oscar si que te nos habias perdido pero gracias a tus consejos pude lograr demasiado, la aplicación me funciono y puede lograr mandar datos del arduino al móvil y con un programa processing los grafico en forma de barras además hice otra App para que guade los datos durante 24 horas y con el móvil puedo accesar a ellos a cualquier hora todo gracias a tus consejos, pero te tengo una preguntototota en mi programa de processing utilizo la instrucción save(“imagen.jpg”) la cual si lo corro en serial por pc me genera la imagen de la pantalla que quiero grabar, cuando la convierto a android, no puedo ver la imagen porque esta en el sistema del android donde no puedo accesar, utilice también para direccionarla
    save(“//sdcard//DCIM//Camera//imagen.jpg”) pero no la genera o si la genera no la veo, ahí es donde me atore ya, y lo quiero hacer para que cuando reciba los datos del arduino pueda generar una imagen y poderla imprimar al accesar a las imágenes del celular, alguna recomendación que me puedas dar, ya habilite también el write_external_storage en los permisos pero nada, que puedo hacer

    Responder
    1. Oscar Autor

      Nunca he hecho un snapshot desde Processing en Android. Pero me choca que la cadena de la ruta que uses sea //sdcard//DCIM//Camera//imagen.jpg en vez de //sdcard//DCIM/Camera/imagen.jpg

      Responder
  31. miguel

    gracias a los consejos de oscar, pude lograr muchas cosas, ya puedo tener control total de la aplciacion y usarla para mis proyectos, envio datos a través de un arduino y los recibo con el celular y manda a graficarlos enforma de barras en el programa processin, además hice otro programa con arduino que almacena datos cada hora durante 24 horas del dia y puedo accesar a ellos con una App que hice gracias a oscar para acceder a los datos y mostrarlos en una grafica horas contra temperatura, este programa me sirvió de antemano para hacer las primeras pruebas de envio de datos y recivierlos en el móvil para darme cuenta como es que se realiza el envio de información y que tipo de datos quisiera mandar y que tipo quisiera recibir agradezco a oscar por todos sus consejos

    Responder
  32. miguel

    ya le encontré oscar, cada vez que mando generar una imagen la ruta esta bien y la genera pero no las podía ver, y baje una aplicación al android que se llama astro para poder accesar al lugar donde las guarda la aplicación que realice y de ahí las puedo seleccionar y las mando a mostrar solamente y asi ya quedan grabadas en la misma ruta que le di y las puedo ver para poderlas bajar e imprimir, gracias estaremos en contacto para cualquier duda que tenga

    Responder
  33. Pedro

    Buenas,espero que me podáis ayudar. Me gustaría saber si existe alguna manera de, una vez desarrollada la aplicación, ajustar el tamaño de la misma automáticamente, dependiendo del teléfono sobre el cual la ejecutemos. Estoy buscando información, pero no encuentro nada referente a esto. Muchas gracias.

    Responder
      1. Pedro

        ¿Existe alguna función u opción para obtener estas medidas automáticamente, o tendría que modificar cada vez que se instale en un determinado dispositivo la aplicación?. Muchas gracias de nuevo.

        Responder
          1. Pedro

            Como bien dices width y height te permiten obtener el ancho y alto, pero de lo que tu defines previamente en size(). Lo que necesitaría sería saber si existe, que igual no es factible de hacer, no lo se, es obtener los valores de size(x,y), pero automáticamente, para poder referenciar las dimensiones a estos valores x e y.De esta manera podría adecuar la aplicación y dependiendo de las dimensiones del teléfono, mostrarla correctamente, y no más chica o grande.
            Muchas gracias de nuevo.

  34. jorge

    Me puedes ayudar?, es que a la hora de compilar un ejemplo mas sencillo el processing me dice que hay problemas; el error es “Error running java.exe compiler” compilater

    Responder

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>