A proposta desse projeto é utilizar a arquitetura cognitiva Clarion para controlar um robô no simulador de robôs v-rep, ou virtual robot experimentation platform. A solução como um todo foi batizada de v-Happy e, para efeitos de testes, o robô escolhido foi um robô móvel Khepera 3. O código foi escrito de modo a permitir que novos comandos sejam transmitidos ao robô, através de um protocolo simples e extensível. Além disso, novas classes de robôs podem ser adicionadas futuramente. A idéia é que o projeto sirva como um "v0" para uma futura implementação do World Server 3D no v-rep, e a arquitetura cognitiva Clarion foi escolhida por ser a única dentre as estudadas nessa disciplina escrita em C#, isto é, sem suporte nativo por parte do simulador v-rep. Assim, entre outras coisas, esse projeto também supre essa demanda.
O download do software v-rep pode ser feito aqui e, neste ponto, não há restrições de Sistema Operacional, isto é, a solução abaixo se conectará ao v-rep em qualquer plataforma, podendo ser, inclusive numa máquina remota.
Como mencionado acima, o v-rep não suporta C#. Seus controladores podem ser escritos em C/C++, Python, Java, Lua, Matlab, Octave ou Urbi. A primeira parte deste projeto consiste neste software, responsável pela ponte entre o Clarion e o v-rep.
Inteiramente escrito em C++, exceto pela API remota (application program interface) do v-rep, que é escrita em C, o código fonte está distribuído em dois diretórios, "v-Happy", que contém as classes específicas do v-Happy Socket, incluíndo a API do v-rep; e "api", que contém classes de propósito gerais escritas por mim já há algum tempo, como por exemplo, classes de tratamento de exceção. O estudo detalhado dessas classes está fora do escopo deste projeto.
Os arquivos extApi.h, extApi.c, extApiInternal.h, extApiPlatform.h e extApiPlatform.c compõe a API do v-rep. Neles, são encontradas funções responsáveis para iniciar uma simulação, carregar um cenário ou interagir com as partes que compõe um robô. A API é escrita em baixo nível e mover um braço do robô é uma tarefa que envolve controlar cada articulação específica, definindo torque e a rotação em radianos, entre outros. Assim, cada robô precisa ser programado no v-Happy Socket segundo suas características específicas.
As funções que compõe a API do v-rep podem ser reconhecidas pelo prefixo "simx". No nosso exemplo, o robô Khepera 3 foi programado para dar movimento aos atuadores dos motores das rodas, podendo ser os sensores e o braço mecânico programados posteriormente.
A classe VREPRobot implementa os métodos moveFront, moveLeft, moveRight, moveReverse e stop, controlando os atuadores dos motores do Khepera 3 através de um handle passado para a função simxSetJointTargetVelocity. O handle é obtido através da função simxGetObjectHandle, que recebe entre os parâmetros o nome do objeto a ser controlado e que foi definido pelo modelador do robô dentro do simulador, tal qual "K3_leftWheelMotor". Mover a articulção do braço do robô Khepera envolve handles específicos para os seguintes atuadores: K3_gripper_armJoint1, K3_gripper_armJoint2, K3_gripper_armAuxJoint1, K3_gripper_armAuxJoint2, K3_gripper_armAuxJoint3, K3_gripper_armAuxJoint4, K3_gripper_fingers, K3_gripper_fingersAux, K3_gripper_fingersAux0; e a complexidade aumenta severamente para robôs mais sofisticados, como o NAO.
A classe VHappy, por sua vez, é responsável por abrir a conexão com o v-rep e aceitar conexões entrantes no v-Happy Socket. É nela também que está definido o protocolo. O protocolo foi simplificado usando o nome dos métodos definidos em VREPRobot, a saber, moveFront, moveLeft, moveRight, moveReverse e stop, além do comando "bye", que encerra a conexão com o cliente e com o v-rep. O trecho de código abaixo é responsável por criar as conexões e tratar o protocolo de comunicação:
int VHappy::main( ) { std::string strIpAddress = static_cast< std::string >( "127.0.0.1" ); int iPort = 19997; int iVHappyPort = 35000; if( this->getArgumentValue( "--vrep_ip" ) != "" ) strIpAddress = this->getArgumentValue( "--vrep_ip" ); if( this->getArgumentValue( "--vrep_port" ) != "" ) iPort = atoi( this->getArgumentValue( "--vrep_port" ).c_str( ) ); if( this->getArgumentValue( "--vHappy_port" ) != "" ) iVHappyPort = atoi( this->getArgumentValue( "--vHappy_port" ).c_str( ) ); // Connect to v-Rep std::cout << "Trying to connect to v-rep on [ " << strIpAddress << " : " << iPort << " ]" << std::endl; this->connect( strIpAddress, iPort ); std::cout << "Connected to v-rep!" << std::endl; VREPRobot vrepRobot( this->simxClientId ); // Accept connections from Clarion ServerSocket serverSocket; std::cout << "Waiting for connections in port [ " << iVHappyPort << " ]..." << std::endl; serverSocket.accept( iVHappyPort ); std::cout << "Client connected!" << std::endl; while( true ) { std::string strReceived = serverSocket.receive( ); std::string strReceivedTruncated = strReceived.substr( 0, strReceived.find_last_of( '\n' ) - 1 ); std::cout << "Received command [ " << strReceivedTruncated << " ] " << std::endl; if( strReceivedTruncated == "moveLeft" ) { vrepRobot.moveLeft( ); } else if( strReceivedTruncated == "moveFront" ) { vrepRobot.moveFront( ); } else if( strReceivedTruncated == "stop" ) { vrepRobot.stop( ); } else if( strReceivedTruncated == "moveReverse" ) { vrepRobot.moveReverse( ); } else if( strReceivedTruncated == "moveRight" ) { vrepRobot.moveRight( ); } else if( strReceivedTruncated == "bye" ) { std::cout << "Exiting..." << std::endl; break; } else { std::cout << "Nothing to do with command [ " << strReceivedTruncated << " ]" << std::endl; } } this->disconnect( ); return 0; }
Neste ponto, já estamos aptos a testar o v-Happy Socket. Para fazê-lo, inicia-se o software v-rep. No Windows, após instalado, ele pode ser encontrado em "Start -> V-REP PRO EDU". No Linux, após descompactar o arquivo "tar.gz", execute "vrep.sh", dentro do diretório de descompactação. No Mac, a aplicação está em "Applications -> v-rep". Após iniciar a aplicação, carregue a cena v-Happy.ttt, que contém o robô Khepera 3 com o código padrão de movimentação em script Lua inteiramente comentado. Em algumas distribuições Linux foi necessário clicar em "Help -> Debug -> Debug C API access (will drastically slow down V-REP)", pois o plugin do v-rep para a API remota não estava habilitado por padrão.
O download do binário do v-Happy Socket compilado para Linux 64 bits pode ser encontrado aqui. Faça o download e execute-o com:
./vHappy --vrep_ip=127.0.0.1 --vrep_port=19997 --vHappy_port=35000
Onde --vrep_ip é o endereço IP da máquina que está rodando o v-rep e --vrep_port é sua porta (lembre-se, por padrão, 19997). O parâmtro --vHappy_port representa a porta que será escutada pelo ServerSocket do v-Happy Socket para conexões entrantes. Neste ponto, caso a conexão com o v-rep tenha acontecido com sucesso, você receberá a seguinte mensagem na tela:
Waiting for connections in port [ 35000 ]...
Para testar o socket, abra um outro terminal e digite:
telnet -r 127.0.0.1 35000
Pronto! Envie os seguintes comandos para o v-rep para ver o v-Happy Socket em ação:
moveLeft
moveFront
stop
moveReverse
moveRight
bye
A tela de saída do v-Happy Socket será algo semelhante à isso:
Trying to connect to v-rep on [ 127.0.0.1 : 19997 ] Connected to v-rep! Waiting for connections in port [ 35000 ]... Client connected! Received command [ moveRight ] Received command [ moveFront ] Received command [ stop ] Received command [ moveReverse ] Received command [ moveLeft ] Received command [ bye ] Exiting...
Para fins de consulta, o download do código fonte do v-Happy Socket pode ser feito aqui. Para compilá-lo, execute:
make clean all
Abaixo podemos ver o v-HappySocket em ação.
A segunda parte do trabalho consiste em conectar a arquitetura cognitiva Clarion ao v-Happy Socket, para que este repasse os comandos para o v-rep, comandos estes, baseados nas regras de tomadas de decisões programadas na arquitetura cognitiva Clarion. O vRepClarion foi escrito em C# e o código responsável pela conexão pode ser visto aqui:
using System; using System.Net; using System.Net.Sockets; using System.Text; //namespace vRepClarion { public class ClientSocket { private bool bIsConnected; private Socket socket; public ClientSocket( ) { this.socket = null; this.bIsConnected = false; } ~ClientSocket( ) { this.disconnect( ); } public void connect( String strIpAddress, int iPort ) { this.socket = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); this.socket.Connect( new IPEndPoint( IPAddress.Parse( strIpAddress ), iPort ) ); this.bIsConnected = true; } public void disconnect( ) { if( this.bIsConnected ) { this.socket.Shutdown( SocketShutdown.Both ); this.bIsConnected = false; } if( this.socket != null ) { this.socket.Close( ); this.socket = null; } } public void send( String strMessage ) { this.socket.Send( Encoding.ASCII.GetBytes( strMessage ) ); } public String receive( ) { byte[ ] bytes = new byte[ 8192 ]; int iBytesReceived = this.socket.Receive( bytes ); return Encoding.ASCII.GetString( bytes, 0, iBytesReceived ); } } //}
A classe VREPRobotClient foi escrita fazendo uso da biblioteca ClarionLibrary.dll, com o objetivo de tomar as decisões de maneira simples, baseadas em um número aleatório. Assim, foram criados DimensionValuePairs para cada um dos cinco movimentos implementados no protocolo de v-Happy Socket, bem como seus ExternalActionChunks. Um agente "vRepRobot" é inicializado com um algorítmo simplificado de Backpropagation. Foi criado também um sensor de informações, responsável por informar ao agente se suas decisões foram boas ou não. A classe VREPRobotClient pode ser vista aqui:
using System; using System.Threading; using Clarion; using Clarion.Framework; //namespace vRepClarion { public class VREPRobotClient { public VREPRobotClient( ){ } public static int Main( string[ ] args ) { String strIpAddress = "127.0.0.1"; int iPort = 35000; int iNumberOfMovements = 100; int iProgress = 0; if( args.Length >= 1 ) strIpAddress = args[ 0 ]; if( args.Length >= 2 ) iPort = ( int ) Convert.ToInt64( args[ 1 ] ); Console.WriteLine( "Initializing the vRepClarion..." ); Console.WriteLine( "Connecting to vHappy on [ " + strIpAddress + " : " + iPort + " ]" ); ClientSocket clientSocket = new ClientSocket( ); clientSocket.connect( strIpAddress, iPort ); DimensionValuePair dvMoveLeft = World.NewDimensionValuePair( "Movement", "Left" ); DimensionValuePair dvMoveFront = World.NewDimensionValuePair( "Movement", "Front" ); DimensionValuePair dvStop = World.NewDimensionValuePair( "Movement", "Stop" ); DimensionValuePair dvMoveReverse = World.NewDimensionValuePair( "Movement", "Reverse" ); DimensionValuePair dvMoveRight = World.NewDimensionValuePair( "Movement", "Right" ); ExternalActionChunk eacMoveLeft = World.NewExternalActionChunk( "Left" ); ExternalActionChunk eacMoveFront = World.NewExternalActionChunk( "Front" ); ExternalActionChunk eacStop = World.NewExternalActionChunk( "Stop" ); ExternalActionChunk eacMoveReverse = World.NewExternalActionChunk( "Reverse" ); ExternalActionChunk eacMoveRight = World.NewExternalActionChunk( "Right" ); // Initialize the Agent Agent agent = World.NewAgent( "vRepRobot" ); SimplifiedQBPNetwork simplifiedQBPNetwork = AgentInitializer.InitializeImplicitDecisionNetwork( agent, SimplifiedQBPNetwork.Factory ); simplifiedQBPNetwork.Input.Add( dvMoveLeft ); simplifiedQBPNetwork.Input.Add( dvMoveFront ); simplifiedQBPNetwork.Input.Add( dvStop ); simplifiedQBPNetwork.Input.Add( dvMoveReverse ); simplifiedQBPNetwork.Input.Add( dvMoveRight ); simplifiedQBPNetwork.Output.Add( eacMoveLeft ); simplifiedQBPNetwork.Output.Add( eacMoveFront ); simplifiedQBPNetwork.Output.Add( eacStop ); simplifiedQBPNetwork.Output.Add( eacMoveReverse ); simplifiedQBPNetwork.Output.Add( eacMoveRight ); agent.Commit( simplifiedQBPNetwork ); simplifiedQBPNetwork.Parameters.LEARNING_RATE = 1; agent.ACS.Parameters.PERFORM_RER_REFINEMENT = false; Console.WriteLine( "Running the Simple v-rep Clarion Controller..." ); Random random = new Random( ); SensoryInformation sensoryInformation; ExternalActionChunk eacChosen; for( int i = 0; i < iNumberOfMovements; i++ ) { sensoryInformation = World.NewSensoryInformation( agent ); // Move if( random.NextDouble( ) < 0.2 ) { sensoryInformation.Add( dvMoveLeft, agent.Parameters.MAX_ACTIVATION ); sensoryInformation.Add( dvMoveFront, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvStop, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveReverse, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveRight, agent.Parameters.MIN_ACTIVATION ); clientSocket.send( "moveLeft" ); } else if( random.NextDouble( ) < 0.4 ) { sensoryInformation.Add( dvMoveLeft, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveFront, agent.Parameters.MAX_ACTIVATION ); sensoryInformation.Add( dvStop, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveReverse, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveRight, agent.Parameters.MIN_ACTIVATION ); clientSocket.send( "moveFront" ); } else if( random.NextDouble( ) < 0.6 ) { sensoryInformation.Add( dvMoveLeft, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveFront, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvStop, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveReverse, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveRight, agent.Parameters.MAX_ACTIVATION ); clientSocket.send( "moveRight" ); } else if( random.NextDouble( ) < 0.8 ) { sensoryInformation.Add( dvMoveLeft, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveFront, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvStop, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveReverse, agent.Parameters.MAX_ACTIVATION ); sensoryInformation.Add( dvMoveRight, agent.Parameters.MIN_ACTIVATION ); clientSocket.send( "moveReverse" ); } else { sensoryInformation.Add( dvMoveLeft, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveFront, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvStop, agent.Parameters.MAX_ACTIVATION ); sensoryInformation.Add( dvMoveReverse, agent.Parameters.MIN_ACTIVATION ); sensoryInformation.Add( dvMoveRight, agent.Parameters.MIN_ACTIVATION ); clientSocket.send( "stop" ); } // Perceive the sensory information agent.Perceive( sensoryInformation ); // Choose an action eacChosen = agent.GetChosenExternalAction( sensoryInformation ); // Deliver appropriate feedback to the agent if( eacChosen == eacMoveLeft ) { if( sensoryInformation[ dvMoveLeft ] == agent.Parameters.MAX_ACTIVATION ) { // The agent was right agent.ReceiveFeedback( sensoryInformation, 1d ); } else { // The agent was wrong agent.ReceiveFeedback( sensoryInformation, 0d ); } } else if( eacChosen == eacMoveFront ) { if( sensoryInformation[ dvMoveFront ] == agent.Parameters.MAX_ACTIVATION ) { // The agent was right agent.ReceiveFeedback( sensoryInformation, 1d ); } else { // The agent was wrong agent.ReceiveFeedback( sensoryInformation, 0d ); } } else if( eacChosen == eacStop ) { if( sensoryInformation[ dvStop ] == agent.Parameters.MAX_ACTIVATION ) { // The agent was right agent.ReceiveFeedback( sensoryInformation, 1d ); } else { // The agent was wrong agent.ReceiveFeedback( sensoryInformation, 0d ); } } else if( eacChosen == eacMoveReverse ) { if( sensoryInformation[ dvMoveReverse ] == agent.Parameters.MAX_ACTIVATION ) { // The agent was right agent.ReceiveFeedback( sensoryInformation, 1d ); } else { // The agent was wrong agent.ReceiveFeedback( sensoryInformation, 0d ); } } else if( eacChosen == eacMoveRight ) { if( sensoryInformation[ dvMoveRight ] == agent.Parameters.MAX_ACTIVATION ) { // The agent was right agent.ReceiveFeedback( sensoryInformation, 1d ); } else { // The agent was wrong agent.ReceiveFeedback( sensoryInformation, 0d ); } } iProgress = ( int ) ( ( i + 1d ) / iNumberOfMovements * 100d ); Thread.Sleep( 1000 ); } // Report results Console.WriteLine( "Results for the Simple v-rep Clarion Controller" ); Console.WriteLine( "At the end of the task, v-rep robot had learned the following rules:" ); foreach( var commitableActionRules in agent.GetInternals( Agent.InternalContainers.ACTION_RULES ) ) { Console.WriteLine( commitableActionRules ); } // Kill the agent to end the task Console.WriteLine( "Killing the agent to end the program..." ); agent.Die( ); Console.WriteLine( "The agent is Dead!" ); Console.Write( "Exiting..." ); clientSocket.send( "bye" ); return 0; } } //}
Para executarmos o vRepClarion, iniciamos o v-rep e carregamos nele a cena v-Happy.ttt. Após isso, executamos o v-Happy Socket com o comando:
./vHappy --vrep_ip=127.0.0.1 --vrep_port=19997 --vHappy_port=35000
Nesse caso, não podemos executar o comando telnet, já que v-Happy Socket suporta apenas uma conexão entrante. Agora, faça o download do binário do vRepClarion, descompacte-o e execute-o com o comando:
mono vRepClarion.exe 127.0.0.1 35000
Onde o primeiro argumento é o enderço IP da máquina que está rodando o v-Happy Socket e o segundo argumento é a porta. Após a execução, um sumário é gerado em tela, similar à este:
Initializing the vRepClarion... Connecting to vHappy on [ 127.0.0.1 : 35000 ] Running the Simple v-rep Clarion Controller... Results for the Simple v-rep Clarion Controller At the end of the task, v-rep robot had learned the following rules: Condition: Dimension Movement, Weight = 1 (Dimension = Movement, Value = Left) Setting = False, (Dimension = Movement, Value = Front) Setting = False, (Dimension = Movement, Value = Stop) Setting = False, (Dimension = Movement, Value = Reverse) Setting = True, (Dimension = Movement, Value = Right) Setting = False Action: ExternalActionChunk Reverse: Dimension SemanticLabel, Weight = 1 (Dimension = SemanticLabel, Value = Reverse) Condition: Dimension Movement, Weight = 1 (Dimension = Movement, Value = Left) Setting = False, (Dimension = Movement, Value = Front) Setting = False, (Dimension = Movement, Value = Stop) Setting = False, (Dimension = Movement, Value = Reverse) Setting = False, (Dimension = Movement, Value = Right) Setting = True Action: ExternalActionChunk Right: Dimension SemanticLabel, Weight = 1 (Dimension = SemanticLabel, Value = Right) Condition: Dimension Movement, Weight = 1 (Dimension = Movement, Value = Left) Setting = False, (Dimension = Movement, Value = Front) Setting = True, (Dimension = Movement, Value = Stop) Setting = False, (Dimension = Movement, Value = Reverse) Setting = False, (Dimension = Movement, Value = Right) Setting = False Action: ExternalActionChunk Front: Dimension SemanticLabel, Weight = 1 (Dimension = SemanticLabel, Value = Front) Condition: Dimension Movement, Weight = 1 (Dimension = Movement, Value = Left) Setting = False, (Dimension = Movement, Value = Front) Setting = False, (Dimension = Movement, Value = Stop) Setting = True, (Dimension = Movement, Value = Reverse) Setting = False, (Dimension = Movement, Value = Right) Setting = False Action: ExternalActionChunk Stop: Dimension SemanticLabel, Weight = 1 (Dimension = SemanticLabel, Value = Stop) Killing the agent to end the program... The agent is Dead! Exiting...
O código fonte do vRepClarion pode ser obtido aqui.
Abaixo podemos ver o vRepClarion em ação.
Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer