monkeyserna edited this page Jun 3, 2014 · 15 revisions

3. Funcionamiento

En este capítulo se explican las distintas comunicaciones que realiza el robot, ya sea para establecer un canal de comunicación entre la aplicación Android o la web y el robot, o bien para el envío de mensajes entre Arduino y Raspberry Pi.

Por un lado está la comunicación cliente-servidor entre la aplicación Android y la Raspberry, que por defecto se realiza a través de una red Wi-Fi Ad Hoc que la Raspberry Pi crea. Mediante esta conexión, el servidor de la cámara envía mediante HTTP la señal de la cámara en tiempo real, es decir, como una página web que contiene una imagen que se refresca 15 veces por segundo. A su vez, el servidor de comandos se encarga de recibir comandos y scripts realizados en el modo de programación gráfica mediante WebSocket.

Por otro lado, la comunicación entre Rasperry Pi y Arduino Due se realiza mediante comunicación en serie USB. Los comandos que el servidor recibe son enviados mediante este tipo de comunicación a la placa Arduino Due, encargada de traducir estos comandos a pulsos eléctricos que proporcionarán el comportamiento asociado a ese comando.

Cliente

Dentro del modo Drive, una vez se ha tocado la pantalla y aparece el triángulo verde en la parte superior derecha de la pantalla, se inicia el envío de comandos de movimiento.

Cada vez que el acelerómetro cambia su posición, se envía un mensaje de 3 bytes (CMD_MOVE_BALANCE <speed> <balance>) mediante WebSocket. Esto significa que existen 255 posibles valores de avance y balance.

Para el avance:

  • 0 indica velocidad mínima (detenido). El dispositivo Android está en posición vertical.
  • 255 indica velocidad máxima. El dispositivo Android está en posición horizontal.

Para el giro:

  • 0 indica balance máximo a la izquierda.
  • 123 indica avance hacia adelante
  • 255 indica balance máximo a la derecha.

Al igual que ocurre con la velocidad, cualquier valor intermedio dentro del rango [0,255] es válido y determina la intensidad del giro.

Mediante combinaciones de inclinaciones se pueden obtener distintas respuestas: desde giros de distintas velocidades sobre una rueda, hasta avances frontales, incluyendo movimientos en espiral, giros pequeños y de mayor ángulo.

Al pulsar de nuevo la pantalla se envía señal de frenado (freno por bloqueo) y se detiene el envío de movimiento. Consecuentemente, el triangulo que indica emisión dejará de estar visible.

WebSocket en Javascript

El modo de control Drive envía mediante WebSockets dos posibles comandos. Un comando asociado con el desplazamiento de avance del robot, y otro para establecer la posición del servo.

En primer lugar, crea la conexión nada más cargar la vista:

function onDeviceReady() {
    //...
    websocket = new WebSocket(WEBHOST);
    //...
}

El valor WEBHOST está definido en el archivo values.js. La dirrección por defecto es ws://192.168.0.123:800.

Las funciones que envían comandos mediante WebSocket son tres.

Cuando la barra desplazable cambia de valor, invoca a una función que envía el comando de movimiento del servo:

function moveServo(){
    var msg = new Uint8Array(2)
    msg[0] = CMD_SERVO;
    msg[1] = document.getElementById("slider").value;
    websocket.send(msg)
}

Por otra parte, cuando el dispositivo Android lee los valores del acelerómetro, llama a la función onAccelerometerChanged. Esta función envía a la Raspberry Pi el comando de movimiento con velocidad y balance, que se calculan con las coordenadas del acelerómetro. La función es la siguiente:

function onAccelerometerChanged(acceleration) {
    var msg = new Uint8Array(3);
    msg[0] = CMD_MOVE_FORWARD;
    msg[1] = map(acceleration.x * 100, 0, 1000, 255, 0,true);
    msg[2] = map(acceleration.y * 100, -1000, 1000, 0, 255,true);

    websocket.send(msg)
}

También está definida una función que detiene el desplazamiento del robot y desactiva las lecturas del acelerómetro:

