trabajo de fin de grado - universidad de...
TRANSCRIPT
Equation Chapter 1 Section 1
Trabajo de Fin de Grado
Grado en Ingeniería de las Tecnologías de
Telecomunicación
Aplicación móvil para el control via bluetooth de un
vehículo autoequilibrado
Autor: Daniel Sojo España
Tutor: Ignacio Alvarado Aldea
Dep. Ingeniería de Sistemas y Automática
Escuela Técnica Superior de Ingeniería
Universidad de Sevilla
Sevilla, 2017
Proyecto Fin de Carrera
Ingeniería de Telecomunicación
Aplicación móvil para el control via bluetooth de un
vehículo autoequilibrado
Autor:
Daniel Sojo España
Tutor:
Ignacio Alvarado Aldea
Dep. Ingeniería de Sistemas y Automática
Escuela Técnica Superior de Ingeniería
Universidad de Sevilla
Sevilla, 2017
Trabajo de Fin de Grado: Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
Autor: Daniel Sojo España
Tutor: Ignacio Alvarado Aldea
El tribunal nombrado para juzgar el Proyecto arriba indicado, compuesto por los siguientes miembros:
Presidente:
Vocales:
Secretario:
Acuerdan otorgarle la calificación de:
Sevilla, 2017
El Secretario del Tribunal
A mi familia
A mis maestros
Resumen
El presente proyecto de fin de grado se centra en el desarrollo de una aplicación móvil para dispositivos
Android. La finalidad de la misma es establecer conexión con un vehículo autoequilibrado de tipo péndulo
invertido controlado por una placa Arduino, previamente desarrollado por otros alumnos de la escuela, y
permitir controlar su movimiento.
El vehículo a controlar dispone de un módulo bluetooth, por lo que se hará uso de dicha tecnología para
establecer una conexión y permitir la comunicación entre el vehículo y el dispositvo móvil Android. Por
supuesto, el dispositivo móvil o smartphone ha de ser compatible con la tecnología Bluetooth.
Así mismo, a fin de cerrar la comunicación y solventar problemas previos, se reescribirá parte del código
ejecutado en la placa Arduino. Concretamente, todo lo relacionado con la gestión de la comunicación
bluetooth y la interpretación de las órdenes de dirección y movimiento.
Índice
Resumen 9
Índice 11
Índice de Tablas 13
Índice de Figuras 15
1 Introducción 1 1.1 Objetivo 1 1.2 Contexto 1
2 Punto de partida 3 2.1 Vehículo autoequilibrado - Mini Segway - 3
2.1.1 Hardware 3 2.1.2 Software 4
2.2 Aplicación original para dispositivos Android 4 2.2.1 Interfaz 4 2.2.2 Programación de la aplicación y consecuencias 6 2.2.3 Principales problemas 6
2.3 Memoria del proyecto original 6
3 Software 7 3.1 Android OS 7
3.1.1 Introducción 7 3.1.2 Versiones. APIs 7 3.1.3 Ciclo de vida de una aplicación 9
3.1.3.1 onCreate() 10 3.1.3.2 onStart() 10 3.1.3.3 onResume() 10 3.1.3.4 onPause() 11 3.1.3.5 onStop() 11 3.1.3.6 onDestroy() 11
3.1.4 Estructura de archivos básica 12 3.2 Arduino 13
3.2.1 Introducción 13 3.2.2 Estructura de un programa 13
4 Hardware 15 4.1 Arduino Mega 2560 15 4.2 Módulo Bluetooth HC-06 16
5 Entorno de Desarrollo 17 5.1 Android 17
5.1.1 Lenguaje de programación. Java. 17 5.1.2 Android SDK. Kit de Desarrollo Software 17
5.1.3 ADB. Android Debug Bridge. 18 5.1.4 ADV. Android Virtual Devices. 18 5.1.5 Entorno de Desarrollo Integrado: Android Studio 19
5.2 Arduino 23 5.2.1 Lenguaje de programación 23 5.2.2 Entorno de Desarrollo Integrado: Arduino IDE 23
6 Control de versiones 25 6.1 Git 25 6.2 Gestor de repositorios Git: GitLab 26
7 Programación 27 7.1 Aplicación móvil para dispositivos Android 27
7.1.1 Interfaz y funcionamiento 27 7.1.2 Preliminares. Archivo manifiesto: AndroidManifest.xml 31 7.1.3 Actividades 33
7.1.3.1 SplashScreenActivity 33 7.1.3.2 MainActivity 33 7.1.3.3 Preferences 33
7.1.4 Clases Java 33 7.1.4.1 Bluetooth 34 7.1.4.2 ReceivedData 37 7.1.4.3 Message 38 7.1.4.4 Joystick 39 7.1.4.5 Accelerometer 41 7.1.4.6 Movement 43 7.1.4.7 MainActivity 44 7.1.4.8 Preferences 48 7.1.4.9 SplashScreenActivity 49 7.1.4.10 Tools 50 7.1.4.11 BluetoothDeviceListCustomAdapter 51
7.1.5 Recursos 52 7.1.5.1 Drawable 52 7.1.5.2 Layout 52 7.1.5.3 Menu 53 7.1.5.4 Mipmap 53 7.1.5.5 Values 53 7.1.5.6 Xml 53
7.2 Programa Arduino 54 7.2.1 Detalle de las nuevas rutinas 54
7.2.1.1 Respuesta a las órdenes de movimiento 54 7.2.1.2 Recepción y envío de mensajes 55
8 Conclusiones 57
9 Bibliografía 59
Apendice A: Código para Arduino 61
Apendice B: Código para Android 77
ÍNDICE DE TABLAS
Tabla 2–1. Componentes principales del vehículo. 3
Tabla 3–1. Distribución relativa de versiones de Android. 8
Tabla 3–2. Versiones del S.O. Android. 8
Tabla 4–1. Detalles técnicos del Arduino Mega 2560. 15
Tabla 6–1. Comandos básicos de Git. 25
ÍNDICE DE FIGURAS
Figura 2-1. Control de dirección. 5
Figura 2-2. Control de velocidad. 5
Figura 2-3. Ajustes de variables. 5
Figura 2-4. Trimado de Ángulo. 6
Figura 3-1. Paquetes según nivel de API. 7
Figura 3-2. Ciclo de vida de una Actividad en Android. 9
Figura 3-3. Estructura de archivos de una aplicación Android. 12
Figura 3-4. Arduino Mega 2560. 13
Figura 3-5. Arduino Nano. 13
Figura 4-1. Módulo Bluetooth HC-06. 16
Figura 5-1. Android SDK Manager. 18
Figura 5-2. Interfaz de Android Studio. 19
Figura 5-3. Android Studio. Barra de herramientas. 19
Figura 5-4. Android Studio. Ruta actual. 20
Figura 5-5. Android Studio. Arbol del proyecto. 20
Figura 5-6. Android Studio. Renderizado de XML. 20
Figura 5-7. Android Studio. ADVs. 21
Figura 5-8. Android Studio. Logcat. 21
Figura 5-9. Android Studio. Monitor de recursos. 22
Figura 5-10. Android Studio. Integración con Git. 22
Figura 5-11. Interfaz de Arduino IDE. 23
Figura 5-12. Interfaz de Arduino IDE. Gestor de Librerías. 24
Figura 6-1. Vistazo a un commit en GitLab. 26
Figura 7-1. Interfaz de la aplicación. Splash Screen. 27
Figura 7-2. Interfaz de la aplicación. Actividad principal – sin conexión. 27
Figura 7-3. Interfaz de la aplicación. Menú lateral – activar Bluetooth. 28
Figura 7-4. Interfaz de la aplicación. Solicitud de activación de la conectividad Bluetooth. 28
Figura 7-5. Interfaz de la aplicación. Menú lateral – dispositivos vinculados. 28
Figura 7-6. Interfaz de la aplicación. Menú lateral – conexión. 29
Figura 7-7. Interfaz de la aplicación. Actividad principal – conexión. 29
Figura 7-8. Interfaz de la aplicación. Joystick. 30
Figura 7-9. Interfaz de la aplicación. Acelerómetro. 30
Figura 7-10. Interfaz de la aplicación. Menú de ajustes. 30
Figura 7-11. Ejes de las medidas inerciales en Android. 42
Figura 7-12. Icono de la aplicación. 53
1
1 INTRODUCCIÓN
1.1 Objetivo
El proyecto que se detalla en este documento se realiza como el correspondiente Trabajo de Fin de Grado, de
cara a la obtención del título de Graduado en Ingeniería de las Tecnologías de Telecomunicación. El mismo ha
sido llevado a cabo bajo la tutoría de Ignacio Alvarado Aldea, profesor del Departamento de Sistemas y
Automática.
El objetivo en sí es el desarrollo de una aplicación para dispositivos móviles Android capaz de establecer
conexión con un pequeño vehículo autoequilibrado de tipo péndulo invertido, permitiendo el control del
mismo. Para ello se hará uso de la tecnología bluetooth, presente tanto en el vehículo en cuestión como en casi
cualquier Smartphone de hoy día.
Establecida la conexión, la aplicación ofrecerá, en un principio, dos métodos de control.
El primero de ellos consiste en un joystick virtual, el cual permitirá dar órdenes de movimiento deslizando un
dedo sobre la pantalla del smarthpone que ejecute la aplicación, permitiendo no solo indicar la dirección del
movimiento sino también la velocidad que se desea alcanzar, según sea la separación entre el joystick virtual y
el centro del mismo.
El segundo método hará uso del acelerómetro disponible en la mayor parte de los dispositivos móviles
Android. En este caso será la inclinación aplicada al dispositivo, relativa a una referencia dada, la que
determine dirección y velocidad de movimiento.
Junto a las órdenes de control, la aplicación ofrecerá la opción de gestionar ciertos valores relativos a las
ecuaciones de control ejecutadas en el Arduino. Para ello se diseñará una pantalla adicional donde modificar
dichos valores, los cuales se detallarán en secciones posteriores de este documento.
Al mismo tiempo se actualizará el software presente en la placa Arduino para permitir el funcionamiento de la
comunicación bluetooth y el tratamiento de los datos recibidos, ya sean órdenes de movimiento o
actualizaciones en el valor de alguna variable del programa que se estaría ejecutando.
El objetivo último de este proyecto es facilitar el control del vehículo y permitir una conexión y ejecución del
programa estable, tanto por parte de la placa Arduino como del dispositivo móvil.
1.2 Contexto
A dia de hoy los dispositivos móviles igualan en potencia computacional a los equipos de escrtorio estandar de
hace unos años y además disponen de multitud de sensores embebidos de serie que les permiten interactuar
con el entorno. Todo ello unido a la popularidad de Android somo Sistema Operativo hace que podamos tener
en nuestras manos una maquina multipropósito potentísima que solo ha de ser programada adecuadamente.
Android ofrece de forma gratuita un entorno de desarrollo propio, Android Studio, así como toda la
documentación necesaria para construir cualquier aplicación desde cero o a partir de un esqueleto base. Es por
ello que los smartphones son algo más que teléfonos inteligentes, ya que con la aplicación adecuada un
terminal podría sustituir a un equipo tradicional en su función.
En este caso se busca crear el equivalente a un mando de control remoto, como el que podríamos encontrar en
un coche o vehículo radio control, con la salvedad de que podemos organizar los mandos y las órdenes
generadas a nuestro gusto, programando la propia aplicación de control.
Introducción
2
3
2 PUNTO DE PARTIDA
El proyecto recogido en esta memoria no parte desde cero, sino que con él se pretende actualizar y continuar
con el trabajo realizado anteriormente por otros alumnos de la escuela, diferenciando dos partes: vehículo
autoequilibrado controlado por Arduino y aplicación móvil.
2.1 Vehículo autoequilibrado - Mini Segway -
En mención a sus autores originales el robot es llamado informalmente “Robot Mini Segway” o simplemente
“Mini Segway”, lo cual deriva de su similitud con los vehículos producidos por la compañía Segway, cuyo
nombre se toma hoy día a la hora de nombrar a este tipo de vehículos o robots.
2.1.1 Hardware
La estructura principal o chasis está constituida por una lámina de aluminio con una sección de madera en la
parte superior, la cual permite la sujeción de una lamina de metacrilato que hace las veces de superfice de
carga y permite suavizar los golpes gracias a unos segmentos de material acolchado en sus extremos.
En el corazón del robot se encuentra la placa microcontroladora Arduino Mega 2560, encargada de la gestión
y control del resto de componentes. Esta se encuentra acoplada a una placa de circuito impreso que facilita la
conexión con el resto de componentes, entre ellos el módulo bluetooth HC-06. En una sección posterior se
hablará detenidamente sobre estos dos componentes en concreto.
Dispone de una unidad de medidas inerciales o IMU (Inertial Measurement Unit), concretamente la
MPU6050, dos controladores de motor modelo TB6612FNG y dos motores EMG-30.
Para alimentar el sistema se optó por una batería de Polímero de Litio, Li-Po, de 1.000mAh y 11.1V de tensión
nominal, con una tasa de descarga de 25C.
En lo que respecta al hardware los cambios introducidos fueron minimos y con el objetivo de restaurar su
funcionamiento en caso de avería en alguno de los compomentes. Dichas modificaciones se detallan más
adelante.
Tabla 2–1. Componentes principales del vehículo.
Dispositivo Modelo
Placa controladora Arduino Mega 2560
Módulo bluetooth HC-06
Unidad de Medidas Inerciales (IMU) MPU6050
Controlador del motor TB6612FNG
Motores EMG-30
Batería Li-Po 11.1V 1.000mAh
Punto de partida
4
2.1.2 Software
En lo relativo al software, la placa microcontroladora Arduino se encontraba completamente programada,
haciendo al vehículo totalmente autosuficiente para mantener el estado de reposo y aceptar ordenes básicas de
control.
El programa emplea 4 interrupciones asociadas a cambios en los pines para realizar la lectura de nuevos
valores en la posición de las ruedas y determinar así su velocidad. Por otro lado, se utlizan dos sucesos
temporales usando los Timers 1 y 5: uno de ellos realiza el cálculo del controlador PI (Proporcional e Integral)
cada 10ms y el otro el control LQR (Linear Quadratic Regulator) cada 40ms.
2.2 Aplicación original para dispositivos Android
2.2.1 Interfaz
Cuando se inició este proyecto ya existía aplicación para dispositivos móviles Android capaz de establecer
conexión con el vehículo a través del módulo bluetooth y enviar órdenes de control básicas estipulando una
velocidad de traslación y/o rotación fijas y una orden de dirección de entre nueve posibles:
(1) – Avanzar y girar a la izquierda
(2) – Avanzar en línea recta
(3) – Avanzar y girar a la derecha
(4) – Girar a la izquierda
(5) – Reposo
(6) – Girar a la derecha
(7) – Retroceder y girara a la izquierda
(8) – Retroceder en línea recta
(9) – Retroceder y girar a la derecha
Estas órdenes de dirección estaban asociadas a un teclado numérico de 9 botones, mientras que el control de
velocidad y traslación se aplicaba con dos barras horizontales.
También daba opción a modificar varias de las variables empleadas por el programa ejecutado en la placa
microcontroladora Arduino, para lo cual se debía cumplimentar un formulario en la parte inferior de la pantalla
principal.
5
Figura 2-1. Control de dirección.
Figura 2-2. Control de velocidad.
Figura 2-3. Ajustes de variables.
Punto de partida
6
Además de los controles indicados existía otro más y es el control del trimado de ángulo. Su finalidad era
compensar manualmente la variación del centro de gravedad, provocado, por ejemplo, al colocar una carga
descentrada sobre el segway.
Figura 2-4. Trimado de Ángulo.
Respecto a este ajuste cabe decir que desaparece en la nueva aplicación, gracias a los avances de la última
alumna de la escuela que ha trabajado con el robot.
2.2.2 Programación de la aplicación y consecuencias
La aplicación original fue desarrollada usando Scratch como lenguaje de programación y App Inventor como
entorno de desarrollo. Scratch es un lenguaje de programación visual en el cual se utilizan bloques lógicos y
funciones prediseñadas para conseguir una determinada funcionalidad. Esto hace posible programar una
aplicación sin disponer de conocimientos sobre el lenguaje de programación en cuestión, sin embargo, se
pierde gran parte del control sobre el código.
Debido a esto la aplicación del presente proyecto se hizo partiendo de cero, manteniendo la estrucutra de los
mensajes para permitir la comunicación entre dispositivo móvil y robot.
2.2.3 Principales problemas
El propio método utilizado en la programación de la aplicación provocó que esta respondiese aun único caso
de uso, totalmente lineal y secuencial, derivando normalmente en un error o cuelgue del programa si se
realizaba incorrectamente. Aun así, siguiendo estrictamente los pasos era frecuente la desconexión prematura
entre el robot y el terminal móvil, o sencillamente el cuelgue de la misma.
Además, solo se ofrecía un mecanismo de control por medio de órdenes simples de dirección y velocidad fija,
lo cual no permitía a un control preciso del sistema.
2.3 Memoria del proyecto original
Junto al hardware y software original también fue facilitada la memoria del proyecto original, en la cual se
detalla desde un punto de vista mucho más técnico el funcionamiento del robot. Dicha memoria se puede
encontrar en la bibliografía de este proyecto.
7
3 SOFTWARE
Como se anticipó en secciones anteriores la aplicación móvil pretende ser ejecutada en un Smartphone
Android y establecerá conexión con un módulo bluetooth controlado por una placa Arduino. Por ello, el punto
de partida pasa por el propio Sistema Operativo y la gestión de una aplicación en el caso de Android, así como
la estrucutra de un programa en Arduino.
3.1 Android OS
3.1.1 Introducción
Android es un Sistema Operativo desarrollado por Google1. Está basado en el núcleo Linux y originalmente
fue diseñado para dispositivos móviles con pantall tactil como smarthpones o tabletas, aunque terminó
diversificándose en todo tipo de dispositivos electrónicos como relojes inteligentes, televisores, equipos
multimedia o vehículos.
El código fuente de Android es distribuido por Google bajo una licencia de código libre. Este carácter “libre”
es precisamente una de las grandes bazas de Android y es lo que ha fomentado el desarrollo de aplicaciones
tanto por parte de empresas como desarrolladores independientes.
3.1.2 Versiones. APIs
La primera versión de Android fue lanzada en septiembre de 2008 con Android 1.0 como nombre, a la cual
seguiría Android 1.1. A modo anecdótico, a todas las versions sucesivas se les ha asignado un nombre en
clave, en orden alfabético, haciendo referencia a algún tipo de dulce o postre.
El interés en estas versiones reside en el salto sucesivo en el nivel de API2. Con cada nuevo nivel se habilitan
nuevas librerías y paquetes para uso del desarrollador en la comunicación con el Sistema Operativo.
Estos paquetes segmentados por nivel de API se pueden ver y analizar de una forma muy cómoda e intuitiva
en developer.android.com
Figura 3-1. Paquetes según nivel de API.
Por ello, uno de los primeros parámetros a definir a la hora de desarrollar una aplicación para Android es el
nivel de API mínimo necesario para que esta pueda funcionar. Cuando mayor sea el nivel fijado, mayor será el
abanico de posibilidades a la hora de interactuar con el Sistema. Sin embargo, desde que se lanza una nueva
1 El desarrollo inicial fue llevado a cabo por Android Inc, empresa que sería adquirida por Google en 2005. 2 Application Programming Inteface o Intefaz de Programación de Aplicaciones.
Software
8
versión hasta que una parte importante de la población llega a hacer uso de la misma transcurre bastante
tiempo, por lo que ha de seleccionarse un nivel intermedio que de acceso a la mayor cantidad de población
posible. A nivel informativo se adjunta la distribución de versiones más actual ofrecida por Google.
Tabla 3–1. Distribución relativa de versiones de Android3.
Versión Nombre en clave API Distribución Compatibilidad
2.3.3 – 2.3.7 Gingerbread 10 0.70 % -
4.0.3 – 4.0.4 Ice Cream Sandwich 15 0.70 % 99.30 %
4.1.X
4.2.X
4.3
Jelly Bean 16
17
18
2.70 %
3.80 %
1.10 %
98.60 %
95.90 %
92.10 %
4.4 KitKat 19 16.00 % 91.00 %
5.0
5.1
Lollipop 21
22
7.40 %
21.80 %
75.00 %
67.60 %
6.0 Marshmallow 23 32.30 % 45.80 %
7.0
7.1
Nougat 24
25
12.30 %
1.20 %
13.50 %
1.20 %
De ella se deduce que, actualmente, fijar el nivel de API en 15 permitiría que nuestra aplicación puediese ser
utilizada prácticamente por todos los dispositivos móviles con Android en activo.
En lo que respecta a la aplicación objeto de esta memoria el nivel de API se fijó en 13 a fin de dar soporte al
smartphone que en un principio se utilizó para las pruebas.
Tabla 3–2. Versiones del S.O. Android.
Nombre en clave Versión Nivel API
Android 1.0 1.0 1
Android 1.1 1.1 2
Cupcake 1.5 3
Donut 1.6 4
Eclair 2.0 – 2.1 5 – 7
Froyo 2.2 – 2.2.3 8
Gingerbread 2.3 – 2.3.7 9 – 10
Honeycomb 3.0 – 3.2.6 11 – 13
Ice Cream Sandwich 4.0 – 4.0.5 14 – 15
Jelly Bean 4.1 – 4.3.1 16 – 18
KitKat 4.4 – 4.4.4 19 – 20
Lollipop 5.0 – 5.1.1 21 – 22
Marshmallow 6.0 – 6.1 23
Nougat 7.0 – 7.1.2 24 – 25
Oreo 8.0 26
3 Datos recopilados por Google durante un período de 7 días hasta 8/8/2017. No se muestran versiones con una distribución inferior al 0,1%.
9
9 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
3.1.3 Ciclo de vida de una aplicación
Las aplicaciones en Android están constituidas por una o varias actividades. Estas pueden atravesar diferentes
estados a lo largo de su ciclo de vida conforme el usuario interactua con ellas. Por este motivo, la clase
Actividad proporciona una serie de callback4 que indican cuando se produce un cambio o transición de estado.
Haciendo uso de estos callbacks el programador puede definir el comportamiento de sus actividades ante las
acciones del usuario, por ejemplo, reaccionando de una forma determinada si el usuario sale y vuelve a entrar a
la aplicación o si esta pasa a un segundo plano por algún otro motivo. Además, una buena implementación de
los mismos ayuda a gestionar correctamente los recursos del sistema y recuperar el estado de la aplicación
cuando se produce una rotación de pantalla o esta regresa desde un segundo plano.
Figura 3-2. Ciclo de vida de una Actividad en Android.
4 Función que se usa como argumento de otra función principal. La función principal ejecutará la función pasada como parámetro cuando se produzca un determinado evento. Esto permite mantener una capa de abstracción.
Software
10
3.1.3.1 onCreate()
El único de los siete callbacks o métodos de obligatoria implementación y el que será lanzado en cuanto el
sistema cree la actividad. En él se ha de situar aquel código de inicialización de la aplicación que solo se
pretende ejecutar una vez al inicio de la misma. Además, es aquí cuando se recibe los datos que fueron
guardados del estado anterior de la actividad, por ejemplo, tras producirse una rotación de pantalla.
@Override
public void onCreate(Bundle savedInstanceState) {
// Llamar a la super-clase para completar la creación de la actividad
super.onCreate(savedInstanceState);
// Recuperar datos guardados (si los hubiese) de un estado anterior
if (savedInstanceState != null) {
// ToDo
}
// Establecer el layout de la interfaz para la actividad
setContentView(R.layout.main_activity);
// ToDo
}
3.1.3.2 onStart()
El sistema llama a este callback cuando la aplicación entra en el estado “Iniciada”. Define el punto intermedio
entre la carga de datos de la actividad y la representación en pantalla de la interfaz de usuario.
@Override
public void onStart() {
// Llamar al método de la super clase
super.onStart();
// ToDo
}
3.1.3.3 onResume()
Llamado cuando la actividad entra en el estado “Reanudada” y pasa a un primer plano. En este estado la
actividad es controlable por el usuario y permanecerá en él mientras no suceda algún evento que la destrulla o
lleve a un segundo plano.
@Override
public void onResume() {
// Llamar al método de la super clase
super.onResume();
// ToDo
}
11
11 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
3.1.3.4 onPause()
El sistema llamará a esta función como primer indicio de que el usuario está abandonado la aplicación. Un
buen uso de este callback es detener procesos que no debiesen continuar ejecutándose en segundo plano.
@Override
public void onPause() {
// Llamar a la super-clase
super.onPause();
// ToDo
}
3.1.3.5 onStop()
Su llamada implica que la actividad deja de estar visible para el usuario. La duración de este evento es superior
al anterior onPause() y por ello es aquí donde se debe realizar cualquier operación de salvaguardado de datos.
Como buena práctica, dado que la aplicación dejará de ser utilizada (al menos temporalmente) deben liberarse
todos los recursos posibles.
@Override
public void onStop() {
// Llamar a la super-clase
super.onStop();
// ToDo
}
3.1.3.6 onDestroy()
Esta llamada se realizará justo antes de que la actividad sea destruida, esto es, deje de estar en memoria. Este
evento se produce bien porque el usuario la ha abandonado completamente o porque el sistema ha decidido
eliminarla de la memoria para recuperar recursos.
@Override
public void onDestroy () {
// Llamar a la super-clase
super.onDestroy();
// ToDo
}
Software
12
3.1.4 Estructura de archivos básica
El código que da forma a una apliación Android se distribuye en un conjunto de directorios dentro del
proyecto. En el nivel más superior distiguimos:
manifest – contiene el archivo AndroidManifest.xml que define a la aplicación. En él encontraremos
los detalles de la aplicación, sus permisos, las actividades que la componen y la interacción entre ellas.
java – le sigue un conjunto de directorios que dan nombre el paquete de nuestra aplicación y que
contienen los archivos de código fuente Java.
res – contienen todos los recursos de nuestra aplicación: vistas de las actividades, iconos de la
aplicación (según resolución de pantalla), imágenes en general, cadenas de texto, valores numéricos
(dimensiones), definiciones de colores, figuras geométricas, etc. Según el tipo son clasificados en sub-
directorios. Además, aquí se puede distinguir entre distintos niveles de API, densidad de píxeles o
lenguajes de interfaz según estos archivos sean distribuidos y nombrados.
Figura 3-3. Estructura de archivos de una aplicación Android.
13
13 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
3.2 Arduino
3.2.1 Introducción
Arduino es una plataforma electrónica y software de código abierto. Las placas micrcontroladoras Arduino
disponen de múltiples interfaces de entrada y/o salida, capaces de leer valores analógicos y/o digitales, realizar
cálculos con los mismos y generar una salida o respuesta determinada. Su carácter open-source y su bajo
precio han hecho que constituyan una opción ideal a la hora de realizar prototipos electrónicos de bajo coste.
Acutalmente existen en el mercado diferentes modelos que varian en tamaño, recursos, poder computacional y
número de conexiones disponibles. Así mismo, se distribuyen innumerables shields o placas de expansión con
distintos componentes que añaden funcionalidades no disponibles de serie como conectividad bluetooth,
sensores de infrarrojos o ultrasonidos, etc.
Figura 3-4. Arduino Mega 2560.
Figura 3-5.
Arduino Nano.
3.2.2 Estructura de un programa
Los programas en Arduino son escritos en un lenguaje similar a C o C++ y se componen de dos bloques
principales:
Setup() – Bloque que será ejecutado una única vez al proveer de alimentación a la placa o tras
provocar un reseteo de la misma. En esta sección se inicializan variables y registros.
Loop() – Bloque que se ejecuta de forma contínua e infinita. Aquí estará el programa en sí.
void setup () {
// Codigo que se ejecutará una única vez tras iniciarse la placa
}
void loop () {
// Codigo que será ejecutado en un bucle infinito
}
Software
14
15
4 HARDWARE
El robot que se desea controlar está formado por varios componentes interconectados a través de una placa de
circuito impreso que a su vez permite a la placa Arduino tomar control sobre ellos.
En lo referente a este proyecto, y en lo que concierne al hardware, son dos los elementos de importancia. La
placa microcontroladora Arduino Mega 2560 y el módulo bluetooth HC-06 conectado a la misma a través de
su puerto serie.
4.1 Arduino Mega 2560
El Arduino Mega 2560, así como el resto de modelos comerciales de Arduino, viene pre-programado con un
bootloader5 que permite al usuario cargar su programa en la placa sin necesidad de hardware adicional,
simplemente conectadolo via USB con un ordenador.
Tabla 4–1. Detalles técnicos del Arduino Mega 2560.
Concepto Valor
Microcontrolador ATmega2560
Voltaje de funcionamiento 5V
Voltaje de entrada (recomendado) 7-12 V
Voltaje de entrada (límite) 6-20 V
Pines digitales de entrada/salida 54 (15 permiten salida PWM)
Pines analógicos de entrada 16
Corriente contínua por pin 20 mA
Corriente contínua para el pin de 3.3V 50 mA
Memoria flash 256 KB (8 KB para el bootloader)
SRAM 8 KB
EEPROM 4 KB
Frecuencia de reloj 16 MHz
5 Programa o gestor de arranque. Pequeña pieza de software encargada de preparar todo lo necesario antes de inciar el sistema operativo o programa principal.
Hardware
16
4.2 Módulo Bluetooth HC-06
Se trata de un dispositivo que se comunica con la placa Arduino a través de su puerto serie, con una velocidad
de trasferencia varaible de entre 1.200 y 1.382.400 baudios6, y permite la recepción y transmisión de datos a
través de Bluetooth.
Con el uso de los demoninados comandos AT se pueden cargar valores en su memoria interna para modificar
su funcionamiento. De este modo se consigue que actue como servidor a la escucha de conexiones entrantes,
respondiendo de forma autónoma y retransmitiendo los datos recibidos via Bluetooth a través del puerto serie
mencionado.
Figura 4-1. Módulo Bluetooth HC-06.
6 Número de símbolos por segundo. Cada síbolo puede constituir uno o más bits.
17
5 ENTORNO DE DESARROLLO
La aplicación móvil que se pretende crear está diseñada para ser instalada y ejecutada en un smartphone con
Sistema Operativo Android, pero a su vez ha de ser capaz de establecer comunicación con un programa
ejecutado en una placa Arduino. Es por ello que este proyecto debe moverse a lo largo de dos entornos
diferentes, cada uno propio de la máquina donde se pretende ejecutar.
5.1 Android
5.1.1 Lenguaje de programación. Java.
Las aplicaciones diseñadas para Android usan Java como lenguaje de programación.
Java se caracteriza por ser de propósito general, concurrente, basado en clases y orientado a objetos. Su idea
original era permitir a los programadores escribir sus programas una única vez, haciendo que dicho programa
pudiese ser ejecuado en cualquier máquina, esto es, crear aplicaciones independientes de la plataforma de
ejecución. Un programa en Java compilado puede ser ejecutado en cualquier plataforma que soporte Java,
independientemente del hardware, usando una máquina virtual de Java.
En Java es primordial el concepto de clase. Tanto código funcional como variables y programas
independientes son encapsulados en lo que se denomina clases. Dentro de las clases podemos encontrar pues
variables y funciones, solo que a estas variables se las llama atributos y a las funciones métodos de la clase.
Tanto clases como variables y métodos pueden ser públicos, protegidos o privados. Esto se consigue
añadiendo una palabra reservada y afecta a su visibilidad desde el punto de vista del código.
En una aplicación Android nos encontramos pues con múltiples documentos o archivos con extensión .java,
contenedores del código de la aplicación, cada uno con el nombre de la clase definida en su interior.
5.1.2 Android SDK. Kit de Desarrollo Software
Para poder desarollar aplicaciones Android se precisan de herramientas software. Todas ellas son facilitadas
por Google y se pueden encontrar en el Kit de Desarrollo Software de Android, abreviado en inglés como
Android SDK.
En el caso de Windows, desde su panel de control se pueden instalar y desinstalar los paqueres que se
precisen, así como actualizar su versión conforme una nueva sea publicada. Dichas actualizaciones están
ligadas directamente al desarrollo de Android como Sistema Operativo.
Entorno de Desarrollo
18
Figura 5-1. Android SDK Manager.
5.1.3 ADB. Android Debug Bridge.
El conector o puente para depuración de Android, abreviado como ADB, es una colección de herramientas que
permiten al programador establecer conexión con un dispositivo Android y ejecutar comandos hacia o desde
él. Con ello se puede obtener información de los dispositivos conectados, transferir archivos, modificar
variables del sistema, instalar aplicaciones, ejecutar comandos dentro del propio sistema operativo Android o,
más importante desde el punto de vista del desarrollo de aplicaciones, depurarlas.
Normalmente, el uso de ADB se realiza por medio del terminal, invocando al programa junto a la función a
ejecutar. Por ejemplo, el comando:
adb devices
Devolvería un listado con los dispositivos Android conectados al ordenador (físicos o emulados) junto a su ID
único. Dicho ID se utiliza en otros comandos para especificar el dispositivo afectado usando el prefijo -s.
adb –s [ID] shell
adb –s [ID] install ruta_al_apk
5.1.4 ADV. Android Virtual Devices.
Si no se dispone de un terminal móvil donde probar y depurar una aplicación, siempre se puede emular. Con
las herramientas software anteriormente mencionadas podemos crear un dispositivo móvil virtual en el que
instalar la aplicación en desarrollo como si de un terminal físico se tratase, eso si, con ciertas limitaciones en lo
que a sensores se refiere.
19
19 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
5.1.5 Entorno de Desarrollo Integrado: Android Studio
A fin de agrupar todo lo anterior y facilitar la programación de aplicaciones para Android, Google ofrece un
entorno de desarrollo específicamente diseñado para tal funcionalidad: Android Studio.
Figura 5-2. Interfaz de Android Studio.
En la figura anterior podemos ver de forma general la interfaz de Android Studio. En la parte superior
encontramos los típicos menús con todas las opciones del IDE, agrupadas según utilidad. Bajo ellos tenemos
una barra de herramientas con las opciones más habituales.
Figura 5-3. Android Studio. Barra de herramientas.
Estas herramientas se dividen en nueve grupos:
Cargar un nuevo archivo o guardar/actualizar el actual.
Deshacer/rehacer
Cortar, copiar o pegar
Buscar o buscar y reemplazar
Navegación adelante o atrás
Construcción y/o ejecución de la aplicación en desarrollo; ya sea en un dispositivo físico o virtual, en
modo normal o depuración. Reinicio o detención de la ejecución.
Actualizar el repositorio local y/o remoto así como ver diferencias entre versiones y revertir cambios.
Ajustes generales y del proyecto.
Sincronización del proyecto con los arhivos Gradle7. Gestión de ADV y SDK. Monitor Android.
7 Sistema de código abierto para la construcción automática de software, en este caso la aplicación Android.
Entorno de Desarrollo
20
Inmediatamente bajo esta barra encontramos una sucesión de directorios con la ruta del fichero actualmente
abierto y en edición.
Figura 5-4. Android Studio. Ruta actual.
Continuando por la izquierda nos encontramos con el proyecto en sí. Android Studio no ofrece varias visiones
del mismo: una visión de su árbol de archivos o la estructura lógica del mismo con clases, métodos y variables.
También encontramos aquí caputras de ejecuciones que hayan sido grabadas para su posterior análisis.
Figura 5-5. Android Studio. Arbol del proyecto.
Aunque la aplicación se programa íntegramente en Java, es preciso usar otro meta-lenguaje llamado XML
para diseñar las vistas. Este IDE ofrece una visión adicional para los archivos XML y no es otra que el
renderizado de los mismos, pudiendo apreciarse en tiempo real cómo afecta la modificación de unas líneas a la
vista que pretenden definir. No solo eso, sino que además se puede aplicar el renderizado a distintos niveles de
API, pudiendo jugar con la representación de cada componente en pantalla según la versión de Android final.
Figura 5-6. Android Studio. Renderizado de XML.
21
21 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
Desde el propio IDE se puede gestionar tanto las librerías del SDK como los dispositivos virtuales o ADV. El
programador puede crear su smartphone o tablet virtual personalizada, escogiendo desde resolución de
pantalla y densidad de píxeles de la misma, hasta la versión de Android ejecutada, tipo de CPU, cantidad de
memoria RAM, etc. Desde el panel de gestión de dispositivos virtuales Android pueden lanzarse estas
máquinas virtuales para posteriormente instalar en ellas la aplicación en desarrollo.
Figura 5-7. Android Studio. ADVs.
Por supuesto, desde Android Studio se pueden leer y registrar todos los logs lanzados por nuestra propia
aplicación en ejecución, el sistema Android o cualquier otra aplicación. Para diferenciar entre ellos basta con
mirar el nombre del paquete que originó el mensaje o bien aplicar un filtro. En la siguiente figura se están
filtrando estos logs buscando la cadena “MyApp.” en ellos.
Figura 5-8. Android Studio. Logcat.
Entorno de Desarrollo
22
Sin embargo, el monitor de Android no se limita solo a logs, también registra el efecto de la aplicación en el
sistema: porcentaje de uso de la CPU, RAM ocupada, uso de red y porcentaje de uso de la GPU.
Figura 5-9. Android Studio. Monitor de recursos.
Para terminar, como se adelantó anteriormente, también tenemos total compatibilidad con el sistema de control
de versiones Git sobre el que se hablará en una sección posterior. Desde su panel podemos ver los archivos con
modificaciones pendientes de registrar, así como dichas modificaciones del código fuente y el histórico de
commits realizados en las distintas ramas.
Figura 5-10. Android Studio. Integración con Git.
23
23 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
5.2 Arduino
5.2.1 Lenguaje de programación
Los programas para Arduino son escritos en un lenguaje de programación propio pero muy similar a C/C++.
En líneas generales, los programas están formados por una estructura definida, un conjunto de constantes y
variables y una colección de funciones.
En lo que respecta a la estructura se distiguen dos bloques fundamentales indicados anteriormente: setup() y
loop(). Las estructuras de control por su parte, son las mismsa que podríamos encontrar en C/C++: if, if-else,
while, do-while, etc.
Las funciones pueden ser definidas por el programador a gusto propio, pero cabe mencionar que existen
multitud de ellas particulares de una placa Arduino y que guardan estrecha relación con la definición de pines
de entrada/salida y el trabajo con los mismos.
5.2.2 Entorno de Desarrollo Integrado: Arduino IDE
Arduino como empresa ofrece un Entrono de Desarrollo propio, tanto web como local. En este proyecto nos
centraremos en la aplicación de escritorio.
Este IDE ofrece un editor de texto donde escribir el código del programa, un área para mensajes informativos,
una consola o terminal y una barra de herramientas con botones para las funciones más comunes.
Figura 5-11. Interfaz de Arduino IDE.
Tomando la figura anterior como referencia y comenzando desde arriba tendríamos:
Menús – Todas las opciones del programa agrupadas en menús. Entre ellas: cargar proyectos,
modificar el formato del editor, verificar/compilar el código, incluir librerías, cambiar la placa de
desarrollo Arduino y/o procesador, etc.
Barra de herramientas – Aquí encontramos cinco operaciones básicas. De izquierda a derecha:
verificar/compilar, subir el programa, nuevo archivo, abrir archivo, guardar archivo.
Paneles – Cada archivo que forma parte del proyecto.
Entorno de Desarrollo
24
Editor de texto – Campo donde ver y editar el código fuente.
Mensajes de estado – Aquí se muestran mensajes referentes al estado de la verificación/compilación,
como errores porducidos o uso de recursos en caso de éxito, así como el estado de la subida del
programa a la placa arduino.
Sin salir del IDE podemos gestionar de forma muy simple las librerías utilizadas, añadiendo o eliminándolas
usando un sencillo buscador. Sin embargo, cabe mencionar que esta biblioteca de librerías no contiene ni
mucho menos todas las disponibles. En la mayoría de los casos se precisan de librerías creadas por
desarrolladores independientes y para ello ha de ser buscada y descargada de forma manual, para
posteriormente incorporada al IDE usando su herramienta para la importación de librerías.
Figura 5-12. Interfaz de Arduino IDE. Gestor de Librerías.
Para terminar, una vez desarrollado y compilado el programa no hará falta más que conectar la placa Arduino
al ordenador donde se esté ejecutando el IDE. Este se comunicará con la placa en cuestión y subirá a ella el
código correspondiente cuando se le ordene.
25
6 CONTROL DE VERSIONES
En cualquier proyecto software es necesario mantener una copia de resplado o cierta redundancia de todo el
trabajo desarrollado, pues al fin y al cabo un ordenador, así como sus medios de almacenamiento, son
electrónica que podría fallar en cualquier momento por algún motivo.
Cuando se trabaja con multitud de archivos de código fuente, además de dicho respaldo, se requiere de cierta
lógica o control sobre las copias salvaguardadas. Llegados a este punto se hace indispensable utilizar algún
software de control de versiones, pues realizar dicho trabajo manualmente sería demasiado tedioso.
6.1 Git
Git es el software de control de versiones por excelencia en la actualidad y el seleccionado para este proyecto.
Esta herramienta soluciona el problema que supone guardar cada versión desarrollada, mantiendo toda su
estrucutra y permitiendo restaurarla en cualquier momento, sin perder ningún dato anterior o posterior.
A los proyectos o directorios sobre los cuales actua Git se los llama repositorios y para actuar sobre ellos
disponemos de un amplio abanico de comandos.
A cada modificación o versión del proyecto guardada se la llama commit. Estas versiones se agrupan a lo largo
de una o varias ramas creadas por el desarrollador, las cuales constituyen líneas independientes de desarrollo.
Tabla 6–1. Comandos básicos de Git.
Comando Argumentos básicos Resultado
git init Ruta al directorio Inicializa un nuevo repositorio en el directorio
indicado.
git status - Muestra el estado de la rama actual como
cambios pendientes de commit.
git add Ruta al archivo/s o
directorio/s
Añade los archivos indicados para un commit
posterior o activa el seguimiento de los mismos
si estaban siendo ignorados.
git commit Nombre para el commit Genera una nueva versión o commit con los
cambios anterioremnte indicados usando add.
git push -
Actualiza el repositorio remoto por defecto
subiendo los cambios realizados por el
programador.
git pull - Actualiza el respositorio local trayendo nuevos
cambios del servidor por defecto.
Control de versiones
26
También cabe mencionar la utilidad de varios archivos/directorios que son generados cuando se crea un
repositorio local:
.git – Directorio oculto donde se almacenan los datos de todas las versiones o commit realizados en el
proyecto
.gitignore – Documento con las rutas que debe ignorar el software a la hora de hacer seguimiento de
los cambios en los archivos. Personalizar el mismo según el proyecto realizado y lenguaje usado
permite hacer seguimiento únicamente de los archivos que lo precisen, omitiendo todo aquello que,
aun estando en el mismo directorio raíz, no sea necesario.
Aun con toda la ayuda que nos ofrece Git, sigue tratándose de una copia local en el disco duro de la máquina
de trabajo, la cual podría fallar.
6.2 Gestor de repositorios Git: GitLab
GitLab es un gestor de repositorios basado en web, con funcionalidades muy similares al más conocido
GitHub.
A diferencia de este, GitLab ofrece a sus usuarios la posibilidad de generar infitios repositorios privados de
forma totalmente gratuita. Además, al ser de código abierto, cualquiera puede instalarse dicho gestor en un
servidor propio, funcionando de forma totalmente indendiente.
Desde su interfaz web podemos gestionar todos nuestros proyectos (repositorios) y realizar multitud de
funciones: modificar su visibilidad, añadir nuevos desarrolladores con distintos permisos, analizar de forma
visual el detalle de cada commit o rama del proyecto, crear y mantener una wiki del proyecto con información
detallada del mismo, etc.
Figura 6-1. Vistazo a un commit en GitLab.
27
7 PROGRAMACIÓN
En esta sección se hablará de todo lo relativo a la programación propiamente dicha de la aplicación que da
origen a esta memoria. Comenzaremos con la aplicación Android, hablando de su interfaz y funcionalidad,
estructura de archivos, clases juto a su finalidad y recursos utilizados. Seguidamente se hablará del programa
desarrollado para ser ejecutado en la placa Arduino, con lo cual se cerraría la comunicación Bluetooth entre
dispositivo móvil y robot.
7.1 Aplicación móvil para dispositivos Android
7.1.1 Interfaz y funcionamiento
Nada más pulsar el icono de la aplicación se lanzará una primera pantalla, típicamente llamada Splash Screen,
que no es más que una primera introducción mostrada mientras se finaliza la carga de la interfaz de la
actividad principal.
Figura 7-1. Interfaz de la aplicación.
Splash Screen.
Figura 7-2. Interfaz de la aplicación.
Actividad principal – sin conexión.
En la Figura de la derecha se puede apreciar la interfaz ofrecida al usuario. Se trata de algo muy simple: tres
items informativos en la parte superior (estado de la conexión, dirección de movimiento y tiempo de bucle8 del
robot), dos opciones de control en la barra intermedia y una zona inferior vacía, destinada principalmente a
8 Tiempo que tarda la placa Arduino en completar el programa dentro de la estrcutra loop(). Es uno de los varios parámetros devueltos por el robot via Bluetooth.
Programación
28
contener el joystick virtual o bien dar detalles del acelerómetro.
En el lateral izquierdo nos encontramos con un menú cuyo objetivo es dirigir al usuario en el proceso de
conexión con el vehículo mediante Bluetooth.
Figura 7-3. Interfaz de la aplicación.
Menú lateral – activar Bluetooth.
Figura 7-4. Interfaz de la aplicación.
Solicitud de activación de la conectividad
Bluetooth.
Si la conectividad Bluetooth está desactivada en el terminal móvil, la aplicación ofrecerá la opción de habilitar
dicha conectividad. Nótese que pulsar en el botón “Activar” no hará que se active la conexión, sino que la
aplicación lanzará una petición (intent) al Sistema Android para realizar dicho proceso; una petición que
tendremos que responder nosotros como usuarios.
Si por el contrario ya teníamos activada dicha conectividad o aceptamos su activación, en este menú se
mostrará un listado con los dipositivos Bluetooth actualmente enlazados o vinculados.
Figura 7-5. Interfaz de la aplicación. Menú lateral – dispositivos vinculados.
29
29 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
Para iniciar la conexión basta con pulsar sobre uno de los dispositivos, identificándolo mediante su nombre o
MAC.
Si el dispositivo en cuestión no hubiese sido vinculado previamente, bajo esta lista estará la opción de ir a los
ajustes del Sistema Android y desde ahí vincular el dispositivo remoto.
Una vez establecida la conexión, nuevamente, se actualizará el menú que pasará a mostar los datos de la
conexión actual junto a la opción de finalizarla.
Figura 7-6. Interfaz de la aplicación. Menú lateral – conexión.
Durante este proceso el estado de la conexión Bluetooth será mostrado y actualizado igualmente en la pantalla
principal, terminando con el estado “conectado” al final del proceso.
Con la conexión establecida solo resta seleccionar un método de control, joystick o acelerómetro, y proceder a
dirigir el vehículo.
Figura 7-7. Interfaz de la aplicación. Actividad principal – conexión.
Programación
30
Figura 7-8. Interfaz de la aplicación.
Joystick.
Figura 7-9. Interfaz de la aplicación.
Acelerómetro.
Por último, pulsando en el icono de la llave inglesa situado en la parte superior derecha se lanzaría la pantalla
de ajustes de la aplicación.
Estos ajustes modifican varaibles de la ley de control implementada en la placa Arduino así como el UUID9
Bluetooth y la frecuencia de muestreo del acelerómetro.
Figura 7-10. Interfaz de la aplicación. Menú de ajustes.
9 Universally Unique Identifier (UUID)
31
31 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.2 Preliminares. Archivo manifiesto: AndroidManifest.xml
El archivo AndroidManifest.xml podría ser un buen punto de partida para realizar un primer acercamiento a la
aplicación. Aquí se define, por así decirlo, el esqueleto de la aplicación: los permisos solicitados, su nombre,
su icono, el tema aplicado y lo más importante las actividades que la componen y sus características.
En mención al autor original del vehículo autoequilibrado se mantuvo el nombre de la aplicación como “Mini
Segway”, pasando el paquete a llamarse “com.daniel.minisegway”.
Dado que la aplicación hará uso de la conectividad Bluetooth del dispositivo Android se ha de marcar tal
permiso como necesario. En este caso se precisan dos permisos relativos a Bluetooth:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
El primer permiso se necesita para poder establecer conexión con un dispositivo previamente emparejado. El
segundo posibilita la búsqueda de dispositivos y el emparejamiento con ellos.
Volviendo a la aplicación, su icono, nombre y tema se asignan como sigue:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/MyAppTheme">
...
</application>
Cabe destacar que se están utilizando referencias a recursos en esas líneas. Por ejemplo, el texto
@mipmap/ic_launcher se refiere a un archivo múltiple, dependiente de la resolución del dispositivo,
localizado en el directorio /res/mipmap. Otro ejemplo sería el texto @string/app_name, el cual se refiere a un
ítem dentro del archivo /res/values/strings.xml el cual se corresponde con el nombre de la aplicación en este
caso.
Programación
32
Dentro de la etiqueta application se listan las actividades que forman la aplicación, en este caso, tres
actividades:
<activity
android:name="com.daniel.minisegway.SplashScreenActivity"
android:screenOrientation="portrait"
android:theme="@style/MyAppTheme.SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.daniel.minisegway.MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/MyAppTheme.NoActionBar">
<intent-filter>
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
</intent-filter>
</activity>
<activity
android:name="com.daniel.minisegway.Preferences"
android:label="@string/app_name"
android:screenOrientation="portrait">
<!--android:parentActivityName=".MainActivity" >-->
<intent-filter>
<action android:name="sekth.droid.Preferences.AppPreferenceActivity" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
En cada una de las actividades se especifica:
Su “nombre”, o más concretamente la clase que define a la actividad.
Una etiqueta asociada a la actividad, usualmente mostrada en la barra superior como título.
La orientación de la pantalla, que en este caso se fuerza a ser vertical.
El tema aplicado.
Los intent10, o tipos de estos, a los que responde la actividad.
Sobre los intents, la primera de las actividades se declara como principal o inicial, y es a la que se tendrá
acceso desde el Launcher del dispositivo móvil (es decir, será la que se ejecute tras pulsar en el icono de la
aplicación). La segunda será receptora de cambios en la conectividad bluetooth del terminal. Y finalmente la
tercera se declara como actividad relativa a una sección de ajustes.
10 Descripción abstracta de una operación a realizarse. El ejemplo más simple es la solicitud de lanzar una actividad.
33
33 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.3 Actividades
Como se adelataba, la aplicación consta de tres actividades: SplashScreenActivity, MainActivity y Preferences.
7.1.3.1 SplashScreenActivity
Esta es la actividad lanzada tras pulsar el icono de la aplicación. Su función es muy sencilla: mostrar una
pantalla simple y atractiva mientras se espera a la carga de la actividad principal.
No se lleva a cabo ningún tipo de proceso, tan solo se lanza un intent con el objetivo de iniciar la actividad
principal. Completado dicho objetivo la actividad SplashScreenActivity es destruida.
7.1.3.2 MainActivity
Esta es la actividad principal, con la que el usuario podrá interactuar, gestionar la conexión Bluetooth y tomar
control del robot.
Es en esta actividad donde se instancian todas las clases utilizadas y se permite la interacción entre ellas.
7.1.3.3 Preferences
Por último encontramos la actividad que controla el menú de opciones. Se trata de una actividad simple, que
sigue las guias ofrecidas por Google a la hora de crear un listado de ajustes u opciones en una aplicación.
7.1.4 Clases Java
Dado que en Java toda funcionalidad se encuentra encapsulada en una clase, incluidas las actividades, el
objetivo de esta sección será listar y detallar las once clases que conforman la aplicación, comenzando con una
breve descripción y prosiguiendo con el detalle de sus atributos y métodos:
Bluetooth – Clase contenedora de todas las operaciones relativas a la conexión bluetooth.
ReceivedData – Clase contenedora y constructora del mensaje recibido por la aplicación via Bluetooth
desde el robot.
Message – Clase contenedora y constructora del mensaje enviado por la aplicación via Bluetooth
hacia el robot.
Joystick – Clase encargada de la gestión del método de control que usa un Joystick virtual.
Accelerometer – Clase encargada de la gestión del método de control que usa el acelerómetro del
dispositivo Android.
Movement – Clase donde se definen los aspectos relativos al movimiento del robot.
MainActivity – Clase correspondiente a la actividad principal de la aplicación.
Preferences – Clase correspondiente a la actividad que gestiona la sección de ajustes o preferencias de
la aplicación.
SplashScreenActivity – Clase correspondiente a una actividad auxiliar usada para lanzar la actividad
principal
Tools – Clase con un cojunto de herramientas de propósito general
BluetoothDeviceListCustomAdapter – Clase utilizada como adaptador a la hora de rellenar el ListView
que muestra los dipositivos bluetooth disponibles.
Programación
34
7.1.4.1 Bluetooth
La clase Bluetooth es la encargada de efectuar todas las operaciones referentes a la conectividad Bluetooth y es
también la clase receptora de los eventos relativos a dicha conectividad. Por ello, lo primero a destacar es que
esta clase hereda de la clase BroadcastReceiver.
public class Bluetooth extends BroadcastReceiver {
...
}
Continuando con los atributos de la clase y como en todas las demás clases, lo primero en definirse es el ID
asignado a los logs generados por la clase, algo muy útil a la hora de depurar la aplicación. En este caso:
private static final String LOG_ID = "MyApp.Bluetooth";
Tanto en esta como en otras clases que van a interactuar con la actividad principal se almacena el contexto de
la actividad principal (de la actividad MainActivity).
private final Context mainContext;
El resto de atributos son propios de la clase:
private BluetoothAdapter bluetoothAdapter;
private BluetoothSocket socket;
// Bandera para recordar el estado del socket (socket.isConnected() requiere API 14)
private Boolean socketIsConnected = false;
// Registros sobre la conexión existente
private String connectedDeviceName;
private String connectedDeviceMac;
// Valores para los Intent
protected static final String EVENT_GENERATED = BuildConfig.APPLICATION_ID +
".bluetooth.EVENT_GENERATED";
protected static final String EXTRA_EVENT = BuildConfig.APPLICATION_ID +
".bluetooth.EXTRA_EVENT";
// El UUID del modulo bluetooth de Arduino 00001101-0000-1000-8000-00805F9B34FB
private UUID BLUETOOTH_UUID;
// Nombre identificador para la conexión bluetooth
private static final String BLUETOOTH_APP_NAME = "MiApp";
// Codificación utilizada para traducir bytes a String
private static final String BYTE_CODIFICATION = "UTF-8";
La instancia de BluetoothAdapter permite interactuar con el estado de la conexión Bluetooth mientras que la
instancia de BluetoothSocket permite realizar la conexión y trasmitir/recibir datos.
Como se puede ver, por limitar el nivel de API a 13 se ha de gestionar manualmente el estado del socket
usando el flag socketIsConnected de la clase. A partir del nivel de API 14 se puede hacer uso del método
isConnected de la instancia de BluetoothSocket.
Otro detalle interesante son las cadenas de texto que van a definir la etiqueta asociada a los eventos (intents)
generados por esta clase. Estos textos se construyen a partir del nombre del paquete más un texto propio.
El UUID se precisa a la hora de obtener el socket de conexión con el dispositivo remoto. En este caso es una
cadena de texto fija dependiente del módulo Bluetooth conectado a la placa Arduino, aunque como se pudo
ver esta cadena puede ser modifiada desde los ajustes de la aplicación.
35
35 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
Como se adelantaba, esta clase generará eventos y dichos eventos se corresponden con los siguientes valores:
// Posibles estados del bluetooth
protected static final int ERROR = 0;
protected static final int NOT_SUPPORTED = 1;
protected static final int DISCOVERING = 2;
protected static final int ENABLED = 3;
protected static final int DISABLED = 4;
protected static final int CONNECTED = 5;
// Códigos de estado para la conexión
protected static final int CONNECTION_ATTEMPT_START = 10;
protected static final int CONNECTION_ATTEMPT_END = 11;
protected static final int SERVER_LISTENING_ATTEMPT_START = 12;
protected static final int SERVER_LISTENING_ATTEMPT_END = 13;
protected static final int CONNECTION_ERROR = 14;
protected static final int ERROR_CLOSING_SOCKET = 15;
protected static final int ERROR_CLOSING_SERVER_SOCKET = 16;
protected static final int CONNECTION_LOST = 17;
Todos ellos son valores estáticos y finales, accesibles a nivel de clase, sin precisar una instancia de la misma.
Cada uno define bien un estado de la conectividad Bluetooth o un estado de la conexión.
Los métodos de esta clase son más de los estrictamente necesarios. Esto es así porque originalmente las
pruebas de conectividad y transmisión de datos fueron realizadas conectando dos dispositivos móviles
Android entre si, de ahí la necesidad, por ejemplo, de generar un servidor de escucha Bluetooth.
public Bluetooth (Context context)
@Override
public void onReceive (Context context, Intent intent)
public void initialize ()
public void setUUID (String uuid)
public void connect (String mac, String name)
public void startServer ()
public void send (String data)
public void receive ()
public void closeSocket ()
public Set<BluetoothDevice> getBondedDevices ()
public int getStatus()
public String getConnectedDeviceName ()
public String getConnectedDeviceMac ()
public void enable (Activity activity)
public void enable ()
public void discard ()
private void sendBroadcastEvent (int state)
private void onDataReceived (String data)
El primer método, cuyo nombre es el mismo que la clase, es el constructor y su función aquí no es más que
inicializar el contexto de la actividad principal y obtener el bluetoothAdapter.
Los eventos relativos al cambio de estado en la conectividad Bluetooth son generados por el Sistema Android
y son recibidos y gestionados por el método onReceive. Por su parte, cada vez que se produce un cambio de
los registrados en los atributos anteriormente citados se ejecuta sendBroadcastEvent. Esto hace que se lance un
intent con información del cambio de estado, que como se verá más adelante es capturado por la actividad
principal para actualizar la interfaz de usuario. En cualquier caso, siempre se puede utilizar el método getStatus
para obtener el estado actual.
Recordando que estamos usando Java, los métodos enable son un claro ejemplo de otra característica de este
lenguaje no citada anteriormente: la sobrecarga de métodos. Si se les pasa un parámetro se solicitará, de parte
de la actividad indicada como parámetro, la activación de la conectividad bluetooth. Si no se indica parámetro,
se forzará la activación de la conectividad sin dar posibilidad al usuario de mediar en la decisión.
Programación
36
Para obtener un listado de los dispositivos bluetooth vinculados al terminal se dispone del método
getBondedDevices, que como se puede apreciar devuelve un conjunto o set de instancias BluetoothDevice.
El método connect es el encargado de iniciar el establecimiento de una conexión, usando una de las tareas
asíncronas que veremos más adelante.
Establecida una conexión, se pueden utilizar los métodos send y receive para enviar y recibir datos en forma
de cadenas de texto (nuevamente, aquí se usan tareas asíncronas), así como los métodos
getConnectedDeviceName y getConnectedDeviceMac para obtener el nombre y MAC del dispositivo. Cada
vez que se reciban datos via Bluetooth se pasarán al método privado onDataReceived.
El cierre de la conexión se hace mediante closeSocket, donde se capturará usando un bloque try/catch si la
operación es correcta. Sin embargo, si se desea no solo cerrar la conexión sino deshabilitar la instancia de la
clase, se dispone del método discard.
Además de estos métodos la clase Bluetooth posee cuatro clases privadas encargadas de las tareas asíncronas:
Connect, Server y Receive que heredan de AsyncTask y Send que hereda de Thread.
private class Connect extends AsyncTask<String, Integer, String> {
@Override
protected void onPreExecute() { ... }
@Override
protected String doInBackground(String... params) { ... }
@Override
protected void onProgressUpdate(Integer... info) { ... }
@Override
protected void onPostExecute(String result) { ... }
}
private class Server extends AsyncTask<String, Integer, String> {
...
}
private class Send extends Thread {
...
}
private class Receive extends AsyncTask<String, String, String>
...
}
Todas estas tareas deben ser asíncronas puesto que su duración es indefinida y no deben bloquear al hilo
principal.
Las tareas (clases) que heredan de AsyncTask implementan los métodos onPreExecute, doInBackground,
onProgressUpdate y onPostExecute, mientras que la tarea que heredan de Thread solo precisa del método run.
37
37 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.4.2 ReceivedData
El objetivo de esta clase es analizar los datos recibidos via Bluetooth y distribuir los valores obtenidos en las
variables correspondientes. Para ello se definen los siguientes atributos:
private static final String LOG_ID = "MyApp.ReceivedData";
/* ----- Parámetros enviados por el Arduino ----- */
private float Kp;
private float Ki;
private float K1;
private float K2;
private float K3;
private float c;
private float loop; // Tiempo bucle principal de Arduino (ms)
private int dir;
private float state; // Estado del sistema
private float xg;
private float xt;
// Almacena las cadenas recibidas para construir la cadena útil
private String dataBuffer = "";
// Tamaño máximo del buffer para datos recibidos
private final static int DATA_BUFFER_MAX_SIZE = 400;
// Separadores de parámetro y cadena utilizados en la RECEPCIÓN bluetooth
private final static String PARAMETER_SEPARATOR = ",";
private final static String CHAIN_SEPARATOR = "^";
private final static int NUMBER_OF_PARAMS_PER_CHAIN = 11;
// Expresión regular para filtrar las cadenas catalogadas como útiles
private final static String REGEX_FILTER = "[0-9,.^-]+";
Aquí se pueden apreciar las variables donde se almacenarán los parámetros extraidos de las cadenas de texto
recibidas. Además, en dichas cadenas se espera encontrar once parámetros separados por comas entre sí, con el
carácter “^” como fin/inicio de cadena.
Para poder analizar las cadenas de texto recibidas, estas se almacenan provisionalmente en dataBuffer, una
cadena de texto de tamaño fijo predefinido a 400 caracteres. Esto se hará siempre y cuando pasen por la
expresión regular usada como filtro, en este caso aceptando únicamente números o los símbolos “.”, “,”, “^” o
“-”.
Para esas operaciones se disponen los siguientes métodos:
public void decrypt (String data)
public float getLoopTime ()
public float getState ()
Con getLoopTime y getState se permite obtener los valores de los atributos loop y state respectivamente,
mientras que el método decrypt es el encargado de tratar las cadenas de texto recibidas como se ha indicado
anteriormente, obteniendo los valores útiles de las variables.
Programación
38
7.1.4.3 Message
Mientras que en la clase anterior se gestionaban los mensajes recibidos, en esta se generan los mensajes
(cadenas de texto) que serán enviados como ordenes a la placa Arduino.
En los atributos de la clase volvemos a encontrarnos con un esquema similar.
private static final String LOG_ID = "MyApp.Message";
// Contexto de la actividad principal
private Context mainContext;
/* ----- Parámetros que componen el mensaje ----- */
private float xg; // Velocidad de giro
private float xt; // Velocidad de traslación
private float Kp; // Constante del motor (1)
private float Ki; // Constante del motor (2)
private float K1; // Constante LQR (1)
private float K2; // Constante LQR (2)
private float K3; // Constante LQR (3)
private float c; // Constante global del LQR
private int dir; // Variable para la dirección (1-9)
// Separadores de parámetro y cadena utilizados en la TRANSMISIÓN bluetooth
private final static String PARAMETER_SEPARATOR = ",";
private final static String START_OF_COMMAND = "<";
private final static String END_OF_COMMAND = ">";
En este caso tenemos una colección de variables que serán utilizadas en las órdenes de control y un conjunto
de parámetros que definen la estructura de los mensajes de control. Así, dichos mensajes estarán compuestos
por nueve parámetros separados por comas y distribuidos en una cadena de texto que comienza con el carácter
“<” y termina con el carácter “>”.
Los parámetros enviados serán: velocidad de rotación (xg), velocidad de traslación (xt), constantes del motor
(Kp y Ki), constantes del LQR (K1, K2, K3 y c) y dirección de movimiento (dir).
Entre sus métodos:
public Message (Context context)
public void send ()
public void sendStopOrder ()
public void setMovement (float xg, float xt, int dir)
public void setLQR (float Kp, float Ki, float K1, float K2, float K3, float c)
Obviando el constructor, que simplemente inicializa los atributos, nos encontramos con dos métodos para
enviar mensajes y dos métodos para modificar los valores de los atributos.
El método send hace uso de la instancia de la clase Bluetooth localizada en la actividad principal para enviar el
mensaje. Esto se puede hacer gracias a que dicha instancia se cataloga como static y por tanto se puede
acceder a ella a nivel de Clase.
Los métodos setMovement y setLQR alteran los valores de los atributos del mensaje, de forma que si hay
cambio alguno se ejecuta inmediatamente el método send, enviando los nuevos valores al robot.
El método sendStopOrder por su parte, hace uso del método setMovement para establecer el reposo como
estado objetivo y si este es distinto del estado previo dicha orden será enviada via Bluetooth.
39
39 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.4.4 Joystick
El Joystick es uno de los dos métodos de control disponibles. Este se entiende como un espacio de la pantalla
reservado para realizar movimientos o desplazamientos con un dedo u objeto capaz de interactuar con la
pantalla. Dado que esta clase necesita recibir las acciones realizadas sobre la pantalla táctil del dispositivo, es
necesario implementar la interfaz OnTouchListener.
public class Joystick implements OnTouchListener {
...
}
En cuanto a sus atributos de clase:
private static final String LOG_ID = "MyApp.Joystick";
// Contexto de la actividad principal
private Context mainContext;
// Layout donde se representará el joystick
private ViewGroup joystickLayout;
// Objetos a representar
private Stick stick;
private Border border;
// Tinte a emplear
private Paint stickPaint;
private Paint borderPaint;
// Posición central del joystick en el layout
private float joystickX;
private float joystickY;
// Posición del "stick" relativa al centro del joystick (coordenadas polares)
private float stickLength;
private double stickAngle;
// Medidas del joystick
private int stickRadius;
private int borderRadius;
// Longitud del "stick" a partir de la cual se descarta el estado de reposo
private int restBreakPoint;
Como se decía, se precisa un espacio reservado para este joystick virtual. Dicho espacio se entenderá como
una instancia de la clase ViewGroup, clase base para layouts y contenedores de vistas.
Se definen también los dos elementos que formarán el joystick: el puntero o stick y el borde o círculo que
limita el movimiento del puntero. Cada uno es en sí una instancia de la clase Stick y Border respectivamente.
La información sobre el color y estilo usados en su representación es contenida en instancias de la clase Paint.
El resto de parámetros se corresponden con posiciones relativas a la vista y a su centro, usadas para el cálculo
de la dirección solicitada, así como velocidad de traslación y/o rotación.
El último de los atributo, restBreakPoint, define la separación mínima que ha de alcanzarse respecto al centro
del joystick para considerarse que se sale del estado de reposo.
Programación
40
Esta clase dispone de dos constructores. El primero está diseñado para indicar expresamente todos los
parámetros del joystick: contexto de la actividad principal, vista donde se situará, radio del puntero, radio de la
circunferencia límite y la separación mínima para salir del reposo. El segundo constructor calcula las
dimensiones anteriores a partir de las dimensinoes de la vista y es el que finalmente se ha utilizado para tener
un joystick de tamaño proporcional a la pantalla donde se representa.
public Joystick(Context context, ViewGroup joystickLayout, int stickRadius, int
borderRadius, int restBreakPoint)
public Joystick(Context context, ViewGroup joystickLayout)
Continuando con sus métodos tenemos:
private void initialize()
public void disable ()
public boolean onTouch (View view, MotionEvent event)
private void analyzeMeasures()
private void draw (View view)
Los dos primeros a destacar son initialize y disable. El primero se encarga de crear las instancias de Stick y
Border acordes a los parámetros previamente definidos e iniciar la escucha de eventos en la pantalla táctil, que
serán recibidos por esta misma clase. El segundo destruye las vistas que hayan podido generarse y deshabilita
la escucha.
Cada vez que se recibe un evento se ejecuta onTouch; método que implementa el definido en la interfaz
OnTouchListener. Aquí se distingue entre el primer contacto con la pantalla, un desplazamiento sobre esta o
una perdida de contacto, dibujando el joystick en su posición, desplazándolo o eliminándolo respectivamente.
Para dibujar estos elementos se utiliza el método draw, pasándole como parámetro el ítem a dibujar (stick o
borde).
Cada vez que se recibe un evento relativo al desplazamiento sobre la pantalla se lanza el método
analyzeMeasures. Es aquí donde se analiza el desplazamiento relativo del stick o puntero respecto al punto
central inicial donde se dibujó el joystick cuando se pulsó por primera vez en la pantalla.
Por su parte, las clases Stick y Border heredan de la clase View, ya que al fin y al cabo son ítems que se
pretende dibujar en la pantalla. Para ello implementan dos métodos: onDraw y setPosition. El primero dibuja
un círculo con cierto radio y color y el segundo ajusta el centro de dicho círculo.
private class Stick extends View {
private float x;
private float y;
private Stick(Context context) { ... }
public void onDraw (Canvas canvas) { ... }
public void setPosition (float x, float y) { ... }
}
private class Border extends View {
private float x;
private float y;
private Border(Context context) { ... }
public void onDraw (Canvas canvas) { ... }
public void setPosition (float x, float y) { ... }
}
41
41 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.4.5 Accelerometer
El segundo método de control consiste en hacer uso del acelerómetro disponible en el terminal Android para
calcular una dirección de movimiento, así como una velocidad de traslación y/o rotación. Por ello,
nuevamente, necesitamos implementar una interfaz, en este caso SensorEventListener.
La implementación de esta interfaz nos da las herramientas (métodos) necesarios para recibir valores de los
distintos sensores disponibles en la plataforma de ejecución. En este caso únicamente nos centraremos en
valores proporcionados por el acelerómetro.
public class Accelerometer implements SensorEventListener {
...
}
Los atributos de esta clase son los que siguen:
private static final String LOG_ID = "MyApp.Accelerometer";
// Contexto de la actividad principal
private final Context mainContext;
// Layout donde representar gráficos del acelerómetro
private ViewGroup accelerometerLayout;
// TextView donde mostrar el refresco de los datos del acelerómetro
private TextView sensorDelay;
private SharedPreferences sharedPreferences;
// Gestor del sensor
protected SensorManager sensorManager;
protected Sensor sensor;
// Flag para determinar si se ha realizado una calibración previa
private boolean calibrated = false;
// Datos obtenidos del acelerómetro
private float gravity_x;
private float gravity_y;
private float gravity_z;
// Posición de reposo introducida por el usuario
private float gravity_x_anchor;
private float gravity_y_anchor;
private float gravity_z_anchor;
// Almacena el tiempo en ms en que se ha recibido respuesta del sensor
private long timeStamp;
// Incremento necesario en los valores de gravedad para determinar el cambio en la
dirección
private static final float sensitivity = 1.5f;
// Valor de la gravedad (m/s^2)
private static final float gravity = 9.81f;
// Constante de tiempo para el filtro paso de baja (milisegundos)
private static final int lowPassFilterTimeConstant = 500000;
Al igual que sucedía con el joystick, necesitamos un layout o vista donde situar el control por acelerómetro,
aunque en este caso dicha vista tendrá un uso diferente al anteriormente visto (no vamos a dibujar nada).
Aquí aparece por primera vez la clase SharedPreferences, que se emplea para acceder a los ajustes de la
aplicación. Como vimos en los detalles de la interfaz, la aplicación tiene una pantalla de ajustes y entre estos
ajustes está la tasa de refresco del acelerómetro; un valor que será utilizado en esta clase.
Programación
42
Las medidas inerciales dadas por Android se distribuyen en tres ejes:
Eje X – Paralelo a la pantalla y perpendicular a los laterales.
Eje Y – Paralelo a la pantalla y a los laterales del dispositivo.
Eje Z – Perpendicular a la pantalla del dispositivo.
Figura 7-11. Ejes de las medidas inerciales en Android.
Por ello tendremos tres variables que almacenarán los valores instantáneos y otras tres para almacenar los
valores del punto de referencia. Por defecto se parte de que no se ha “calibrado” el acelerómetro, es decir, no
se tiene una referencia a partir de la cual calcular una diferencia. Esta “calibración” se realiza ejecutando el
método setReference, el cual almacena las medidas actuales como punto de referencia.
Cabe mencionar que, en general, las medidas obtenidas tienden a ser muy dispares. Por ello se usará un filtro
paso de baja a la hora de actualizar los valores finales de la medición.
public Accelerometer (Context context, ViewGroup accelerometerLayout)
private void initialize ()
public void disable ()
@Override
public void onAccuracyChanged (Sensor sensor, int accuracy)
@Override
public void onSensorChanged (SensorEvent event)
private void analyzeMeasures ()
public void setReference()
Como sucedía con el joystick se tiene un método, initialize, para iniciar la escucha de eventos y otro método,
disable, para deshabilitarla.
Gracias a implementar la interfaz SensorEventListener podemos usar los métodos onAccuracyChanged y
onSensorChanged, los cuales son lanzados cuando se modifica la precisión del sensor y cuando se tiene una
nueva medida, respectivamente.
También volvemos a tener el método analyzeMeasures que cumple una función similar al situado en la clase
Joystick; en este caso, traducir las medidas inerciales junto al punto de referencia en una dirección y velocidad
de traslación y/o rotación.
43
43 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.4.6 Movement
El objetivo de las clases Joystick y Accelerometer es proporcionar un mecanismo de control y, en definitiva,
proporcionar una dirección de movimiento y una velocidad relativa de traslación y rotación. Para unificar estas
medidas se tiene la clase Movement.
private static final String LOG_ID = "MyApp.Movement";
// Valor mínimo que pueden tomar la velocidad de traslación o de rotación,
respectivamente
protected static final int MIN_TRANSLATION_SPEED = 0;
protected static final int MIN_TURN_SPEED = 0;
// Valor máximo que pueden tomar la velocidad de traslación o de rotación,
respectivamente
protected static final int MAX_TRANSLATION_SPEED = 4;
protected static final int MAX_TURN_SPEED = 4;
// Posibilidades de movimiento aceptadas por Arduino
protected static final int FORWARD_PLUS_LEFT = 1;
protected static final int FORWARD = 2;
protected static final int FORWARD_PLUS_RIGHT = 3;
protected static final int TURN_LEFT = 4;
protected static final int STILL = 5;
protected static final int TURN_RIGHT = 6;
protected static final int BACKWARD_PLUS_LEFT = 7;
protected static final int BACKWARD = 8;
protected static final int BACKWARD_PLUS_RIGHT = 9;
Aquí se definen las velocidades mínimas y máximas de traslación y rotación, junto a las nueve posibles
direcciones originalmente establecidas. Como se verá más adelante, dichas direcciones pasarán a ser
redudantes y serán ignoradas por el robot tras actualizarse su programa.
Los dos métodos que ofrece permiten traducir una velocidad relativa entre 0.0 y 1.0 a una velocidad útil de
cara a las órdenes, entre el mínimo y el máximo posible.
public static float getTranslationSpeed (float relativeTranslationSpeed)
public static float getTurnSpeed (float relativeTurnSpeed)
Programación
44
7.1.4.7 MainActivity
La clase MainActivity compone la actividad principal de la aplicación. Dado que hablamos de una aplicación,
esta clase debe heredar de alguna de las clases propias de una actividad, en este caso, de AppCompatActivity.
Se selecciona tal clase para heredar de ella porque es la clase base para actividades con características
incorporadas en su action bar, siendo esta la sección superior de la aplicación – la barra que contiene el título
de la aplicación y otras herramientas como en este caso el acceso a las opciones.
public class MainActivity extends AppCompatActivity {
...
}
Además de los atributos vistos anteriormente, esta clase, una vez instanciada, contiene las instancias de todos
los elementos de la interfaz de usuario con los que el usuario puede interactuar: vistas o layouts y widgets. Esto
es así debido a que en Android solo el hilo principal de la aplicación puede realizar modificaciones sobre la
UI11. El resto de clases, o instancias de las mismas, utilizan el contexto principal para interactuar con dichos
elementos si así fuese necesario.
private static final String LOG_ID = "MyApp.MainActivity";
private SharedPreferences sharedPreferences;
// Receptor de anuncios enviados a difusión
private BroadcastReceiver localBroadcastReceiver;
// Ventana lateral
private DrawerLayout drawer;
// Vistas dinámicas
private ViewAnimator bluetoothViewAnimator;
private ViewAnimator controlViewAnimator;
// Vistas de los ViewAnimator
private final static int BLUETOOTH_VIEW_ANIMATOR_DISABLED = 0;
private final static int BLUETOOTH_VIEW_ANIMATOR_ENABLED = 1;
private final static int BLUETOOTH_VIEW_ANIMATOR_CONNECTED = 2;
private final static int CONTROL_VIEW_ANIMATOR_JOYSTICK = 0;
private final static int CONTROL_VIEW_ANIMATOR_ACCELEROMETER = 1;
private final static int CONTROL_VIEW_ANIMATOR_NO_CONTROL_METHOD = 2;
// Máximo de dispositivos a listar
private static final int MAX_BLUETOOTH_DEVICES_TO_LIST = 3;
// Layouts
private RelativeLayout joystickLayout;
private RelativeLayout accelerometerLayout;
// Widgets
private ListView bondedDevicesList;
private ProgressBar progressBar;
private TextView connectedDeviceName;
private TextView connectedDeviceMac;
private TextView bluetoothStatus;
protected static TextView direction;
protected static TextView arduinoLoop;
private RadioGroup controlTypeSelector;
11 User Interface o Interfaz de Usuario.
45
45 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
Esta actividad usa dos ViewAnimator en su interfaz para condensar la información en la pantalla. Estos no son
más que clases que permite animar la transición entre las vistas que contiene. Uno de ellos se emplea en la
pantalla principal, para alternar la entre control via joystick y control via acelerómetro. El otro se utiliza en el
menú lateral izquierdo y alterna su contenido desde la solicitud de activación de la conectividad Bluetooth,
hasta la visualización del estado de la conexión, pasando por el listado de dispositivos emparejados.
Para la gestión de todo el sistema se emplean cinco objetos, instancias de las clases vistas hasta el momento:
// Objetos para la gestión del sistema
protected static Bluetooth bluetooth;
protected static Message message;
protected static ReceivedData receivedData;
protected Accelerometer accelerometer;
protected Joystick joystick;
Cabe destacar que bluetooth, message y receivedData son de tipo static puesto que solo se instancian una
única vez al cargarse la aplicación y de este modo se permite su acceso desde otras clases.
@Override
public void onCreate(Bundle savedInstanceState) { ... }
El punto crítico de esta clase es la creación de la actividad. En este método, onCreate, se llevan a cabo los
siguientes procesos en el siguiente orden:
1. Se establece el layout de la actividad.
2. Se fuerza a mantener activa la pantalla.
3. Se obtiene el objeto sharedPreferences necesario para interactuar con los valores dados a los ajustes
de la aplicación.
4. Se asigna como ActioBar la barra de herramientas con el botón para acceder a los ajustes.
5. Se configura el menú lateral izquierdo para que actúe como tal.
6. Se configuran los dos viewAnimator.
7. Se obtiene la referencia a los layouts del joystick y acelerómetro, para posteriormente instanciar en
ellos dichos “widget”. Se asignan también todos los widgets de la vista a su atributo correspondiente
en la clase para facilitar su acceso más adelante.
8. Usando el atributo Build.VERSION.SDK_INT se comprueba si el nivel de API del Sistema Operativo
destino es inferior a 21; en dicho caso se fuerza a que el texto de los botones pase a minúsculas. Esto
se requiere nuevamente porque hasta ese nivel de API el texto de los botones se muestra en
mayúsculas por defecto.
9. Se crean las instancias de las clases Bluetooth, Message y ReceivedData y se inicializa el objeto
bluetooth.
10. Se inicializa el listado de dispositivos bluetooth vacío y se asocia un listener ante las pulsaciones en
sus ítems, el cual lanzará el método connect de la clase Bluetooth, usando los datos del dispositivo
Bluetooth sobre el que se pulsó.
11. Se asocia un listener ante la pulsación sobre el estado de la conexión Bluetooth. Esto abrirá el menú
lateral para proseguir con la conexión o ver su estado detallado.
12. Se asocia un listener ante la pulsación sobre alguno de los botones para el cambio de método de
control.
13. Finalmente, se crea y registra un receptor de mensajes a difusión locales, localBroadcastReceiver,
para recibir cambios en el estado de la conectividad bluetooth. Estos cambios serán lanzados por la
instancia generada de la clase Bluetooth. Ante dichos cambios se ejecutará el método
onBluetoothEventReceived indicando el código del evento.
Programación
46
Si continuamos con los métodos propios de la actividad nos encontramos con onResume y onDestroy.
@Override
protected void onResume()
@Override
protected void onDestroy()
Tras crearse la aplicación, o bien regresar de un segundo plano, se ejecuta el método onResume. Aquí se fuerza
siempre a obtener el estado de la conexión Bluetooth para poder actualizar la interfaz de usuario según
corresponda. Además, se actualizan todos los valores que encontramos en la pantalla de ajustes.
Al destruirse la aplicación, o más bien justo antes de ser destruida, se desactiva completamente la clase y se
deja de estar a la escucha de eventos procedientes de ella.
Los métodos asociados a los botones que encontramos en el menú lateral son: buttonBluetoothSettings,
buttonEnableBluetooth y buttonCloseConnection. Cada uno hace lo que se puede deducir: lanzar los ajustes
del Sistema Android relativos a la conectividad Bluetooth, solicitar al sistema la activación de la conectividad
Bluetooth y cerrar la conexión, respectivamente.
public void buttonBluetoothSettings(View view)
public void buttonEnableBluetooth(View view)
public void buttonCloseConnection(View view)
La actualización del listado de dispositivos, la selección de un método de control y la desactivación del mismo
se realiza usando los métodos: updateBondedDevicesList, updateControlLayout y disableControls.
Aquí se ha de incidir en que deshabilitar los métodos de control, disableControls, no solo impide al usuario
controlar el robot sino que le envía la orden de volver al estado de reposo.
private void updateControlLayout()
private void disableControls()
public void updateBondedDevicesList()
Como se mencionó, la interfaz va actualizandose conforme se reciben ciertos eventos o sucesos desde el objeto
bluetooth. El método onBluetoothEvenReceived analiza el evento y actúa frente a él; así por ejemplo cuando se
inicia la conexión se activa una barra de progreso y cuando finaliza se oculta además de mostrarse un mensaje
de éxito o error.
Por otro lado la aplicación también responde a otro tipo de eventos: cambios en el foco de la interfaz principal.
El foco se pierde no solo tras enviar la aplicación a un segundo plano, también al abrir el menú lateral o
desplegar la barra de estado del propio Sistema Android; en definitiva, cualquier suceso que “oculte” la
interfaz principal.
Siempre que se pierde el foco se deshabilitan los métodos de control. Esto se hace para evitar enviar órdenes
de control erróneas al robot mientras, por ejemplo, desplegamos la barra de notificaciones de nuestro terminal
para ver un mensaje o accedemos a los ajustes de la aplicación.
private void onBluetoothEventReceived(int event)
public void onWindowFocusChanged(boolean hasFocus)
47
47 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
Tras estos nos encontramos con tres métodos creados usando la guía de Android y cuyo objetivo es generar y
responder ante eventos en el menú de opciones. Un menú que en este caso está formado únicamente por un
ítem del cual solo se ve su icono: la llave inglesa en la parte superior derecha de la interfaz.
@Override
public boolean onCreateOptionsMenu(Menu menu)
@Override
public boolean onOptionsItemSelected(MenuItem item)
Por último queda una pequeña funcionalidad para la cual se sobreescribe el método onBackPressed, de modo
que su acción por defecto (salir de la aplicación) no se ejecute si tenemos abierto el menú lateral. En dicho
caso, en lugar de salir de la aplicación se cerrará tal menú.
@Override
public void onBackPressed()
Programación
48
7.1.4.8 Preferences
Esta clase forma la tercera actividad que encontramos en la aplicación y es la encargada de administrar la
pantalla de opciones ofrecida por esta.
Para ello lo primero es heredar de la clase PreferenceActivity, que es la clase base para una actividad que
pretende mostrar una colección de opciones al usuario
Adicionalmente es necesario implementar la interfaz OnSharedPreferenceChangeListener, a fin de actuar
inmediatamente cada vez que un ajuste es modificado por el usuario.
public class Preferences extends PreferenceActivity implements
OnSharedPreferenceChangeListener {
...
}
Esta vez solo tenemos un atributo, la mencionada etiqueta para los logs de la clase:
private static final String LOG_ID = "MyApp.Preferences.java";
Dando un vistazo a los métodos nos encontramos:
@Override
public void onCreate(Bundle savedInstanceState)
// Listener llamado ante la modificación de algún parámetro de ajuste
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String
key)
private void initSummary(Preference p)
private void updatePrefSummary(Preference p)
protected void setValue (SharedPreferences sharedPreferences, String key, String
value)
@Override
protected void onResume()
@Override
protected void onPause()
Como se trata de una actividad, se ha de implementar obligatoriamente el método onCreate. En él se establece
el layout de la actividad y se inicializa el listado de preferencias con sus valores almacenados usando el
método initSummary.
Cada vez que se altera el valor de un ajuste se lanzará el método onSharedPreferenceChanged, el cual
determina el ajuste que ha sido editado y si no es nulo actualiza el valor usando updatePrefSummary.
Los métodos onResume y onPause son utilizados para habilitar y deshabilitar la escucha de cambios en los
ajustes, evitando el uso de recursos del sistema si la actividad de ajustes no es interactiva de cara al usuario.
49
49 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.4.9 SplashScreenActivity
La clase SplashScreenActivity define la primera de las actividades de la aplicación y con ello la más simple. Su
código se resume como sigue:
public class SplashScreenActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivity(new Intent(this, MainActivity.class));
finish();
}
}
Su única función es ser ejecutada e inmediatamente lanzar la actividad principal, destruyéndose a sí misma en
el proceso.
Esto, que en un principio no tiene sentido, se hace simplemente para mostrar al usuario la “vista” asociada a
esta actividad; una vista que está vacía pero gracias al tema aplicado hace que se muestre inmediatamente una
imagen de fondo formada por un bitmap personalizado.
Programación
50
7.1.4.10 Tools
El objetivo inicial de esta clase era contener todas aquellas funciones auxiliares que puediesen ser necesarias
pero sin tener un propósito específico que las ligase a alguna de las clases anteriores. Al final solo úna función
llegó a cumplir dichos requisitos de generalidad:
public static boolean setListViewHeight(ListView listView, int maxItemsToShow)
Su utilidad se basa en, dado una instancia de la clase ListView, modificar el alto de su vista asociada para
mostrar exactamente la cantidad de ítems indicada.
El único uso que se hace de esta función es a la hora de listar los dispositivos bluetooth vinculados con el
terminal. De este modo en el listado solo se verá completamente la información de maxItemsToShow
dispositivos (tres en el programa actual), necesitando hacer scroll para visualizar el resto.
51
51 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.4.11 BluetoothDeviceListCustomAdapter
Cuando se listan los dispositivos Bluetooth vinculados con el terminal móvil se hace usando una instancia de
ListView. Un ListVIew define un grupo de vistas que muestra una lista de elementos desplazables, donde los
elementos de la lista son insertados con un Adapter que toma a su vez el contenido de una determinada fuente.
Esta clase define el Adapter personalizado usado para rellenar la lista de dispositivos Bluetooth. Por ello la
clase hereda de BaseAdapter, que como se puede deducir es la clase base o común a la hora de crear un
Adapter.
public class BluetoothDeviceListCustomAdapter extends BaseAdapter {
...
}
Sus atributos son el ya mencionado ID de logs, el conjunto de dispositivos bluetooth a introducir en la lista y el
layout o vista final generada.
private static final String LOG_ID = "MyApp.BLCustomAdapter";
private ArrayList<BluetoothDevice> bondedDevices;
// inflater para crear un view a partir de xml
private static LayoutInflater inflater = null;
En cuanto a sus métodos tenemos:
public BluetoothDeviceListCustomAdapter(Context context, Set<BluetoothDevice>
bondedDevices)
@Override
public int getCount()
@Override
public Object getItem (int position)
@Override
public long getItemId (int position
@Override
public View getView(final int position, View convertView, ViewGroup parent)
El constructor de la clase toma el listado de dispositivos Bluetooth dado y obtiene una instancia de
LayoutInflater usando el contexto recibido como parámetro.
El resto de métodos sobreescriben los heredados de BaseAdapter a fin de dotar a la clase de la funcionalidad
propia de un Adapter. De ellos el interesante es getView, pues es el que toma la vista (archivo xml) que define
un ítem de la lista e introduce en ella el nombre del dispositivo y su MAC, devolviendo la vista generada para
su inserción en el ListView.
El intermediario entre los datos a introducir y la vista final es la denominada clase Holder. Esta no es más que
un contenedor con dos TextView o vistas de texto que serán enlazadas a los correspondientes TextView de la
vista definida en xml para un ítem de la lista.
// Clase para almacenar los datos relativos a una entrada en el ListView
public class Holder {
TextView name;
TextView mac;
}
Programación
52
7.1.5 Recursos
En el directorio res (resources o recursos) del proyecto encontramos los archivos XML que definen las vistas
de la aplicación, así como cualquier otro recurso incorporado a esta más allá del propio código Java: imágenes,
figuras geométricas, estilos, colores, cadenas de texto, valores numéricos (dimensiones), etc. Dentro de este
directorio encontramos un conjunto de sub-directorios que clasifican el contenido:
drawable – Aquí se situan los gráficos usados por la aplicación que son independientes de la densidad
de píxeles.
layout – Contiene los archivos xml que definen las vistas de la aplicación.
menu – Archivos xml que definen menús.
mipmap – Contiene el/los icono/s de la aplicación.
values – Contiene archivos xml que definen distintos tipos de variables: cadenas de texto,
dimensiones, estilos, etc. Encontramos archivos tales como: strings.xml, dimens.xml, styles.xml,
colors.xml, themes.xml, etc.
xml – Archivos xml varios.
7.1.5.1 Drawable
En este directorio nos encontramos con los siguientes archivos:
rounded_button.xml – Define los estilos aplicados a los botones de la aplicación
rounded_button_only_bottom.xml – Define el mismo estilo anterior pero con solo la parte inferior
redondeada.
rounded_button_only_top.xml – Igual que el anterior pero en este caso solo la parte superior está
redondeada
splash_screen_background.xml – Fondo aplicado a la actividad SpashScreenActivity.
ui_background.xml – Define los estilos usados en cada bloque de la aplicación; da un aspecto de
tarjetas a los bloques con esquinas redondeadas y sombra bajo ellos.
segway.png – Imagen en formato png del vehículo autoequilibrado.
7.1.5.2 Layout
Las vistas usadas en esta aplicación son las que siguen:
bluetooth_list_view_device_info.xml – Define la representación en pantalla de cada ítem en el listado
de dispositivos bluetooth.
main_activity_content.xml – Vista principal de la aplicación. Está compuesta por tres bloques: datos
de la conexión y orden de movimiento, botones para seleccionar la opción de control y espacio
reservado para el joystick o información del acelerómetro.
main_activity_app_bar.xml – La vista anterior (main_activity_content.xml) más la barra superior de la
aplicación.
header.xml – Vista de la cabecera (imagen y título) situada en el menú lateral izquierdo.
main_activity_nav.xml – Vista anterior (header.xml) más el resto del menú lateral izquierdo.
main_activity.xml – Vista conjunta de la interfaz principal (main_activity_app_bar.xml) más el menú
lateral izquierdo completo (main_activity_nav.xml). Este es el layout utilizado por la actividad
MainActivity.
plus_less_button.xml – Vista auxiliar utilizada orginalmente para diseñar un widget personalizado, el
cual consta de dos botones que incremente/decrementan un valor mostrado entre ellos.
53
53 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
7.1.5.3 Menu
Aquí solo encontraremos un archivo, settings.xml, pues esta aplicación solo consta de un menú. Dicho menú es
simplemente el icono de la llave inglesa que encontramos en la parte superior derecha en la interfaz principal
de la aplicación.
7.1.5.4 Mipmap
Aquí está el icono de la aplicación en distintas resoluciones, cargándose uno u otro dependiendo de la densidad
de píxeles de la pantalla del dispositivo donde sea instalada.
Figura 7-12. Icono de la aplicación.
7.1.5.5 Values
Contiene los archivos:
colors.xml – Como se puede deducir del nombre aquí se definen los colores utilizados en la
aplicación.
dimens.xml – Aquí se especifican dimensiones estándar para los ítems o widgets usados en las vistas,
así como tamaños de letra.
strings.xml – Este documento contiene todas las cadenas de texto usadas por la aplicación, ya sea en la
propia interfaz o en mensajes emergentes o Toasts12.
styles.xml – Aquí se definen los temas usados por la aplicación. Cada tema se puede componer de un
gran abanico de opciones que sobreescribirán a los estilos por defecto de, por ejemplo, botones u otros
widgets, colores, etc.
7.1.5.6 Xml
Por último en este directorio encontraremos el archivo preferences.xml. En él se definen los parámetros que se
listarán en la pantalla de ajustes, indicando su tipo y valor por defecto entre otros aspectos. Estos a su vez están
clasificados, en este caso, en dos secciones como se pudo ver en el vistazo a la interfaz.
12 Pequeño mensaje emergente que aparece normalmente en la parte inferior de la pantalla.
Programación
54
7.2 Programa Arduino
El programa estaba formado por tres archivos principales de código fuente:
PI_modificado.ino – Archivo principal del proyecto.
Transf_bluetooth.ino – Define dos funciones: RecepcionBT() y EnvioBT(). Usadas para recibir y
enviar datos a través del puerto serie conectado al módulo Bluetooth.
Mando_Motor.ino – Define la función Mando_Motor(). Esta función se encarga de establecer las
referencias para los motores y controlar con ello la dirección y velocidad de movimiento/rotación del
robot.
Originalmente, la segunda parte de este proyecto solo consistía en modificar el programa ejecutado en la placa
arudino, a fin de que fuese posible la comunicación entre este y la aplicación móvil. Para ello tan solo se
realizaron unas sencillas modificaciones en las funciones RecepcionBT y EnvioBT variando el número de
parámetros que se esperaba recibir y el número de parámetros que se debía enviar.
Sin embargo, futuras decisiones hicieron que se llevase a cabo tres modificaciones:
1. Primero se rediseñó el código fuente del archivo Mando_Motor.ino, a fin de eliminar la dirección
como parámetro de control, usando únicamente la referencia de velocidad de traslación y rotación,
que seguirían siendo de tipo flotante pero ahora con signo (para distinguir entre adelante/atrás e
izquierda/derecha). Esto dio como resultado un nuevo fichero, control_motor.ino, junto a una nueva
función: updateEngineReferences().
2. Seguidamente se crearon funciones intermedias para manipular los valores recibidos via Bluetooth y
aplicar un filtro de paso de baja a las referencias de velocidad recibidas via Bluetooth. Todas ellas se
encuentran en asignacion_variables.ino.
3. Por último se reescribó el archivo Transf_bluetooth.ino, sustituyendo las funciones bloqueantes
Serial.parseInt() y Serial.parseFloat() por otras personalizadas que buscarían realizar la misma
función sin detener potencialmente la ejecución del programa. Este nuevo archivo se llamó
bluetooth.ino.
7.2.1 Detalle de las nuevas rutinas
7.2.1.1 Respuesta a las órdenes de movimiento
Tras eliminar la dirección como parámetro y pasar a tener referencias de velocidad de traslación y rotación con
signo, basta con utilizar dicho signo para inferir la dirección del movimiento o el sentido de giro. En este caso
se utilizan valores positivos en la velocidad de traslación, xt, para iniciar un movimiento hacia delante y
valores positivos de la velocidad de traslación, xg, para iniciar una rotación a la izquierda. Signos opuestos
implican direcciones opuestas.
void updateEngineReferences () { dtheta_r = 0.76*PI*xt; u_mi = -u_m - 10.0*xg; u_md = -u_m + 10.0*xg; if (u_mi>=0) { digitalWrite(PinIn1I, HIGH); digitalWrite(PinIn2I, LOW); } else { digitalWrite(PinIn1I, LOW); digitalWrite(PinIn2I, HIGH); } if (u_md>=0) { digitalWrite(PinIn1D, HIGH); digitalWrite(PinIn2D, LOW);
55
55 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
} else { digitalWrite(PinIn1D, LOW); digitalWrite(PinIn2D, HIGH); } PWMI = 14 + abs(u_mi); PWMD = 14 + abs(u_md); if (PWMI > 255) { PWMI = 255; } if (PWMD > 255) { PWMD = 255; } if (abs(kalAngleY) > 30) { PWMI = 0; PWMD = 0; ivr = 0; } analogWrite(PinPWMI, PWMI); analogWrite(PinPWMD, PWMD); }
7.2.1.2 Recepción y envío de mensajes
En el nuevo archivo bluetooth.ino se siguen manteniendo tanto la función original para el envio de mensajes
como la función para la recepción de mensajes que usa llamadas bloqueantes.
Esta última sin embargo se acompaña ahora de un conjunto adicional de funciones que hacen uso de un buffer,
un array de caracteres, intero para extraer de él los datos útiles recibidos. Entre ellas:
updateBuffer() – Toma byte a byte todos los datos ofrecidos por el módulo Bluetooth y los guarda en
en el array de caracteres que hace las veces de buffer, sobreescribiendo los datos iniciales del mismo
al llenar dicho array (funciona como una “lista” circular).
processBuffer() – Analiza los datos (caracteres) almacenados en el array y busca en ellos un conjunto
que se corresponda con el modelo que se ha seguido: carácter de inicio de cadena (carácter “<”) más
una serie de valores (enteros o flotantes con o sin signo) seguidos de un separador de valor (coma) si
hubiese otro valor tras él y finalmente el cierre de cadena (carácter “>”). Si encuentra dicha cadena
toma los valores presentes en ella y los asigna a cada una de las variables de la sentencia de control.
Programación
56
57
8 CONCLUSIONES
Este proyecto parte, en cieta medida, de una de las propuestas de mejora del proyecto original que completó el
desarrollo del vehículo autoequilibrado. En ella se mencionaba una futura aplicación, que hiciese uso de los
sensores incorporados hoy día en la mayoría de los smarthphones y diese la posibilidad de controlar el robot
con ellos.
Con esa idea, junto al afán de aprender nuevas tecnologías, nace este proyecto. A dicho método de control se
le sumaría posteriormente el joystick virtual. Para finalizar, se llevaron a cabo las modificaciones mencionadas
en el programa Arduino, a fin de corregir algunos problemas previos y simplificar parte del mismo.
Con ello sin embargo, no se cierra el ciclo. Aun quedan muchas líneas de progreso abiertas, varias de ellas
propuestas para este mismo proyecto pero que no fueron posibles abarcar.
Entre estas propuestas, la principal consistiría en pasar a ejecutar la ecuación relativa a la ley de control en el
dispositivo Android. No cabe decir que estaríamos comparando el poder computacional de un
microprocesador que funciona a 16MHz, como es el caso de la placa Arduino, con otro de varios núcleos y a
frecuencias que superan el gigahercio. Además, sumado a este incremento en la capacidad de realiar
operaciones, se abriría una nueva puerta al estudio de los mecanismos de control, ya que entrarían en partido
los retrasos producidos por la comunicación bluetooth así como las pérdidas de datos inherentes a ella.
Conclusiones
58
59
9 BIBLIOGRAFÍA
Se ha recurrido principalmente a dos documentos a la hora de solventar dudas y tomar un punto de partida:
o Vehículo autoequilibrado de tipo péndulo invertido de bajo coste. Antonio J. Croche Navarro (2014).
o Apuntes de la asignatura Diseño de aplicaciones móviles.
El resto de información necesaria para la elaboración del presente proyecto fue obtenida de diferentes recursos
online:
o https://developer.android.com/reference/packages.html
o https://developer.android.com/about/dashboards/index.html?hl=es-419
o https://developer.android.com/guide/components/activities/activity-lifecycle.html
o https://developer.android.com/reference/android/Manifest.permission.html
o https://www.arduino.cc/en/Guide/Environment
o https://www.arduino.cc/en/Main/arduinoBoardMega2560/
o https://www.arduino.cc/en/Reference/HomePage
o https://en.wikipedia.org/wiki/Java_(programming_language)
o https://gitlab.com
o https://developer.android.com/guide/topics/ui/menus.html
o https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html
o https://www.bignerdranch.com/blog/splash-screens-the-right-way
o http://iconhandbook.co.uk/reference/chart/android
Bibliografía
60
61
APENDICE A: CÓDIGO PARA ARDUINO
Archivo: PI_modificado.ino
1. #include <I2C.h> // Esta librería permite comunicar con dispositivos I2C TWI. http://arduino.cc/es/Reference/Wire#.U0WDz6KeZdg
2. #include "Kalman.h" 3. 4. #define RESTRICT_PITCH // Si esta linea se comenta, el giro alrededor del eje X se restrin
ge a ±90º, mientras que el giro alrededor de Y se restringe a ±180º. Si esta linea no se comenta, el que queda restringido a ±90º es el eje Y
5. 6. 7. Kalman kalmanX; 8. Kalman kalmanY; //Aquí está creando los objetos kalmanX y KalmanY de tipo Kalman(librería)
. Más adelante si quiero usar una función dentro de Kalman.h la usaré particularizada al objeto haciendo: kalmanX.funcion()o kalmanY.funcion()
9. 10. /* IMU Data */ 11. double accX, accY, accZ; //componentes de la acelareción 12. double gyroX, gyroY, gyroZ; //componentes de giro 13. int16_t tempRaw; //Aquí se almacenará la temperatura en crudo (no en ºC) 14. 15. 16. double gyroXangle, gyroYangle; // Angle calculate using the gyro only 17. double compAngleX, compAngleY; // Calculated angle using a complementary filter 18. double kalAngleX, kalAngleY; // Calculated angle using a Kalman filter 19. 20. uint32_t timer; 21. uint8_t IMUData[14]; // Buffer for I2C data Creación del vector IMUData reservando 14 in
dices (para la comunicación de la IMU) 22. 23. /* PARA LOS ENCODERS, VELOCIDAD Y ACELERACIÓN */ 24. 25. //Motor Izquierdo 26. #define encoder0PinA 18 27. #define encoder0PinB 19 28. volatile float encoder0Pos = 0; 29. unsigned long time1; 30. volatile float ivr; 31. float pos0; 32. float vel; 33. float acel; 34. float vel0; 35. 36. //Motor Derecho 37. #define encoder1PinA 2 38. #define encoder1PinB 3 39. volatile float encoder1Pos = 0; 40. unsigned long time2; 41. float pos0d; 42. 43. 44. 45. ////////////////////////// 46. 47. /* PARA EL MOTOR */ 48. 49. #define Pinpot1 A2
Apendice A: Código para Arduino
62
50. #define Pinpot2 A1 51. #define Pinpot3 A0 52. #define PinPWMD 7 //Lo he cambiado del 8 al 7 para poder subir la frecuencia PWM tal y co
mo dice la pag. web que encontré. 53. #define PinPWMI 9 54. #define PinIn1I 8 55. #define PinIn2I 10 56. #define PinIn1D 5 57. #define PinIn2D 6 58. 59. 60. float PWMD; 61. float PWMI; 62. 63. ///////////////////////// 64. 65. /* PARA CALCULAR EL CONTROL PID*/ 66. 67. float u; 68. float phi_r,dphi_r,dtheta_r; 69. float phi, dphi; 70. float Ahora; 71. float Cambiot, Ultimot; 72. float Kp,Ki; 73. float r,e; 74. float Ahora1, Cambiot1, Ultimot1; 75. float ITerm; 76. float u_m; 77. float cuenta1; 78. 79. 80. /* PARA MANIOBRAS*/ 81. 82. float u_mi, u_md; 83. float tiempoDir; 84. float Estado; 85. 86. /* PARA EL FILTRO DE LA VELOCIDAD*/ 87. 88. float wf, wf_1, wf_2, wf0; 89. float vel_1, vel_2, acelf; 90. float veli, veld; 91. 92. 93. /* PARA LAS VAR DE LA COMUNICACIÓN BLUETOOTH*/ 94. float K1, K2, K3, c, K4; //variables para las constantes del LQR 95. int Dir; //variable para la dirección (de 1 a 9) 96. float xg, xt; //velocidad de giro y de traslación 97. int trim; 98. int cuenta; 99. 100. float K1_r, K2_r, K3_r, c_r; //variables para las constantes en proceso de recepción de
l LQR 101. int Dir_r; //variable para la dirección (de 1 a 9) 102. float xg_r, xt_r; 103. float trim_r; 104. 105. float Kp_r, Ki_r; //para la recepcion constantes control motor 106. 107. float tiempo0; //Para el cálculo de tiempo de bucle de programa. 108. float bucle; 109. 110. float bluetoothInterval = 100; // Cada cuantos ms se han de ejecutar la funciones
de adquisición de datos 111. float bluetoothIntervalTimer = 0; // Inicio a 0 del contador 112. 113. /* PARA EL TRIMADO*/
63
63 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
114. float trim_estado, adicion, trimado; 115. float kalAngleY_trimado; 116. 117. ///////////////////////// 118. 119. 120. void setup() { 121. Serial.begin(115200); 122. 123. //configuración Timer 1 y 5 124. noInterrupts(); //desactiva todas las interrupciones para inicializar la in
terrupción temporal 125. TCCR5A = 0; //inicializamos los registros de control 126. TCCR5B = 0; 127. TCNT5 = 0; 128. 129. OCR5A = 9999; // valor CTC que se guarda en el registro OCR1A 130. TCCR5B |= (1 << WGM52); // modo output compare 131. TCCR5B |= (1 << CS50); // preescalador de 64 132. TCCR5B |= (1 << CS51); 133. TIMSK5 |= (1 << OCIE5A); // activa la interrupción por comparación temporal 134. 135. TCCR1A = 0; //inicializamos los registros de control 136. TCCR1B = 0; 137. TCNT1 = 0; 138. 139. OCR1A = 19999; // valor CTC que se guarda en el registro OCR3A 140. TCCR1B |= (1 << WGM12); // modo output compare 141. TCCR1B |= (1 << CS11); // preescalador de 8 142. TIMSK1 |= (1 << OCIE1A); // activa la interrupción por comparación temporal 143. 144. interrupts(); // activa todas las interrupciones 145. 146. 147. I2c.begin(); 148. //TWBR = ((F_CPU / 400000L) - 16) / 2; // Poner frecuencia I2C a 400kHz TWBR –
TWI Bit Rate Register - porque en el Datasheet de la MPU6050 dicen que la máx. velocidad del bus es 400kHz.
149. I2c.setSpeed(1); //Poner frecuencia a 400kHz 150. I2c.pullup(1); //activamos la resistencia de pull-up 151. I2c.timeOut(40); //habilita el recibir los errores por el envío fallido y no se queda
colgado ya que te dice el error y sigue funcionando 152. 153. IMUData[0] = 7; // >>Config tasa muestreo. Registro 0x19hex=25dec (Nuestro
giroscopo funciona a 8kHz, ver pag.12-13 Mapa Registro pdf) 154. IMUData[1] = 0x00; // >>Sincronización y Filtrado. Registro 0x1Ahex=26dec (ver pag
.13 Mapa Registro pdf) 155. IMUData[2] = 0x00; // >>Configuración Giróscopos. Registro 0x1Bhex=27dec Lo pone
a máxima sensibilidad/minimo rango y deshabilita el autotest(ver pag.14 Mapa Registro pdf)
156. IMUData[3] = 0x00; // >>Configuración Acelerómetros. Registro 0x1Chex=28dec Lo pone a máxima sensibilidad/minimo rango y deshabilita el autotest (ver pag.15-16 Mapa Registro pdf)
157. while (IMUWrite(0x19, IMUData, 4, false)); // >>Mete los anteriores 4 valores en cada posición del registro y es un while porque devuelve un 0 cuando es con éxito y si no lo reintenta (ver archivo i2C.ino)
158. while (IMUWrite(0x6B, 0x01, true)); // >>Deshabilita modo standby. Usa como señal de reloj el giroscopo del eje X
159. // Esto lo mete en el Registro 0x69hex=107dec (ver pag.41 Mapa Registro pdf)
160. 161. while (IMURead(0x75, IMUData, 1)); //Lee los registros 0x75h
ex=117dec; y los mete en el vector IMUData. Como el registro 0x75 es el último, el resto de valores del vector quedará con los valores que tenía antes.
162. if (IMUData[0] != 0x68) { // Read "WHO_AM_I" register //Este registro "Quién Soy?" (ver pag. 46) devuelve el valor 0x68 para decir cuál es la dirección del dispositivo (aunque realmente sabemos que la dirección será sólo 0x68 cuando el pin AD0 esté LOW y 0x69 cuando el pin AD0 esté HIGH. Esto aparece en el datasheet)
Apendice A: Código para Arduino
64
163. Serial.print(F("Error reading sensor")); 164. while (1); 165. } 166. 167. delay(100); // Espera a que se estabilice el sensor 168. 169. /* Seleccionar kalman and gyro starting angle */ 170. while (IMURead(0x3B, IMUData, 6)); //Lectura de los Registros de Ace
lerómetros:0x3Bhex=59dec y 0x3Chex=60dec; 0x3Dhex=61dec y 0x3Ehex=62dec; 0x3Fhex=63dec y 0x40hex=64dec; y los mete en el vector IMUDataver pag.30 Mapa Registro pdf)
171. accX = (IMUData[0] << 8) | IMUData[1]; //(IMUData[0] << 8)=esto es un desplazamiento de 8 bits a la izquierda de forma que el IMUData[0]queda en las posiciones más significativas y a la derecha quedan ceros.
172. accY = (IMUData[2] << 8) | IMUData[3]; // Con la operación | se suman bit a bit las posiciones menos significatvas con el valor IMUData[1]. Así se obtiene un número
173. accZ = (IMUData[4] << 8) | IMUData[5]; // de 16 bit a partir de dos números de 8 bit (porque los registros sólo pueden guardar 8 bits)
174. 175. 176. #ifdef RESTRICT_PITCH 177. double roll = atan2(accY, accZ) * RAD_TO_DEG; 178. double pitch = atan(-accX / sqrt(accY * accY + accZ * accZ)) * RAD_TO_DEG; 179. #else 180. double roll = atan(accY / sqrt(accX * accX + accZ * accZ)) * RAD_TO_DEG; 181. double pitch = atan2(-accX, accZ) * RAD_TO_DEG; 182. #endif 183. 184. kalmanX.setAngle(roll); // Seleccionar ángulo de inicio 185. kalmanY.setAngle(pitch); 186. gyroXangle = roll; 187. gyroYangle = pitch; 188. compAngleX = roll; 189. compAngleY = pitch; 190. 191. timer = micros(); 192. 193. /////// CÓDIGO PARA LA POSICION, VELOCIDAD Y ACELERACIÓN ////// 194. 195. //Motor izquierdo 196. pinMode(encoder0PinA, INPUT); 197. digitalWrite(encoder0PinA, HIGH); // activar resistencias pullup 198. pinMode(encoder0PinB, INPUT); 199. digitalWrite(encoder0PinB, HIGH); // activar resistencias pullup 200. 201. //Recordar que son encoders de cuadratura, hay dos sensores por cada motor colocados
a 90º uno del otro 202. attachInterrupt(5, doEncoderA0, CHANGE); // Encoder A en la interrupción 5 (pin 18) 203. attachInterrupt(4, doEncoderB0, CHANGE); // Encoder B en la interrupción 4 (pin 19)
204. 205. 206. //Motor derecho 207. pinMode(encoder1PinA, INPUT); 208. digitalWrite(encoder1PinA, HIGH); // activar resistencias pullup 209. pinMode(encoder1PinB, INPUT); 210. digitalWrite(encoder1PinB, HIGH); // activar resistencias pullup 211. 212. //Recordar que son encoders de cuadratura, hay dos sensores por cada motor colocados
a 90º uno del otro 213. attachInterrupt(0, doEncoderA1, CHANGE); // Encoder A en la interrupción 0 (pin 2) 214. attachInterrupt(1, doEncoderB1, CHANGE); // Encoder B en la interrupción 1 (pin 3) 215. 216. time1=millis(); // Inicializamos tiempo y posicion para el calculo de velocidad 217. time2=millis(); // Inicializamos tiempo y posicion para el calculo de velocidad 218. 219.
65
65 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
220. /////////// MOTOR ///////////// 221. //Para subir la frecuencia del PWM pin 7 del Mega 2560 de 489Hz a 31333Hz 222. TCCR4B = TCCR4B & 0b000 | 0x01; //timer 4 lo que va después del palo es el valor de f
recuencia al que se le cambia 223. //Para subir la frecuencia del PWM pin 9 del Mega 2560 de 489Hz a 31333Hz 224. TCCR2B = TCCR2B & 0b000 | 0x01; //timer 2 225. 226. pinMode(PinPWMD, OUTPUT); 227. pinMode(PinPWMI, OUTPUT); 228. pinMode(PinIn1I, OUTPUT); 229. pinMode(PinIn2I, OUTPUT); 230. pinMode(PinIn1D, OUTPUT); 231. pinMode(PinIn2D, OUTPUT); 232. 233. ///CONTROLADOR//// 234. phi_r=0; dphi_r=0;dtheta_r=0; 235. u=0; 236. Kp=0.2;//0.2;//0.35;//0.25;//0.25;//0.22;//0.3;//0.25;//0.21; //0.2;//Kp=0.1; 237. Ki=0.1;//0.1;//0.07;//0.06;//0.06;//0.08;//0.07;//0.06;//0.08;//0.09; //0.1;//Ki=0.4;
238. K1=-332.8746; K2=-52.7835; K3=-7; c=2.0; Dir=5; xg=0.0; xt=0.0; K4=-
0.3; ivr=0; //cuenta1=0; 239. Kp_r = 0; 240. Ki_r = 0; 241. K1_r = 0; 242. K2_r = 0; 243. K3_r = 0; 244. c_r = 0; 245. Dir_r = Dir; 246. xg_r = xg; 247. xt_r = xt; 248. /////////////////////////////// 249. 250. ///FILTRO VELOCIDAD//// 251. wf_2=1.0; 252. wf_1=1.0; 253. wf=1.0; 254. vel_2=1.0; 255. vel_1=1.0; 256. wf0=1.0; 257. 258. // Inicializar el buffer para bluetooth 259. initializeBuffer(); 260. 261. } 262. 263. 264. void loop() { 265. 266. if ( (millis() - bluetoothIntervalTimer) >= bluetoothInterval) { 267. // Enviar reporte via bluetooth 268. sendData(); 269. // Actualizar el buffer con los posibles nuevos datos recibidos via bluetooth 270. updateBuffer(); 271. // Procesar los bytes recibidos y extraer de ellos los valores pertinentes 272. processBuffer(); 273. // Aplicar los datos recibidos a las variables globales (aquí se aplica el filtro)
274. applyData(); 275. // Resetear el inicio de intervalo 276. bluetoothIntervalTimer = millis(); 277. } 278. 279. 280. 281. /////CÓDIGO PARA LA IMU//// 282. 283. /* Update all the values */
Apendice A: Código para Arduino
66
284. while (IMURead(0x3B, IMUData, 14)); 285. accX = ((IMUData[0] << 8) | IMUData[1]) -
857.08;//883.8 Su offset:+ 2000.0; //Los valores de accX vienen de los registros 0x3B y 0x3C por eso los combina y le suma un offset (2000 que no sé como ha calculado). Esto puede verse en esta página donde hacen lo mismo: http://www.botched.co.uk/pic-tutorials/mpu6050-setup-data-aquisition/
286. accY = ((IMUData[2] << 8) | IMUData[3]) - 114.5; //Su offset:+ 3319.84; //Para el resto es analogo a lo explicado para el de arriba
287. accZ = ((IMUData[4] << 8) | IMUData[5]) + 134.75; //Su offset:+ 664.48; 288. tempRaw = (IMUData[6] << 8) | IMUData[7]; 289. gyroX = (IMUData[8] << 8) | IMUData[9]; 290. gyroY = (IMUData[10] << 8) | IMUData[11]; 291. gyroZ = (IMUData[12] << 8) | IMUData[13]; 292. 293. double dt = (double)(micros() - timer) / 1000000; // Calcular el delta t 294. timer = micros(); 295. 296. 297. #ifdef RESTRICT_PITCH 298. double roll = atan2(accY, accZ) * RAD_TO_DEG; 299. double pitch = atan(-accX / sqrt(accY * accY + accZ * accZ)) * RAD_TO_DEG; 300. #else 301. double roll = atan(accY / sqrt(accX * accX + accZ * accZ)) * RAD_TO_DEG; 302. double pitch = atan2(-accX, accZ) * RAD_TO_DEG; 303. #endif 304. 305. double gyroXrate = gyroX / 131.0; // Convierte a deg/s 306. double gyroYrate = gyroY / 131.0; // Convierte a deg/s 307. 308. #ifdef RESTRICT_PITCH 309. // Solución al problema de transición cuando el ángulo del acelerometro salta entre
-180 y 180 grados 310. if ((roll < -90 && kalAngleX > 90) || (roll > 90 && kalAngleX < -90)) { 311. kalmanX.setAngle(roll); 312. compAngleX = roll; 313. kalAngleX = roll; 314. gyroXangle = roll; 315. } else 316. kalAngleX = kalmanX.getAngle(roll, gyroXrate, dt); // Cálculo del ángulo con el F
iltro Kalman 317. 318. if (abs(kalAngleX) > 90) 319. gyroYrate = -
gyroYrate; // Invierte la velocidad angular, de forma que cumple la lectura restringida del acelerometro
320. kalAngleY = kalmanY.getAngle(pitch, gyroYrate, dt); 321. #else 322. // Solución al problema de transición cuando el ángulo del acelerometro salta entre
-180 y 180 grados 323. if ((pitch < -90 && kalAngleY > 90) || (pitch > 90 && kalAngleY < -90)) { 324. kalmanY.setAngle(pitch); 325. compAngleY = pitch; 326. kalAngleY = pitch; 327. gyroYangle = pitch; 328. } else 329. kalAngleY = kalmanY.getAngle(pitch, gyroYrate, dt); // Cálculo del ángulo con el
Filtro Kalman 330. 331. if (abs(kalAngleY) > 90) 332. gyroXrate = -
gyroXrate; // Invierte la velocidad angular, de forma que cumple la lectura restringida del acelerometro
333. kalAngleX = kalmanX.getAngle(roll, gyroXrate, dt); // Cálculo del ángulo con el Filtro Kalman
334. #endif 335.
67
67 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
336. gyroXangle += gyroXrate * dt; // Calcula el ángulo obtenido del giroscopo sin ningun
filtro 337. gyroYangle += gyroYrate * dt; 338. 339. compAngleX = 0.93 * (compAngleX + gyroXrate * dt) + 0.07 * roll; // Cálculo del ángul
o usando filtro complementario 340. compAngleY = 0.93 * (compAngleY + gyroYrate * dt) + 0.07 * pitch; 341. 342. // Resetear el ángulo del giroscopo si ha sufrido gran deriva. 343. if (gyroXangle < -180 || gyroXangle > 180) 344. gyroXangle = kalAngleX; 345. if (gyroYangle < -180 || gyroYangle > 180) 346. gyroYangle = kalAngleY; 347. 348. 349. ///////// CÓDIGO PARA POSICIÓN, VELOCIDAD Y ACELERACIÓN //////// 350. 351. if ((millis()-
time1)>=10){ //Al reducir esto de 20 a 10 parece que mejora un poco el comportamiento, aunque no lo suficiente, sigue respondiendo muy lento.
352. veli=((encoder0Pos-pos0)*1000.0/(millis()-time1))*(PI/180.0); 353. pos0=encoder0Pos; 354. veld=((encoder1Pos-pos0d)*1000.0/(millis()-time1))*(PI/180.0); 355. pos0d=encoder1Pos; 356. // veld=veli; 357. // if (Estado==10 || Estado==30 || Estado==40 || Estado==60 || Estado==70 || Estado=
=90) { 358. // veld=-veli; 359. // } 360. 361. vel=(veli+veld)/2.0; 362. 363. // wf=1.8430*wf_1 - 0.8545*wf_2 + 0.0029*vel + 0.0057*vel_1 + 0.0029*vel_2; 364. //Voy a cambiar ahora el filtro de Wn=0.0354 a Wn=0.0784, para ver si responde más
rápido 365. wf=1.6544*wf_1 - 0.7059*wf_2 + 0.0129*vel + 0.0257*vel_1 + 0.0129*vel_2; 366. 367. wf_2=wf_1; 368. wf_1=wf; 369. vel_2=vel_1; 370. vel_1=vel; 371. 372. acel=(vel-vel0)*1000.0/(millis()-time1); 373. 374. acelf=(wf-wf0)*1000.0/(millis()-time1); 375. 376. wf0=wf; 377. vel0=vel; 378. time1=millis(); 379. 380. } 381. 382. 383. ////////////////////////////////////////////////////////////////// 384. 385. ///////// CÓDIGO PARA CONTROLADOR //////// 386. 387. 388. /*Ahora=millis(); 389. Cambiot=Ahora-Ultimot;*/ 390. 391. // Trimado(); 392. 393. /*if (Cambiot>=40){ 394. phi=kalAngleY_trimado*(PI/180); 395. dphi=gyroYrate*(PI/180); 396. ivr=(encoder0Pos+encoder1Pos)/2.0*(PI/180.0); 397. u=K1*(phi_r-phi) + K2*(dphi_r-dphi) + K3*(dtheta_r-wf) + K4*ivr;
Apendice A: Código para Arduino
68
398. r=-u*c; //Voy a tocar esto a ver si responde más rápido. 399. Ultimot=Ahora; 400. }*/ 401. /*if (cuenta1>=500){ 402. dtheta_r=2;}/*quitar lo de mando_motor añadir cuenta1 en isr, cambiar dthet
a incial*/ 403. dphi=gyroYrate*(PI/180); 404. 405. // Actualizar las referencias de los motores 406. updateEngineReferences(); 407. 408. 409. 410. //////////////////////////////////////////////////////////////// 411. 412. /*SECCIÓN PARA EL ENVÍO*/ 413. 414. // Tiempo transcurrido en el bucle 415. bucle=micros()-tiempo0; 416. 417. // Calcular cuanto tiempo de menos ha trascurrido. Este bucle debe durar ~10ms 418. long forcedDelay = 11000 - bucle; 419. if (forcedDelay > 0) { 420. delayMicroseconds(forcedDelay); 421. } 422. 423. // Recalcular el tiempo de bucle y resetear el contador. 424. bucle=micros()-tiempo0; 425. tiempo0=micros(); 426. 427. } 428. 429. 430. 431. 432. ISR(TIMER5_COMPA_vect) 433. { 434. //cuenta1=cuenta1+1; 435. phi=kalAngleY*(PI/180); 436. //phi=kalAngleY_trimado*(PI/180); 437. /*dphi=gyroYrate*(PI/180);*/ 438. 439. ivr=ivr+dtheta_r-wf; 440. u=K1*(phi_r-phi) + K2*(dphi_r-dphi) + K3*(dtheta_r-wf) + K4*ivr; 441. //u=0.1; 442. r=-u*c; //Voy a tocar esto a ver si responde más rápido. 443. 444. //if (cuenta1>=300) r=-0.3; 445. } 446. ISR(TIMER1_COMPA_vect) // rutina de interrupción con Output Compare 447. { 448. e=r-acelf; 449. 450. ITerm += Ki*e; 451. if((abs(ITerm))>255.0){ //antiwindup 452. if (ITerm>0) ITerm=255.0; 453. else ITerm=-255.0;} 454. 455. u_m=Kp*e+ITerm; 456. } 457. 458. ///// A CONTINUACIÓN VIENEN LAS FUNCIONES DE LAS INTERRUPCIONES PARA LOS ENCODERS /////
// 459. 460. //INTERRUPCIONES ENCODERS MOTOR IZQUIERDO 461. void doEncoderA0() {
69
69 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
462. if (digitalRead(encoder0PinA) == HIGH) { // busca un cambio de bajo-a-
alto en el canal A 463. if (digitalRead(encoder0PinB) == LOW) { // comprueba el canal B para saber en qué
sentido gira 464. encoder0Pos++;} // Horario 465. else {encoder0Pos--;} // Antihorario 466. } 467. 468. else{ // en otro caso sería un cambio de alto-a-bajo en canal A 469. if (digitalRead(encoder0PinB) == HIGH) { // comprueba el canal B para saber en qué
sentido gira 470. encoder0Pos++;} // Horario 471. else {encoder0Pos--;} // Antihorario 472. } 473. } 474. 475. void doEncoderB0(){ 476. if (digitalRead(encoder0PinB) == HIGH) { //busca un cambio de bajo-a-
alto en el canal B 477. if (digitalRead(encoder0PinA) == HIGH) { // comprueba el canal A para saber en qué
sentido gira 478. encoder0Pos++;} // Horario 479. else {encoder0Pos--;} // Antihorario 480. } 481. 482. else { // en otro caso sería un cambio de alto-a-bajo en canal B 483. if (digitalRead(encoder0PinA) == LOW) { // comprueba el canal A para saber en qué
sentido gira 484. encoder0Pos++;} // Horario 485. else {encoder0Pos--;} // Antihorario 486. } 487. } 488. 489. //INTERRUPCIONES ENCODERS MOTOR DERECHO 490. void doEncoderA1(){ 491. if (digitalRead(encoder1PinA) == HIGH) { // busca un cambio de bajo-a-
alto en el canal A 492. if (digitalRead(encoder1PinB) == LOW) { // comprueba el canal B para saber en qué
sentido gira 493. encoder1Pos--;} // Horario 494. else {encoder1Pos++;} // Antihorario 495. } 496. 497. else{ // en otro caso sería un cambio de alto-a-bajo en canal A 498. if (digitalRead(encoder1PinB) == HIGH) { // comprueba el canal B para saber en qué
sentido gira 499. encoder1Pos--;} // Horario 500. else {encoder1Pos++;} // Antihorario 501. } 502. } 503. 504. void doEncoderB1(){ 505. if (digitalRead(encoder1PinB) == HIGH) { //busca un cambio de bajo-a-
alto en el canal B 506. if (digitalRead(encoder1PinA) == HIGH) { // comprueba el canal A para saber en qué
sentido gira 507. encoder1Pos--;} // Horario 508. else {encoder1Pos++;} // Antihorario 509. } 510. 511. else { // en otro caso sería un cambio de alto-a-bajo en canal B 512. if (digitalRead(encoder1PinA) == LOW) { // comprueba el canal A para saber en qué
sentido gira 513. encoder1Pos--;} // Horario 514. else {encoder1Pos++;} // Antihorario 515. } 516. }
Apendice A: Código para Arduino
70
Archivo: control_motor.ino
1. void updateEngineReferences () { 2. 3. dtheta_r = 0.76*PI*xt; 4. u_mi = -u_m - 10.0*xg; 5. u_md = -u_m + 10.0*xg; 6. 7. if (u_mi>=0) { 8. digitalWrite(PinIn1I, HIGH); 9. digitalWrite(PinIn2I, LOW); 10. } else { 11. digitalWrite(PinIn1I, LOW); 12. digitalWrite(PinIn2I, HIGH); 13. } 14. 15. if (u_md>=0) { 16. digitalWrite(PinIn1D, HIGH); 17. digitalWrite(PinIn2D, LOW); 18. } else { 19. digitalWrite(PinIn1D, LOW); 20. digitalWrite(PinIn2D, HIGH); 21. } 22. 23. PWMI = 14 + abs(u_mi); 24. PWMD = 14 + abs(u_md); 25. 26. if (PWMI > 255) { PWMI = 255; } 27. if (PWMD > 255) { PWMD = 255; } 28. 29. if (abs(kalAngleY) > 30) { 30. PWMI = 0; 31. PWMD = 0; 32. ivr = 0; 33. } 34. 35. analogWrite(PinPWMI, PWMI); 36. analogWrite(PinPWMD, PWMD); 37. }
71
71 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
Archivo: asignacion_variables.ino
1. // Valor aplicado al filtro 2. float alpha = 0.75; 3. 4. 5. 6. void applyData () { 7. setKs(Kp_r, Ki_r, K1_r, K2_r, K3_r); 8. setC(c_r); 9. setDir(Dir_r); 10. setTranslationSpeed(xt_r); 11. setTurnSpeed(xg_r); 12. } 13. 14. 15. void setKs (float kp_r, float ki_r, float k1_r, float k2_r, float k3_r) { 16. // Si son cero no se actualiza el valor 17. if (kp_r != 0) { Kp = kp_r; } 18. if (ki_r != 0) { Ki = ki_r; } 19. if (k1_r != 0) { K1 = k1_r; } 20. if (k2_r != 0) { K2 = k2_r; } 21. if (k3_r != 0) { K3 = k3_r; } 22. } 23. 24. 25. void setC (float c_r) { 26. // Si es cero no se actualiza el valor 27. if (c_r != 0) { c = c_r; } 28. } 29. 30. 31. void setDir (int dir_r) { 32. // Si es cero no se actualiza el valor 33. if (dir_r != 0) { 34. Dir = dir_r; 35. if (Dir == 1 || Dir == 3 || Dir == 7 || Dir == 9) { 36. tiempoDir = millis(); 37. } 38. } 39. } 40. 41. 42. void setTranslationSpeed (float xt_r) { 43. xt = alpha * xt + (1 - alpha) * xt_r; 44. } 45. 46. 47. void setTurnSpeed (float xg_r) { 48. xg = alpha * xg + (1 - alpha) * xg_r; 49. }
Apendice A: Código para Arduino
72
Archivo: bluetooth.ino
1. #define SOC '<' 2. #define EOC '>' 3. #define PS ',' 4. #define EMPTY '*' 5. #define BUFFER_LENGTH 100 6. #define PARAM_LENGHT 10 7. #define MAX_INTEGERS 1 8. #define MAX_FLOATS 8 9. 10. #define INTEGERS_EXPECTED 1 11. #define FLOATS_EXPECTED 8 12. 13. 14. // Buffer para los datos recibidos via bluetooth e índice del último dato insertado 15. char buffer[BUFFER_LENGTH]; 16. int bufferNextIndex = 0; 17. 18. // Flag para indicar la recepción del final de comando (EOC) y su índice en el buffer 19. boolean eocFound = false; 20. int eocIndex = 0; 21. 22. // Contienen los valores recibidos, según sean enteros o flotantes 23. int integers[MAX_INTEGERS]; 24. int integersNextIndex = 0; 25. int integersCount = 0; 26. float floats[MAX_FLOATS]; 27. int floatsNextIndex = 0; 28. int floatsCount = 0; 29. 30. 31. 32. void initializeBuffer () { 33. // Inicializar el buffer 34. for (int i=0 ; i < BUFFER_LENGTH ; i++) { buffer[i] = EMPTY; } 35. } 36. 37. int getNextBufferIndex (int index) { 38. return ++index < BUFFER_LENGTH ? index : 0; 39. } 40. 41. int getPrevBufferIndex (int index) { 42. return --index >= 0 ? index : BUFFER_LENGTH - 1; 43. } 44. 45. int addToBuffer (char character) { 46. int index = bufferNextIndex; 47. buffer[index] = character; 48. bufferNextIndex = getNextBufferIndex(index); 49. return index; 50. } 51. 52. void addInteger (int integer) { 53. if (integersNextIndex < MAX_INTEGERS) { 54. integers[integersNextIndex++] = integer; 55. } 56. integersCount++; 57. } 58. 59. void addFloat (float floatValue) { 60. if (floatsNextIndex < MAX_FLOATS) { 61. floats[floatsNextIndex++] = floatValue; 62. } 63. floatsCount++; 64. }
73
73 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
65. 66. 67. void updateBuffer () { 68. // Almacenar todos los Bytes disponibles 69. while (Serial.available() > 0) { 70. // Byte recibido 71. char character = (char) Serial.read(); 72. // Insertarlo en el buffer 73. int lastItemIndex = addToBuffer(character); 74. // Marcar el flag de comado recibido 75. if ( character == EOC ) { 76. eocIndex = lastItemIndex; 77. eocFound = true; 78. } 79. } 80. } 81. 82. 83. void processBuffer () { 84. // Comprobar que se haya encontrado el fin de comando (EOC). En otro caso abortar ope
raciones 85. if (eocFound) { 86. // Flags 87. boolean stopProcess = false; 88. boolean isInteger; 89. // Índice relativo al buffer 90. int index = eocIndex; 91. // Datos del parámetro reconstruido 92. char paramData[PARAM_LENGHT+1]; // +1 (final de cadena '\0') 93. int paramDataIndex; 94. // Inicializar a 0 y añadir final de cadena. El índice comienza desde el final 95. for (int i=0 ; i<PARAM_LENGHT ; i++) { paramData[i] = '0'; } paramData[sizeof(param
Data)-1] = '\0'; 96. paramDataIndex = PARAM_LENGHT-1; 97. isInteger = true; // Por defecto, es un entero 98. 99. // Recorrer los datos recibidos, partiendo del final de comando (EOC) 100. while (!stopProcess) { 101. // Indice del dato y el dato en sí a tratar (tomado del buffer) 102. index = getPrevBufferIndex(index); 103. char character = buffer[index]; 104. // Checkear el tipo de dato 105. if (character != PS && character != SOC && character != EOC && character !
= EMPTY) { 106. // Datos de un parámetro 107. if (character == '-') { 108. // Mover el simbolo "-" al inicio de la cadena 109. paramData[0] = character; 110. } else { 111. if (paramDataIndex > 0) { 112. paramData[paramDataIndex--] = character; 113. } else { 114. // Fallo al recopilar datos de un parámetro. Se ha excedido el tamañ
o máximo de parámetro 115. // Serial.println("paramData[] full"); 116. stopProcess = true; 117. } 118. } 119. // Marcar el flag de flotante, si corresponde 120. if (character == '.') { isInteger = false; } 121. } else { 122. // Se trata de un separador de parámetro (PS), inicio de comando (SOC) o
caracter vacío (EMPTY) 123. if (character == PS || character == SOC) { 124. // Discrimiar el tipo de dato y almacenarlo 125. if (isInteger) { 126. addInteger(atoi(paramData)); 127. } else {
Apendice A: Código para Arduino
74
128. addFloat(atof(paramData)); 129. } 130. // Operar según el discriminador 131. if (character == SOC) { 132. // Fin de parámetro y comando 133. stopProcess = true; 134. } else if (character == PS) { 135. // Fin de parámetro. Reinicializar 136. for (int i=0 ; i<PARAM_LENGHT ; i++) { paramData[i] = '0'; } 137. paramDataIndex = PARAM_LENGHT-1; 138. isInteger = true; 139. } 140. } else { 141. // Fallo en la estructura de la cadena de comando 142. // Serial.println("Unexpected parameter end"); 143. stopProcess = true; 144. } 145. } 146. } 147. 148. // Procesado finalizado. Resetear el flag. 149. eocFound = false; 150. 151. // Comprobar que se hayan recibido (exáctamente) los parámetros esperados 152. if (integersCount == INTEGERS_EXPECTED && floatsCount == FLOATS_EXPECTED) {
153. // Tomar los enteros 154. Dir_r = integers[0]; 155. // Tomar los flotantes 156. c_r = floats[0]; 157. K3_r = floats[1]; 158. K2_r = floats[2]; 159. K1_r = floats[3]; 160. Ki_r = floats[4]; 161. Kp_r = floats[5]; 162. xt_r = floats[6]; 163. xg_r = floats[7]; 164. } 165. // En cualquier caso, se descartan los valores almacenados 166. integersCount = integersNextIndex = 0; 167. floatsCount = floatsNextIndex = 0; 168. } 169. } 170. 171. 172. void sendData () { 173. Serial.print("^"); 174. Serial.print(Kp); Serial.print(","); 175. Serial.print(Ki); Serial.print(","); 176. Serial.print(K1); Serial.print(","); 177. Serial.print(K2); Serial.print(","); 178. Serial.print(K3); Serial.print(","); 179. Serial.print(c); Serial.print(","); 180. Serial.print(bucle); Serial.print(","); 181. Serial.print(Dir); Serial.print(","); 182. Serial.print(Estado); Serial.print(","); 183. Serial.print(xg); Serial.print(","); 184. Serial.print(xt); 185. 186. delay(1); 187. } 188. 189. 190. 191. void getBluetoothData () { 192. while (Serial.available() > 0) { 193. xg_r = Serial.parseFloat();
75
75 Aplicación móvil para el control via bluetooth de un vehículo autoequilibrado
194. xt_r = Serial.parseFloat(); 195. Kp_r = Serial.parseFloat(); 196. Ki_r = Serial.parseFloat(); 197. K1_r = Serial.parseFloat(); 198. K2_r = Serial.parseFloat(); 199. K3_r = Serial.parseFloat(); 200. c_r = Serial.parseFloat(); 201. Dir_r = Serial.parseInt(); 202. } 203. }
Apendice A: Código para Arduino
76
77
APENDICE B: CÓDIGO PARA ANDROID
Dada su extensión, el código relativo a la aplicación móvil se adjunta en el CD que contiene todos los datos de
este proyecto.