您的位置:首页 > 运维架构

CoppeliaSim 脚本(1)simulation scripts

2020-03-06 16:20 2161 查看

本文为官方文档翻译,仅供个人参考,如有侵权,联系删除

Embedded scripts

CoppeliaSim是一个高度可定制的仿真器,几乎每个仿真步骤都是用户自定义的。 通过集成的脚本解释器可以实现这种灵活性。 脚本语言是Lua。

Lua脚本可以很容易的被C/C++
代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。
Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。
一个完整的Lua解释器不过200k,在所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。(来自百度百科)

https://www.runoob.com/lua/lua-tutorial.html (菜鸟联盟 Lua教程)

CoppeliaSim扩展了Lua的命令并添加了CoppeliaSim特定的命令,这些命令可以通过其sim前缀来识别(例如sim.handleCollision)。

CoppeliaSim支持两种类型的嵌入式脚本:
仿真脚本(Simulation scripts): 仿真脚本是仅在仿真期间执行的脚本,用于自定义仿真或仿真模型。主仿真循环通过主脚本(main script)处理,模型/机器人通过子脚本(child script)控制。
自定义脚本(Customization scripts): 这些脚本也可以在不运行仿真时执行,并且可以用于自定义仿真场景或仿真器本身。

1.Simulation scripts

仿真脚本是仅在仿真运行时才执行的嵌入式脚本。 有两种类型的仿真脚本:
主脚本(main script): 默认情况下,每个场景都有一个处理所有功能的主脚本(负责调用子脚本(请参见下文))。 没有主脚本,仿真将无法运行。 可以自定义主脚本,但是最好在子脚本中完成所有自定义工作。
子脚本(child script): 每个场景对象(object)都可以与一个子脚本相关联,该子脚本将处理仿真的特定部分。 它们的一个特殊之处是它们也可以开辟一个线程来运行。 子脚本最常见的用途是让它们控制模型(例如,机器人)。
由于子脚本已附加到场景对象(即它们是关联的脚本),因此它们还将在复制和粘贴操作期间被复制,这是一个重要功能,可轻松扩展仿真场景。 关联脚本构成了CoppeliaSim分布式控制体系结构的基础。

1.1The main script

默认情况下,CoppeliaSim中的每个场景都有一个主脚本。它包含允许模拟运行的基本代码。没有主脚本,运行中的仿真将无法执行任何操作。
主脚本包含系统调用的函数。如果未定义给定函数,则该调用将被忽略。除初始化功能外,所有其他功能均为可选。默认的主脚本通常包含4个函数:
初始化函数:sysCall_init 该部分仅在模拟开始时执行一次。该代码负责准备仿真等。
驱动函数:sysCall_actuation 此部分将在每个仿真周期中执行。该代码负责以通用方式处理仿真器的所有致动功能(逆运动学,动力学等)。三个命令特别重要:sim.launchThreadedChildScripts,sim.resumeThreads和sim.handleChildScripts 。 sim.launchThreadedChildScripts / sim.resumeThreads 启动/恢复子脚本线程,而sim.handleChildScripts 调用非线程子脚本的sysCall_actuation函数 。没有这些命令,子脚本将无法执行或无法执行其启动功能,并且特定的模型功能或行为将无法按预期运行。
传感函数:sysCall_sensing 此部分将在每个仿真周期中执行。该代码负责以通用方式处理模拟器的所有感测功能(接近传感器,碰撞检测等)。两个命令特别重要:sim.resumeThreads和sim.handleChildScripts。 sim.resumeThreads恢复线程子脚本,而sim.handleChildScripts 调用非线程子脚本的sysCall_sensing函数 。没有这些命令,子脚本将无法执行其感应功能,并且特定的模型功能或行为将无法按预期运行。
回复函数:sysCall_cleanup 该部分将在模拟结束之前执行一次。该代码负责恢复对象的初始配置,清除传感器状态,碰撞状态等。

