El Nokia N95 viene con un interesante sensor en su interior. Dispone de un acelerómetro de tres ejes (ortogonales entre sí) que permiten determinar las aceleraciones que se producen en el dispositivo. La pena del caso, es que no dispone de un giroscopio o "algo" similar que permita determinar la orientación respecto de lo que normalmente viene a ser nuestro sistema de referencia, el suelo. Claro que el propio acelerómetro sirve "casi" como giroscopio si se mantiene el centro de gravedad del mismo estático respecto del espacio, pero esto limita enormemente su aplicación práctica.
La aplicación que se describe, se compone de los siguientes elementos:
Hasta ahora los SDK para los móviles que yo venía utilizando se basaban en MIDlets programados en Java. Venía utilizando el excelente IDE NetBeans que tan buen resultado me ha dado. Java es un muy buen lenguaje de programación y las librerías disponibles son utilizadas por muchos fabricantes y una ingente cantidad de programadores que la hacen robusta y muy bien documentada.
Hace un tiempo que tenía ganas de meterle mano a los S60 de Symbian para poder programarlos en C++, que te permite un mayor control y cercanía con los dispositivos. Cual ha sido mi sorpresa al ver que la plataforma de desarrollo de Symbian (al menos para los S60) deja bastante que desear. No es que no vaya bien, pero existen una (demasiada grande) cantidad de circunstancias que la hacen bastante incómoda (al menos para mí).
No es este el sitio para documentar todos los incovenientes que le veo, pero sí los enumeraré rápidamente:
Vamos, que no estoy nada contento con el SDK, es bastante feo. Pero en fin, no todo es malo. Poco a poco, te vas "acostumbrando" a ciertas "incomodidades" (nunca vistas) y te vas centrando en lo que interesa.
Debo reconocer que con mi móvil N95 debo tener algún problema (y/o en mi Windows XP), TODOS los ejemplos que he probado (código fuente o ya compilados) no me han permitido de forma satisfactoria establecer una comunicación serie. Curiosamente se supone (y normalmente lo es) que es la forma más simple de establecer comunicación. Yo (que soy muy cabezón) al final lo he conseguido, pero sólo cuando el móvil conecta con el PC (que es como "parece" que indican en una de las documentaciones).
Lo que ya no he conseguido (ni siquiera me van los MIDlet que sí me ivan en el Nokia 6288) es que el N95 despliegue un servicio Bluetooth y éste sea reconocido por el PC. En este sentido, de momento he desistido...
Por lo demás, realizar la conexión requiere de una cantidad bastante importante de incomodidades y que, dado lo patético de la API "estándar" Bluetooth y el SDK S60 lo mejor es tomar uno de los ejemplos (¡verifica primero que te funciona!) e ir "despiezando" según sea necesario, tal es lo que hice yo con el "S60CppExamples\Chat" (si hijo si, el de los 26 archivos...).
En esta misma sección tienes el artículo Bluetooth que hace que el JSR-82 alcance el estatus de deidad suprema cuando lo comparamos con establecer conectividad mediante Bluetooth en el SDK del S60 (que viene a estar en el más bajo de los inframundos).
Aun cuando la documentación al respecto es también sensiblemente rala, no ha sido difícil crear una sencilla clase que controle el sensor (y eso que fué de lo primero que hice, si, antes que el fastidioso tema del Bluetooth). Con los comentarios en el código supongo te bastará:
Este es el archivo de cabecera acelerometro.h:
/* ============================================================================ Name : acelerometro.h Author : josejuan Version : 1.0 Copyright : Do you like it? more on http://jose-juan.computer-mind.com Description : CAcelerometro declaration ============================================================================ */ #ifndef ACELEROMETRO_H #define ACELEROMETRO_H #include <e32std.h> #include <e32base.h> #include <avkon.hrh> #include <aknnotewrappers.h> #include <rrsensorapi.h> // Una sencilla clase que representa un vector en R3 #include "Vxyz.h" // Debemos declararla antes para la clase CAcelerometroObserver class CAcelerometro; // Define un observador de acelerómetros, deberá implementarla // cualquier clase que quiera "escuchar" el acelerómetro class CAcelerometroObserver { public: // Evento que será llamado cuando un acelerómetro aporte datos virtual void AcelerometroDatos( CAcelerometro *sensor ) = 0; }; // La clase acelerómetro class CAcelerometro : public CBase, // debe implementar MRRSensorDataListener para "escuchar" el sensor public MRRSensorDataListener { public: static const TInt K_MUESTRAS_P2 = 3; // Debe ser: // K_MUESTRAS = 2^K_MUESTRAS_P2 static const TInt K_MUESTRAS = 8; // Número de muestras para hacer promedio static const TInt K_SENSOR_ACC = 0x10273024; // id del acelerómetro static const float K_S2G = 0.0315691336554f; // de valor de sensor a fuerza G // lo he tenido que calcular "a ojo". ~CAcelerometro(); // Los incómodos constructores de Symbian S60 static CAcelerometro * NewL( CAcelerometroObserver *iObserver ); static CAcelerometro * NewLC( CAcelerometroObserver *iObserver ); // Callback para obtener los datos del sensor void HandleDataEventL( TRRSensorInfo aSensor, TRRSensorEvent aEvent ); // Debe llamarse para inicializar y tomar el control del sensor void GetSensor( void ); // Libera el sensor void ReleaseSensor( void ); // Indica si está listo TBool Ready( void ); TInt ax( void ); // devuelve el valor ponderado del eje X. TInt ay( void ); // devuelve el valor ponderado del eje Y. TInt az( void ); // devuelve el valor ponderado del eje Z. private: TInt iA; // índices del buffer circular TInt A[ K_MUESTRAS + 1 ][3]; // muestras para el acelerómetro y sumatorio // Acceso al sensor CRRSensorApi * iAccSensor; // Observador del acelerómetro CAcelerometroObserver *iObserver; // El constructor "de verdad" CAcelerometro( CAcelerometroObserver *iObserver ); }; #endif // ACELEROMETRO_H
Y este es el archivo de código acelerometro.cpp:
/* ============================================================================ Name : acelerometro.cpp Author : josejuan Version : 1.0 Copyright : Do you like it? more on http://jose-juan.computer-mind.com Description : CAcelerometro implementation ============================================================================ */ #include "acelerometro.h" CAcelerometro::CAcelerometro( CAcelerometroObserver *_iObserver ) { // Nos quedamos con el que quiere "observarnos" iObserver = _iObserver; // inicializamos los valores de muestra y sumatorio for( TInt i = 0; i <= K_MUESTRAS; i++ ) for( TInt c = 0; c < 3; c++ ) A[i][c] = 0; // primer valor del anillo de valores iA = 0; } CAcelerometro::~CAcelerometro() { // Obvio ReleaseSensor(); } TBool CAcelerometro::Ready( void ) { // Está listo si tenemos sensor return iAccSensor != NULL; } void CAcelerometro::GetSensor( void ) { // Lista de sensores RArray<TRRSensorInfo> l; // La solicitamos CRRSensorApi::FindSensorsL( l ); // Si tenemos sensor, lo liberamos ReleaseSensor(); // Para cada sensor... for( TInt i = 0, n = l.Count(); i < n; i++ ) /// ...¿es el nuestro? if( l[i].iSensorId == K_SENSOR_ACC ) { // Nos quedamos con el sensor iAccSensor = CRRSensorApi::NewL( l[i] ); // Y nos ponemos a nosotros, a la esucha iAccSensor->AddDataListener( this ); // No hay mas que uno break; } } void CAcelerometro::ReleaseSensor( void ) { // Si hay sensor... if( iAccSensor ) { // ...dejamos de ecuchar... iAccSensor->RemoveDataListener(); // ...liberamos el objeto... delete iAccSensor; // ...y ya no tenemos sensor. iAccSensor = NULL; } } // Devuelve los valores actuales de aceleración: TInt CAcelerometro::ax( void ) { // El último valor contiene la suma de los anteriores, // debemos dividir por el número de muestras return A[K_MUESTRAS][0] >> K_MUESTRAS_P2; } TInt CAcelerometro::ay( void ) { // idem return A[K_MUESTRAS][1] >> K_MUESTRAS_P2; } TInt CAcelerometro::az( void ) { // idem return A[K_MUESTRAS][2] >> K_MUESTRAS_P2; } // Capta un evento de muestra del acelerómetro void CAcelerometro::HandleDataEventL( TRRSensorInfo s, TRRSensorEvent e ) { // ¿Es el nuestro?, no se muy bien porqué lo ponen en los ejemplos // yo he visto que sólo es llamado con este valor. Es probable que // sea por si se utiliza el mismo "escuchador" para varios sensores: if( s.iSensorId == K_SENSOR_ACC ) { // a la media le restamos el que sale y añadimos el que entra // (el que entra reemplaza al que sale) A[K_MUESTRAS][0] += e.iSensorData1 - A[iA][0]; A[iA][0] = e.iSensorData1; A[K_MUESTRAS][1] += e.iSensorData2 - A[iA][1]; A[iA][1] = e.iSensorData2; A[K_MUESTRAS][2] += e.iSensorData3 - A[iA][2]; A[iA][2] = e.iSensorData3; // la siguiente "celda" en la que dejaremos un valor será // el siguiente... iA++; // ...en módulo K_MUESTRAS... iA %= K_MUESTRAS; // Si tenemos un escuchante... if( iObserver != NULL ) // ...se lo decimos. iObserver->AcelerometroDatos( this ); } } // ¡Puaj! CAcelerometro * CAcelerometro::NewLC( CAcelerometroObserver *iObserver ) { CAcelerometro * self = new ( ELeave ) CAcelerometro( iObserver ); CleanupStack::PushL( self ); return self; } // ¡Puaj! CAcelerometro * CAcelerometro::NewL( CAcelerometroObserver *iObserver ) { CAcelerometro * self = CAcelerometro::NewLC( iObserver ); CleanupStack::Pop(); // self; return self; }
Bien, como ves, la clase que controla el acelerómetro es bien sencilla, lo único "interesante" es la forma en la que construímos el anillo de valores (yo no lo he visto por ahí) pero es bastante simple y no merece la pena detenerse.
Una vez nos hemos hecho con el intratable código para hacer de cliente Bluetooth, realizar las modificaciones pertinentes para que nos envíe los datos es muy sencilla. Como único punto a comentar es la codificación de los valores de aceleración, yo los paso como una cadena de la forma {ejeX};{ejeY};{ejeZ}{CR}. Sí, se que no es muy elegante y que además de las conversiones se pasarán bastantes más bytes de los necesarios (casi el triple) sin embargo la llamada para enviar los datos es void CBluetoothBt::SendMessageL( TDes& aText ) y por tanto la codificación de TDes (en mi compilación) es Unicode y no quería perder más tiempo con la dichosa clase. La pérdida no es grande y sólo utilizando algún Profiler podríamos realmente estimar la pérdida de rendimiento (probablemente despreciable).
Bueno, en tal caso, para usar la clase del acelerómetro sólo debemos ponernos en nuestra clase como escuchante:
class CBluetoothRemoteSensorContainerView: public CAknView, public MLog, // Escuchante del acelerómetro public CAcelerometroObserver { ...
Declarar una variable para el acelerómetro (en la misma clase):
... CAcelerometro *iAcelerometro; ...
En el constructor, lo creamos (pero no lo inicializamos):
... iAcelerometro = CAcelerometro::NewL( this ); ...
En el destructor, deberemos liberarlo limpiamente:
if( iAcelerometro != NULL ) { delete iAcelerometro; iAcelerometro = NULL; }
Allí donde queramos iniciar la conexión (conectar con el PC y empezar a enviar los datos), iniciaremos la conexión Bluetooth y luego iniciaremos el sensor del acelerómetro:
... // Conectar con el PC: iBluetoothBt->ConnectL(); // Iniciar el sensor: iAcelerometro->GetSensor(); ...
Controlar la lectura del sensor y enviarla es sencillo:
void CBluetoothRemoteSensorContainerView::AcelerometroDatos( CAcelerometro *sensor ) { // Sólo si se ha terminado de enviar la última petición enviamos otra if( iBluetoothBt->State() == EConnected ) { // Codificamos datos TBuf<200> n; n.Num( iAcelerometro->ax() ); n.Append( _L(";") ); n.AppendNum( iAcelerometro->ay() ); n.Append( _L(";") ); n.AppendNum( iAcelerometro->az() ); n.Append( _L("\r") ); // Enviamos iBluetoothBt->SendMessageL( n ); } }
Y ya está, hemos controlado el acelerómetro y enviado los datos al PC (u otro dispositivo conectado).
Ya ves cómo cuando las cosas están medianamente bien hechas es todo mucho más sencillo, utilizando InTheHand, que es un wrapper para las conexiones Bluetooth bajo .NET, que viene a dejarlo como una conexión por Sockets o TCP/IP tradicional, hacer el servidor que recibe los datos desde el sensor se trivializa. He aquí el código de mi clase que con la cantidad de comentarios que tiene apenas tiene 184 línea de código.
/* * JJBM, 31/01/2009, BluetoothSerialPortServer.cs * */ using System; using System.Collections.Generic; using System.Text; using System.Net.Sockets; using System.Threading; using InTheHand.Net.Sockets; using InTheHand.Net.Bluetooth; using System.Windows.Forms; namespace S60SensorBt { /// <summary> /// Contiene los datos generados por un evento de recepción de datos. /// </summary> public class BluetoothSerialPortServerDataEventArgs : EventArgs { /// <summary> /// Máximo de datos que se podrán recibir en un comando. /// </summary> private const int MAX_BUFF_DATA = 1024; /// <summary> /// Marca de fín de comando. /// </summary> private const byte EOF_DATA = 13; /// <summary> /// Son los datos recibidos. No todo el buffer puede estar ocupado, debe revisarse idatos. /// </summary> public byte [] datos; /// <summary> /// Indica la cantidad de datos recibidos. /// </summary> public int idatos; /// <summary> /// Devuelve los datos en forma de cadena de texto. /// </summary> public string Texto { get { if( idatos <= 0 ) return string.Empty; return Encoding.ASCII.GetString( datos, 0, idatos ); } } public BluetoothSerialPortServerDataEventArgs() { datos = new byte [ MAX_BUFF_DATA ]; idatos = 0; } /// <summary> /// Añade un byte a la entrada de datos e indica si ha llegado al fin de línea. /// </summary> /// <param name="b">byte a añadir.</param> /// <returns><c>True</c> si se ha llegado al fin de línea.</returns> public bool Put( byte b ) { if( b == EOF_DATA ) return true; datos [ idatos++ ] = b; if( idatos >= MAX_BUFF_DATA ) return true; return false; } } /// <summary> /// Se produce cuando se reciben una línea de datos de la conexión cliente. Se espera a recibir una marca de fin de línea. Si los datos superan la longitud máxima admitida, también se produce el evento. /// </summary> /// <param name="sender">Objeto que produce el evento.</param> /// <param name="e">Datos de la recepción de datos.</param> public delegate void BluetoothSerialPortServerDataEventHandler( object sender, BluetoothSerialPortServerDataEventArgs e ); /// <summary> /// Establece un servicio al que se puede conectar mediante el servicio Bluetooth "SerialPort". /// </summary> class BluetoothSerialPortServer { /// <summary> /// Es el servidor que se queda a la escucha. /// </summary> private BluetoothListener bl = null; /// <summary> /// Se produce cuando se recibe una línea de datos del cliente. /// </summary> public event BluetoothSerialPortServerDataEventHandler OnDataEvent; private void DataEvent( BluetoothSerialPortServerDataEventArgs e ) { BluetoothSerialPortServerDataEventHandler d = OnDataEvent; if( d != null ) { // Esta triquiñuela es lo más raro del código, debemos invocar al método // enviando un mensaje en lugar de una llamada directa, esto es porque // se está ejecutando en otro thread diferente al del Windows Form if( d.Target is Control ) { Control t = d.Target as Control; t.Invoke( d, new object [] { this, e } ); } else { d( this, e ); } } } /// <summary> /// Stream de lectura/escritura privado de la única conexión disponible. /// </summary> private NetworkStream s = null; /// <summary> /// Thread del proceso de lectura de datos. /// </summary> private Thread tR = null; /// <summary> /// Única conexión cliente admitida. /// </summary> private BluetoothClient cli = null; /// <summary> /// Indica si existe una conexión de cliente activa. /// </summary> public bool Conectado { get { return cli != null && cli.Connected; } } /// <summary> /// Proceso que se ejecuta en un Thread independiente. /// </summary> public void SerialPortReader() { try { BluetoothSerialPortServerDataEventArgs e = new BluetoothSerialPortServerDataEventArgs(); for( ; ; ) { int b = s.ReadByte(); if( b < 0 ) Thread.Sleep( 200 ); // ¿fin de secuencia? else if( e.Put( (byte) b ) ) { DataEvent( e ); e = new BluetoothSerialPortServerDataEventArgs(); } } } catch( ThreadAbortException ) { } } /// <summary> /// Inicia un proceso de escucha para aceptar una única conexión cliente. Es síncrono. /// </summary> public void Start() { bl = new BluetoothListener( BluetoothService.SerialPort ); bl.Start( 1 ); cli = bl.AcceptBluetoothClient(); s = cli.GetStream(); tR = new Thread( new ThreadStart( SerialPortReader ) ); tR.Start(); } /// <summary> /// Detiene la conexión establecida. /// </summary> public void Stop() { tR.Abort(); cli.Close(); bl.Stop(); } /// <summary> /// Envía un mensaje a la conexión establecida. /// </summary> /// <param name="msg">Mensaje a enviar.</param> public void Send( string msg ) { byte [] buf = Encoding.ASCII.GetBytes( msg ); s.Write( buf, 0, buf.Length ); } } }
Utilizar los datos del sensor ya no es tan interesante (cuando sólo ponemos un simple ejemplo). Por eso no entro en detalles. La aplicación permite mover una bola en la pantalla mediante el móvil utilizando tres formas de control:
Receptor del sensor en ejecución
Abajo a la izquierda vemos un indicador para cada uno de los tres ejes. Las barras de scroll verticales permiten controlar el índice de rozamiento y el factor de escala.
No hace falta que instales nada en el PC, sólo necesitas los archivos S60SensorBt.exe y InTheHand.Net.Personal.dll. Para el móvil sólo tienes que instalar el BluetoothRemoteSensor.sisx. Estos archivos los puedes bajar de aquí INSTALABLES.
Cómo ejecutar el ejemplo
Si quieres el código fuente, sólo tienes que pedírmelo a la dirección de correo.