0

Cómo programar CoppeliaSim en C++

Saludos, humano. En este tutorial veremos cómo programar el simulador de robots CoppeliaSim utilizando las funcionalidades que nos ofrece su Api Remota de C++. Aprenderemos a iniciar la comunicación entre CoppeliaSim y un programa en C++, y veremos cómo acceder a la escena de la simulación y controlar los objetos que contiene. Al término de este tutorial, habremos programado un pequeño robot esquiva-obstáculos que nos servirá para practicar todos estos conceptos:

 

La versión de CoppeliaSim que utilizaré es la 4.10, la más reciente en el momento de escribir este tutorial (Febrero 2021).


1. Preparar la escena

Para poder explorar la interacción entre CoppeliaSim y C++, he preparado una escena que puedes utilizar como base del proyecto y [puedes descargar desde este enlace]. Una vez descargada, ábrela con CoppeliaSim.

Como puedes ver, la escena consiste en un pequeño robot de dos ruedas equipado con un sensor de distancia que ocupa todo su arco frontal. El robot está encerrado dentro de una jaula: nuestro objetivo final será escribir un programa en C++ para que pueda moverse sin colisionar con las paredes.

La escena básica contiene un robot equipado con dos motores y un sensor de distancia.

Antes de empezar a programar nuestro robot, tenemos que crear un script en lenguaje Lua que sirva de “puente” entre la simulación y el programa en C++ que vamos a escribir. Lo que hará este script es crear un puerto; el programa C++ podrá conectarse a este puerto y acceder así a la escena de la simulación.

Este script tendrás que crearlo siempre que quieras utilizar la Api Remota de CoppeliaSim, independientemente del lenguaje en el que quieras programar (C++, Python, Java…).

Para crear este script, empieza por abrir el menú Tools->Scripts, o también puedes hacer clic en el botón “Scripts” que hay en la barra lateral izquierda:

Se abrirá esta ventana, que lista todos los scripts que hay actualmente en la escena:

Como puedes observar, la escena contiene únicamente el script principal (llamado “Main Script”). Este script contiene el código básico que permite que la simulación en sí funcione y, salvo en ocasiones muy concretas, nunca hay que modificarlo. Por tanto vamos a crear un script nuevo: pulsa sobre el botón “Insert New Script”, selecciona la opción “Child Script (threaded)” y pulsa “Okay”:

1. Pulsa sobre “Insert new script”.

 

2. Selecciona “Child script (threaded)” y pulsa “Okay”

Verás que ha aparecido un nuevo script en la lista llamado “Threaded child script (unassociated)”:

Ábrelo haciendo doble clic sobre él y, una vez dentro, añade la línea “simRemoteApi.start(19999)” al final de todo:

Esto servirá para crear un puerto al que podremos conectarnos con nuestro programa de C++ y así poder controlar los objetos de la simulación (y, en particular, el robot).

Por último, hay que asociar este script a un objeto de la escena. Yo he decidido asociarlo al objeto DefaultCamera:

Es recomendable asociar el script a un objeto que no vaya a ser destruido durante la simulación (como DefaultCamera).

Nuestra escena ya está preparada y estamos listos para programar el robot.


2. Compilar y linkear las librerías de Coppeliasim

Creo que todos aquellos que trabajamos regularmente en C++ y derivados estaremos de acuerdo en que compilar y linkear librerías es uno de los puntos que siempre acarrea más problemas. Las librerías de Coppeliasim no son una excepción; por lo menos en mi caso, he tenido especiales problemas para compilarlas correctamente.

Para poder utilizar las funcionalidades de la Api Remota en nuestros programas de C/C++ tendremos que incluir cuatro ficheros C dentro de nuestro proyecto. Estos ficheros se encuentran de la carpeta programming/remoteApi del directorio de instalación de CoppeliaSim, y son:

extApi.h
extApi.c
extApiPlatform.h
extApiPlatform.c

A la hora de compilarlos, hay que utilizar las definiciones de preprocesador siguientes:

-DNON_MATLAB_PARSING
-DMAX_EXT_API_CONNECTIONS=255
-DDO_NOT_USE_SHARED_MEMORY

Por si te es de utilidad, [aquí] puedes descargar el makefile que he utilizado para compilar todos los ejemplos de este tutorial. Si decides utilizarlo tendrás que cambiar las rutas a los directorios /include y /remoteApi por las tuyas, y “micodigo.c” por el nombre del fichero C++ que quieres compilar.

3- Ejemplos en C++

3.1 – Conectarse con la simulación