function sysCall_init()
-- Initialization part:
sim.handleSimulationStart()
sim.openModule(sim.handle_all)
sim.handleGraph(sim.handle_all_except_explicit,0)
end

function sysCall_actuation()
-- Actuation part:
sim.resumeThreads(sim.scriptthreadresume_default)
sim.resumeThreads(sim.scriptthreadresume_actuation_first)
sim.launchThreadedChildScripts()
sim.handleChildScripts(sim.syscb_actuation)
sim.resumeThreads(sim.scriptthreadresume_actuation_last)
sim.handleCustomizationScripts(sim.syscb_actuation)
sim.handleAddOnScripts(sim.syscb_actuation)
sim.handleSandboxScript(sim.syscb_actuation)
sim.handleModule(sim.handle_all,false)
sim.resumeThreads(2)
sim.handleMechanism(sim.handle_all_except_explicit)
sim.handleIkGroup(sim.handle_all_except_explicit)
sim.handleDynamics(sim.getSimulationTimeStep())
end

function sysCall_sensing()
-- Sensing part:
sim.handleSensingStart()
sim.handleCollision(sim.handle_all_except_explicit)
sim.handleDistance(sim.handle_all_except_explicit)
sim.handleProximitySensor(sim.handle_all_except_explicit)
sim.handleVisionSensor(sim.handle_all_except_explicit)
sim.resumeThreads(sim.scriptthreadresume_sensing_first)
sim.handleChildScripts(sim.syscb_sensing)
sim.resumeThreads(sim.scriptthreadresume_sensing_last)
sim.handleCustomizationScripts(sim.syscb_sensing)
sim.handleAddOnScripts(sim.syscb_sensing)
sim.handleSandboxScript(sim.syscb_sensing)
sim.handleModule(sim.handle_all,true)
sim.resumeThreads(sim.scriptthreadresume_allnotyetresumed)
sim.handleGraph(sim.handle_all_except_explicit,sim.getSimulationTime()+sim.getSimulationTimeStep())
end

function sysCall_cleanup()
-- Clean-up part:
sim.resetCollision(sim.handle_all_except_explicit)
sim.resetDistance(sim.handle_all_except_explicit)
sim.resetProximitySensor(sim.handle_all_except_explicit)
sim.resetVisionSensor(sim.handle_all_except_explicit)
sim.closeModule(sim.handle_all)
end

function sysCall_suspend()
sim.handleChildScripts(sim.syscb_suspend)
sim.handleCustomizationScripts(sim.syscb_suspend)
sim.handleAddOnScripts(sim.syscb_suspend)
sim.handleSandboxScript(sim.syscb_suspend)
end

function sysCall_suspended()
sim.handleCustomizationScripts(sim.syscb_suspended)
sim.handleAddOnScripts(sim.syscb_suspended)
sim.handleSandboxScript(sim.syscb_suspended)
end

function sysCall_resume()
sim.handleChildScripts(sim.syscb_resume)
sim.handleCustomizationScripts(sim.syscb_resume)
sim.handleAddOnScripts(sim.syscb_resume)
sim.handleSandboxScript(sim.syscb_resume)
end

不建议修改默认的主脚本。 原因如下:CoppeliaSim的优势之一是可以将任何模型(机器人,执行器,传感器等)复制到场景中并立即投入使用。 修改主脚本时,您冒着模型无法再按预期运行的风险(例如,如果您的主脚本缺少sim.handleChildScripts命令,则复制到场景中的所有模型将根本无法运行)。 另一个原因是,保留默认的主脚本可使旧场景轻松调整以适应新功能(例如,如果新的CoppeliaSim版本引入了简洁的命令sim.doMagic(),则旧场景将被自动更新以使该命令也被自动调用)。
但是,如果由于某种原因确实需要修改场景的主脚本,则可以通过双击场景层次结构顶部的世界图标旁边的浅红色脚本图标来执行此操作。
从打开主脚本的那一刻起,它将被标记为自定义脚本,并且将不再自动更新。
主脚本中的大多数命令的行为或操作方式均相似。 如果以距离计算功能为例,则在常规部分中具有:
sim.handleDistance(sim.handle_all_except_explicit): 此命令的作用是为所有已注册并在距离计算对话框中列出的距离对象计算最小距离。除已标记为显式处理的对象外,所有距离对象均使用该命令进行处理(即计算)。
任何新的距离对象都将由上述命令自动处理(只要未将其标记为显式处理即可)。完全相同的机制适用于碰撞检测,接近传感器和视觉传感器模拟,逆运动学等。这是一种强大的机制,无需编写任何代码即可运行简单的模拟。
主脚本中最重要的命令是sim.handleChildScripts,在驱动函数和传感函数中调用sim.handleChildScripts。没有此命令,将不会执行非线程子脚本。如果查看默认的主脚本,您会注意到,驱动函数允许启动或修改场景内容(例如sim.handleIkGroup,sim.handleDynamics等),而传感函数则允许感测和探测场景内容(例如sim.handleCollision,sim.handleDistance,sim.handleProximitySensor等)。以下说明了仿真配备了接近传感器的移动机器人时默认主脚本中发生的情况:

考虑到上述顺序,子脚本将始终从上一周期的传感(发生在上一周期仿真的末尾,在主脚本内部,通过sim.handleProximitySensor)读取接近传感器的状态(通过sim.readProximitySensor),然后做出对障碍的响应。
如果需要显式处理传感器,请确保始终在传感函数中进行操作,否则可能会遇到显示错误的情况,如下图所示:

1.2Child scripts

CoppeliaSim每个场景支持无限数量的子脚本。 每个子脚本代表一段用Lua编写的代码,允许在仿真中处理特定功能。 子脚本附加到场景对象(或与场景对象关联),并且可以从场景层次结构中的脚本图标轻松识别它们:

双击脚本图标可以打开脚本编辑器。 您可以更改脚本的属性,或通过脚本对话框将其与另一个对象关联。 通过选择对象,然后通过[menu bar --> Add --> Associated child script],可以将新的子脚本附加到对象。
子脚本与场景对象的关联具有重要的积极影响:
很好的可移植性: 子脚本将与其关联的对象一起保存/加载。使用子脚本,您可以创建可移植的代码和仿真模型,而无需依赖任何系统特定的插件。一个功能齐全的模型可以包含在一个文件中(可以在各种平台上使用而无需修改),而依靠插件进行模型控制则不是这种情况。此外,出于同样的原因,依赖于子脚本的模型也无需长期维护(例如,新的OS版本将不需要您调整部分代码或重新编译)。
固有的可复制性: 如果具有附加子脚本的对象被复制,则其子脚本也将被复制。复制的子脚本的内容将与原始子脚本的内容相同,但是,复制的子脚本将知道已被复制并正确重定向对象访问(例如,如果原始子脚本正在访问“机器人”,则复制的子脚本会自动在“ robot”后加上名称后缀,以访问重复的“ robot”而不是原始的“ robot”)。有关更多详细信息,请参阅有关以编程方式访问通用类型对象的部分。自动名称后缀调整允许复制对象和行为,而无需重写/调整任何代码。
不同模型版本之间没有冲突: 如果您修改给定模型的子脚本(例如,根据需要对其进行自定义),则对其他类似模型不会有任何影响。当依靠插件而不是子脚本进行模型控制时,这是一个更为关键的方面:实际上,对于插件,您总是冒着与以前的插件版本发生冲突的风险。
非常轻松的与仿真循环同步: 子脚本可以运行线程和非线程(请参阅下文)。甚至子脚本的线程版本也可以轻松地与仿真循环同步,这是一个强大的功能。
子脚本可以有两种不同的类型:非线程子脚本( non-threaded child scripts)或线程子脚本(threaded child scripts):

1.2.1Non-threaded child scripts

非线程子脚本包含阻塞函数。这意味着每次调用它们时,它们都应执行一些任务,然后返回控制。如果未返回控制,则整个仿真将停止。主脚本在每个仿真周期中,在主脚本的驱动和传感函数里,两次调用非线程子脚本函数。系统还将在适当的时候调用子脚本(例如,在子脚本初始化,清理或触发回调函数时)。应尽可能选择非线程脚本而非线程脚本。
非线程子脚本遵循精确的调用或执行顺序: 默认情况下,子脚本的调用以叶对象(或无子对象)开始,以根对象(或无父对象)结束 。(???)默认主脚本中的sim.handleChildScripts命令对非线程子脚本的调用进行处理。
想象一个自动门的仿真模型的示例:前面和后面的接近传感器可以检测到正在接近的人。当人员足够靠近时,门将自动打开。下面的代码显示了一个典型的非线程子脚本,该脚本说明了上面的示例:

-- 变量无需自动声明,不加local则为全局变量
function sysCall_init()		--仿真开始时运行一次,获取对象句柄
sensorHandleFront=sim.getObjectHandle("DoorSensorFront")
sensorHandleBack=sim.getObjectHandle("DoorSensorBack")
motorHandle=sim.getObjectHandle("DoorMotor")
end

function sysCall_actuation()	--每个仿真周期均运行
resF=sim.readProximitySensor(sensorHandleFront)
resB=sim.readProximitySensor(sensorHandleBack)	--读取传感器状态
if ((resF>0)or(resB>0)) then
sim.setJointTargetVelocity(motorHandle,-0.2)
else
sim.setJointTargetVelocity(motorHandle,0.2)
end
end

function sysCall_sensing()

end

function sysCall_cleanup()
-- Put some restoration code here
end

function sysCall_dynCallback(inData)
-- See the dynamics callback function section in the user manual for details about the input argument
end

function sysCall_jointCallback(inData)
-- See the joint callback function section in the user manual for details about input/output arguments
return outData
end

function sysCall_contactCallback(inData)
-- See the contact callback function section in the user manual for details about input/output arguments
return outData
end

function sysCall_beforeCopy(inData)
for key,value in pairs(inData.objectHandles) do
print("Object with handle "..key.." will be copied")
end
end

function sysCall_afterCopy(inData)
for key,value in pairs(inData.objectHandles) do
print("Object with handle "..key.." was copied")
end
end

function sysCall_beforeDelete(inData)
for key,value in pairs(inData.objectHandles) do
print("Object with handle "..key.." will be deleted")
end
-- inData.allObjects indicates if all objects in the scene will be deleted
end

function sysCall_afterDelete(inData)
for key,value in pairs(inData.objectHandles) do
print("Object with handle "..key.." was deleted")
end
-- inData.allObjects indicates if all objects in the scene were deleted
end

非线程子脚本的结构和主脚本类似。

1.2.2Threaded child scripts

线程子脚本是将在线程中启动的脚本。线程子脚本的启动(和恢复)由默认主脚本代码通过sim.launchThreadedChildScripts和sim.resumeThreads函数处理。线程化子脚本的启动/恢复以精确的顺​​序执行。当线程子脚本的执行仍在进行时,将不会再次启动它。线程化的子脚本结束后,仅当取消选中脚本属性中的“一次执行”项时,才能重新启动该子脚本。场景层次结构中的线程子脚本图标以浅蓝色而不是白色显示,表明它将在线程中启动。
与非线程子脚本相比,线程子脚本有几个缺点:它们更加占用资源,它们会浪费一些处理时间,并且对仿真停止命令的响应可能会稍差一些。
下面显示了一个典型的线程化子脚本代码,但是它并不是完美的,因为它浪费了循环中的宝贵计算时间(该代码从上面的示例中处理自动滑动门):

function sysCall_threadmain()
-- Put some initialization code here:
sensorHandleFront=sim.getObjectHandle("DoorSensorFront")
sensorHandleBack=sim.getObjectHandle("DoorSensorBack")
motorHandle=sim.getObjectHandle("DoorMotor")

-- Here we execute the regular thread code:
while sim.getSimulationState()~=sim.simulation_advancing_abouttostop do
resF=sim.readProximitySensor(sensorHandleFront)
resB=sim.readProximitySensor(sensorHandleBack)
if ((resF>0)or(resB>0)) then
sim.setJointTargetVelocity(motorHandle,-0.2)
else
sim.setJointTargetVelocity(motorHandle,0.2)
end
-- this loop wastes precious computation time since we should only read new
-- values when the simulation time has changed (i.e. in next simulation step).
end
end

function sysCall_cleanup()
-- Put some clean-up code here:
end

线程子脚本应为两部分:
主体部分: 该部分将在线程启动时执行,直到线程结束前不久。 这可以在仿真的开始,也可以在仿真的中间:请记住,与子脚本关联的对象可以随时复制/粘贴到场景中,甚至在仿真运行时也可以。 通常您会在这部分中放置一些初始化代码以及主循环:循环中的代码负责处理犯这个很难的特定部分(例如处理自动滑动门)。 在上面的特定示例中,该循环浪费了宝贵的计算时间,并且与主仿真循环异步运行。
恢复部分:该部分将在仿真结束之前或线程结束之前执行一次。
CoppeliaSim使用线程来模仿协程的行为,而不是传统上使用协程,因此具有很大的灵活性和控制力:默认情况下,线程化的子脚本将执行约1-2毫秒,然后自动切换到另一个线程。 可以使用sim.setThreadSwitchTiming或sim.setThreadAutomaticSwitch更改此默认行为。 切换当前线程后,它将在下一个仿真周期(即在currentTime + simulationTimeStep时间)恢复执行。 线程切换是自动的(在指定时间之后发生),但是sim.switchThread命令允许在需要时缩短该时间。 使用以上三个命令,可以实现与主仿真循环的出色同步。 以下代码(从上面的示例处理自动滑门)显示了与主仿真循环的子脚本同步:

function sysCall_threadmain()
-- Put some initialization code here:
sim.setThreadAutomaticSwitch(false) -- disable automatic thread switches
sensorHandleFront=sim.getObjectHandle("DoorSensorFront")
sensorHandleBack=sim.getObjectHandle("DoorSensorBack")
motorHandle=sim.getObjectHandle("DoorMotor")

-- Here we execute the regular thread code:
while sim.getSimulationState()~=sim.simulation_advancing_abouttostop do
resF=sim.readProximitySensor(sensorHandleFront)
resB=sim.readProximitySensor(sensorHandleBack)
if ((resF>0)or(resB>0)) then
sim.setJointTargetVelocity(motorHandle,-0.2)
else
sim.setJointTargetVelocity(motorHandle,0.2)
end
sim.switchThread() -- Explicitely switch to another thread now!
-- from now on, above loop is executed once in each simulation step.
-- this way you do not waste precious computation time and run synchronously.
end
end

function sysCall_cleanup()
-- Put some clean-up code here
end

现在对于每个主仿真循环while循环将只执行一次,而不会浪费时间一次又一次地读取传感器状态。 默认情况下,线程始终在主脚本调用sim.resumeThreads(sim.scriptthreadresume_default)时恢复。 如果需要确保线程仅在主脚本处于感测阶段时运行,则可以使用API函数sim.setThreadResumeLocation重新确定线程的恢复位置。
无法将CoppeliaSim线程的类似于协程的行为与常规线程区分开,不同之处在于,如果外部命令(例如Lua库提供的套接字通信命令)被阻止,则CoppeliaSim也将显示为阻止。 在这种情况下,可以按以下示例定义非阻塞节:

sim.setThreadIsFree(true) -- Start of the non-blocking section

http = require("socket.http")
print(http.request("http://www.google.com")) -- this command may take several seconds to execute

sim.setThreadIsFree(false) -- End of the non-blocking section
  • 点赞
  • 收藏
  • 分享
  • 文章举报
qq_29696095 发布了5 篇原创文章 · 获赞 0 · 访问量 540 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: