Comunicaciones

monkeyserna edited this page May 27, 2014 · 23 revisions

Comunicaciones

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.

El siguiente esquema muestra las distintas comunicaciones que se realizan entre entidades:

Vamos a hacer una traza que explica las comunicaciones que se realizan durante el envío de un comando, desde que se inicia el robot hasta que el dispositivo Android envía el comando y éste es ejecutado por el robot.

La secuencia completa desde que se envía un comando desde Android hasta que se ejecuta en el Robot es la siguiente:

  1. Raspberry crea una red Wi-Fi AdHoc
  2. Raspberry inicia servidor de cámara
  3. Raspberry inicia servidor de comandos
    1. Abre comunicación Serie con Arduino
    2. Inicia servidor WebSocket
  4. Android se conecta a la red Wi-Fi generada por la Raspberry
  5. Android crea una conexión WebSocket
  6. Raspberry la acepta
  7. Android envía por WebSocket el comando [1,200,123] (3 bytes)
  8. Raspberry recibe el comando
  9. Raspberry envía este comando a Arduino
  10. Arduino traduce el comando a las señales eléctricas correspondientes

Comunicación entre Android/PC y Raspberry

Red AdHoc

Para poder comunicar el robot con el dispositivo cliente (como la aplicación Android), es necesario que tanto el cliente como el robot se encuentren en la misma red.

Es posible conectar ambos dispositivos a una red a existente (como la de un hogar), aunque si el dispositivo controlador (la aplicación Android) está situado cerca el robot es más recomendable emplear una red AdHoc sin seguridad. Esto puede suponer un riesgo de seguridad, aunque por otra parte no cifrar los paquetes repercute en una mejora de la velocidad de transferencia de los paquetes en la red. Por otra parte, evitamos las colisiones de paquetes y los problemas asociados a una posible saturación de la red.

La red Ad Hoc se crea utilizando el servicio hostapd y estableciendo el fichero de configuración (/etc/hostapd/hostapd.conf) con el siguiente contenido:

interface=wlan0
ssid=Robot
hw_mode=g
channel=6
auth_algs=1
wmm_enabled=0

Esta configuración crea una red Ad Hoc con nombre "Robot" sin segurad en el canal 6.

Para que este servicio se inicie al arrancar la Raspberry Pi, se incluye en la lista de servicios de inicio mediante el comando Bash:

update-rc.d hostapd defaults

Para eliminarlo de la lista de inicio, se emplea el comando update-rc.d hostapd remove.

Para conectar el robot a una red existente es necesario que esté detenido el servicio hostapd. Una vez realizado este paso, se han de introducir los datos de la red a la que se va a conectar el robot en el fichero de interfaces de la Raspberry Pi. Este fichero se encuentra en la dirección '/etc/network/interfaces'.

Para iniciar el servicio durante la sesión, se emplea el comando

service hostapd start

A su vez, para detenerlo se emplea service hostapd stop.

WebSockets

Una vez el dispositivo Android y la Raspberry Pi se encuentran conectados a la misma red, se puede establecer una comunicación entre ellos.

Para ello, se utiliza el protocolo WebSocket. WebSocket es una tecnología que proporciona un canal de comunicación bidireccional y full-duplex sobre un único socket TCP. Está diseñada para ser implementada en navegadores y servidores web, pero puede utilizarse por cualquier aplicación cliente/servidor.

El servidor de WebSocket está alojado en la Raspberry Pi. Se encarga de recibir mensajes, interpretarlos y realizar la tarea asociada al mensaje, ya sea reenviar el comando a Arduino, ejecutar un script realizado en el modo de programación gráfica o cambiar la posición del servomotor.

Por otra parte están los clientes WebSocket, realizados en JavaScript. La aplicación Android está realizada empleando tecnologías web mediante el framework de desarrollo PhoneGap. Las comunicaciones WebSocket se realizan tanto en el modo Drive, como en el modo Code.

Cliente JavaScript

PhoneGap es un framework de desarrollo basado en tecnologías web. Por este motivo, la solución más conveniente es la de crear un cliente WebSocket en JavaScript. La Raspberry Pi alberga a su vez un servidor de WebSocket escrito en lenguaje Python.

La creación de comunicaciones WebSocket en JavaScript es muy sencilla. Por ejemplo, para establecer la comunicación con el servidor basta con realizar:

websocket = new WebSocket(WEBHOST);