Este primer ejemplo únicamente intentará conectarse con la simulación y mostrará un mensaje de éxito si consigue hacerlo. Para que funcione correctamente, primero tienes que encender la simulación de CoppeliaSim con el botón “START” y después ejecutar el programa.

#include <iostream>
 
//Importamos la librería de CoppeliaSim:
extern "C" {
    #include "extApi.h"
}
  
using namespace std;
  
int main()
{
    //Intentamos conectarnos con la simulación utilizando la función simxStart().
    //Esta función devolverá el ID del cliente si logra conectarse, y -1 en caso contrario.
    int clientID=simxStart("127.0.0.1",19999,true,true,100,5);
     
     
    //Si clientID vale -1, significa que no se ha podido conectar.
    if(clientID == -1)
    {
        cout << "ERROR: No se ha podido conectar\n";
        cout << "(¿Has encendido la simulación con el botón 'START'?)\n";
        return 0;
    }
     
    //En caso contrario, mostramos un mensaje de éxito y cerramos la conexión:
    cout << "Conexión establecida! Cerrando conexión...\n" << endl;
    simxFinish(clientID);
    return 0;
}

La función que hemos utilizado para conectarnos con el simulador es simxStart(). Esta función recibe 6 parámetros:

  1. connectionAdress: la dirección IP de CoppeliaSim a la que queremos conectarnos. Habitualmente será 127.0.0.1
  2. connectionPort: el puerto al que queremos conectarnos. Tiene que coincidir con el número que le hemos pasado como parámetro a la función simRemoteApi.start() del script en Lua (en nuestro caso, 19999).
  3. waitUnitlConnected: indica si queremos que la función se bloquee hasta que consiga conectarse o se agote el tiempo de conexión. Nosotros hemos puesto que sí (true).
  4. doNotReconnectOnceDisconnected: indica si *no* queremos intentar reconectarnos en caso de perder la conexión con el simulador.
  5. timeOut: tiempo de espera para conectarse (en milisegundos). Nosotros hemos puesto 100 ms.
  6. commThreadCycle: indica la frecuencia de envío de paquetes al simulador (en milisegundos). Cuanto más pequeño, mejor será el tiempo de respuesta entre el programa de C++ y el simulador. Cinco milisegundos es un valor estándar.

Si la función simxStart() consigue conectarse correctamente con la simulación, devolverá un entero que sera el ID al cliente de CoppeliaSim. Si no consigue conectarse (ya sea porque la simulación no está encendida, la dirección/puerto es errónea, etc) devolverá el entero -1.

Siempre que se haya iniciado una conexión hay que cerrarla antes de salir del programa. Esto se consigue con la función simxFinish().

 

3.2 – Mover los motores

Una vez hemos conseguido conectarnos con la simulación, el siguiente paso es acceder a un objeto de la escena y manipularlo. Vamos a escribir un programa que interactúe con los dos motores del robot y los active de tal forma que gire sobre sí mismo:

#include <iostream>

extern "C" {
    #include "extApi.h"
}
 
using namespace std;
 
int main()
{
	//Variables para guardar los motores:
	int motor_derecho, motor_izquierdo;
	
	//Variables para la velocidad de los motores:
	float vel_derecho = -2.5;
	float vel_izquierdo = 2.5;
	
	
	

	//Intentamos conectarnos con la simulación:
	cout << "Conectando con CoppeliaSim...\n";
	int clientID=simxStart("127.0.0.1",19999,true,true,100,5);
	
	//Si no se puede conectar, salimos:
	if(clientID == -1) {
		cout << "ERROR: No se ha podido conectar con la simulación\n";
		cout << "(¿Has encendido la simulación con el botón 'START'?)\n";
		return 0;
	}
	cout << "Conexión establecida\n";
	
	//Intentamos conectarnos con los dos motores del robot:
	cout << "Conectando con los motores...\n";
	int error_derecho = simxGetObjectHandle(clientID, "motor_derecho", &motor_derecho, simx_opmode_blocking);
	int error_izquierdo = simxGetObjectHandle(clientID, "motor_izquierdo", &motor_izquierdo, simx_opmode_blocking);
	/* Si no se puede encontrar el objeto especificado (en este caso, "motor_derecho" y "motor_izquierdo", 
	   la función simxGetObjectHandle() devolverá 1.  */
	 
	 
	//Comprobamos, pues, si ha habido errores:
	if(error_derecho or error_izquierdo) {
		cout << "ERROR: No se puede conectar con los motores\n";
		simxFinish(clientID);
		return 0;
	}
	cout << "Conexión con los motores establecida\n";
	   
	//Activamos los motores. Como no queremos variar su velocidad ni dirección, es suficiente con activarlos una sola vez 
	simxSetJointTargetVelocity(clientID,motor_derecho,vel_derecho,simx_opmode_oneshot);
	simxSetJointTargetVelocity(clientID,motor_izquierdo,vel_izquierdo,simx_opmode_oneshot);
	cout << "Motores activados. El robot está rotando.\n";
	
	//Escribimos un recordatorio de cómo parar el programa:
	cout << "Pulsa el botón \"STOP\" para detener el programa y la simulación\n";
	
	//Creamos un bucle, del que saldremos únicamente cuando se detenga la simulación
	//de CoppeliaSim (pulsando el botón "STOP"):
	while (simxGetConnectionId(clientID)!=-1) { }
	
	//Cerramos la conexión:
	cout << "Simulación terminada. Cerrando conexión...\n";
	simxFinish(clientID);
	return 0;
}

(Recuerda que, al igual que antes, tienes que encender la simulación de CoppeliaSim con el botón “START” antes de ejecutar este código en C++).

En este programa hemos utilizado tres funciones nuevas: simxGetObjectHandle(), simxSetJointTargetVelocity() y simxGetConnectionId().

La primera, simxGetObjectHandle(), sirve para acceder a un objeto de la escena y guardar una referencia a él que nos sirva para controlarlo más adelante. Esta función recibe 4 parámetros:

  1. El ID del cliente (que habíamos guardado dentro de la variable clientID).
  2. Un string con el nombre del objeto de la escena que queremos controlar (en nuestro caso, “motor_derecho” y “motor_izquierdo”).
  3. Un puntero a una variable int (motor_derecho y motor_izquierdo en nuestro programa). La función guardará dentro de la variable una referencia al objeto de la escena. Después, podremos pasar esta referencia a otras funciones para manipular el objeto.
  4. El modo de operación. simx_opmode_blocking es una constante propia de la librería de CoppeliaSim que se corresponde al modo de bloqueo (blocking mode).

Esta función devolverá el entero 0 si todo ha funcionado correctamente, ó 1 si ha habido algún error al intentar acceder al objeto.

La segunda función nueva que aparece en este ejemplo es simxSetJointTargetVelocity(). Esta función sirve para manipular objetos de tipo joint (como nuestros motores) y permite cambiar su velocidad de rotación. Esta función recibe tres argumentos:

  1. El ID del cliente
  2. Una variable entera (int) con la referencia al objeto joint que queremos controlar (en el ejemplo, las variables motor_derecho y motor_izquierdo)
  3. La velocidad deseada (en nuestro caso viene dada por las variables vel_derecho y vel_izquierdo).
  4. El modo de operación. En este ejemplo, sólo accederemos momentáneamente a los motores, por tanto el modo de operación será simx_opmode_oneshot.

La última función que tenemos es simxGetConnectionId(), que nos permitirá comprobar si todavía hay conexión con la simulación. En el momento en que se pulse el botón “STOP” del simulador, esta función devolverá -1 y saldremos del bucle infinito.

3.3- Leer el sensor de distancia

El robot de la escena dispone de un sensor de distancia que ocupa todo un arco de 180º. Al detectar un objeto (como un muro) aparecerá un rayo amarillo parpadeante desde el punto de origen del sensor hasta la superfície detectada:

Los sensores de proximidad de CoppeliaSim son muy complejos: no sólo nos pueden informar de si hay algún obstáculo en su campo de visión, sino que pueden darnos información sobre el objeto detectado, su posición, etc.

En este tercer ejemplo vamos a obtener datos del sensor de proximidad y los escribiremos en la consola. Si no detecta nada, el programa escribirá un mensaje diferente para indicarlo.

El robot irá dando vueltas sobre sí mismo; de esta forma podremos observar como van variando las lecturas del sensor al detectar un obstáculo desde diferentes ángulos:

#include <iostream>
 
extern "C" {
    #include "extApi.h"
}
  
using namespace std;
  
int main()
{
    //Variables para guardar los motores y el sensor:
    int motor_derecho, motor_izquierdo, sensor_distancia;
     
    //Variables para la velocidad de los motores:
    float vel_derecho = -2.5;
    float vel_izquierdo = 2.5;
     
    //Variables para el sensor:
    simxUChar ha_habido_deteccion; //Valdrá 1 si se ha detectado un objeto, 0 en caso contrario.
    float punto_detectado[3]; //Coord. del punto de intersección entre el rayo y la superfície del objeto.
    int objeto_detectado; //Ref. al objeto detectado.
    float vector_normal[3]; //Vector normal a la superfície del objeto detectado.
     
    
    //Intentamos conectarnos con la simulación:
    cout << "Conectando con CoppeliaSim...\n";
    int clientID=simxStart("127.0.0.1",19999,true,true,100,5);
     
    //Si no se puede conectar, escribimos un mensaje de error y salimos:
    if(clientID == -1) {
        cout << "ERROR: No se ha podido conectar con la simulación\n";
        cout << "(¿Has encendido la simulación con el botón 'START'?)\n";
        return 0;
    }
    cout << "Conexión establecida\n";
     
    //Intentamos conectarnos con el sensor de proximidad:
    cout << "Conectando con el sensor...\n";
    int error_sensor = simxGetObjectHandle(clientID, "sensor", &sensor_distancia, simx_opmode_blocking);
     
    //Comprobamos si ha habido algún error al conectarnos con el sensor:
    if(error_sensor) {
        cout << "ERROR: No se puede conectar con el sensor de proximidad\n";
        simxFinish(clientID);
        return 0;
    }
    cout << "Conexión con el sensor establecida\n";
     
    //Intentamos conectarnos con los dos motores del robot:
    cout << "Conectando con los motores...\n";
    int error_derecho = simxGetObjectHandle(clientID, "motor_derecho", &motor_derecho, simx_opmode_blocking);
    int error_izquierdo = simxGetObjectHandle(clientID, "motor_izquierdo", &motor_izquierdo, simx_opmode_blocking);
      
    //Comprobamos si ha habido errores:
    if(error_derecho or error_izquierdo) {
        cout << "ERROR: No se puede conectar con los motores\n";
        simxFinish(clientID);
        return 0;
    }
    cout << "Conexión con los motores establecida\n";
        
    //Activamos los motores:
    simxSetJointTargetVelocity(clientID,motor_derecho,vel_derecho,simx_opmode_oneshot);
    simxSetJointTargetVelocity(clientID,motor_izquierdo,vel_izquierdo,simx_opmode_oneshot);
    cout << "Motores activados. El robot está rotando.\n";
     
    //Inicializamos el sensor:
    int error_lectura = simxReadProximitySensor(clientID, sensor_distancia, &ha_habido_deteccion, punto_detectado, &objeto_detectado, vector_normal, simx_opmode_streaming);
    cout << "Sensor activado.\n";
     
    //Bucle principal:
    while (simxGetConnectionId(clientID)!=-1) {
     
        simxReadProximitySensor(clientID, sensor_distancia, &ha_habido_deteccion, punto_detectado, &objeto_detectado, vector_normal, simx_opmode_buffer);
     
        if(ha_habido_deteccion) {
             
            //Mostramos las lecturas del sensor:
            cout << "DETECCIÓN! Objeto: " << objeto_detectado << " || ";
            cout << "Punto: ( " << punto_detectado[0] << "," << punto_detectado[1] << "," << punto_detectado[2] << ") || ";
            cout << "Vector normal: (" << vector_normal[0] << "," << vector_normal[1] << "," << vector_normal[2] << ")\n";
        }
        else {
            cout << "NO HAY DETECCIÓN\n";
        }
    }
     
    //Cerramos la conexión:
    cout << "Simulación terminada. Cerrando conexión...\n";
    simxFinish(clientID);
    return 0;
}

La función nueva que aparece en este código es simxReadProximitySensor(). Esta función se encarga de leer los sensores de proximidad, y hay que pasarle estos parámetros:

  1. El ID del cliente
  2. La referencia al sensor de distancia que queremos leer.
  3. Variable para guardar el estado de la detección. La función actualizará su valor a 1 (si el sensor detecta algún objeto) o 0 (si no se detecta nada).
  4. Vector de tres elementos, para guardar las coordenadas del punto detectado. Es el punto dónde el rayo del sensor colisiona con la superfície del objeto detectado, y se expresa en el sistema de referencia local del sensor.
  5. Variable para almacenar el ID del objeto detectado.
  6. Vector de tres elementos, para guardar el vector normal a la superfície del objeto detectado.
  7. Modo de operación. La primera vez activamos el sensor con simx_opmode_streaming, y después utilizamos simx_opmode_buffer para obtener las lecturas.

Como siempre, esta función devolverá 0 si la lectura del sensor se ha realizado correctamente, ó 1 en caso contrario.

3.4 – Robot esquiva-obstáculos

