先简单说明一下CoppeliaSim中控制机器人仿真的7种方式:
- 1.编写child script
- 优点:这是最便捷的控制方法,且与最后三种控制方式比较,不会出现通信延迟.
- 缺点:无法选择编程语言(只有
Lua
),执行速度比较慢,无法访问Lua以外的外部函数库。
- 2.编写plugin
- 这种方式通常与第一种方式联合使用;
- 优点:可以访问外部函数库,同样没有通信延迟;
- 缺点:比编程更加复杂,且需要外部工具编译。
- 3,4.编写外部客户端应用(remote API)
- 如果需要从外部应用(或机器人,或另一台计算机)运行控制程序,这是一种非常方便和简单的方法,它支持
C/C++
、Python
、Matlab
等语言。
- 如果需要从外部应用(或机器人,或另一台计算机)运行控制程序,这是一种非常方便和简单的方法,它支持
- 5.通过ROS node
- 该方法与remote API相似,是一种便利的方法让多个分布式进程互相通信。虽然remote API更加轻量和快速,但它只能与CoppeliaSim进行信息交互;
- 可以与任意数量的进程通信,且存在大量可兼容的库可供使用;
- 缺点:相比于remote API,该方法更加笨重且复杂。
- 6.BlueZero node
- 与ROS类似,是一种便利的方法让多个分布式进程互相通信;
- 轻量,可跨平台。
- 7.编写外部通信应用
- 优点:可以选择编程语言,且控制代码可以运行在机器人或不同计算机上.
- 缺点:相比于remote API,该方法更加单调乏味。
由于不熟悉Lua
语言,于是最好的选择就是使用remote API进行仿真控制,因此主要需要熟悉这部分的操作。
1.remote API的简单介绍
remote API是整个CoppeliaSim API框架的一部分,它可以允许CoppeliaSim 与外部应用进行交互。最大的特点就是它支持C/C++
、Python
、Matlab
等语言。这对于不会Lua
的人来说,十分友好。
remote API分为两个部分,分别为服务器端以及客户端。外部应用通过与服务器端进行交互,从而实现与CoppeliaSim的联合仿真。
-
the client side (i.e. your application):应用端,支持C/C++, Java, Python, Matlab, Octave and Lua。这部分内容是我们实现的仿真的重点。
-
the server side (i.e. CoppeliaSim):服务器端,只能通过CoppeliaSim的插件实现。服务器服务可以由两种方式开启:
1.At CoppeliaSim start-up (continuous remote API server service). 这种启动方式会尝试读取相关的配置文件
remoteApiConnections.txt
,根据其内容来启动服务器服务。通过这种方式启动,服务器会一直运行,即使仿真不在运行。2.From within a script (temporary remote API server service). 通过场景脚本启动,这是我们最常用的启动方式。 这种服务是暂时的,当仿真开始时,服务器会启动;当仿真结束后,服务器则会自动关闭。
以下程序是一个简单的脚本,在初始化时会建立一个暂时的remote API的服务器。
function sysCall_init() repeat until (simRemoteApi.start(19999,1300,false,true)~=-1) end function sysCall_actuation() -- put your actuation code here end function sysCall_sensing() -- put your sensing code here end function sysCall_cleanup() -- do some clean-up here end
Enabling the remote API - server side (coppeliarobotics.com)
注:在使用remote API时,不要忘记在CoppeliaSim设置服务端的启动脚本。
目前为止,remote API有两个版本:
- The B0-based remote API (v2):新版是基于BlueZero中间件和它对CoppeliaSim的接口插件的。 相比于第一版,新版的remote API的使用更加简单和灵活,而且易于扩展。目前它支持以下语言:C++, Java, Python, Matlab and Lua。
- The legacy remote API(v1):旧版的remote API,相比于第二版,旧版更加轻量,而且需要的依赖少于新版。目前它支持以下语言:C/C++, Java, Python, Matlab, Octave and Lua。
无论是使用哪一个版本,都需要先在本地文件夹内放入指定的源文件。应该放哪些文件以及这些文件的位置,可以参考官方文档:
- Enabling the B0-based remote API - client side (coppeliarobotics.com)
- Enabling the remote API - client side (coppeliarobotics.com)
2.remote API的工作方式(如何与服务端通信)
Remote API modus operandi (coppeliarobotics.com)
remote API函数与regular API函数十分类似,但是有两个主要的区别:
- 大部分remote API都会返回一个值:
return code
,它是服务端返回给客户端的应答。 - 大部分remote API需要两个额外的输入参数:
- 运行模式
operation mode
- 客户端ID
clientID
- 运行模式
remote API的工作方式本质上就是通过网络套接字,与服务端进行通信。传统的网络通信过程就是
- 客户端向服务端发送请求,比如获得机械臂某个关节的角度;
- 服务端接收到请求后,将需要的数据和应答发送回客户端。在这个过程中,客户端处于堵塞的情况。
在大多数情况下,这种通信方式会浪费大量时间。remote API允许用户选择允许方式的类型,提供了4种主要的运行机制来控制仿真的进程,用户可以根据需求灵活改变通信方式。 比如,对于一些重要的命令,可以采用堵塞方式,而对于如设置机械臂关节角的命令则可以使用非堵塞的形式。
4.1.Blocking function calls
这种运行方式就是最传统的堵塞机制,客户端在接收到服务端数据前会一直处于堵塞状态。
// Following function (blocking mode) will retrieve an object handle:
if (simxGetObjectHandle(clientID,"myJoint",&jointHandle,simx_opmode_blocking)==simx_return_ok)
{
// here we have the joint handle in variable jointHandle!
}
4.2.Non-blocking function calls
处于这种通信方式下,客户端只会向服务端发送请求,但没有必要等待服务端的回复。如以下情况,客户端发送请求后,服务端只需要设置相应的关节角度即可,没有必要向客户端发送回复。
// Following function (non-blocking mode) will set the position of a joint:
simxSetJointPosition(clientID,jointHandle,jointPosition,simx_opmode_oneshot);
在一些情况下,我们需要用一条消息来发送多个数据,因为我们希望这些数据可以同时到达服务端。比如,我们希望机械臂的三个关节同时运动。在这种情况下,用户可以通过暂时中断通信来实现,如下所示:
simxPauseCommunication(clientID,1); // 中断通信
simxSetJointPosition(clientID,joint1Handle,joint1Value,simx_opmode_oneshot);
simxSetJointPosition(clientID,joint2Handle,joint2Value,simx_opmode_oneshot);
simxSetJointPosition(clientID,joint3Handle,joint3Value,simx_opmode_oneshot);
simxPauseCommunication(clientID,0); // 恢复通信
// Above's 3 joints will be received and set on the CoppeliaSim side at the same time
4.3.Data streaming
这种通信方式比较特殊,类似于话题订阅的形式。客户端先向服务端说明需要什么类型的数据,然后当服务端有空时就会将这类数据发送给客户端。需要注意,服务端向客户端发送数据并不是一次就结束的,而是一直进行的。 相当于在客户端与服务端架设了一条数据流,服务端会一直向客户端发送客户端订阅的数据。
// Streaming operation request (subscription) (function returns immediately (non-blocking)):
simxGetJointPosition(clientID,jointHandle,&jointPosition,simx_opmode_streaming);
// The control loop:
while (simxGetConnectionId(clientID)!=-1) // while we are connected to the server..
{
// Fetch the newest joint value from the inbox (func. returns immediately (non-blocking)):
if (simxGetJointPosition(clientID,jointHandle,&jointPosition,simx_opmode_buffer)==simx_return_ok)
{
// here we have the newest joint position in variable jointPosition!
}
else
{
// once you have enabled data streaming, it will take a few ms until the first value has
// arrived. So if we landed in this code section, this does not always mean we have an error!!!
}
}
// Streaming operation is enabled/disabled individually for each command and
// object(s) the command applies to. In above case, only the joint position of
// the joint with handle jointHandle will be streamed.
// Before disconnecting from CoppeliaSim, make sure to disable the streamings you previously enabled:
simxGetJointPosition(clientID,jointHandle,&jointPosition,simx_opmode_discontinue);
simxGetPingTime(clientID,&pingTime); // needed to insure that above reaches CoppeliaSim before disconnection
若要服务端停止发送,则客户端必须向服务端发送一个停止的请求,使用simx_opmode_discontinue
模式。 若客户端没有发送停止请求,那么服务端会一直发送无用的数据,最终会导致运行速度的下降。
此外,当我们用特定命令(如:simxGetJointPosition
)架设好数据流之后,那么这个命令就不应该再使用除simx_opmode_buffer
和simx_opmode_discontinue
之外的其它模式,否则客户端这边的数据流就会中断,但是服务端那边仍然会一直发送数据。但是,对于其它还没有架设数据流的命令来说,则没有这种限制。
4.4.Non-blocking, non-streaming data query
这种通信方式相当于一次性的Data streaming。它只会执行一次,而不会向Data streaming一样会一直发送数据。
// Request vision sensor data (function returns immediately (non-blocking)):
simxGetVisionSensorImage(clientID,sensorHandle,res,&img,0,simx_opmode_oneshot);
...
// Check later if some data has arrived (non-blocking):
if (simx_return_ok==simxGetVisionSensorImage(clientID,sensorHandle,res,&img,0,simx_opmode_buffer))
{ // yes, we have an image!
...
}
4.5.Synchronous operation
了解以上4种通信模式后,你可能会发现,在仿真运行时,这些方式都没有考虑客户端的运行情况。也就是说,服务端与客户端默认是异步运行的。这就会导致两边的仿真时间很可能会不一致。
Synchronous operation就是为了解决这个问题而设计的。在这种模式下,服务端的仿真运行完全交由客户端控制。 话句话说,只有当客户端发送可以运行下一步时,服务端才会运行下一步,否则就会一直停在之前一步。
remote API必须处于同步模式下才能使用这种通信方式。
simxSynchronous(clientID,true); // Enable the synchronous mode (Blocking function call)
simxStartSimulation(clientID,simx_opmode_oneshot);
// The first simulation step waits for a trigger before being executed
simxSynchronousTrigger(clientID); // Trigger next simulation step (Blocking function call)
// The first simulation step is now being executed
simxSynchronousTrigger(clientID); // Trigger next simulation step (Blocking function call)
// The second simulation step is now being executed
...
当调用simxSynchronousTrigger
时,下一步仿真就会开始计算。但是这并不意味着当该函数返回后,这一步仿真就已经完成计算了。 因此,在使用时必须保证仿真已经运行结束,否则可能会得到错误的数据。
如下图所示,程序在第二次调用simxGetJointPosition
时,显然上一步仿真还没有处理结束,因此获得的是上一步的数据;在第三次调用simxGetJointPosition
时,仿真才处理结束。显然,程序已经无法保证获得的数据是哪一步的了。
下面提供了一个最简单的方法解决上述问题,即在每次调用simxSynchronousTrigger
后,均调用一次simxGetPingTime
。simxGetPingTime
会在这一步仿真处理结束前,让程序一直处于堵塞状态,如下图所示。
simxSynchronous(clientID,true); // Enable the synchronous mode (Blocking function call)
simxStartSimulation(clientID,simx_opmode_oneshot);
// The first simulation step waits for a trigger before being executed
simxSynchronousTrigger(clientID); // Trigger next simulation step (Blocking function call)
// The first simulation step is now being executed
simxGetPingTime(clientID); // After this call, the first simulation step is finished (Blocking function call)
// Now we can safely read all streamed values