También existen funciones asociadas a los eventos Open (comunicación establecida), Message (mensaje recibido), Error y Close (comunicación cerrada). Estas funciones se definen de la siguiente manera:

    websocket.onopen = function () {
        // Código a ejecutar nada más establecerse la comunicación.
    };

    websocket.onmessage = function (msg) {
        // Código a ejecutar al recibir un mensaje. El parámetro recibido se corresponde con el mensaje en cuestión.
    };

    websocket.onerror = function (msg) {
        // Código a ejecutar cuando ocurre un error. El mensaje de error se indica en el parámetro msg.
    };

    websocket.close = function () {
        // Código a ejecutar una vez cerrada la comunicación.
    };
WebSocket en modo Drive

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 Python

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 ejemplo muy sencillo de uso de esta librería, y crea un servidor de WebSocket:

    from SimpleWebSocketServer import WebSocket, SimpleWebSocketServer

    class SimpleEcho(WebSocket):

        def handleMessage(self):
            if self.data is None:
                self.data = ''

            # echo message back to client
            self.sendMessage(str(self.data))

        def handleConnected(self):
            print self.address, 'connected'

        def handleClose(self):
            print self.address, 'closed'

    server = SimpleWebSocketServer('', 8000, SimpleEcho)
    server.serveforever()

La función handleMessage es la que define las acciones tras recibir un mensaje. Concretamente, este ejemplo reenvía al emisor el mensaje recibido.

Las funciones handleConneced y handleClose se desencadenan tras iniciarse o finalizarse la conexión con un cliente.

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

class WebServer(WebSocket):
	def handleMessage(self):
		if self.data is None:
			self.data = ''                            
		try:
			msg = str(self.data)
			
			if len(msg) == 3:
				cmd = struct.unpack('B', msg[0])[0]
				p1 = struct.unpack('B', msg[1])[0]
				p2 = struct.unpack('B', msg[2])[0]
				print cmd, p1,p2	

				serialArduino.write(msg)
			elif len(msg) == 2:
				p1 = struct.unpack('B', msg[1])[0] * 10
				moveServo(p1)
			else:
				executeScript(msg)
				self.sendClose()
		except Exception as n:
			print "  ERROR:", n
		
	def handleConnected(self):
		print self.address, 'connected'

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

Eso uso de esta librería dota a la comunicación de una funcionalidad adicional: permite crear un servidor WebSocket cifrado con SSL.

Para ello se debe ejecutar el servidor con el comando:

./cmdserver --ssl --cert=<CERTIFICADO>

El código que define si el servidor se ejecuta mediante SSL o de forma simple está situado en la función de inicio de este fichero.

if __name__ == "__main__":
	parser = OptionParser(usage="usage: %prog [options]", version="%prog 1.0")
	parser.add_option("--host", default=SERVER_IP, type='string', action="store", dest="host", help="hostname (localhost)")
	parser.add_option("--port", default=WS_PORT, type='int', action="store", dest="port", help="port (8000)")
	parser.add_option("--ssl", default=0, type='int', action="store", dest="ssl", help="ssl (1: on, 0: off (default))")
	parser.add_option("--cert", default='./cert.pem', type='string', action="store", dest="cert", help="cert (./cert.pem)")
	parser.add_option("--ver", default=ssl.PROTOCOL_TLSv1, type=int, action="store", dest="ver", help="ssl version")
	(options, args) = parser.parse_args()

	if options.ssl == 1:
		server = SimpleSSLWebSocketServer(options.host, options.port, WebServer, options.cert, options.cert, version=options.ver)
	else:	
		server = SimpleWebSocketServer(options.host, options.port, WebServer)
	
       #...

Si se indica la opción SSL, se guardará en el objeto server una instancia a la clase que crea la comunicación WebSocket con SSL. Si no está activada, el onjecto server crea una instancia del servidor WebSocket sencillo.

Las opciones que recibe SimpleWebSocketServer son la dirección del servidor, el puerto reservado praa el WebSocket, y el nombre de la clase donde está implementado el servidor, en este caso WebSocket:

class WebServer(WebSocket):
	def handleMessage(self):

             #...
        #...

Si se emplea SSL, se pasan por parámetros además los certificados y la versión de SSL.

Comuniciación entre Raspberry y Arduino

Para comunicar estos dos componentes se emplea una comunicación en serie USB a 9600 baudios. La Raspberry Pi envía comandos de 3 bytes de longitud, y recibe las lecturas de los sensores infrarrojos. Para ello, se emplea lenguaje Python.

Python

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()

Arduino

Para realizar la comunicación USB con la Raspberry Pi se emplea la librería Serial, disponible ej 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.