function stopCharlie() {
    var msg = new Uint8Array(3)
    msg[0] = CMD_STOP;
    msg[1] = CMD_NOPARAM
    msg[2] = CMD_NOPARAM
    websocket.send(msg)

    if (watchID) {
        navigator.accelerometer.clearWatch(watchID);
        watchID = null;
    }

    isSending = false;

    document.getElementById("play").style.display = 'none';
    toast("STOPPED. Tap to start.");
}

Cuando el usuario abandona el modo Drive, se cierra la conexión:

function onBackKeyDown() {
    websocket.close();
    window.location.href = "index.html";
}

Servidor de comandos

El servidor de comandos realiza una gran parte de las funcionalidades de la Raspberry Pi. Sus tareas son:

  • Crear un servidor de WebSocket para recibir comandos y scripts desde Android.
  • Establecer una comunicación serie por USB con Arduino para enviar los distintos comandos disponibles.
  • Realizar lecturas del sonar.
  • Controlar el servo.
  • Ejecutar scripts creados en el modo de programación.
  • Indicar la ejecución de un script (proveniente del modo de programación) mediante un LED.

En cuanto al servidor de WebSocket, Python no tiene ninguna librería de creación de WebSockets por defecto. Sin embargo, sí existen multitud de librerías realizadas por la comunidad disponibles. En este caso, la librería escogida es SimpleWebSocketServer.

A continuación se expone un fragmento del fichero cmdserver.py, donde se define el servidor de WebSocket:

class WebServer(WebSocket):
	def handleMessage(self):
		# self.data contiene el mensaje recibido. Se procesa el mensaje y se realizan las acciones consecuentes.
		
	def handleConnected(self):
		print self.address, 'connected'

	def handleClose(self):
		time.sleep(2)
		servo.stop_servo(SERVOPIN)
		print self.address, 'closed'

La función handleMessage es la que define las acciones tras recibir un mensaje. Las funciones handleConneced y handleClose se desencadenan tras iniciarse o finalizarse la conexión con un cliente.

Comandos Raspberry Pi

Si el mensaje recibido es de 2 bytes se trata de un comando que debe ejecutar la Raspberry Pi. Este tipo de mensaje únicamente ocurre cuando el usuario desplaza la barra de control del servo en el modo de control Drive. Por tanto, únicamente existe un comando de este tipo, que se encarga de establecer la posición del servo.

	def handleMessage(self):
		#...
		elif len(msg) == 2:
			p1 = struct.unpack('B', msg[1])[0] * 10
			moveServo(p1)
		#...

Por tanto, lo que el servidor realiza cuando recibe este tipo de comando es convertir el valor al rango que especifica el servo y emplear la función moveServo, que también es empleada por los scripts realizados en el modo de programación gráfica.

Ejecución de scripts

La programación gráfica basa en bloques traduce el script realizado por el usuario al lenguaje en el que está escrito el servidor de comandos, para que éste lo ejecute. Por tanto, cuando el usuario decide ejecutar un script realizado mediante bloques, se traduce a Python y se envía al servidor de comandos. El servidor al recibir un script invoca a la función executeScript con el mensaje recibido, que contiene la traducción a Python que será ejecutada.

def executeScript(receivedCommand):
	print "SCRIPT RECEIVED:\n",receivedCommand
	initScript()
	print "STARTING BLOCKLY SCRIPT..."
	exec(receivedCommand)
	print "...BLOCKLY SCRIPT DONE"
	destructScript()

La función muestra una serie de comandos de control y realiza 3 acciones. En primer lugar, llama a la función que inicia los elementos conectados a los pines GPIO de la Raspberry Pi (el sónar y el servo) para activar su posible uso por parte del código recibido. El código recibido se ejecuta mediante el comando exec. Una vez finalizada la ejecución se desactivan los pines GPIO, con el objetivo principal de disminuir el consumo eléctrico del robot.

La función initScript activa los pines GPIO de la Raspberry PI y configura las entradas y salidas empleadas por el sonar y el led de control y enciende el led que indica que un script realizado mediante el bloque de programación gráfica está siendo ejecutado. Además, realiza una primera lectura del sonar, ya que en ocasiones la primera lectura devuelve valores anómalos.

def initScript():
	print "INIT GPIO..."
	GPIO.setwarnings(False)
	GPIO.setmode(GPIO.BCM)	
	GPIO.setup(LEDPIN,GPIO.OUT)	
	GPIO.setup(TRIGPIN,GPIO.OUT)
	GPIO.setup(ECHOPIN,GPIO.IN)
	GPIO.output(LEDPIN, GPIO.HIGH)
	GPIO.output(TRIGPIN, GPIO.LOW)
	readSonar() #FIRST READ IS UNSTABLE
	time.sleep(0.1)
	print "...SONAR GPIO"

Por tanto, esta función activa los pines de la Raspberry Pi y configura el pin al que está conectado el led como salida y lo enciende. Con respecto a las conexiones con el sonar, establece el pin ECHOPIN como entrada y TRIGPIN como salida. A continuación se realiza una primera lectura del sonar, cuya primera lectura es poco fiable.

Cuando el script finaliza su ejecución, se procede a apagar el led indicador, y a desactivar los pines GPIO.

def destructScript():	
	GPIO.output(LEDPIN, GPIO.LOW)
	GPIO.cleanup()	

Funciones auxiliares

Para proporcionalidad funcionalidad a los bloques propios se emplean una serie de funciones auxiliares que hacen posible la ejecución de acciones como la lectura del sonar, la posición del servo o las transmisiones con Arduino.

En el siguiente ejemplo se muestra la traducción a Python de un bloque gráfico:

Como se puede observar, para la traducción se emplean tanto funciones nativas de Python (como la función time.sleep) como propias (sendToArduino y readSonar).

Las traducciones a Python de los bloques gráficos tienen disponibles 5 funciones auxiliares.

Función Descripción
readSonar Devuelva la lectura del sonar en centímetros.
moveServo Establece la posición del servo.
sendToArduino Envía un comando a Arduino.
receiveFromArduino Envía un comando a Arduino y recibe un valor de vuelta. Esta función devuelve el valor recibido.
map Realiza un mapeado de un valor X desde [in_min,in_max] hasta [out_min,out_max].

Para realizar comunicaciones por USB, Python dispone de la librería Serial. Esta librería se importa al comienzo del código mediante la instrucción:

import serial

Para comenzar la comunicación, se indica el punto de montaje en el que se encuentra conectado el Arduino Due, y se establece la velocidad a 9600 baudios. La inicialización del servicio se realiza antes de iniciar el WebSocket:

DUE_PORT =  '/dev/ttyACM0' 
DUE_BAUDS = 9600
serialArduino = serial.Serial(DUE_PORT,DUE_BAUDS) 

Los comandos que recibe Arduino son enviados en mensajes de 3 bytes mediante la función sendToArduino:

#Enviar comando
def sendToArduino(cmd,p1,p2):
	msg = bytearray(3)
	msg[0] = struct.pack('B', int(cmd))
	msg[1] = struct.pack('B', int(p1))
	msg[2] = struct.pack('B', int(p2))
	serialArduino.write(msg)

Para recibir un comando se emplea la función readLine del objeto que gestiona la comunicación serie. El servidor únicamente espera recibir mensajes desde Arduino después de enviar un comando relacionado con la lectura de sensores. Por tanto, la función primero envía un comando y a continuación espera la respuesta y devuelve su valor:

#Recibir comando
def receiveFromArduino(cmd,p1,p2):
        sendToArduino(cmd,p1,p2)
	return serialArduino.readline()

Servidor de cámara

La cámara puede ser visualizada tanto desde un navegador web como desde la aplicación nativa.

La cámara Raspberry Pi está conectada directamente a la GPU mediante un conector FLEX de 15 pines, lo que permite procesar las imágenes a mayor velocidad haciendo uso del procesador gráfico, en lugar de la unidad principal (CPU). Esto permite comprimir las imagenes a alta velocidad, incluso obtener una calidad de imagen en full HD 1080p. Aun así, la configuración empleada para la emisión de la camara es bastante más reducida, concretamente de 320x240p a 15 imágenes por segundo.

El driver empleado para crear una interfaz que permita emitir la señal en streaming es uv4l-raspicam. Este servicio se encarga de iniciar el funcionamiento de la cámara a la resolución y tasa de refresco deseada.

Una vez iniciado el servicio uv4l, la cámara comienza a funcionar. En este momento, se crea un servidor de video en JPG a través de HTTP (sobre TCP/IP).

Para comenzar el servidor, el script general de arranque del robot delega en cam.sh (situado en la carpeta raíz del servidor) la tarea de iniciar tanto el driver como el servidor JPG.

Una vez activada la cámara, se inicia el servidor mjpg-streamer, una aplicación en linea de comandos que permite crear un servidor, para retransmitir imágenes JPG sobre una red basada en IP, desde la cámara hasta un navegador convencional. Soporta la compresión por hardware (GPU) de la cámara, lo que permite reducir drásticamente el uso de la CPU de este servidor, haciendo está aplicación un servicio ligero.

El puerto que emplea es el 8080, y la IP del robot por defecto es 192.168.0.123,por lo que para visualizar la cámara se accede a http://192.168.0.123:8080.

La web a la que se accede es la siguiente:

<html>
  <head>
    <title>CAM</title>
    <style>...</style>
  </head>
  <body>
      <img id="cam" src="/?action=stream" />
  </body>
</html>

Como se puede observar, para obtener la señal de la cámara se hace uso de la API del servidor. Concretamente se obtiene la señal de la URL /?action=stream.

Otra funcionalidad interesante puede ser la de capturar una instantánea (una fotograría). Para ello, se realizaría escribiendo la url http://192.168.0.123:8080/?action=snapshot.

Arduino

La comunicación con Arduino puede ocurrir en dos casos:

  • El modo Drive envía un mensaje WebSocket de 3 bytes.
  • La ejecución de un script escrito en el modo Code utiliza la función sendToArduio o receiveFromArduino.

Los comandos que Arduino recibe tienen siempre una longitud de 3 bytes. Estos comandos son transmitidos como mensajes de 3 bytes de longitud, donde el primer byte se corresponde con el valor del comando en el rango [0,16]. Los otros 2 bytes están reservados a posibles parámetros.

Los comandos implementados en Arduino son 17 y están numerados del 0 al 16, donde:

  • Los 11 primeros están relacionados con el movimiento de los motores (comandos 0 al 10).
  • El comando 11 está destinado al zumbador
  • El comando 12 al encendido y apagado de los leds
  • Los 4 últimos (13 al 16) realizan distintas lecturas de los sensores infrarrojos CNY70.

Para realizar la comunicación USB con la Raspberry Pi se emplea la librería Serial, disponible en el repertorio de librerías por defecto de Arduino. Por tanto, no es necesario importar esta función.

En primer lugar, en la inicialización del código se invoca a la función que da comienzo a la escucha del puerto serie a 9600 baudios:

void setup() 
{
   //...
   Serial.begin(9600);
   //...
}

El bucle principal de Arduino espera a recibir comandos de 3 bytes de longitud e invoca a la función asociada a cada comando. Para ello, en cada iteración comprueba si el buffer del puerto contiene al menos un mensaje. En caso de que sí haya un mensaje en el buffer, un switch se encarga de llamacar a la función que desencadene las acciones asociadas al comando recibido:

//Receive a message (3 bytes)
if (Serial.available() >= 3)
{
	byte cmd = Serial.read();  //Command
	byte p1  = Serial.read();  //First parameter
	byte p2  = Serial.read();  //Second parameter

	switch (cmd)
	{
		//Command 0 -> Stop
		case 0:
		stopMotors();
		break;
                //...
        }
}

Los comandos de lectura de los sensores infrarrojos envían mensajes a Raspberry Pi empleando el comando:

Serial.print(msg);
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.