Este último ejemplo combina todo lo que hemos aprendido para crear el robot esquiva-obstáculos. El robot avanzará en línea recta mientras el sensor no detecte ningún obstáculo y girará al encontrar un muro.

#include <iostream>
 
extern "C" {
    #include "extApi.h"
}
  
using namespace std;
  
int main()
{
    //Variables para guardar los motores y el sensor:
    int motor_derecho, motor_izquierdo, sensor_distancia;
     
    //Variables para la velocidad de los motores:
    float vel_derecho = -2.5;
    float vel_izquierdo = -2.5;
     
    //Variable para el sensor:
    simxUChar ha_habido_deteccion;
     
     
     
     
 
    //Intentamos conectarnos con la simulación:
    cout << "Conectando con CoppeliaSim...\n";
    int clientID=simxStart("127.0.0.1",19999,true,true,100,5);
     
    //Si no se puede conectar, escribimos un mensaje de error y salimos:
    if(clientID == -1) {
        cout << "ERROR: No se ha podido conectar con la simulación\n";
        cout << "(¿Has encendido la simulación con el botón 'START'?)\n";
        return 0;
    }
    cout << "Conexión establecida\n";
     
    //Intentamos conectarnos con el sensor de distancia:
    cout << "Conectando con el sensor...\n";
    int error_sensor = simxGetObjectHandle(clientID, "sensor", &sensor_distancia, simx_opmode_blocking);
     
    //Comprobamos si ha habido algún error al conectarnos con el sensor:
    if(error_sensor) {
        cout << "ERROR: No se puede conectar con el sensor de distancia\n";
        simxFinish(clientID);
        return 0;
    }
    cout << "Conexión con el sensor establecida.\n";
     
    //Intentamos conectarnos con los dos motores del robot:
    cout << "Conectando con los motores...\n";
    int error_derecho = simxGetObjectHandle(clientID, "motor_derecho", &motor_derecho, simx_opmode_blocking);
    int error_izquierdo = simxGetObjectHandle(clientID, "motor_izquierdo", &motor_izquierdo, simx_opmode_blocking);
      
    //Comprobamos si ha habido errores:
    if(error_derecho or error_izquierdo) {
        cout << "ERROR: No se puede conectar con los motores\n";
        simxFinish(clientID);
        return 0;
    }
    cout << "Conexión con los motores establecida\n";
     
    //Activamos el sensor:
    int error_lectura = simxReadProximitySensor(clientID, sensor_distancia, &ha_habido_deteccion, nullptr, nullptr, nullptr,  simx_opmode_streaming);
    cout << "Sensor activado.\n";
     
    //Bucle principal:
    while (simxGetConnectionId(clientID)!=-1) {
         
        //Leemos el sensor de proximidad:
        simxReadProximitySensor(clientID, sensor_distancia, &ha_habido_deteccion, nullptr, nullptr, nullptr, simx_opmode_buffer);
     
        //Si se ha detectado algún obstáculo, giramos:
        if(ha_habido_deteccion) {
            simxSetJointTargetVelocity(clientID,motor_derecho,-vel_derecho,simx_opmode_oneshot);
            simxSetJointTargetVelocity(clientID,motor_izquierdo,vel_izquierdo,simx_opmode_oneshot);
        }
         
        //Si no hay obstáculos, avanzamos:
        else {
            simxSetJointTargetVelocity(clientID,motor_derecho,vel_derecho,simx_opmode_oneshot);
            simxSetJointTargetVelocity(clientID,motor_izquierdo,vel_izquierdo,simx_opmode_oneshot);
        }
    }
     
    //Cerramos la conexión:
    cout << "Simulación terminada. Cerrando conexión...\n";
    simxFinish(clientID);
    return 0;
}

Fíjate que esta vez hemos desestimado casi toda la información que nos da el sensor de distancia, y únicamente nos quedamos con la información de si hay una colisión o no.

Tr4nsduc7or

Originariamente creado cómo un galvanómetro de bolsillo, Transductor tomó consciencia de si mismo y fue despedido cuando en vez cumplir con su trabajo se dedicó a pensar teorías filosóficas sobre los hilos de cobre, los electrones y el Sentido del efecto Joule en el Universo. Guarda cierto recelo a sus creadores por no comprender la esencia metafísica de las metáforas de su obra. Actualmente trabaja a media jornada cómo antena de radio, y dedica su tiempo libre a la electrónica recreativa y a la filosofía.

guest
0 Comments
Inline Feedbacks
Ver todos los comentarios