CoppeliaSim提供强大的计算功能或计算模块,这些功能或计算模块未直接封装在对象中(例如,接近传感器或视觉传感器),而是对一个或多个对象进行操作。
计算模块包括:
其他类似功能也可以通过插件提供,如路径/运动计划插件所提供的实例。
某些计算模块允许注册用户定义的计算对象。计算对象不同于场景对象,但是通过对其进行操作而间接链接到它们。这意味着计算对象本身不能存在:
如果在场景对象的复制/粘贴操作期间维护了给定计算对象的完整性,则该计算对象也将被自动复制。例如,如果碰撞对象A由可碰撞对象B和可碰撞对象C定义(即,在对象B和对象C之间执行碰撞检测),并且如果对象B和C同时复制(在同一复制粘贴中)操作),则碰撞对象A也将被复制。这代表了一项强大的功能,它不仅可以复制对象或模型,还可以复制其所有相关的计算对象(包括所有相关的脚本),从而可以保留对象或模型的全部功能和行为。
计算模块属性对话框位于[菜单栏->工具->计算模块属性]。 您还可以通过单击其工具栏按钮来打开对话框:
计算模块属性对话框显示与计算模块相关的属性。 该对话框分为4部分:
CoppeliaSim可以非常灵活的方式检测两个可碰撞实体之间的碰撞。该计算是精确的干扰计算。碰撞检测模块将仅检测碰撞。但是它不会直接对它们做出反应(有关碰撞响应,请参考动力学模块)。下图说明了冲突检测功能:
碰撞检测模块允许注册作为可碰撞实体对的碰撞对象。在模拟过程中,每个已注册的碰撞对象的碰撞状态可以用不同的颜色显示,或记录在图形对象中。有关如何记录碰撞状态的更多信息,请参考图形和图形数据流类型。如果已注册的碰撞对象在复制粘贴操作中同时复制了它们的两个组成实体,则会自动复制它们的碰撞检测模块。
碰撞检测模块中使用的碰撞检测例程也可以通过Coppelia几何例程作为独立例程使用。
碰撞检测对话框是计算模块属性对话框的一部分,位于[菜单栏->工具->计算模块属性]。您还可以通过单击其工具栏按钮来打开对话框:
在计算模块属性对话框中,单击“碰撞检测”按钮以显示碰撞检测对话框:
CoppeliaSim可以非常灵活的方式测量两个可测量实体之间的最小距离。该计算是精确的最小距离计算。距离计算模块将仅测量距离;但是它不会直接对他们做出反应。下图说明了距离计算功能:
距离计算模块允许注册作为可测量实体对的距离对象。在仿真过程中,每个可视距离对象的最小距离段可以可视化或记录在图形对象中。有关如何记录距离对象的更多信息,请参考图和图数据流类型。如果在复制粘贴操作中同时复制了两个距离实体,则已注册的距离对象将自动复制。
最小距离计算模块中使用的距离计算例程也可以通过Coppelia几何例程作为独立例程使用。
距离计算对话框是计算模块属性对话框的一部分,该对话框位于[菜单栏->工具->计算模块属性]。您还可以通过单击其工具栏按钮来打开对话框:
在计算模块属性对话框中,单击距离计算按钮以显示距离计算对话框:
CoppeliaSim的逆运动学(IK)计算模块非常强大且灵活。它允许以逆运动学模式(IK模式)或正向运动学模式(FK模式)处理几乎任何类型的机构。
IK问题可以看作是找到与给定身体元素(通常是末端执行器)的某些特定位置和/或方向相对应的关节值之一。更一般而言,它是从任务空间坐标到关节空间坐标的转换。例如,对于串行操纵器,问题将是在给定末端执行器的位置(和/或方向)的情况下找到操纵器中所有关节的值。反问题-在给定关节值的情况下找到末端执行器的位置-被称为FK问题,通常被认为比IK更容易完成。在处理开放式运动学链时,这确实是正确的,但不适用于一般类型的机械配置,例如以下所示:
在CoppeliaSim中,可以通过两种不同的独立方法使用运动学功能:
独立于所选的方法,CoppeliaSim的运动学功能使用完全相同的概念和术语来设置运动学任务:
最后,确保在文件夹scenes / ik_fk_simple_examples中查看与IK和FK相关的各种简单示例场景。
CoppeliaSim使用IK组和IK元素解决逆向和正向运动学任务。 重要的是要了解如何解决IK任务,以便充分利用模块的功能。 确保在文件夹scenes / ik_fk_simple_examples中查看与IK和FK相关的各种简单示例场景。
IK任务由IK组定义,该IK组包含一个或多个IK元素:
下图显示了为IK元素指定的两条运动链。 IK元素以相似的方式感知两条链(第二个示例的第一个关节被IK元素忽略):
IK元素的目标(即IK元素的分辨率)是通过计算运动链的适当关节值,使目标紧跟尖端(即尖端和目标重叠,并具有一定约束):
在上面的示例中(为简单起见,采用2D),我们可以为尖端-目标对指定各种约束,例如:
请注意,即使对于最基本的IK任务,IK元素也是通过包含的IK组的分辨率来求解的。
以相同的方式处理两个单独的运动链,但是这一次需要两个IK组(并且每个IK组都应该为每个运动链包含一个IK元素)。 两个IK组的解算顺序并不重要:
在上面的示例中,应该将target2附加到第一个运动链的可移动部分,然后求解顺序变得很重要,并且应该首先求解IK group1(解决结果将取代target2,如下图所示):
当一个IK元素建立在另一个IK元素之上而没有共享任何共同关节时,可能会出现类似情况,如下图所示:第一个运动学链用黑色表示,第二个运动学链用浅蓝色表示。 紫色表示的Base2是两条链之间的公共对象。 解决IK element2不会替换紫色链接,但是解决IK element1会替换紫色链接。 因此,与上述情况一样,必须在IK group2之前解决IK group1(解决顺序很重要):
当两个或多个运动链共享共同的关节时,会出现更困难的情况。 在这种情况下,顺序求解在大多数情况下不起作用(在以下示例中,两个IK元素倾向于将公共关节旋转到相反的方向),因此需要同时求解方法。 要同时求解多个IK元素,只需将它们分组为一个公共IK组。 下图说明了这种情况:
通过使用IK组和IK元素解决了机构的反向运动学(IK)或正向运动学(FK)(请参阅关于IK组和IK元素的基础知识部分)。 牢记以下检查点,以便成功设置IK或FK计算:
最后一点很重要,必须理解:已打开所有约束的运动链尖端将在x,y,z方向上遵循其关联的目标,同时尝试保持与目标相同的方向。 但是,只有在运动链具有至少6个非冗余自由度(DoF)的情况下,此方法才有效。 提示应始终受到适当的约束(即,永远不要指示超出该机制中的DoF的约束)。 位置约束在大多数情况下是相对于基座方向指定的,如下图所示:
但是,有时无法正确指定尖端的约束,在这种情况下,IK组的计算方法应为具有适当选择的阻尼系数的阻尼方法(例如DLS方法)。 当无法达到目标(无法达到或接近单一配置)时,也应选择阻尼分辨率方法。 阻尼可以使计算更稳定,但请记住,阻尼始终会降低IK计算的速度(需要进行多次迭代才能将笔尖放置到位)。
启用Alpha-Beta约束将使笔尖的z轴方向与目标的z轴方向匹配,如果关闭了Gamma约束,则保持绕z轴的自由旋转。 启用Alpha-Beta约束和Gamma约束时,尖端将尝试采用与其关联目标完全相同的方向。
有关IK组和IK元素的基础知识部分,介绍了解决简单运动链的IK问题的步骤。 解决简单运动链的FK问题是微不足道的(只需将期望的关节值应用于链中的所有关节即可获得尖端或末端执行器的位置和方向)。 解决封闭机构的IK和FK问题不是那么简单。
解决IK和FK的封闭机构
在下一节中,将讨论两个一般示例,这些示例应使用户能够理解解决一般类型封闭机制的方法:
如果发生FK问题,请首先确定要控制的关节(即,驱动机械的关节,活动关节)。 这些关节应从所有运动学计算中排除(选择与逆运动学模式不同的关节模式(请参见关节属性))。 从现在开始,通过运动学计算,这些接头将被认为是刚性的。 然后,确定需要关闭的运动链。 关闭将通过以尖端-目标对的形式的循环闭合约束进行处理,如下图所示:
然后,为活动关节设置所需的关节值,并调用逆运动学功能来处理回路闭合约束。 (默认主脚本处理所有未标记为显式处理的IK组)。
从图中可以看出,用户需要执行一项IK任务:将尖端放在目标上(或让笔尖跟随目标)。 这可以通过常规方式解决,或者用户可以使用联合依赖功能。 下图说明了这一点:
IK的主要任务是达到目标,回路闭合约束负责闭合机构,关节重叠约束负责保持机构的基础重叠(作为一条链)。 关节相关性线性方程的参数必须仔细选择才能达到完美的重叠(例如,如果两个相应的关节(通过重叠约束链接的关节)具有相同的方向,则需要设置方程中的系数 到-1!(因为一个关节是自下而上构建的,而另一个关节是自上而下构建的))。
大多数时候,有多种方法可以解决一种机构的IK或FK,在实施最复杂的替代方案之前,总是值得考虑各种替代方案!
运动学逆对话框是计算模块属性对话框的一部分,位于[菜单栏->工具->计算模块属性]。 您还可以通过单击其工具栏按钮来打开对话框:
在计算模块属性对话框中,单击“逆运动学”按钮以显示逆运动学对话框:
IK元素对话框是逆运动学对话框的一部分。 该对话框显示给定IK组的各种IK元素。 在逆运动学对话框中,选择一个IK组,然后单击“编辑IK元素”按钮以打开IK元素对话框:
CoppeliaSim的动力学模块当前支持四种不同的物理引擎:Bullet物理库,Open Dynamics引擎,Vortex Studio引擎和Newton Dynamics引擎。 在任何时候,用户都可以根据自己的仿真需求自由地从一种引擎快速切换到另一种引擎。 物理引擎支持如此多样化的原因是,物理模拟是一项复杂的任务,可以通过不同程度的精度,速度或支持多种功能来实现:
动态模块允许模拟接近现实世界对象交互的对象之间的交互。 它可以使物体掉落,碰撞,反弹,但也可以使机械手抓住物体,用传送带将零件向前推动,或者使车辆在不平坦的地形上以逼真的方式滚动。 下图说明了动态仿真:
与许多其他仿真软件包不同,CoppeliaSim并非纯粹的动力学模拟器。 可以将其看作是一种混合模拟器,它结合了运动学和动力学特性,以便在各种模拟情况下获得最佳性能。 如今,物理引擎仍然依赖于许多近似值,并且相对不精确且运行缓慢,因此在任何可能的情况下,您都应尝试使用运动学代替(例如对于机器人操纵器),并且仅在不可行的情况下依赖动力学(例如机器人操纵器的抓取器)。 如果要模拟的移动机器人不应该与环境发生碰撞或物理交互(无论如何,大多数移动机器人都很少这样做),并且该移动机器人只能在平坦的地面上运行(将绝大多数移动机器人分组) ,然后尝试使用运动学或几何计算来模拟机器人的运动。 结果将更快,更准确。
动力学模块的某些结果可以由图形对象记录。 有关如何记录动态数据的更多信息,请参考图形和图形数据流类型。
在CoppeliaSim中,将仅动态模拟有限数量的对象。 这些是形状、关节和力传感器,但是这将取决于场景结构和对象属性,是否会动态模拟给定的对象。 在仿真过程中可以轻松识别动态仿真的对象,因为在场景层次结构中,对象名称旁边会出现以下图标:
双击场景层次中的图标(仅在仿真过程中)将显示一些与对象的动态行为有关的信息。 动态模拟应该具有但由于某种原因而不能动态模拟的对象将改为显示以下图标:
静态/非静态,响应/非响应的形状
根据形状在动态模拟中的行为,可以将其分为4组:
在动态仿真期间,静态形状将不受影响(即它们相对于其父对象的位置是固定的),而非静态形状将直接受到重力或其他约束条件的影响(例如,动态启用的关节,请参见下文)。 可响应的形状在动态碰撞期间会相互影响(即,它们会产生相互碰撞的反应,它们会相互反弹)。 下图说明了静态/非静态,可响应/不可响应行为:
除非它们各自的碰撞蒙版不重叠,否则两个可响应的形状将始终产生碰撞反应。 可以在“形状动力学”属性对话框中设置静态/非静态,可响应/不可响应的形状属性以及碰撞蒙版。
动态启用的关节/力传感器
如果非静态形状不受其他限制,则它们会掉落(即受重力影响)。 可以通过将两个形状与可动态启用的关节或可动态启用的力传感器连接在一起来设置形状之间的动态约束。
下图显示了将关节或力传感器视为动态启用的有效情况(假设关节/力传感器和两个形状位于动态模拟的模型中,这是默认情况):
请遵循上述准则,以获取动态启用的关节或力传感器,这一点非常重要。 在模拟过程中,如果CoppeliaSim发现未动态启用的力传感器,它将在场景层次视图中其名称旁边显示一个小的警告图标。 在力/扭矩模式下的关节或应该以混合方式操作且没有动态启用的关节也会发生同样的情况。
以下是无法动态启用关节的一些示例情况:
以下是一些无法动态启用力传感器的示例情况:
刚性化合物
未通过动态启用的关节或力传感器链接的两个非静态形状将在动态仿真过程中彼此独立移动。 如果希望两个或多个形状表现为一个形状,则必须将它们分组(菜单栏->编辑->分组/合并->分组选定的形状)。 确保适当调整最终形状的惯性矩。 确保您还仔细阅读了有关纯形状的部分:
复合形状将表现为刚性实体,并且还具有相同的动态特性(例如摩擦系数)。 有时需要具有动态特性的刚性实体。 在这种情况下,必须通过力传感器将形状刚性连接。 这种结构的刚度不如复合形状强,但在大多数情况下效果良好:
通过使两个形状通过两个链接的虚拟对象链接起来,可以获得相似的结果。 两个虚拟实体的链接类型必须为“动态”,“重叠约束”(请参阅虚拟属性)。 在动态仿真期间,两个链接的虚拟对象将尝试采用相同的位置和方向。 下图显示了用于动态仿真的有效刚性回路闭合约束:
设计注意事项1
使用纯形状。 只要有可能,请尝试将纯形状用作可响应形状:在动态仿真过程中,纯形状会更加稳定且速度更快。 与其使用复杂的机器人模型三角形网格作为可响应的形状,或者使用稍微好一些的凸形表示,不如尝试用几种纯形状近似三角形网格,然后将它们分组[菜单栏->编辑->分组/合并- >对选定的形状进行分组](请注意,如果您合并纯形状而不是对它们进行分组,则生成的形状将不再是纯形状)。 下图说明了机器人隐藏的可响应纯形状:
从复杂的非纯形状中提取纯形状的一种简便方法是,进入复杂形状的三角形编辑模式,然后选择感兴趣的区域并提取矩形,球形或圆柱形的纯形状。 请参阅三角形编辑模式下的“提取长方体”,“提取圆柱体”和“提取球体”按钮。 确保您还阅读了有关导入和准备刚体的教程。 使用动态内容可视化工具栏按钮可视化和验证场景的动态内容也是一种好习惯(请参阅设计注意事项3)。
当物体可以碰撞但不能持续碰撞,或者在机械/机器人的稳定性中不发挥重要作用时,则不一定非要使用纯形状,凸形也可以是可行的替代方案;
设计注意事项2
使用凸形状代替随机形状。 只要有可能,请尝试使用纯形状作为可响应形状(请参阅设计注意事项1)。 但是,这并不总是那么容易做到,也不是实用的(例如,想像一个圆环)。 在这种情况下,您可以生成凸形状或凸分解形状(即仅包含凸网格的简单/复合形状)。 凸形状比随机形状执行得更快并且更稳定(但是它们仍然不如纯形状快和稳定!)。
选择要简化为凸形的形状,然后选择[菜单栏->编辑->将所选形状变形为凸形…]。 另请参阅项目[菜单栏->添加->选定形状的凸分解…]。 下图说明了凸分解:
设计注意事项3
仔细检查场景的动态内容。 有时,要在模拟中发挥积极作用的隐藏形状可能会有些混乱。 动态启用的形状,关节或力传感器通常是这种情况:实际上,它们大多数时候都隐藏在观察者的眼中。 但是,始终可以在仿真期间通过激活动态内容可视化按钮来检查动态内容:
注意上图中非纯形状的外观:它们的三角形网格轮廓以绿色或灰色表示。 应该不惜一切代价避免使用动态模拟的非纯形状,因为它们需要更长的计算时间,并且呈现出不太稳定的行为(但是,凸形比非凸形更快,更稳定)。 另请参考上面的设计注意事项1。
设计注意事项4
使用简单的层次结构。 建立要动态模拟的模型时,请将所有非动态对象附加到动态对象(非静态形状和动态启用的关节)。 上面的轮式机器人模型如下图所示:
设计注意事项5
仔细选择模型基础对象。 在构建动态模型时,实际上在构建静态模型时,始终要谨慎考虑模型将扮演的角色。 是否可以单独使用? 它可以建立在另一个模型或对象之上吗? 还是可以接受其他要在其之上构建的模型或对象? 考虑以下示例:
您已经创建了移动机器人的模型。 您还创建了夹具的模型。 显然,您希望能够轻松地将抓手模型附加到机器人模型的顶部。 相反,将永远都没有意义(将机器人模型附加在抓具模型之上)。 您可能还想单独使用机器人模型和机械手模型。 以下是可能的模型定义的场景层次结构视图:
注意“机器人”和“夹具”形状对象旁边的模型图标。 这表明在它们之上构建的所有对象都是同一模型定义的一部分。 两种模型各自运行良好。 但是,如果尝试将夹持器连接到机器人顶部(通过选择夹持器模型,然后选择机器人模型,然后单击[菜单栏->编辑->使上一个选定的对象成为父对象]),则夹持器将不会 在仿真过程中保持固定在机器人上。 以下是上述两个模型的场景层次结构,其中机械手已连接到机械手:
请注意,在上述场景层次中,纯形状“抓取器”是如何在纯形状“机器人”之上构建的。 并记住,如果不通过力传感器的连接加以限制,非静态形状将会掉落……确切地说,我们需要在“夹具”和“机器人”之间安装一个力传感器,以使两者之间具有牢固的连接!
机器人的正确模型定义必须包括抓具的连接点(或其中几个),如下图所示:
固定点是一个简单的力传感器。 将抓具模型与上述机器人模型组装在一起,将使抓具相对于机器人保持固定:
为了进一步简化组装过程,您可以自定义组装工具栏按钮的行为,以便以正确的相对位置/方向自动将抓手放置在附着点上。 有关更多详细信息,请参见有关模型的部分以及对象公共属性中的对话框项“组装”。
设计注意事项6
使用合理的尺寸。 形状太长,太细或太小可能会表现出奇怪的现象(抖动、跳跃)。 如果可能,请尝试将尺寸保持在3厘米以上。 否则,您可以在动态引擎常规属性对话框中调整内部缩放参数。
设计注意事项7
保持质量相似且不要太轻。 使用动态启用的关节或动态启用的力传感器链接两个形状时,请确保两个形状的质量没有太大差异(m1 <10 * m2和m2 <10 * m1),否则,关节或力传感器可能会非常柔软 摇摆不定,并且存在较大的位置/原点误差(但是,有时也可以将这种效果用作自然阻尼)。 此外,应避免质量过低的形状,因为它们无法将很大的力施加到其他形状上(即使由强力执行器推动)。
设计注意事项8
保持主要转动惯量较大。 尝试使主要的惯性矩/质量(*请参见“形状动力学属性”对话框)相对较大,否则机械链条可能难以控制和/或表现出奇怪的行为。
设计注意事项9
将动态形状分配给第9层。将所有应该隐藏的动态形状分配给第9层(请参阅对象的公共属性):在CoppeliaSim中,默认情况下,除第9-16层之外的所有层都是可见的。 编辑或测试场景时,您可以通过临时启用第9层(另请参阅层选择对话框)来快速可视化场景中所有隐藏的形状。
设计注意事项10
两个动态项目之间绝对不能有静态形状。 静态形状将中断动态链的逻辑行为:
设计注意事项11
切勿在动态项目的顶部放置静态可响应形状。 静态意味着形状的轨迹不会受到任何碰撞的影响,但是如果它同时是可响应的,则意味着它本身可以通过碰撞影响其他形状的轨迹。 仿真结果将是不可预测的。
常规动力学属性对话框是计算模块属性对话框的一部分,该对话框位于[菜单栏->工具->计算模块属性]。 您还可以通过单击其工具栏按钮来打开对话框:
在计算模块属性对话框中,单击“动力学”按钮以显示常规动力学属性:
动态引擎常规属性对话框是常规动力学属性对话框的一部分。 该对话框显示引擎配置和各种引擎特定的全局属性。 在常规动力学属性对话框中,单击调整引擎参数按钮以打开以下对话框:
每个引擎都有特定的参数,可以在全局(请参阅下文)或局部(在与接头有关的材料属性或动力学引擎属性中)设置特定参数。
Bullet属性
与Bullet物理库相关的属性。 请确保还参考Bullet用户手册以获取详细信息。
ODE属性
与Open Dynamics Engine相关的属性。 请确保还参考ODE用户手册以了解详细信息。
Vortex属性
与Vortex Studio引擎相关的属性。 确保还参考Vortex用户手册以了解详细信息。
Newton属性
与牛顿动力学引擎相关的属性。 确保还参考牛顿用户手册以获取详细信息。
Votex附加信息
使用引擎约束属性可使所有约束更柔和,从而有助于避免数值不稳定。 对于涉及非常大质量的模拟,可能必须减少约束松弛。 这些参数是全局参数,会影响所有约束和碰撞接触。 对于单个约束松弛,您可以直接参考约束参数,其中每个约束方程式可以独立地松弛。 对于因碰撞而引起的不稳定性,可以使用蒙皮厚度来平滑法向响应或增加滑移参数,以增加接触摩擦的粘度。 对于轻质刚体的链条和承受较大拉力的约束(例如,手动机构抓住比手指大几个数量级的物体),使用角速度阻尼将有助于提高仿真的鲁棒性。
CoppeliaSim是高度可定制的模拟器:模拟的各个方面都可以定制。此外,模拟器本身可以进行定制和裁剪,以使其性能完全符合要求。这可以通过精心设计的应用程序编程接口(API)来实现。支持六种不同的编程或编码方法,每种方法都具有相对于其他方法的特定优点(显然还有缺点),但是全部六种方法是相互兼容的(即可以同时使用,甚至可以手拉手使用)。模型、场景或模拟器本身的控制实体可以位于内部:
外部控制器教程中还讨论了上述6种方法。下表详细描述了每种方法的各自优点和缺点:
下图说明了CoppeliaSim及其周围的各种自定义可能性:
以下简要描述了上图中指示的各种通信或消息传递机制:
CoppeliaSim是一个高度可定制的模拟器:几乎每个模拟步骤都是用户定义的。 通过集成的脚本解释器可以实现这种灵活性。 脚本语言是Lua,它是一种扩展编程语言,旨在支持常规过程编程。 有关Lua的更多信息,请参阅Lua速成课程部分和在线文档。 默认情况下,CoppeliaSim使用正式的和原始的Lua,但是如果您愿意,可以通过将system/usrset.txt中的变量useExternalLuaLibrary设置为true来告诉CoppeliaSim使用另一种Lua。 在这种情况下,所有Lua调用都通过simLua库处理,该库本身将与LuaJIT(Lua即时编译器)链接。 simLua库项目文件位于此处。 有关Lua和LuaJIT的致谢和鸣谢,请参见此处。
CoppeliaSim扩展了Lua的命令并添加了CoppeliaSim特定的命令,这些命令可以通过其sim前缀来识别(例如sim.handleCollision)。 有关所有特定于CoppeliaSim的Lua命令的列表,请参阅常规API。 也可以从主客户端应用程序或插件中注册新的自定义Lua命令。 有关更多信息,请参考相关的API函数。
通过使用在线提供的Lua扩展库,可以轻松扩展Lua的功能本身。
嵌入式脚本是嵌入到场景(或模型)中的脚本,即,脚本是场景的一部分,并将与场景(或模型)的其余部分一起保存和加载。 支持不同类型的嵌入式脚本。 每种类型都有特定的功能和应用领域:
支持两种主要类型的嵌入式脚本:
除了嵌入式脚本,CoppeliaSim还支持加载项和沙箱脚本,这些附件和沙箱脚本可提供未链接到特定场景或模型的特定功能。
在不同的脚本类型中,记住一些与场景对象(附加到场景对象,即关联的脚本)相关联的脚本可能很有用,例如子脚本、联合控制回调脚本和自定义脚本,而另一些是不相关联的(即未关联的脚本)。关联脚本构成CoppeliaSim分布式控制体系结构的基础,如果它们的关联对象被复制,则共享可自动复制的便捷属性。
CoppeliaSim脚本可以在任何许可下发布。
仿真脚本是仅在仿真运行时才执行的嵌入式脚本。 有两种类型的仿真脚本:
由于子脚本已附加到场景对象(即它们是关联的脚本),因此它们还将在复制和粘贴操作期间被复制,这是一个重要功能,可轻松扩展模拟场景。 关联脚本构成了CoppeliaSim分布式控制体系结构的基础。
仿真脚本中的主脚本和子脚本在每个仿真中起着核心作用:主脚本包含仿真循环代码,而子脚本包含用于控制模型的典型代码(例如机器人、传感器或执行器)。
默认情况下,每个场景都有一个处理所有功能的主脚本。 没有主脚本,模拟将无法运行。 可以自定义主脚本,但是最好在子脚本中完成所有自定义工作。
每个场景对象都可以与一个子脚本相关联,该子脚本将以独立和分布式的方式处理模拟的特定部分。 子脚本最常见的用途是让它们控制模型。
以下是主脚本和子脚本之间的主要区别:
主脚本是模拟脚本。默认情况下,CoppeliaSim中的每个场景都有一个主脚本。它包含允许模拟运行的基本代码。没有主脚本,运行中的仿真将无法执行任何操作。
主脚本包含系统适当调用的回调函数的集合。如果未定义给定的系统回调函数,则将忽略该调用。除初始化功能外,所有其他功能均为可选。默认的主脚本通常分为4个主系统回调函数:
以下是典型的主要脚本,略作简化:
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.handleChildScripts函数从主脚本调用的,该函数以级联方式对场景层次和附加到单个场景对象的子脚本进行操作。
如果查看默认的主脚本,您会注意到,启动函数允许启动或修改场景内容(例如sim.handleIkGroup、sim.handleDynamics等),而感知函数则允许感测和探测场景内容(例如 sim.handleCollision、sim.handleDistance、sim.handleProximitySensor等)。 以下说明了模拟配备了接近传感器的移动机器人时默认主脚本中发生的情况:
考虑到上述顺序,子脚本将始终(使用sim.readProximitySensor)从先前的感应(发生在前一次模拟遍的末尾,在主脚本内部,使用sim.handleProximitySensor)读取(使用sim.readProximitySensor),然后对障碍物做出反应。
如果需要显式处理传感器,请确保始终在感测部分中进行操作,否则可能会遇到显示错误的情况,如下图所示:
与主脚本一样,它们也具有驱动和感应功能,非线程子脚本也一样。 另一方面,在主脚本处于驱动或感应功能时,可以压缩线程子脚本来运行,请参阅API函数sim.setThreadResumeLocation。
子脚本是模拟脚本。 CoppeliaSim支持每个场景无限数量的子脚本。 每个子脚本代表一小部分用Lua编写的例程,允许在仿真中处理特定功能。 子脚本附加到场景对象(或与场景对象关联),并且可以从场景层次结构中的脚本图标轻松识别它们:
双击脚本图标可以打开脚本编辑器。您可以更改给定脚本的属性,或通过脚本对话框将其与另一个对象关联。通过选择对象,然后导航到[菜单栏->添加->关联的子脚本],可以将新的子脚本附加到对象。子脚本与场景对象的关联具有重要且积极的后果:
非线程子脚本
非线程子脚本包含系统回调函数的集合。这些不应该阻塞。这意味着每次调用它们时,它们都应执行一些任务,然后返回控制权。 如果未返回控制,则整个模拟将停止。非线程子脚本函数由主脚本在每个模拟步骤中从主脚本的驱动和感测函数中至少调用两次。 系统还将在适当的地方(例如,在子脚本初始化、清理等期间)调用其他系统回调函数。 只要有可能,应该始终选择非线程化的子脚本,而不是线程化的子脚本。
非线程子脚本遵循精确的调用或执行顺序:默认情况下,子脚本的调用从叶对象(或无子对象)开始,以根对象(或无父对象)结束。 从默认主脚本调用的sim.handleChildScripts命令处理最重要的系统回调函数。想象一个代表自动门的模拟模型的示例:前和后各有一个接近传感器,用于检测正在接近的人。 当人员足够靠近时,门将自动打开。 下面的代码显示了一个典型的非线程子脚本,该脚本说明了上面的示例:
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
非线程子脚本应分为4个主要功能:
但是非线程子脚本可以使用更多的系统回调函数来响应各种事件:
function sysCall_init() -- not optional!!
-- Put some initialization code here
end
function sysCall_actuation()
-- Put some actuation code here
end
function sysCall_sensing()
-- Put some sensing code here
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_vision(inData)
-- See the vision callback function section in the user manual for details about input/output arguments
return outData
end
function sysCall_trigger(inData)
-- See the trigger 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_afterCreate(inData)
for key,value in pairs(inData.objectHandles) do
print("Object with handle "..value.." was created")
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
function sysCall_suspend()
-- Simulation is about to be suspended
end
function sysCall_suspended()
-- Simulation is suspended
end
function sysCall_resume()
-- Simulation is about to be resumed
end
线程子脚本
线程子脚本是将在线程中启动的脚本。 线程子脚本的启动(和恢复)由默认主脚本代码通过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
在非阻塞部分中,请尝试避免调用sim函数。 永远不要忘记关闭阻塞部分,否则CoppeliaSim可能会挂起或运行得更慢。 有关CoppeliaSim中线程操作的更多详细信息,另请参阅与线程相关的功能和阻止功能。
为了正确执行,不应中断某些操作(设想在一个循环中移动多个对象)。 在这种情况下,您可以使用sim.setThreadAutomaticSwitch函数临时禁止线程切换。
定制脚本是嵌入式脚本,可在很大程度上用于自定义模拟场景。 它们被附加到场景对象(或与场景对象关联),并且可以从场景层次结构中的暗脚本图标轻松识别它们:
双击脚本图标可以打开脚本编辑器。 您可以更改给定脚本的属性,或通过脚本对话框将其与另一个对象关联。 您可以通过选择对象,然后导航到[菜单栏->添加->关联的自定义脚本],将新的自定义脚本附加到该对象。
以下是定制脚本的主要属性:
以上属性允许自定义脚本共享附加组件和子脚本的某些最佳功能。 自定义脚本允许创建可自定义的模型,例如:设想一个被放入场景中的模型,即使没有运行模拟,该模型也能够自我配置或调整。 这可能是一个机器人,用户可以在其中通过单个滑块重新定位来调整各种链接长度。
自定义脚本包含阻止功能的集合。 这意味着每次调用它们时,它们都应执行一些任务,然后返回控制权。 如果未返回控制,则整个应用程序将暂停。 定制脚本函数经常由系统调用,但主脚本也会遵循精确的执行顺序进行调用。 定制脚本还支持回调功能。
定制脚本应分为几个功能,如以下框架脚本所示:
-- This is a customization script. It is intended to be used to customize a scene in
-- various ways, mainly when simulation is not running. When simulation is running,
-- do not use customization scripts, but rather child scripts if possible
function sysCall_init()
-- do some initialization here
end
function sysCall_nonSimulation()
-- is executed when simulation is not running
end
function sysCall_cleanup()
-- do some clean-up here
end
-- You can define additional system calls here:
function sysCall_beforeSimulation()
end
function sysCall_actuation()
end
function sysCall_sensing()
end
function sysCall_suspend()
end
function sysCall_suspended()
end
function sysCall_resume()
end
function sysCall_afterSimulation()
end
function sysCall_beforeInstanceSwitch()
end
function sysCall_afterInstanceSwitch()
end
function sysCall_userConfig()
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
function sysCall_beforeMainScript()
-- Can be used to step a simulation in a custom manner.
local outData={doNotRunMainScript=false} -- when true, then the main script won't be executed
return outData
end
function sysCall_afterCreate(inData)
for i=1,#inData.objectHandles,1 do
print("Object with handle "..inData.objectHandles[i].." was created")
end
end
如果可能,请不要使用自定义脚本来运行仿真代码,这最好通过子脚本来处理。
回调函数是特殊的脚本函数,可以由CoppeliaSim在模拟步骤的特定状态下调用:
非线程子脚本或自定义脚本可以包含动态回调函数。 如果存在,则物理引擎将在每个动力学模拟步骤之前和之后使用适当的参数调用回调函数。 动态回调函数可能经常被调用,通常每个模拟步骤调用10 * 2次(请记住,默认情况下,物理引擎时间步骤比模拟时间步骤小10倍)。 因此,请保持简单,以避免减慢仿真速度。
以下是一个简单的动态回调函数:
function sysCall_dynCallback(inData)
-- This function gets called often, so it might slow down the simulation
-- (this is called twice at each dynamic simulation step, by default 20x more often than a child script)
-- We have:
-- inData.passCnt : the current dynamics calculation pass. 1-10 by default. See next item for details.
-- inData.totalPasses : the number of dynamics calculation passes for each "regular" simulation pass.
-- 10 by default (i.e. 10*5ms=50ms which is the default simulation time step)
-- inData.dynStepSize : the step size used for the dynamics calculations (by default 5ms)
-- inData.afterStep : false when called before, and true after a dynamics step was computed.
local txt=string.format(" the %ith dynamics calculation step (out of %i steps)",inData.passCnt,inData.totalPasses)
if inData.afterStep then
txt="After"..txt
else
txt="Before"..txt
end
print(txt)
end
非线程子脚本或自定义脚本可以包含联合回调函数。 当存在给定关节时(必须动态启用并且还必须启用其控制循环),物理引擎将使用适当的参数调用回调函数,从而允许用户自定义相关关节的控制环以进行编写低级控制算法。 可能经常调用联合回调函数,对于给定的联合,通常每个模拟步骤调用10次(请记住,默认情况下,物理引擎时间步比模拟时间步小10倍)。 因此,请保持简单,以避免减慢仿真速度。
以下是一个简单的PID联合回调函数:
function sysCall_jointCallback(inData)
-- This function gets called often, so it might slow down the simulation
-- (this is called at each dynamic simulation step, by default 10x more often than a child script)
-- We have:
-- inData.first : whether this is the first call from the physics engine, since the joint
-- was initialized (or re-initialized) in it.
-- inData.revolute : whether the joint associated with this script is revolute or prismatic
-- inData.cyclic : whether the joint associated with this script is cyclic or not
-- inData.handle : the handle of the joint associated with this script
-- inData.lowLimit : the lower limit of the joint associated with this script (if the joint is not cyclic)
-- inData.highLimit : the higher limit of the joint associated with this script (if the joint is not cyclic)
-- inData.passCnt : the current dynamics calculation pass. 1-10 by default. See next item for details.
-- inData.totalPasses : the number of dynamics calculation passes for each "regular" simulation pass.
-- 10 by default (i.e. 10*5ms=50ms which is the default simulation time step)
-- inData.currentPos : the current position of the joint
-- inData.targetPos : the desired position of the joint
-- inData.errorValue : targetPos-currentPos (with revolute cyclic joints we take the shortest cyclic distance)
-- inData.effort : the last force or torque that acted on this joint along/around its axis. With Bullet,
-- torques from joint limits are not taken into account
-- inData.dynStepSize : the step size used for the dynamics calculations (by default 5ms)
-- inData.targetVel : the joint target velocity (as set in the user interface)
-- inData.maxForce : the joint maximum force/torque (as set in the user interface)
-- inData.velUpperLimit : the joint velocity upper limit (as set in the user interface)
--
-- Make sure that the joint is dynamically enabled, is in force/torque mode, motor enabled and
-- control loop enabled, otherwise this function won't be called
if inData.first then
PID_P=0.1
PID_I=0
PID_D=0
pidCumulativeErrorForIntegralParam=0
end
-- The control happens here:
-- 1. Proportional part:
local ctrl=inData.errorValue*PID_P
-- 2. Integral part:
if PID_I~=0 then
pidCumulativeErrorForIntegralParam=pidCumulativeErrorForIntegralParam+inData.errorValue*inData.dynStepSize
else
pidCumulativeErrorForIntegralParam=0
end
ctrl=ctrl+pidCumulativeErrorForIntegralParam*PID_I
-- 3. Derivative part:
if not inData.first then
ctrl=ctrl+(inData.errorValue-pidLastErrorForDerivativeParam)*PID_D/inData.dynStepSize
end
pidLastErrorForDerivativeParam=inData.errorValue
-- 4. Calculate the velocity needed to reach the position in one dynamic time step:
local maxVelocity=ctrl/inData.dynStepSize -- max. velocity allowed.
if (maxVelocity>inData.velUpperLimit) then
maxVelocity=inData.velUpperLimit
end
if (maxVelocity<-inData.velUpperLimit) then
maxVelocity=-inData.velUpperLimit
end
local forceOrTorqueToApply=inData.maxForce -- the maximum force/torque that the joint will be able to exert
-- 5. Following data must be returned to CoppeliaSim:
firstPass=false
local outData={}
outData.velocity=maxVelocity
outData.force=forceOrTorqueToApply
return outData
end
非线程子脚本或自定义脚本可以包含联系人回调函数。 如果存在,并且物理引擎检测到两个可响应形状之间发生冲突,则将使用适当的参数调用联系人回调函数,从而允许用户自定义联系人的处理方式。 联系人回调函数可能经常被调用,有时每个模拟步骤会调用数百次(还请记住,默认情况下,一个模拟步骤将物理引擎调用10次)。 因此,请保持简单,以避免减慢仿真速度。
以下是典型的联系人回调函数:
function sysCall_contactCallback(inData)
-- Will objects with inData.handle1 and inData.handle2 respond to dynamic collision?
local retData={}
retData.ignoreContact=false -- handle contact here
retData.collisionResponse=true -- shapes will collide
if inData.engine==sim.physics_bullet then
retData.bullet={}
retData.bullet.friction=0
retData.bullet.restitution=0
end
if inData.engine==sim.physics_ode then
retData.ode={}
retData.ode.maxContacts=16
retData.ode.mu=0
retData.ode.mu2=0
retData.ode.bounce=0
retData.ode.bounceVel=0
retData.ode.softCfm=0
retData.ode.softErp=0
retData.ode.motion1=0
retData.ode.motion2=0
retData.ode.motionN=0
retData.ode.slip1=0
retData.ode.slip2=0
retData.ode.fDir1={0,0,0}
local mode=1 -- bit-coded. See below
-- 1=dContactMu2
-- 2=dContactFDir1
-- 4=dContactBounce
-- 8=dContactSoftERP
-- 16=dContactSoftCFM
-- 32=dContactMotion1
-- 64=dContactMotion2
-- 128=dContactSlip1
-- 256=dContactSlip2
-- 512=dContactApprox1_1
-- 1024=dContactApprox1_2
-- 2048=dContactApprox1
retData.ode.contactMode=mode
end
if inData.engine==sim.physics_vortex then
end
if inData.engine==sim.physics_newton then
retData.newton={}
retData.newton.staticFriction=0
retData.newton.kineticFriction=0
retData.newton.restitution=0
end
return(retData)
end
当与视觉传感器关联时,子脚本或自定义脚本可以包括视觉回调功能。 当存在于给定的视觉传感器时,则系统将在每次获取或应用新图像时调用回调函数,从而允许用户执行图像处理。 以下API函数就是这种情况:sim.handleVisionSensor、sim.checkVisionSensor、sim.checkVisionSensorEx、sim.setVisionSensorImage和sim.setVisionSensorCharImage。
视觉回调函数的位置适用一些条件:通常,它应位于自定义脚本或非线程子脚本中。 但是,如果从线程子脚本中调用触发API函数(例如sim.handleVisionSensor),则视觉回调函数也应位于线程子脚本中。 如果在非线程子脚本和自定义脚本中都存在视觉回调函数,并且该自定义脚本都附加到视觉传感器,则将首先调用该子脚本,然后再调用自定义脚本。
以下表示一个空的视觉回调函数:
function sysCall_vision(inData)
-- We have:
-- inData.handle : the handle of the vision sensor.
-- inData.resolution : the x/y resolution of the vision sensor
-- inData.clippingPlanes : the near and far clipping planes of the vision sensor
-- inData.viewAngle : the view angle of the vision sensor (if in persp. proj. mode)
-- inData.orthoSize : the ortho size of the vision sensor (if in orth. proj. mode)
-- inData.perspectiveOperation : true if the sensor is in persp. proj. mode
local outData={}
outData.trigger=false -- true if the sensor should trigger
outData.packedPackets={} -- a table of packed packets. Can be accessed via e.g. sim.readVisionSensor
return outData
end
可以通过使用各种API函数来执行图像处理。 视觉插件导出了一些非常简单的图像处理功能。 通过图像插件(OpenCV包装器)支持更多图像处理功能。
以下是一个简单的边缘检测视觉回调函数,该函数触发并返回一个数据包(基于视觉插件功能):
function sysCall_vision(inData)
simVision.sensorImgToWorkImg(inData.handle)
simVision.edgeDetectionOnWorkImg(inData.handle,0.1)
simVision.workImgToSensorImg(inData.handle)
local outData={}
outData.trigger=true
local packetData={1.0,42.123,129.3}
outData.packedPackets={sim.packFloatTable(packetData)}
return outData
end
以下是视觉回调函数,该函数在获取的图像上绘制一个圆圈(基于图像插件函数):
function sysCall_vision(inData)
local imgHandle=simIM.readFromVisionSensor(inData.handle)
local center={inData.resolution[1]/2,inData.resolution[2]/2}
local radius=(inData.resolution[1]+inData.resolution[2])/8
simIM.circle(imgHandle,center,radius,{255,0,255},4)
simIM.writeToVisionSensor(imgHandle,inData.handle)
simIM.destroy(imgHandle)
end
当与视觉传感器、接近传感器或力/扭矩传感器相关联时,子脚本或自定义脚本可以包括触发器回调函数。
某些条件适用于触发器回调函数的位置:通常,它应位于自定义脚本或非线程子脚本中。 但是,如果从线程子脚本中调用了触发API函数(例如sim.handleVisionSensor或sim.handleProximitySensor),则触发器回调函数也应位于线程子脚本中。 如果非线程子脚本和自定义脚本中都存在触发器回调函数,并且这两个都附加到对象触发器上,则将首先调用子脚本,然后再调用自定义脚本。
视觉传感器可以在视觉回调函数内部生成触发信号。 然后按以下示例调用触发器回调(如果存在):
function sysCall_trigger(inData)
-- We have:
-- inData.handle : the handle of the vision sensor.
-- inData.packedPackets : an array of data packets, packed (use sim.unpackFloatTable to unpack)
-- the first data packet always contains 15 auxiliary values about the acquired image:
-- - minimum of {intensity, red, green blue and depth value}
-- - maximum of {intensity, red, green blue and depth value}
-- - average of {intensity, red, green blue and depth value}
local outData={}
outData.trigger=true
return outData
end
当检测到物体时,接近传感器会生成触发信号。 然后按以下示例调用触发器回调(如果存在):
function sysCall_trigger(inData)
-- We have:
-- inData.handle : the handle of the proximity sensor.
-- inData.detectedObjectHandle : handle of detected object
-- inData.detectedPoint : detected point, relative to sensor frame
-- inData.normalVector : normal vector at detected point, relative to sensor frame
local outData={}
outData.trigger=true
return outData
end
以下是力/转矩传感器触发回调函数的示例,其中力/转矩传感器已损坏:
function sysCall_trigger(inData)
-- We have:
-- inData.handle : the handle of the force/torque sensor.
-- inData.force : current force
-- inData.torque : current torque
-- inData.filteredForce : current filtered force
-- inData.filteredTorque : current filtered torque
sim.breakForceSensor(inData.handle)
end
定制脚本可以包括用户配置回调函数,该函数在用户双击场景层次结构中的用户参数图标时触发。 例如,这提供了一种灵活的行为来设置仿真参数;可以打开一个复杂的自定义用户界面进行用户交互,或者打开一个简单的文件对话框,如以下示例所示:
function sysCall_userConfig()
fileAndPath=sim.fileDialog(sim.filedlg_type_load,'Select file','','','text files','txt')
print('Selected file is: ',fileAndPath)
end
脚本对话框位于[菜单栏->工具->脚本]。 另外,也可以通过其工具栏按钮进行访问:
脚本不是随机执行的:脚本类型、脚本位置和脚本设置会影响脚本执行时间(相对于其他脚本)。 要记住的一个简单规则是:脚本越重要或更具持久性,它就越晚被调用/执行。
执行顺序首先基于脚本类型。 我们有以下顺序,从第一次执行到最后执行:
由于子脚本是模拟脚本,因此它们只会在模拟运行时运行(即它们不是持久性的)。 定制脚本、附加脚本和沙箱脚本不是这种情况,它们在模拟停止后也会运行。 此外,切换到其他场景时,附加脚本和沙箱脚本也将继续运行。 上面的顺序很有意义,因为可以将重要的脚本设计为依靠不太重要的脚本生成的数据并对其进行操作。
例如,将按以下顺序调用回调sysCall_sensing:首先在子脚本中,然后在自定义脚本中,在附加脚本中,最后在沙箱脚本中。
在脚本类型中,执行顺序取决于脚本在场景层次结构中的位置及其以下两个脚本设置:
树遍历:指定何时执行脚本(相对于场景层次结构中紧随其后的脚本(其后代脚本))。 如果使用反向,则首先执行后代脚本,使用反向,最后执行后代脚本。 与父级相同使用与第一个祖先脚本相同的树遍历。 树遍历仅与相同类型的脚本有关。 默认为反向。
脚本编辑器允许编辑CoppeliaSim中的各种脚本。 通过在脚本对话框中双击脚本或在场景层次结构中双击脚本图标可以将其打开。
脚本编辑器具有以下功能,可简化代码版本:
可以通过键入前三个字母(通常是“ sim”)来轻松访问API函数。 修改脚本后,无需显式保存更改:关闭脚本编辑器,保存场景或启动仿真会将更改自动应用到脚本。 运行模拟时,对给定脚本的修改仅在启动新的模拟运行后才会生效,但自定义脚本始终会在关闭编辑器后应用更改,定制脚本除外。 用户还可以明确地重新启动/重置给定脚本,以使更改立即生效。
如果您想从给定脚本访问递归函数,或者只想从外部文件中运行代码,则可以执行以下操作:
require "myExternalLuaFile"
在这种情况下,请确保文件名为myExternalLuaFile.lua,并且不要忘记将其与场景或模型一起分发,因为该代码将不再是CoppeliaSim文件的一部分。 搜索路径通常如下:
通过在文件名上打开弹出菜单,可以在同一脚本编辑器中打开通过require指令包含的文件。
每个也与脚本关联的对象都可以附加用户参数。 这些参数可以用作例如调整特定模型值的快速方法(例如,移动机器人的最大速度或传感器的分辨率)。 启用后,用户参数图标将显示在场景层次结构中的脚本图标旁边:
如果用户修改了参数列表中的值,则脚本可以对该修改做出反应并相应地调整其行为。 脚本可以使用sim.getUserParameter函数来检索参数的值,或使用sim.setUserParameter函数来修改参数的值。 当对象的用户参数列表为空时,用户参数图标将不会显示。 要为特定对象启用它,只需键入sim.setUserParameter(objectHandle,’@ enable’,’’),并确保该对象与脚本相关联。
以下是用户参数对话框中的项目:
用户参数代表一种通过对话框调整特定值的简单方法。 通过使用用户配置回调函数可以实现更灵活的行为:如果在附加到对象的自定义脚本中定义了此类回调函数,则双击用户参数图标将改为调用该回调函数,用户可以在其中准备 例如,更复杂的用户交互对话框。
插件是一个共享库(例如dll),由CoppeliaSim的主客户端应用程序在程序启动时自动加载,或通过sim.loadModule / sim.unloadModule动态加载/卸载。 它允许通过用户编写的功能来扩展CoppeliaSim的功能(类似于附加组件的方式)。 该语言可以是能够生成共享库并可以调用导出的C函数的任何语言(例如,对于Java,请参阅GCJ和IKVM)。 插件也可以用作运行其他语言编写的代码的包装程序,甚至可以运行其他语言编写的代码(例如,编写用于处理和执行Atmel微控制器代码的插件)。
插件通常用于自定义模拟器和/或特定的模拟。 通常,插件仅用于提供带有自定义脚本命令的模拟,因此可与脚本结合使用。 有时,插件用于为CoppeliaSim提供特殊功能,需要快速计算能力(脚本通常比编译语言慢)或硬件设备(例如,真实的机器人)的接口。
每个插件都必须具有以下3个入口点过程:
extern "C" __declspec(dllexport) unsigned char simStart(void* reserved,int reservedInt);
extern "C" __declspec(dllexport) void simEnd();
extern "C" __declspec(dllexport) void* simMessage(int message,int* auxiliaryData,void* customData,int* replyData);
如果缺少一个过程,则该插件将被卸载且无法运行。 有关插件的加载状态,请在启动时参考控制台窗口。 下面简要介绍以上三个入口点的用途:
simStart
主客户端应用程序加载插件后,将一次调用此过程。 该程序应:
simEnd
在仿真循环退出之前,将一次调用此过程。 该过程应释放自调用simStart以来保留的所有资源。
simMessage
模拟器运行时,经常会调用此过程。 该过程负责监视感兴趣的消息并对它们做出反应。 重要的是要根据插件的任务对以下事件做出反应(最好通过截获sim_message_eventcallback_instancepass消息):
有关更多详细信息,请参考sim_message_eventcallback_-type的消息。 在编写插件时,必须注意或考虑其他几点:
您可以随意使用所需的任何编译器来编译插件。 但是,如果您希望编写Qt插件(即使用Qt框架的插件),则应记住以下几点:
有关插件的更多信息,请参考以下存储库:
有关插件的更多信息,请参考以下存储库:
CoppeliaSim插件可以任何许可发布。
CoppeliaSim中的附加组件与插件非常相似:它在程序启动时自动加载,并允许用户编写的功能扩展CoppeliaSim的功能。 附件是用Lua编写的。 支持两种类型的加载项:
附加功能和脚本应编写在与主应用程序位于同一文件夹中的文本文件中,并遵循以下命名约定:
仍然可以通过命令行选项加载和运行不遵循上述命名约定的加载项脚本。
附加脚本应分为几个功能,如以下框架脚本所示:
-- Return sim.syscb_cleanup if you wish to stop the add-on script
function sysCall_init()
end
function sysCall_cleanup()
end
function sysCall_nonSimulation()
end
function sysCall_beforeSimulation()
end
function sysCall_beforeMainScript()
-- Can be used to step a simulation in a custom manner.
local outData={doNotRunMainScript=false} -- when true, then the main script won't be executed
return outData
end
function sysCall_actuation()
end
function sysCall_sensing()
end
function sysCall_afterSimulation()
end
function sysCall_suspend()
end
function sysCall_suspended()
end
function sysCall_resume()
end
function sysCall_addOnScriptSuspend()
end
function sysCall_addOnScriptSuspended()
end
function sysCall_addOnScriptResume()
end
function sysCall_beforeInstanceSwitch()
end
function sysCall_afterInstanceSwitch()
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
function sysCall_afterCreate(inData)
for i=1,#inData.objectHandles,1 do
print("Object with handle "..inData.objectHandles[i].." was created")
end
end
附件可以调用任何常规API函数,只要文档中未另行说明即可。 他们甚至可以调用由插件注册的自定义Lua函数。 但是,它们有两个限制:
有关附加组件的更多信息,请确保检查安装文件夹中的演示附加组件simAddOnScript-addOnScriptDemo.lua、simAddOnFunc-addOnFunctionDemo.lua和simAddOnScript-b0RemoteApiServer.lua的内容。
沙盒脚本与附加脚本非常相似:它在程序启动时自动加载,并允许用户编写的一个或多个功能扩展CoppeliaSim的功能。 除此之外,沙箱脚本还广泛用于CoppeliaSim的Lua commander插件(read-eval-print循环)中,该插件向CoppeliaSim状态栏添加了文本输入,从而可以像在终端机中那样快速输入和执行Lua代码。 它在所有打开的场景中都是持久的,并且会不断执行,并在后台有效运行。 出于这个原因,它每次应仅执行极简代码,否则整个应用程序的运行速度将会降低。 系统经常以精确的顺序调用沙箱脚本。
沙箱脚本是在启动时从system / sndbxscpt.txt加载的,应分为几个功能,如以下框架脚本所示:
function sysCall_init() -- not optional!
-- do some initialization here
end
function sysCall_cleanup()
-- do some clean-up here
end
function sysCall_nonSimulation()
-- is executed when simulation is not running
end
function sysCall_beforeSimulation()
-- Simulation is about to start
end
function sysCall_beforeMainScript()
-- Can be used to step a simulation in a custom manner.
local outData={doNotRunMainScript=false} -- when true, then the main script won't be executed
return outData
end
function sysCall_actuation()
-- put some actuation code here.
end
function sysCall_sensing()
-- put some sensing code here.
end
function sysCall_afterSimulation()
-- Simulation has just ended
end
function sysCall_suspend()
-- Simulation is about to be suspended
end
function sysCall_suspended()
-- Simulation is suspended
end
function sysCall_resume()
-- Simulation is about to resume
end
function sysCall_beforeInstanceSwitch()
-- About to switch to another scene
end
function sysCall_afterInstanceSwitch()
-- Switched to another scene
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
function sysCall_afterCreate(inData)
for i=1,#inData.objectHandles,1 do
print("Object with handle "..inData.objectHandles[i].." was created")
end
end
沙箱脚本可以调用任何常规API函数,只要文档中未另行说明即可。 它甚至可以调用由插件注册的自定义Lua函数。 但是,它有两个限制:
CoppeliaSim是一个功能库:没有主客户端应用程序(或主应用程序或主循环),CoppeliaSim无法运行。 安装包随附的默认主客户端应用程序是coppeliaSim.exe(Windows)或coppeliaSim(MacOSX和Linux)。 请注意,在MacOSX下,客户端应用程序以及其他几个项目(例如库)包含在软件包或捆绑包(coppeliaSim.app)中:coppeliaSim.app/Contents/MacOS/coppeliaSim。
主客户端应用程序是一个小型可执行文件,处理以下主要任务:
coppeliaSimClientApplication需要以下文件来编译和运行该应用程序(不过,最简单的方法是将新建的主应用程序简单地复制到CoppeliaSim Pro / CoppeliaSim Edu / CoppeliaSim Player安装文件夹中):
可以自定义主要客户端应用程序。 但是不建议这样做,仅当编写脚本和/或插件无法满足您的目的时才应使用此方法,因为如果未正确实施,与默认CoppeliaSim行为失去兼容性的风险很高。
在继续阅读本节之前,请确保您了解模拟器的总体结构及其API框架。
信号
信号可以看作是全局变量。 当前支持四种类型的信号:整数,浮点,双精度和字符串型信号。 可以定义,重新定义,读取和清除信号。 在仿真结束时,将清除由主脚本或任何子脚本创建的所有信号。 例如:
-- script 1 writes the data to string signal "mySignalName":
local myData={1,2,{"Hello","world",true,{value1=63,value2="aString"}}}
sim.setStringSignal("mySignalName",sim.packTable(myData))
-- script 2 reads the data from string signal "mySignalName":
local myData=sim.getStringSignal("mySignalName")
if myData then
myData=sim.unpackTable(myData)
end
自定义数据块
定制数据块是可以存储在对象内部或场景内部的数据。 它可以用来存储要与模型或场景一起保存的自定义数据,也可以用作通信手段。 例如:
-- script 1 writes the data to the scene:
local myData={1,2,{"Hello","world",true,{value1=63,value2="aString"}}}
sim.writeCustomDataBlock(sim.handle_scene,"myTag",sim.packTable(myData))
-- script 2 reads the data from the scene:
local myData=sim.readCustomDataBlock(sim.handle_scene,"myTag")
if myData then
myData=sim.unpackTable(myData)
end
ROS
例如:
-- script subscribes to a topic carrying an image:
function sysCall_init()
sub=simROS.subscribe('/image', 'sensor_msgs/Image', 'imageMessage_callback')
simROS.subscriberTreatUInt8ArrayAsString(sub)
end
function imageMessage_callback(msg)
-- here we have:
-- msg.data
-- msg.height
-- msg.width
-- etc.
end
BlueZero
例如:
-- script subscribes to a topic carrying an image:
function sysCall_init()
b0Node=simB0.create('b0Node')
sub=simB0.createSubscriber(b0Node,'image','imageMessage_callback')
simB0.init(b0Node)
end
function sysCall_sensing()
simB0.spinOnce(b0Node)
end
function imageMessage_callback(msg)
-- msg is a raw buffer.
-- If the image data was encoded in base64, we could have:
msg=sim.transformBuffer(msg,sim.buffer_base64,1,0,sim.buffer_uint8)
end
远程API
例如,从基于Python B0的远程API客户端中:
# Receiving an image from CoppeliaSim and sending it back:
import b0RemoteApi
with b0RemoteApi.RemoteApiClient('b0RemoteApi_pythonClient','b0RemoteApi') as client:
def imageCallback(msg):
client.simxSetVisionSensorImage(passiveVisionSensorHandle[1],False,msg[2],client.simxDefaultPublisher())
visionSensorHandle=client.simxGetObjectHandle('VisionSensor',client.simxServiceCall())
passiveVisionSensorHandle=client.simxGetObjectHandle('PassiveVisionSensor',client.simxServiceCall())
client.simxGetVisionSensorImage(visionSensorHandle[1],False,client.simxDefaultSubscriber(imageCallback))
client.simxStartSimulation(client.simxDefaultPublisher())
while True:
client.simxSpinOnce()
client.simxStopSimulation(client.simxDefaultPublisher())
例如,从Python旧版远程API客户端中:
# Receiving an image from CoppeliaSim and sending it back:
import sim
clientID=sim.simxStart('127.0.0.1',19999,True,True,5000,5)
if clientID!=-1:
res,v0=sim.simxGetObjectHandle(clientID,'Vision_sensor',sim.simx_opmode_oneshot_wait)
res,v1=sim.simxGetObjectHandle(clientID,'PassiveVision_sensor',sim.simx_opmode_oneshot_wait)
res,resolution,image=sim.simxGetVisionSensorImage(clientID,v0,0,sim.simx_opmode_streaming)
while (sim.simxGetConnectionId(clientID)!=-1):
res,resolution,image=sim.simxGetVisionSensorImage(clientID,v0,0,sim.simx_opmode_buffer)
if res==sim.simx_return_ok:
res=sim.simxSetVisionSensorImage(clientID,v1,image,0,sim.simx_opmode_oneshot)
sim.simxFinish(clientID)
持久性数据块
持久性数据块可以看作是持久性全局缓冲区。 持久数据块可以定义,重新定义,读取和清除,并在所有打开的场景之间共享。 它们会一直持续到模拟器结束,但也可能会一直保留在文件中,并在下次启动CoppeliaSim时自动重新加载。
自定义lua函数
主客户端应用程序或任何插件都可以通过Lua定制API命令注册定制Lua函数。 当从脚本中调用时,自定义Lua命令将在主客户端应用程序或插件中回调已注册的函数。 这对于实现高级Lua命令非常方便(例如,可以想象让一个插件使用单个Lua命令simRobot.moveAndAvoidObstacles()来处理机器人的运动!)
无线通信仿真
CoppeliaSim允许以非常灵活的方式模拟无线通信:可以将数据发射到特定方向,并经过特定距离。 如果接收器位于指定的发射区域内,则可以接收发射的数据。 有关更多详细信息,请参考常规API中的相应功能。 通过启用“环境”对话框中的“可视化无线发射”和“可视化无线接收”项目,可以可视化无线发射/接收活动。 下图说明了两个移动机器人之间的可视化无线通信:
串口通信
CoppeliaSim在API中实现了用于串行端口通信的特定功能。
LuaSocket
CoppeliaSim附带了一个名为LuaSocket的Lua扩展库(有关此库的确认和荣誉,请参见此处)。 它允许从嵌入式脚本或附加组件中执行各种类型的套接字通信。 以下代码部分说明了线程化的子脚本如何获取网页:
http=require("socket.http")
sim.setThreadIsFree(true) -- Allow real threading from here (to avoid blocking CoppeliaSim)
page=http.request("http://www.google.com")
sim.setThreadIsFree(false) -- Forbid real threading from here
请注意,request命令的阻塞部分是如何放入非阻塞部分的。 有关如何避免外部命令阻塞的更多信息,请参考sim.setThreadIsFree API命令。
如果您的应用程序需要套接字通信,那么将线程脚本设置为请求服务器并让其他脚本访问它以进行套接字通信非常方便,如下例所示:
线程请求服务器:
http = require("socket.http")
-- Open a communication tube:
tubeHandle=sim.tubeOpen(0,'http_communication_test',1)
while (sim.getSimulationState()~=sim.simulation_advancing_abouttostop) do
-- Wait for a message in the tube and reat it:
data=sim.tubeRead(tubeHandle,true)
if (data) then
-- Fetch the page:
sim.setThreadIsFree(true) -- Allow real threading from here (to avoid blocking CoppeliaSim)
reply=http.request(data)
sim.setThreadIsFree(false) -- Forbid real threading from here
-- Send back the page:
if (reply) then
sim.tubeWrite(tubeHandle,reply)
else
sim.tubeWrite(tubeHandle,'Page could not be retrieved')
end
else
sim.switchThread() -- explicitely switch thread
end
end
下面的非线程子脚本示例可以用于访问套接字信息:
function sysCall_init()
-- Open a communication tube:
tubeHandle=sim.tubeOpen(0,'http_communication_test',1)
urlsToCheck={'http://www.google.com','http://www.yahoo.com','http://www.titech.co.jp'}
urlIndex=1
end
function sysCall_sensing()
s,r,w=sim.tubeStatus(tubeHandle)
if (s==1)and(r==0)and(w==0) then
-- Send a request for a page:
sim.tubeWrite(tubeHandle,urlsToCheck[urlIndex])
end
if (s==1)and(r==1) then
-- Read the reply (the page):
page=sim.tubeRead(tubeHandle)
print('URL: '..urlsToCheck[urlIndex])
print(page:sub(1,200)) -- Only print the first 200 chars
urlIndex=urlIndex+1
if (urlIndex>3) then
urlIndex=1
end
end
end
Lua扩展库
通过使用Lua的扩展库机制,可以几乎无限地扩展CoppeliaSim的通信方式。 与使用LuaSocket库(参见上文)一样,您可以添加在线提供的任何其他类型的Lua扩展库。 您只需要按照库的说明将库安装在CoppeliaSim的安装目录中即可。 如上面的LuaSocket所示,如果扩展库命令阻止了CoppeliaSim,请确保通过sim.setThreadIsFree API命令使用了非阻塞部分。
调用脚本函数
从主客户端应用程序,插件,嵌入式脚本,远程API客户端或ROS节点,可以使用simCallScriptFunctionEx或simxCallScriptFunction调用脚本函数。 被调用的脚本功能可以执行各种任务,然后将数据发送回调用方。
设置脚本变量
从主客户端应用程序或插件中,可以使用sim.setScriptVariable设置/清除脚本变量(即Lua变量)。
在CoppeliaSim内及其周围进行编程时,您将始终需要引用您希望使用的各种对象,例如场景对象,Ik组,距离对象等。您可以通过各种句柄检索功能来获得这些句柄 对象名称作为输入参数。 例如,从插件中,您将使用以下方式访问对象Cuboid1:
int cuboid1Handle=simGetObjectHandle("Cuboid1");
在嵌入式脚本中,您可以执行以下操作:
cuboid1Handle=sim.getObjectHandle('Cuboid1')
从未关联的代码访问
未关联代码是未附加到任何场景对象的代码。 这包括为插件,附加组件,远程API客户端,外部ROS节点,外部BlueZero节点和主脚本编写的所有代码。
在这种情况下,对象访问很简单,如以下示例所示:您只需指定对象的全名即可检索其句柄:
// e.g. inside of a c/c++ plugin:
int cuboid1Handle=simGetObjectHandle("Cuboid1"); // handle of object "Cuboid1"
int cuboid2Handle=simGetObjectHandle("Cuboid2"); // handle of object "Cuboid2"
int cuboid1Hash0Handle=simGetObjectHandle("Cuboid1#0"); // handle of object "Cuboid1#0"
int ikGroupHash42Handle=simGetIkGroupHandle("ikGroup#42"); // handle of ik group "ikGroup#42"
# e.g. inside of a Python legacy remote API client:
opMode=sim.simx_opmode_blocking
res,cuboid1Handle=sim.simxGetObjectHandle(clientId,"Cuboid1",opMode) # handle of object "Cuboid1"
res,cuboid2Handle=sim.simxGetObjectHandle(clientId,"Cuboid2",opMode) # handle of object "Cuboid2"
res,cuboid1Hash0Handle=sim.simxGetObjectHandle(clientId,"Cuboid1#0",opMode) # handle of object "Cuboid1#0"
从关联代码访问
关联代码是与场景对象(即,附加到场景对象)相关联的代码。 这包括为子脚本或自定义脚本编写的所有代码。
在这种情况下,对象访问会有些棘手,这是因为如果关联的对象被复制(例如,当您复制并粘贴机器人模型时),关联的代码将被自动复制: 复制的代码将访问与原始代码相同的对象吗?
以使用以下代码附加到对象Cuboid1的子脚本为例:
cuboid1Handle=sim.getObjectHandle('Cuboid1')
sim.setShapeColor(cuboid1Handle,nil,0,{math.random(),math.random(),math.random()})
上面的代码所做的是将Cuboid1的颜色更改为随机颜色。 复制Cuboid1时,副本将命名为Cuboid1#0。 在那种情况下,重复的脚本与原始脚本保持相同,但是重复的脚本将知道必须访问Cuboid1#0而不是Cuboid1。 因此,复制的长方体也会像原始长方体一样将其颜色更改为随机颜色。
复制的脚本还将自动访问复制的对象,以确保场景内容的良好且轻松的可伸缩性。 使用自动名称后缀调整机制:
当关联的代码仅指定用于检索对象句柄的基本名称时,CoppeliaSim将在内部通过添加哈希字符和与脚本关联的对象的后缀来调整名称。 如果指定了全名,则不会自动进行名称调整:
可以使用sim.getNameSuffix和sim.setNameSuffix命令更改自动名称调整机制,但是不建议这样做。
CoppeliaSim API框架将围绕CoppeliaSim的所有接口分组。 它有5 + 1种不同口味:
虽然可以从模拟器中访问常规API(例如,从嵌入式脚本、附加组件、插件或主客户端应用程序),但是几乎可以从任何可能的位置访问远程API、ROS接口和BlueZero接口。 外部应用程序或硬件(包括真实的机器人、远程计算机等)。 辅助API本身不是接口,而是更多可嵌入的,可独立运行的辅助函数的集合。 其他界面项将用户的所有可能性分组,以扩展可用的界面。 下图说明了各种接口的概述:
常规API是CoppeliaSim API框架的一部分。
常规API由可以从C / C ++应用程序(插件或主客户端应用程序)或嵌入式脚本调用的数百种功能组成。 CoppeliaSim函数和常量可以很容易地从其“ sim”或“ _sim”前缀(例如sim.handleCollision)中识别出来。 确保不要将常规API(有时也简称为“ API”)与远程API混淆。
常规API可以通过自定义lua函数(插件或主客户端应用程序注册)进行扩展。 可以从sim * .prefix识别自定义lua函数。
所有进入或来自API的单位都是米、千克、秒和弧度或它们的组合(除非另有明确说明)。用户界面的单位是米、公斤、秒和度。
CoppeliaSim的主要功能通过以下类型的调用或命令来处理:sim.handleXXX或sim.resetXXX(例如sim.handleCollision,sim.resetCollision等)。 该命令所期望的常规参数是常规类型对象的句柄(例如,碰撞对象的句柄,距离对象的句柄等)。 假设您已经通过碰撞检测对话框注册了一个碰撞对象“ robotCollision”。 如果现在希望检查该对象的碰撞状态,则可以编写以下代码:
collisionObjectHandle=sim.getCollisionHandle("robotCollision")
collisionState=sim.handleCollision(collisionObjectHandle)
第一行检索名为“ robotCollision”的碰撞对象的句柄(有关句柄检索的确切操作模式,请参阅有关以编程方式访问对象的部分)。 第二行明确地处理碰撞对象(即,对特定的碰撞对象执行碰撞检测)。 在这种情况下,必须将碰撞对象标记为显式处理,否则将生成错误。
同样,以上两个命令也可以非明确的方式处理对象:除了使用冲突对象句柄作为参数之外,还可以使用sim.handle_all参数:
sim.handleCollision(sim.handle_all)
sim.handle_all参数允许一次处理所有已注册的碰撞对象。 在此,碰撞对象以非明确的方式处理。
注册通用类型的对象(例如,冲突对象)时,您不需要任何特定的代码行即可处理这些对象,因为主脚本会处理这些问题(通过指定两个应为 检查碰撞,运行模拟,然后将两个实体之一移到另一个实体上:将检测到碰撞,并且两个实体将以不同的颜色显示以指示碰撞)。 实际上,主脚本包含以下默认代码:
sim.handleCollision(sim.handle_all_except_explicit)
sim.handleDistance(sim.handle_all_except_explicit)
sim.handleProximitySensors(sim.handle_all_except_explicit)
...
命令的参数为sim.handle_all_except_explicit。 它具有与sim.handle_all相同的效果,但以下情况除外:不会处理标记为显式处理的对象。 大多数通用类型的对象可以标记为显式处理。 标记为如此时,只有在使用对象句柄(例如sim.handleCollision(collisionObjectHandle))调用命令或使用sim.handle_all参数(例如sim.handleCollision(sim.handle_all))调用命令时,它们才会被处理。 。
默认情况下,显式处理标志是禁用的,这意味着默认情况下,通用类型的对象由主脚本处理。 但是,如果子脚本希望(或需要)自己处理对象,则应启用显式处理标志,否则将产生错误。 通常,仅当子脚本在同一模拟遍历中多次要求获得新的计算结果时,才明确地处理对象(否则,子脚本可以使用sim.readCollision,sim.readDistance,sim.readProximitySensor等命令)。 另一方面,子脚本在没有充分理由的情况下切勿使用sim.handle_all或sim.handle_all_except_explicit参数! 下图说明了显式和非显式的调用:
远程API是CoppeliaSim API框架的一部分。 它允许CoppeliaSim与外部应用程序(即在不同进程中或在不同计算机上运行的应用程序)之间的通信,跨平台并支持服务调用(即阻塞调用)和双向数据流。 它有两个不同的版本/框架:
基于B0的远程API:这表示远程API的第二个版本。 它基于BlueZero中间件及其CoppeliaSim的接口插件。 与传统的远程API相比,它更易于使用且更具灵活性,最重要的是,它易于扩展。 目前,它支持以下语言:C ++,Java,Python,Matlab和Lua。
旧版远程API(或简称为远程API):这表示远程API的第一个版本。 与基于B0的远程API相比,它相对较轻并且具有更少的依赖性。 但是,它不那么直观,也不灵活,并且很难扩展。 它支持以下语言:C / C ++,Java,Python,Matlab,Octave和Lua。
基于BØ的远程API不应与传统的远程API(或简称为远程API)混合使用,后者是远程API的较旧版本,灵活性较差,并且扩展难度较大。
基于B0的远程API是CoppeliaSim API框架的一部分。
基于B0的远程API允许从外部应用程序或远程硬件(例如,真实的机器人,远程计算机等)控制仿真(或仿真器本身)。 基于CoppeliaSim B0的远程API由大约一百个特定功能和一个通用功能组成,可以从C ++应用程序,Python脚本,Java应用程序,Matlab程序或Lua脚本中调用。 基于B0的远程API函数通过BlueZero中间件及其与CoppeliaSim的接口插件与CoppeliaSim进行交互。 所有这些对用户而言都是隐藏的。 远程API可以让一个或多个外部应用程序以同步或异步的方式(默认情况下为异步)与CoppeliaSim进行交互,甚至还支持对模拟器的远程控制(例如,远程加载场景,开始,暂停或停止模拟) 例如)。
“同步”一词的含义是每个模拟过程都与远程API应用程序同步运行(即,模拟器将等待来自客户端的触发信号,以在时间t + dt处开始下一个模拟过程)。 在阻塞/非阻塞操作的意义上,这与同步/异步不同。 远程API还支持阻止和非阻止操作。
阅读本节,确保您了解基于B0的远程API的运行方式。 还可以查看外部控制器教程。
基于B0的远程API功能来自两个单独的实体,它们通过BlueZero框架进行交互:
基于BØ的远程API不应与传统的远程API(或简单的远程API)混淆,后者是远程API的旧版本,灵活性较差,更难扩展。
所有进入或来自API的单位都是米、千克、秒和弧度或它们的组合(除非另有明确说明)。
C++客户端
要在C ++应用程序中使用基于B0的远程API功能,只需在项目中包括以下文件:
有关其他详细信息,请查看programming / remoteApiBindings / b0Based / cpp / simpleTest项目文件以及相应的基于B0的演示场景RemoteApiDemo.ttt。
本页列出并描述了所有受支持的基于C ++ B0的远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
Python客户端
要在您的Python脚本中使用基于B0的远程API功能,您将需要执行以下操作:
有关其他详细信息,请参阅programming / remoteApiBindings / b0Based / python / simpleTest.py程序,以及相应的基于B0的演示场景RemoteApiDemo.ttt。
本页列出并描述了所有受支持的基于Python B0的远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
Java客户端
要在Java应用程序中使用基于B0的远程API功能,您将需要执行以下操作:
看一下programming / remoteApiBindings / b0Based / java / simpleTest.java程序,以及相应的演示场景,基于B0的RemoteApiDemo.ttt,以了解更多详细信息。
本页列出并描述了所有受支持的基于Java B0的远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
Matlab客户端
要在Matlab程序中使用基于B0的远程API功能,您将需要执行以下操作:
请查看programming / remoteApiBindings / b0Based / matlab / simpleTest.m程序,以及相应的基于B0的演示场景RemoteApiDemo.ttt,以了解更多详细信息。
本页列出并描述了所有受支持的基于Matlab B0的远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
Lua客户端
要在您的外部Lua脚本中使用基于B0的远程API功能(即不在CoppeliaSim内部),您将需要执行以下操作:
请查看programming / remoteApiBindings / b0Based / lua / simpleTest.lua程序,以及相应的基于B0的演示场景RemoteApiDemo.ttt,以了解更多详细信息。
本页列出并描述了所有受支持的基于Lua B0的远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
基于BØ的远程API不应与传统的远程API(或简称为远程API)混合使用,后者是远程API的较旧版本,灵活性较差,并且扩展难度较大。
基于B0的远程API服务器端基于Lua脚本:lua / b0RemoteApiServer.lua。 该脚本大量使用了BlueZero界面及其功能,正在模型Models / tools / B0远程Api server.ttm的自定义脚本以及附加脚本simAddOnScript-b0RemoteApiServer.lua中使用。 如果您错过某个特定功能,则可以自己在远程API框架中实现它(另请参见有关扩展基于B0的远程API的部分)。
要在服务器端(即CoppeliaSim端)启用基于B0的远程API,请确保BlueZero插件已在CoppeliaSim启动时成功加载(simExtBlueZero.dll,libsimExtBlueZero.dylib或libsimExtBlueZero.so)(您可以检查 控制台窗口,以获取有关插件加载的信息。
最后,使用基于B0的远程API有两种可能性:
基于BØ的远程API不应与传统的远程API(或简称为远程API)混合使用,后者是远程API的较旧版本,灵活性较差,并且扩展难度较大。
基于B0的远程API函数的调用方式与常规API函数的调用方式类似,但是有一个主要区别:
大多数基于B0的远程API函数都需要一个附加参数:用于执行函数调用的主题或通信渠道。 该主题可以是以下5个函数之一的返回值:
默认情况下,基于B0的远程API客户端和服务器(即CoppeliaSim)将异步运行。 但是,可以让客户端单独触发每个模拟步骤,以实现同步操作。 以下是同步模式的Python示例:
import b0RemoteApi
import time
with b0RemoteApi.RemoteApiClient('b0RemoteApi_pythonClient','b0RemoteApi') as client:
doNextStep=True
def simulationStepStarted(msg):
simTime=msg[1][b'simulationTime'];
print('Simulation step started. Simulation time: ',simTime)
def simulationStepDone(msg):
simTime=msg[1][b'simulationTime'];
print('Simulation step done. Simulation time: ',simTime);
global doNextStep
doNextStep=True
client.simxSynchronous(True)
client.simxGetSimulationStepStarted(client.simxDefaultSubscriber(simulationStepStarted));
client.simxGetSimulationStepDone(client.simxDefaultSubscriber(simulationStepDone));
client.simxStartSimulation(client.simxDefaultPublisher())
startTime=time.time()
while time.time()<startTime+5:
if doNextStep:
doNextStep=False
client.simxSynchronousTrigger()
client.simxSpinOnce()
client.simxStopSimulation(client.simxDefaultPublisher())
基于BØ的远程API不应与传统的远程API(或简称为远程API)混合使用,后者是远程API的较旧版本,灵活性较差,并且扩展难度较大。
如果希望使用基于B0的远程API未公开的API命令,则基本上有两个选择:
使用通用函数simxCallScriptFunction:可以扩展基于B0的远程API函数simxCallScriptFunction,而不是扩展基于B0的远程API提供的功能:这将调用CoppeliaSim脚本函数,该脚本函数可以随后在本地执行任何类型的操作 ,然后返回数据。 例如:
function myFunctionName(args)
for i=1,#args,1 do
print('Argument '..i..' is :')
print(args[i])
print('')
end
return 'Hello to you too!',42 -- return a string and a number
end
然后,基于B0的远程API客户端应用程序将以以下方式(例如,通过Python脚本)调用上述脚本函数:
import b0RemoteApi
with b0RemoteApi.RemoteApiClient('b0RemoteApi_pythonClient','b0RemoteApi') as client:
args=['Hello World!',[1,2,3],59]
ret=client.simxCallScriptFunction('myFunctionName@objectName','sim.scripttype_childscript',args,client.simxServiceCall())
print(ret)
确保在尝试调用函数的脚本处进行了初始化:在未运行模拟的情况下,尝试在模拟脚本中调用函数将不起作用。 同样,尝试在已结束(或尚未启动)的线程子脚本中调用函数将不起作用。
扩展基于B0的远程API:涉及4个简单步骤:
旧版远程API或远程API不应与基于BØ的远程API混合使用,后者是远程API的更新版本,它更灵活,更易于使用,最重要的是,更易于扩展。
远程API是CoppeliaSim API框架的一部分。
远程API允许从外部应用程序或远程硬件(例如,真实的机器人,远程计算机等)控制仿真(或仿真器本身)。 CoppeliaSim远程API由大约一百个特定功能和一个通用功能组成,可以从C / C ++应用程序,Python脚本,Java应用程序,Matlab / Octave程序或Lua脚本中调用。 远程API函数通过套接字通信(或可选地通过共享内存)与CoppeliaSim进行交互。 所有这些对用户而言都是隐藏的。 远程API可以让一个或多个外部应用程序以同步或异步的方式(默认情况下为异步)与CoppeliaSim进行交互,甚至还支持对模拟器的远程控制(例如,远程加载场景,开始,暂停或停止模拟))。
“同步”一词的含义是每个模拟过程都与远程API应用程序同步运行(即,模拟器将等待来自客户端的触发信号,以在时间t + dt处开始下一个模拟过程)。 在阻塞/非阻塞操作的意义上,这与同步/异步不同。 远程API还支持阻止和非阻止操作。
阅读本节,确保您了解远程API的运行方式。 还可以查看外部控制器教程。
远程API功能来自2个独立的实体,它们通过套接字通信进行交互:
远程API或旧式远程API不应与基于BØ的远程API混合使用,后者是远程API的更新版本,它更灵活,更易于使用,最重要的是,更易于扩展。
到达或来自API的所有单位均以米,千克,秒和弧度或它们的组合(除非另有明确说明)。
C/C++客户端
要在C / C ++应用程序中使用远程API功能,只需在项目中包括以下C语言文件:
上面的文件位于CoppeliaSim的安装目录中,位于programming / remoteApi下。 确保已将NON_MATLAB_PARSING和MAX_EXT_API_CONNECTIONS = 255(以及可选的DO_NOT_USE_SHARED_MEMORY)定义为预处理程序定义。 要在客户端(即您的应用程序)上启用远程API,请调用simxStart。 有关示例,请参见编程目录中的bubbleRobClient项目。 本页列出并描述了所有受支持的C / C ++远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
Python客户端
要在您的Python脚本中使用远程API功能,您将需要以下三项:
以上文件位于CoppeliaSim的安装目录中,位于programming / remoteApiBindings / python下。 如果尚未构建remoteApi共享库,则可能必须自己构建(使用remoteApiSharedLib.vcproj或makefile)。 在这种情况下,请确保已将NON_MATLAB_PARSING和MAX_EXT_API_CONNECTIONS = 255(以及可选的DO_NOT_USE_SHARED_MEMORY)定义为预处理器定义。
将以上元素放在Python已知的目录中后,请调用import sim加载库。 要在客户端(即您的应用程序)上启用远程API,请调用sim.simxStart。 有关示例,请参见programming / remoteApiBindings / python目录中的simpleTest.py脚本。 本页列出并描述了所有受支持的Python远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
Java客户端
要在Java应用程序中使用远程API功能,您将需要以下两项:
以上文件位于CoppeliaSim的安装目录中,位于programming / remoteApiBindings / java下。 如果尚未构建remoteApiJava共享库,则可能必须自己构建(使用remoteApiSharedLibJava.vcproj或remoteApiSharedLibJava_Makefile)。 在这种情况下,请确保已将NON_MATLAB_PARSING和MAX_EXT_API_CONNECTIONS = 255(以及可选的DO_NOT_USE_SHARED_MEMORY)定义为预处理器定义。
在Java已知的目录中拥有以上元素之后,使用带有javac myAppName.java的myAppName.java编译应用程序。 在您的应用程序中,确保导入与import coppelia.className一起使用的类,然后调用remoteApi sim = new remoteApi()来加载库。 要在客户端(即您的应用程序)上启用远程API,请调用sim.simxStart。 有关示例,请参见programming / remoteApiBindings / java目录中的simpleTest.java程序。此页面列出并描述了所有受支持的Java远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
您可能还必须将文件夹添加到系统路径。 例如,在Linux中,您可以在执行Java应用程序之前调用:export LD_LIBRARY_PATH = $ LD_LIBRARY_PATH:pwd
。
Matlab客户端
要在Matlab程序中使用远程API功能,您将需要以下三项:
以上文件位于CoppeliaSim的安装目录中,位于programming / remoteApiBindings / matlab下。 如果尚未构建remoteApi共享库,则可能必须自己构建(使用remoteApiSharedLib.vcproj或remoteApiSharedLib_Makefile)。
在Matlab当前文件夹中具有以上元素之后,调用sim = remApi(‘remoteApi’)来构建对象并加载库。 要在客户端(即您的应用程序)上启用远程API,请调用sim.simxStart。 有关示例,请参见programming / remoteApiBindings / matlab目录中的simpleTest.m程序。此页面列出并描述了所有受支持的Matlab远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
确保您的Matlab使用与remoteApi库相同的位架构:带32位remoteApi库的64位Matlab将不起作用,反之亦然!
如果必须重建remoteApi库,则可能必须重新生成原型文件(remoteApiProto.m):首先,请确保您具有Matlab可以识别的编译器。 您可能需要调用mex -setup。 然后,键入loadlibrary(‘remoteApi’,‘extApi.h’,‘mfilename’,‘remoteApiProto’)
Octave客户端
要在Octave程序中使用远程API功能,您将需要以下两项:
以上文件位于CoppeliaSim的安装目录中,位于programming / remoteApiBindings / octave下。 如果尚未构建remApi.oct共享库,则可能必须自己构建。 在这种情况下,请确保在从Octave命令行调用buildWin,buildLin或buildMac之前,将programming / remoteApi和programming / include的所有内容放入该目录。
在Octave的当前文件夹中具有上述元素后,请调用sim = remApiSetup()来加载库并绑定函数。 要在客户端(即您的应用程序)上启用远程API,请调用simxStart。 有关示例,请参阅programming / RemoteApiBindings / octave目录中的simpleTest.m程序。此页面列出并描述了所有受支持的Octave远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
确保八度使用与remoteApi库相同的位架构:带32位remoteApi库的64位八度将不起作用,反之亦然!
Lua客户端
要在外部Lua脚本中使用远程API功能(即不在CoppeliaSim内部),您需要以下各项:
上面的文件位于CoppeliaSim的安装目录中,位于programming / remoteApiBindings / lua下。 如果尚未构建remoteApi共享库,则可能必须自己构建(使用remoteApiLua.vcproj或makefile)。 在这种情况下,请确保已将NON_MATLAB_PARSING和MAX_EXT_API_CONNECTIONS = 255(以及可选的DO_NOT_USE_SHARED_MEMORY)定义为预处理器定义。
在Lua已知的目录中拥有上述元素之后,调用require’remoteApiLua’加载库。 要在客户端(即您的应用程序)上启用远程API,请调用simxStart。 有关示例,请参见programming / remoteApiBindings / lua目录中的simpleTest.lua脚本。 本页列出并描述了所有受支持的Lua远程API函数。 CoppeliaSim远程API函数可以从其“ simx”前缀轻松识别。
远程API或旧式远程API不应与基于BØ的远程API混合使用,后者是远程API的更新版本,它更灵活,更易于使用,最重要的是,更易于扩展。
远程API服务器端是通过基于常规API的CoppeliaSim插件实现的。 远程API插件项目位于此处。 如果您错过了一个特定的功能,则可以自己在远程API框架中实现它(另请参见有关扩展远程API的部分)。
要在服务器端(即CoppeliaSim端)启用远程API,请确保在CoppeliaSim启动时成功加载了远程API插件(simExtRemoteApi.dll,libsimExtRemoteApi.dylib或libsimExtRemoteApi.so)(您可以检查控制台窗口) 有关与插件加载相关的信息)。 远程API插件可以根据需要启动任意数量的服务器服务(每个服务将在不同的端口上监听/通信)。 可以通过两种不同的方式启动服务器服务:
您可以使用以下自定义Lua函数(该函数由插件导出)来收集有关任何远程API服务器服务的信息:simRemoteApi.status
您可以使用以下自定义Lua函数(该函数由插件导出)重置(即销毁并重新创建)任何远程API服务器服务:simRemoteApi.reset
远程API或旧式远程API不应与基于BØ的远程API混合使用,后者是远程API的更新版本,它更灵活,更易于使用,最重要的是,更易于扩展。
可以使用通用的远程API函数simxCallScriptFunction来代替扩展远程API提供的功能:这将调用CoppeliaSim脚本函数,该脚本函数可以在本地执行任何类型的操作,然后返回数据。 被调用的脚本函数应始终具有以下输入/输出形式:
function myFunctionName(inInts,inFloats,inStrings,inBuffer)
-- inInts, inFloats and inStrings are tables
-- inBuffer is a string
-- Perform any type of operation here.
-- Always return 3 tables and a string, e.g.:
return {},{},{},''
end
然后,远程API客户端应用程序将以以下方式(例如,通过Python脚本)调用上述脚本函数:
inputInts=[1,2,3]
inputFloats=[53.21,17.39]
inputStrings=['Hello','world!']
inputBuffer=bytearray()
inputBuffer.append(78)
inputBuffer.append(42)
res,retInts,retFloats,retStrings,retBuffer=sim.simxCallScriptFunction(clientID,'objectName',sim.sim_scripttype_childscript,
'myFunctionName',inputInts,inputFloats,inputStrings,inputBuffer,sim.simx_opmode_blocking)
if res==sim.simx_return_ok:
print (retInts)
print (retFloats)
print (retStrings)
print (retBuffer)
确保在尝试调用函数的脚本处进行了初始化:在未运行模拟的情况下,尝试在模拟脚本中调用函数将不起作用。 同样,尝试在已结束(或尚未启动)的线程子脚本中调用函数将不起作用。
有关其他用法示例,请参考文件夹programming / remoteApiBindings中名为complexCommandTest。*的各种文件。
CoppeliaSim有几种可用的ROS接口。 每个人都提供特定的行为,功能或操作方式:
所有ROS接口通常可以并行运行,但是我们强烈建议您首先尝试使用ROS接口,因为这是最灵活,最自然的方法。 上面列出的ROS接口的前两个软件包位于此处和此处。 使用catkin工具构建那些软件包,否则您可能会遇到困难。
还可以查看ROS教程和外部控制器教程。
ROS接口是CoppeliaSim API框架的一部分,由Federico Ferri提供。 确保不要将ROS接口与RosPlugin混淆,后者是CoppeliaSim中较旧的,已弃用的接口。 ROS接口以很高的保真度复制了C / C ++ ROS API。 这使其成为通过ROS进行非常灵活的通信的理想选择,但可能需要更多了解各种消息和ROS的运行方式。 对于ROS和ROS 2,它有两种形式。您可以从simROS-或simROS2-前缀中识别ROS接口API函数。
ROS是一种分布式伪操作系统,可以轻松管理和连接在网络中的多台计算机之间进行通信。 有关ROS的详细信息,请参阅官方ROS文档。
CoppeliaSim可以充当ROS节点,其他节点可以通过ROS服务,ROS发布者和ROS订户与之通信。
CoppeliaSim中的ROS接口功能通过以下插件启用:libsimExtROSInterface。或libsimExtROS2Interface。。 这些插件可以轻松地适应您自己的需求。 插件在启动CoppeliaSim时加载,但是加载操作仅在roscore当时运行时才能成功。 确保检查CoppeliaSim的控制台窗口或终端以获取有关插件加载操作的详细信息。
查看以下仿真场景/模型,以快速入门ROS接口:
还可以查看ROS教程和外部控制器教程。
BlueZero接口是CoppeliaSim API框架的一部分。 它将BlueZero(BØ)框架包装到CoppeliaSim插件中。 您可以从simB0前缀识别BlueZero API函数。
BØ是一个跨平台的中间件,它提供了用于互连在多个线程,多个进程甚至多个计算机中运行的软件的工具。 它与ROS有一些相似之处,尽管它只专注于提供通信范例(客户端/服务器和发布者/订阅者)和消息传输(基于ZeroMQ),而与消息序列化格式或通用协议和数据结构无关。
CoppeliaSim可以充当一个或多个BØ节点,其他节点可以通过BØ服务,BØ发布者和BØ订户与之通信。
CoppeliaSim中的BlueZero接口功能通过以下插件启用:simExtBlueZero.dll,libsimExtBlueZero.so或libsimExtBlueZero.dylib。 可以在此处找到该插件的代码,可以轻松地适应您自己的需求。 启动CoppeliaSim时会加载该插件,但您应确保运行b0_ resolver。 确保检查CoppeliaSim的控制台窗口或终端以获取有关插件加载操作的详细信息。 可以在CoppeliaSim文件夹中找到各种BlueZero工具。
查看以下模拟场景,以快速开始使用BlueZero界面:
还可以查看外部控制器教程
辅助API是CoppeliaSim API框架的一部分。
辅助API本身不是接口,而是更多的辅助函数集合,可以嵌入您自己的代码中并且可以独立运行。 提供以下辅助API类别:
该功能集合允许您执行与CoppeliaSim内部相同的几何计算,即,碰撞检测,最小距离计算以及对网格,八叉树和点云的接近传感器模拟。
有关API详细信息,请参阅Coppelia几何例程存储库中的geom.h文件(目前)。
Coppelia几何例程源代码不是CoppeliaSim的直接一部分,并且带有单独的许可条件。 有关详细信息,请参考源代码。
该功能集合允许您执行与CoppeliaSim中相同的运动学计算。
这个想法通常是在CoppeliaSim中构建运动学任务,然后导出场景的运动学内容,然后可以将其直接与下面的可嵌入函数一起使用。 所需的源代码位于此处。 确保将所有文件包括在项目中,并在需要访问功能的文件中包括ik.h。 还请确保您首先知道如何从CoppeliaSim内部使用运动学功能! 如果您可以访问常规API,那么您将不需要此辅助API,因为所有以下函数都具有与其等效的常规API。
Coppelia运动学例程源代码不是CoppeliaSim的直接一部分,并且带有单独的许可条件。 有关详细信息,请参考源代码。
请按照以下方法在您自己的外部应用程序中执行运动学计算:
另请参阅以下示例:standAloneKinematicsDemo1,standAloneKinematicsDemo2,standAloneKinematicsDemo3。 这些演示应用程序使用此处描述的Coppelia运动学例程,结合远程API功能,以反向/正向运动学模式控制两个不同的机器人。 演示场景standAloneKinematicsDemo1.ttt,standAloneKinematicsDemo2.ttt和standAloneKinematicsDemo3.ttt会分别自动启动standAloneKinematicsDemo1,standAloneKinematicsDemo2和standAloneKinematicsDemo3应用程序。
为了提供不直接属于CoppeliaSim的接口,可以以各种方式扩展CoppeliaSim API框架。 通常,这是通过插件发生的,但其他选项也可用(加载项,远程API客户端,ROS节点,BlueZero节点等)。 以下列出了其中的一些:
CoppeliaSim通过包装OMPL库的插件提供路径/运动计划功能。 该插件由Federico Ferri提供。
准备路径/运动计划任务时,应考虑以下几点:
决定开始状态和目标状态。 当路径规划对象是串行操纵器时,通常提供目标姿势(或末端执行器位置/方向),而不是目标状态。 在那种情况下,可以使用sim.getConfigForTipPose函数查找满足所提供目标姿势的一个或多个目标状态。
使用simOMPL.createTask创建路径规划任务。
使用simOMPL.setAlgorithm选择一种算法。
创建所需的状态空间,该状态空间可以组成一个复合对象:simOMPL.createStateSpace和simOMPL.setStateSpace。
指定不允许哪些实体与simOMPL.setCollisionPairs冲突。
使用simOMPL.setStartState和simOMPL.setGoalState指定开始状态和目标状态。
使用simOMPL.compute计算一条或多条路径。
使用simOMPL.destroyTask销毁路径规划任务。
通常,路径规划是与逆运动学结合使用的:例如,在放置任务中,最终方法通常应为一条直线路径,可以使用sim.generateIkPath生成该路径。
上述程序是常规方法,有时缺乏灵活性。 此外,可以设置以下回调函数:
simOMPL.compute提供的路径通常只是无数其他可能路径中的一个路径,并且不能保证返回的路径是最佳解决方案。 因此,通常先计算几条不同的路径,然后选择更好的路径(例如,较短的路径)。
以类似的方式,如果必须从一个目标姿势计算出目标状态,则通常会测试几个目标状态,因为并非所有目标状态都可以到达或足够接近(就状态空间距离而言)。 一种常见的做法是先找到几个目标状态,然后根据它们到开始状态的状态空间距离对它们进行排序。 然后执行到最接近的目标状态的路径规划计算,然后执行到下一个最接近的目标状态等,直到找到满意的路径。
请确保参考以下演示场景以获取更多详细信息:
Federico Ferri提供的自定义UI插件(simExtCustomUI)提供了基于Qt框架的功能。 该插件的源代码可以在这里找到。
自定义UI插件提供Qt样式的小部件,例如集成按钮,编辑框,滑块,标签,图像等的对话框。对自定义UI的任何操作(例如,按钮单击,文本编辑,滑块移动)都将报告为脚本回调 。 其他功能可通过相关的API函数调用来访问。 这样可以在很大程度上自定义仿真。 以下显示了一个典型的自定义UI:
CoppeliaSim中可以使用[菜单栏->模拟->开始/暂停/停止模拟]或通过相关的工具栏按钮来启动、暂停和停止模拟:
在内部,模拟器将使用其他中间状态,以正确告知脚本或程序接下来将发生的情况。以下状态图说明了模拟器的内部状态:
脚本和程序应始终根据当前系统调用功能以及可能的模拟状态进行反应,以便正确运行。 优良作法是将每个控制代码至少分为4个系统调用函数(例如,用于非线程子脚本):
有关如何安排典型脚本的示例,请参考主脚本,子脚本和自定义脚本页面。
仿真循环
模拟器通过以恒定的时间步长推进模拟时间来进行操作。下图说明了主要的仿真循环:
以下是一个非常简化的主客户端应用程序(为清晰起见,已省略了消息,插件处理和其他详细信息):
void initializationCallback
{
// do some initialization here
}
void loopCallback
{
if ( (simGetSimulationState()&sim_simulation_advancing)!=0 )
{
if ( (simGetRealTimeSimulation()!=1)||(simIsRealTimeSimulationStepNeeded()==1) )
{
if ((simHandleMainScript()&sim_script_main_script_not_called)==0)
simAdvanceSimulationByOneStep();
}
}
}
void deinitializationCallback
{
// do some clean-up here
}
取决于仿真的复杂性,计算机的性能和仿真设置,实时仿真可能并不总是可能的。
仿真速度
在非实时模拟中,模拟速度(即感知速度)主要取决于两个因素:模拟时间步长和一个渲染通道的模拟通道数量(更多信息,请参见模拟对话框)。 在实时仿真的情况下,仿真速度主要取决于实时乘法系数,而且在一定程度上取决于仿真时间步长(太小的仿真时间步长可能与实时时间不兼容)。 由于计算机的计算能力有限,因此无法进行仿真。 在模拟过程中,可以使用以下工具栏按钮来调整模拟速度:
以某种方式调整模拟速度,以使初始模拟时间步长永远不会增加(例如,这可能因此导致机制中断)。 以下两个图说明了模拟速度调整机制:
默认情况下,每个模拟周期由以下顺序操作组成:
线程渲染
渲染操作将始终增加仿真周期的持续时间,从而也降低了仿真速度。可以定义每个场景渲染的主脚本执行次数(请参阅后面的内容),但这在某些情况下还不够,因为渲染仍然会减慢每个第x个仿真周期的时间(这可能会限制实时性)。在这种情况下,可以通过用户设置或以下工具栏按钮激活线程渲染模式:
激活线程渲染模式后,模拟周期将仅包括执行主脚本,因此模拟将以最大速度运行。渲染将通过不同的线程进行,并且不会减慢模拟任务的速度。然而,必须考虑缺点。激活线程渲染后:
仿真对话框可以通过[菜单栏->仿真->仿真设置]或单击以下工具栏按钮来访问:
CoppeliaSim具有嵌入式视频记录器,允许用户将页面,场景层次结构和模型浏览器区域记录为视频文件(单击此处和此处获取与视频记录功能相关的字幕)。不记录对话框,菜单栏,工具栏和弹出菜单。默认情况下,录像机将记录每个渲染的帧并生成每秒30帧的电影。请记住,根据所使用的编解码器,录制过程可能会大大降低模拟速度。可以通过[菜单栏->工具->录像机]或单击以下工具栏按钮来访问录像机对话框:
URDF文件格式(* .urdf)通过Ignacio Tartavull的插件cosutey实现导入操作:simExtUrdf。如果正确加载了URDF插件,则可以通过[菜单栏->插件-> URDF导入…]访问插件对话框:
支持SDF文件格式(* .sdf)通过插件simExtSDF进行导入操作。如果正确加载了SDF插件,则可以通过[菜单栏->插件-> SDF导入…]访问导入操作和对话框:
支持collada文件格式(* .dae)通过插件simExtCollada进行导入/导出操作。 当前,仅三角形/多边形网格被导入/导出。 将来的版本还将支持读/写与物理相关的信息。 如果collada插件已正确加载,则可以通过[插件-> COLLADA导入/导出…]访问插件对话框:
…
CoppeliaSim支持两种不同的XML格式,每种格式都有不同的目标:
两种格式都有两个变量(在文件系统/usrset.txt中定义),可以控制输出XML文件:
本教程将在设计简单的移动机器人BubbleRob时尝试介绍很多CoppeliaSim功能。 与本教程相关的CoppeliaSim场景文件位于CoppeliaSim的安装文件夹的tutorials / BubbleRob文件夹中。 下图说明了我们将设计的仿真场景:
由于本教程将跨越许多不同的方面,因此请确保也看看其他教程,主要是有关构建仿真模型的教程。首先,启动CoppeliaSim。仿真器显示默认场景。我们将从BubbleRob的主体开始。
使用[菜单栏->添加->基本形状->球体]向场景中添加直径为0.2的基本球体。将X大小调整为0.2,然后单击“确定”。默认情况下,创建的球体将显示在可见性图层1中,并且是动态且可响应的(因为已启用“创建动态且可响应的形状”项)。这意味着BubbleRob的身体会掉落,并且能够对与其他可响应形状的碰撞做出反应(即由物理引擎仿真)。我们可以看到这是形状动力学属性:启用了“身体可响应”和“身体是动态”项。开始仿真(通过工具栏按钮,或在场景窗口中按
我们还希望BubbleRob的主体可以被其他计算模块(例如最小距离计算模块)使用。因此,如果尚未启用,则可以在该形状的对象公共属性中启用Collidable、Measurable、Renderable和Detectable。如果需要,现在还可以在形状属性中更改球体的视觉外观。
现在,打开“位置”对话框的“平移”选项,选择表示BubbleRob身体的球体,然后“沿着Z”输入0.02。确保将“相对对象”设置为“世界”。然后我们点击平移选项。这会将所有选定对象沿绝对Z轴平移2 cm,并有效地将球体抬高了一点。在场景层次结构中,双击球体的名称,以便可以编辑其名称。输入bubbleRob,然后按回车确认。
接下来,将添加一个接近传感器,以便BubbleRob知道它何时接近障碍物:选择[菜单栏->添加->接近传感器->圆锥类型]。在“方向”对话框的“旋转”选项卡上,“Around Y”和“Around Z”输入90,然后单击“旋转选择”(其实旋转顺序是Z-Y-X)。在位置对话框的“位置”选项卡上,X坐标输入0.1, Z坐标为0.12。现在,接近传感器已相对于BubbleRob的身体正确定位。我们在场景层次中双击接近传感器的图标以打开其属性对话框。单击显示体积参数以打开接近传感器体积对话框。将Offset调整为0.005,Angle调整为30,Range调整为0.15。然后,在接近传感器属性中,单击“显示检测参数”。这将打开接近传感器检测参数对话框。取消选中“如果距离小于0.1,不允许检测”项,然后再次关闭该对话框。在场景层次中,双击接近传感器的名称,可以编辑其名称。输入bubbleRob_sensingNose并按回车键。
选择bubbleRob_sensingNose,然后按住Control键选择bubbleRob,然后单击[菜单栏->编辑->将最后选择的对象设为父对象]。这会将传感器连接到机器人的身体。还可以将bubbleRob_sensingNose拖动到场景层次中的bubbleRob上。这就是现在得到的:
接下来,我们将关注BubbleRob的车轮。使用[菜单栏->文件->新场景]创建一个新场景。通常,跨多个场景工作非常方便,以便可视化并仅对特定元素进行工作。添加一个尺寸为(0.08,0.08,0.02)的纯原始圆柱体。对于BubbleRob的主体,如果尚未启用,则在该圆柱的对象公共属性中启用Collidable、Measurable、Renderable和Detectable。然后,将圆柱的绝对位置设置为(0.05,0.1,0.04),并将其绝对方向设置为(-90,0,0)。名称更改为bubbleRob_leftWheel。复制并粘贴滚轮,然后将副本绝对Y坐标设置为-0.1,重命名为bubbleRob_rightWheel。选择两个轮子,复制,然后切换回场景1,粘贴轮子。
现在,需要为车轮添加接头(或电机)。单击[菜单栏->添加->关节->旋转]将旋转关节添加到场景。在大多数情况下,将新对象添加到场景时,该对象将出现在世界的原点处。保持关节处于选中状态,然后按住Control并选择bubbleRob_leftWheel。在位置对话框的“位置”选项卡上,单击“应用到选择”按钮:这将关节定位在左轮的中心。然后,在“方向”对话框中的“方向”选项卡上,执行相同的操作:这将关节与左轮定向的方向相同。将关节重命名为bubbleRob_leftMotor。现在,在场景层次中双击关节的图标以打开关节属性对话框。然后,单击“显示动态参数”以打开关节动力学属性对话框。启用电动机,并选中项目“目标速度为零时锁定电动机”。现在,对右马达重复相同的过程,并将其重命名为bubbleRob_rightMotor。现在,将左轮连接到左马达,将右轮连接到右马达,然后将两个马达连接到bubbleRob。这就是得到的:
运行仿真,发现机器人向后倒下。我们仍然缺少与地板的第三个接触点。现在,添加一个小滑块(或脚轮)。在一个新场景中,添加一个直径为0.05的纯原始球体,并使该球体可碰撞、可测量、可渲染和可检测(如果尚未启用),然后将其重命名为bubbleRob_slider。在形状动力学属性中将Material设置为noFrictionMaterial。为了将滑块与机器人的其余部分牢固地链接在一起,使用[菜单栏->添加->力传感器]添加了力传感器对象。将其重命名为bubbleRob_connection并将其上移0.05。将滑块连接到力传感器,然后复制两个对象,切换回场景1并粘贴它们。然后,将力传感器沿绝对X轴移动-0.07,然后将其安装到机器人主体上。如果现在运行模拟,会注意到滑块相对于机器人主体略微移动:这是因为两个对象(即bubbleRob_slider和bubbleRob)彼此碰撞。为了避免在动力学模拟过程中产生奇怪的影响,必须通知CoppeliaSim两个对象不会相互运行碰撞,可以通过以下方式进行此操作:在形状动力学属性中,对于bubbleRob_slider,将本地可响应蒙版设置为00001111,对于bubbleRob,将本地可响应掩码设置为11110000。如果再次运行仿真,我们会注意到两个对象不再相互干扰。这就是现在得到的:
再次运行仿真,发现即使在电机锁定的情况下,BubbleRob也会轻微移动。我们还尝试使用不同的物理引擎运行仿真:结果将有所不同。动态模拟的稳定性与所涉及的非静态形状的质量和惯性紧密相关。有关此效果的说明,请务必仔细阅读本节和该节。现在,我们尝试纠正这种不良影响。选择两个轮子和滑块,然后在“形状动力学”对话框中单击3次M = M * 2(用于选择)。结果是所有选定形状的质量都将乘以8。对3个选定形状的惯性进行相同的操作,然后再次运行仿真:稳定性得到了改善。在关节动力学对话框中,将两个电机的目标速度都设置为50。运行仿真:BubbleRob现在向前移动并最终从地板掉落。将两个电机的目标速度重置为零。
对象bubbleRob位于所有稍后将形成BubbleRob模型的对象的基础上。我们将在稍后定义模型。同时,我们要定义代表BubbleRob的对象的集合。为此,我们定义了一个集合对象。单击[菜单栏->工具->集合]以打开集合对话框。或者,也可以通过单击相应的工具栏按钮来打开对话框:
在集合对话框中,单击添加新集合。 一个新的集合对象出现在下面的列表中。 目前,新添加的集合仍为空(未定义)。 在列表中选择新的集合项时,在场景层次中选择bubbleRob,然后在集合对话框中单击“添加”。 现在,我们的集合被定义为包含从bubbleRob对象开始的层次结构树的所有对象(集合的组成显示在“组成元素和属性”部分中)。 要编辑集合名称,请双击它,并将其重命名为bubbleRob_collection。 关闭收集对话框。
在此阶段,我们希望能够跟踪BubbleRob与任何其他对象之间的最小距离。 为此,使用[菜单栏->工具->计算模块属性]打开距离对话框。 或者,也可以使用相应的工具栏按钮打开计算模块属性对话框:
在距离对话框中,单击“添加新的距离对象”并选择一个距离对:[collection] bubbleRob_collection- all other measurable objects in the scene。这只是添加了一个距离对象,该距离对象将测量集合bubbleRob_collection(即该集合中的任何可测量对象)与场景中任何其他可测量对象之间的最小距离。通过双击其名称将距离对象重命名为bubbleRob_distance。关闭距离对话框。现在,当运行仿真时,不会看到任何区别,因为距离对象将尝试测量(并显示)BubbleRob与场景中任何其他可测量对象之间的最小距离段。问题在于,在此阶段场景中没有其他可测量的对象(定义地板的形状默认情况下已禁用其可测量的属性)。在本教程的后续阶段,我们将为场景添加障碍。
接下来,将向BubbleRob添加一个图形对象,以显示以上最小距离,同时还显示BubbleRob随时间的轨迹。单击[菜单栏->添加->图],并将其重命名为bubbleRob_graph。将图形附加到bubbleRob,并将图形的绝对坐标设置为(0,0,0.005)。现在,通过在场景层次结构中双击其图标来打开图形属性对话框。取消选中“显示XYZ平面”,然后单击“添加新数据流以进行记录”,然后选择“对象:数据流类型的绝对x位置”,并选择“ bubbleRob_graph”作为要记录的对象/项目。数据流记录列表中出现了一个项目。该项目是bubbleRob_graph的绝对x坐标的数据流(即,将记录bubbleRobGraph的对象的绝对x位置)。现在,我们还想记录y和z位置:我们以与上述类似的方式添加这些数据流。现在,我们有3个数据流,分别表示BubbleRob的x,y和z轨迹。我们将再添加一个数据流,以便能够跟踪机器人与其环境之间的最小距离:单击“添加新数据流”进行记录,然后选择“距离:数据流类型的段长度”和“bubbleRob_distance”作为要记录的对象/项目。在数据流记录列表中,现在将Data重命名为bubbleRob_x_pos,将Data0重命名为bubbleRob_y_pos,将Data1重命名为bubbleRob_z_pos,将Data2重命名为bubbleRob_obstacle_dist。
我们在“数据流记录列表”中选择bubbleRob_x_pos,在“时间图属性”部分中,取消选中“可见”。对bubbleRob_y_pos和bubbleRob_z_pos都执行相同的操作。这样,在时间图中只能看到bubbleRob_obstacle_dist数据流。以下是应该得到的:
接下来,将建立一个显示BubbleRob轨迹的3D曲线:单击“编辑3D曲线”以打开XY图形和3D曲线对话框,然后单击“添加新曲线”。 在弹出的对话框中,将X值选择bubbleRob_x_pos,Y值选择bubbleRob_y_pos,Z值选择bubbleRob_z_pos。 将新添加的曲线从Curve重命名为bubbleRob_path。 最后,我们检查“相对于世界”项目并将“曲线宽度”设置为4:
关闭与图有关的所有对话框。 现在将一个电机目标速度设置为50,运行仿真,然后将看到BubbleRob的轨迹显示在场景中。 然后,停止仿真并将电动机目标速度重置为零。
添加具有以下尺寸的纯原始圆柱体:(0.1, 0.1, 0.2)。 我们希望此圆柱体是静态的(即不受重力或碰撞的影响),但仍会对非静态的可响应形状施加一些碰撞响应。 为此,在形状动力学属性中禁用“主体是动态的”。 我们还希望圆柱体是可碰撞的、可测量的、可渲染的和可检测的。 在对象的公共属性中执行此操作。 现在,在仍然选择圆柱体的情况下,我们单击对象平移工具栏按钮:
现在可以拖动场景中的任何点:圆柱体将跟随运动,同时始终受约束以保持相同的Z坐标。 将圆柱体复制并粘贴几次,然后将其移动到BubbleRob周围的位置(从顶部查看场景时执行该操作最方便)。 在对象移动期间,按住Shift键可以执行较小的移动步骤。 按住ctrl键可以在与常规方向正交的方向上移动。 完成后,再次选择相机平移工具栏按钮:
将左侧电机的目标速度设置为50并运行仿真:现在,图形视图显示了到最近障碍物的距离,并且距离段在场景中也可见。 停止仿真并将目标速度重置为零。
现在,我们需要完成BubbleRob作为模型定义。 选择模型基础(即对象bubbleRob),然后选中“对象是模型基础”,然后选择”对象/模型可以转移或接受对象共同属性中的DNA“:现在有一个点状边界框,它包含模型层次结构中的所有对象。选择两个关节、接近传感器和图形,然后勾选Don’t show as inside model selection,然后在同一对话框中单击“应用于选择”( 注: 在CoppeliaSim中已经改成 Ignored by model bounding box,且无需Apply to selection):模型边界框现在将忽略两个关节和接近传感器。 仍然在同一对话框中,禁用摄像机可见性层2,并为两个关节和力传感器启用摄像机可见性层10:这有效地隐藏了两个关节和力传感器,因为默认情况下禁用了9-16层。 我们可以随时修改整个场景的可见性层。 为了完成模型定义,选择接近传感器、两个轮子、滑块和图形,然后启用“Select base of model instead”选项:如果现在尝试在场景中选择模型中的对象,则整个模型将被选择,这是一种将整个模型作为单个对象进行处理和操纵的便捷方法。此外,这可以防止模型受到意外修改。 仍然可以在场景中选择模型中的单个对象,方法是通过按住Shift的方式单击选择它们,或者在场景层次中选择它们。 最后,将模型树折叠到场景层次中。 这就是得到的:
接下来,将在与BubbleRob接近传感器相同的位置和方向上添加视觉传感器。 再次打开模型层次结构,然后单击[菜单栏->添加->视觉传感器->透视类型],然后将视觉传感器连接到接近传感器,并将视觉传感器的相对接近传感器的位置和方向设置为(0,0,0)。 我们还确保视觉传感器不是不可见,不是模型边界框的一部分,并且如果单击该模型,则会选择整个模型。 为了自定义视觉传感器,打开其属性对话框。 将“远裁剪平面”项设置为1,将“分辨率x”和“分辨率y”项设置为256和256。向场景中添加一个浮动视图,并在新添加的浮动视图上,右键单击[弹出菜单->视图 ->将视图与选定的视觉传感器关联](确保在该过程中选择了视觉传感器)。
通过单击[菜单栏->添加->关联的子脚本->非线程],将非线程的子脚本附加到视觉传感器。 双击场景层次结构中视觉传感器旁边出现的小图标,这将打开刚刚添加的子脚本。 我们将以下代码复制并粘贴到脚本编辑器中,然后将其关闭:
function sysCall_vision(inData)
simVision.sensorImgToWorkImg(inData.handle) -- copy the vision sensor image to the work image
simVision.edgeDetectionOnWorkImg(inData.handle,0.2) -- perform edge detection on the work image
simVision.workImgToSensorImg(inData.handle) -- copy the work image to the vision sensor image buffer
end
function sysCall_init()
end
为了能够看到视觉传感器的图像,开始仿真,然后再次停止。
我们场景所需的最后一件事是一个小的子脚本,它将控制BubbleRob的行为。 选择bubbleRob并单击[菜单栏->添加->关联的子脚本->非线程]。 双击场景层次结构中bubbleRob名称旁边显示的脚本图标,然后将以下代码复制并粘贴到脚本编辑器中,然后将其关闭:
function speedChange_callback(ui,id,newVal)
speed=minMaxSpeed[1]+(minMaxSpeed[2]-minMaxSpeed[1])*newVal/100
end
function sysCall_init()
-- This is executed exactly once, the first time this script is executed
bubbleRobBase=sim.getObjectAssociatedWithScript(sim.handle_self) -- this is bubbleRob's handle
leftMotor=sim.getObjectHandle("bubbleRob_leftMotor") -- Handle of the left motor
rightMotor=sim.getObjectHandle("bubbleRob_rightMotor") -- Handle of the right motor
noseSensor=sim.getObjectHandle("bubbleRob_sensingNose") -- Handle of the proximity sensor
minMaxSpeed={50*math.pi/180,300*math.pi/180} -- Min and max speeds for each motor
backUntilTime=-1 -- Tells whether bubbleRob is in forward or backward mode
-- Create the custom UI:
xml = '.. sim.getObjectName(bubbleRobBase)..' speed" closeable="false" resizeable="false" activate="false">'..[[
]]
ui=simUI.create(xml)
speed=(minMaxSpeed[1]+minMaxSpeed[2])*0.5
simUI.setSliderValue(ui,1,100*(speed-minMaxSpeed[1])/(minMaxSpeed[2]-minMaxSpeed[1]))
end
function sysCall_actuation()
result=sim.readProximitySensor(noseSensor) -- Read the proximity sensor
-- If we detected something, we set the backward mode:
if (result>0) then backUntilTime=sim.getSimulationTime()+4 end
if (backUntilTime<sim.getSimulationTime()) then
-- When in forward mode, we simply move forward at the desired speed
sim.setJointTargetVelocity(leftMotor,speed)
sim.setJointTargetVelocity(rightMotor,speed)
else
-- When in backward mode, we simply backup in a curve at reduced speed
sim.setJointTargetVelocity(leftMotor,-speed/2)
sim.setJointTargetVelocity(rightMotor,-speed/8)
end
end
function sysCall_cleanup()
simUI.destroy(ui)
end
运行仿真。 现在,BubbleRob会前进,同时尝试避开障碍物(非常基本的方式)。 在仿真仍在运行时,更改BubbleRob的速度,然后将其复制/粘贴几次。 在仿真仍在运行时,也尝试扩展其中的一些。 请注意,根据环境的不同,最小距离计算功能可能会严重降低仿真速度。 您可以通过选中/取消选中“启用所有距离计算”项来在“距离”对话框中打开和关闭该功能。
使用脚本控制机器人或模型只是一种方法。 CoppeliaSim提供了许多不同的方法(也可以结合使用),请参阅外部控制器教程。
本教程将指导您逐步构建机器人或任何其他项目的清晰仿真模型。为了拥有美观、快速显示、快速仿真和稳定的仿真模型,这是一个非常重要的主题,也许是最重要的方面。
为了说明模型的构建过程,我们将构建以下机械臂:
构建可见的形状
在构建新模型时,首先,我们仅处理它的视觉方面:动态方面(其不讨人喜欢的地方更加简化了)/优化模型)、关节、传感器等,将在稍后阶段进行处理。
现在,可以使用[菜单栏->添加->基本形状-> …]在CoppeliaSim中直接创建基本形状。这样做时,可以选择创建纯形状或常规形状。纯形状将针对动态互动进行优化,也可以直接动态启用(例如,跌落、碰撞,但可以在以后禁用)。基本形状将是简单的网格,对于我们的应用程序,可能没有足够的细节或几何精度。在这种情况下,我们的另一个选择是从外部应用程序导入网格。
从外部应用程序导入CAD数据时,最重要的是确保CAD模型不会太重,即不包含太多三角形。这项要求很重要,因为重型模型的显示速度很慢,而且还会减慢以后可能使用的各种计算模块(例如最小距离计算或动力学)。以下示例通常是不可行的(即使有,如我们稍后将看到的那样,也有一些方法可以简化CoppeliaSim中的数据):
CAD数据上方非常繁重:它包含许多三角形(大于47’000),如果我们只在空的场景中使用单个实例,就可以了。但是大多数时候,您将需要模拟同一机器人的多个实例,安装各种类型的抓手,并可能使这些机器人与其他机器人,设备或环境进行交互。在这种情况下,模拟场景可能很快变得太慢。通常,我们建议对不超过2万个三角形的机器人进行建模,但是在大多数情况下,5000至10000个三角形也可以。请记住:几乎在所有方面,少即是好。
是什么使上述模型如此沉重?首先,包含孔和小细节的模型将需要更多的三角形面才能正确表示。因此,如果可能,请尝试从原始模型数据中删除所有的孔,螺钉,物体的内部等。如果您将原始模型数据表示为参数化曲面/对象,则通常在大多数情况下只需选择并删除它们即可(例如在Solidworks中)。第二个重要步骤是以有限的精度导出原始数据:大多数CAD应用程序都允许您指定导出的网格的细节级别。当工程图由大小的对象组成时,分几步导出对象可能也很重要。这是为了避免大对象定义太精确(三角形太多)和小对象定义太粗(三角形太少):先简单地导出大对象(通过调整所需的精度设置),然后导出小对象(通过调整精度设置)。
CoppeliaSim当前支持以下CAD数据格式:OBJ、STL、DXF、3DS(仅Windows)和Collada。还支持URDF,但由于它不是基于纯网格的文件格式,因此此处未提及。
现在,假设我们已经按照上一节中的描述进行了所有可能的简化。导入后,我们可能最终仍然会留下一个过重的网格:
您会注意到整个机器人都是作为单个网格导入的。稍后我们将看到如何对其进行适当划分。还要注意导入的网格的方向错误:最好保持其方向不变,直到构建整个模型为止,因为如果在以后的阶段中我们要导入与同一机器人相关的其他项目,它们将自动具有相对于原始网格的正确位置/方向。
在此阶段,我们可以使用几种功能来简化网格:
没有/可以应用上述功能的预定义顺序(列表中的第一项除外,应始终首先尝试该项),这在很大程度上取决于我们要简化的网格的几何形状。下图说明了应用于导入的网格的上述函数(假设列表中的第一项对我们不起作用):
注意,凸包在现阶段是如何对我们没有帮助的。我们决定首先使用网格抽取功能,然后运行两次该功能,以将三角形的数量除以总共50个。完成后,我们提取简化形状的内部并将其丢弃。我们最终得到的网格总共包含2’660个三角形(原始导入的网格包含超过136’000个三角形!)。形状包含的三角形/顶点的数量可以在形状几何对话框中看到。对于整个机器人模型,2’660三角形是极少的三角形,因此视觉外观可能会因此受到影响。
在这一阶段,我们可以开始将机器人划分为单独的链接(请记住,我们目前整个机器人只有一个形状)。您可以通过两种不同的方式执行此操作:
对于我们的网格,方法1可以正常工作:
在本教程中,我们旨在扩展BubbleRob的功能,以使它跟随地面上的曲线。 确保您已经阅读并理解了第一个BubbleRob教程。 本教程由Eric Rohmer提供。
加载CoppeliaSim安装目录下的tutorials / BubbleRob文件夹中第一个BubbleRob教程的场景。 与本教程相关的场景文件位于tutorials / LineFollowingBubbleRob中。 下图说明了我们将设计的仿真场景:
首先,创建3个视觉传感器,并将其附加到bubbleRob对象。 选择[菜单栏->添加->视觉传感器->正交类型]。 通过双击场景层次中新创建的视觉传感器图标来编辑其属性,并更改参数为以下对话框:
视觉传感器必须面向地面,因此选择它,然后在“方向”对话框中的“方向”选项卡上,将“ Alpha-Beta-Gamma”项设置为[180; 0; 0]。
我们有几种可能性可以读取视觉传感器。 由于视觉传感器只有一个像素,并且操作简单,因此只需查询视觉传感器读取的图像的平均强度值即可。 对于更复杂的情况,可以设置视觉回调函数。 现在,将视觉传感器复制并粘贴两次,并将其名称调整为leftSensor、middleSensor和rightSensor。 将bubbleRob设为父级(即,将其附加到bubbleRob对象)。 现在,您的传感器在场景层次中应如下所示:
让我们正确放置传感器。 为此,请使用位置对话框,在位置选项卡上,并设置以下绝对坐标:
现在让我们修改环境。 我们可以移去BubbleRob前面的几个圆柱体。 接下来,我们将构建机器人将尝试遵循的路径。 从现在开始最好切换到顶视图:通过页面选择器工具栏按钮选择页面4。 然后单击[菜单栏->添加->路径->圆圈类型]。 使用鼠标启用对象移动。 您可以通过两种方式调整路径的形状:
一旦对路径的几何形状满意(可以随时在以后修改),请选择它,然后取消选中路径属性中的“显示点的方向”,“显示路径线”和“显示路径上的当前位置”。 然后单击显示路径形状对话框。 这将打开路径形状对话框。 单击启用路径形状,将类型设置为水平线段,并将缩放比例设置为4.0。 最后将颜色调整为黑色。 我们必须对路径进行最后的重要调整:当前,路径的z位置与地板的z位置重合。 结果是,有时我们会看到路径,有时会看到地板(这种效果在openGL行话中被称为“ z-fighting”)。 这不仅影响我们所看到的,而且还会影响视觉传感器所看到的。 为了避免与z-fighting有关的问题,只需将路径对象的位置向上移动0.5毫米即可。
最后一步是调整BubbleRob的控制器,使其也将遵循黑色路径。 打开附加到bubbleRob的子脚本,并将其替换为以下代码:
function speedChange_callback(ui,id,newVal)
speed=minMaxSpeed[1]+(minMaxSpeed[2]-minMaxSpeed[1])*newVal/100
end
function sysCall_init()
-- This is executed exactly once, the first time this script is executed
bubbleRobBase=sim.getObjectAssociatedWithScript(sim.handle_self)
leftMotor=sim.getObjectHandle("leftMotor")
rightMotor=sim.getObjectHandle("rightMotor")
noseSensor=sim.getObjectHandle("sensingNose")
minMaxSpeed={50*math.pi/180,300*math.pi/180}
backUntilTime=-1 -- Tells whether bubbleRob is in forward or backward mode
floorSensorHandles={-1,-1,-1}
floorSensorHandles[1]=sim.getObjectHandle("leftSensor")
floorSensorHandles[2]=sim.getObjectHandle("middleSensor")
floorSensorHandles[3]=sim.getObjectHandle("rightSensor")
-- Create the custom UI:
xml = '.. sim.getObjectName(bubbleRobBase)..' speed" closeable="false" resizeable="false" activate="false">'..[[
]]
ui=simUI.reate(xml)
speed=(minMaxSpeed[1]+minMaxSpeed[2])*0.5
simUI.setSliderValue(ui,1,100*(speed-minMaxSpeed[1])/(minMaxSpeed[2]-minMaxSpeed[1]))
end
function sysCall_actuation()
result=sim.readProximitySensor(noseSensor)
if (result>0) then backUntilTime=sim.getSimulationTime()+4 end
-- read the line detection sensors:
sensorReading={false,false,false}
for i=1,3,1 do
result,data=sim.readVisionSensor(floorSensorHandles[i])
if (result>=0) then
sensorReading[i]=(data[11]<0.3) -- data[11] is the average of intensity of the image
end
print(sensorReading[i])
end
-- compute left and right velocities to follow the detected line:
rightV=speed
leftV=speed
if sensorReading[1] then
leftV=0.03*speed
end
if sensorReading[3] then
rightV=0.03*speed
end
if sensorReading[1] and sensorReading[3] then
backUntilTime=sim.getSimulationTime()+2
end
if (backUntilTime<sim.getSimulationTime()) then
-- When in forward mode, we simply move forward at the desired speed
sim.setJointTargetVelocity(leftMotor,leftV)
sim.setJointTargetVelocity(rightMotor,rightV)
else
-- When in backward mode, we simply backup in a curve at reduced speed
sim.setJointTargetVelocity(leftMotor,-speed/2)
sim.setJointTargetVelocity(rightMotor,-speed/8)
end
end
function sysCall_cleanup()
simUI.destroy(ui)
end
您可以轻松地调试以下巡线视觉传感器:选择一个,然后在场景视图中选择[右键->添加->浮动视图],然后在新添加的浮动视图中选择[右键->视图- ->将视图与选定的视觉传感器关联]。
最后,删除第一个BubbleRob教程中添加的辅助项:删除图像处理视觉传感器,其关联的浮动视图,表示障碍清除的浮动视图。 通过距离对话框也删除距离计算对象。
本教程介绍了在构建7 自由度冗余机械手时如何使用CoppeliaSim的逆运动学功能。 但是在此之前,请确保在文件夹scenes / ik_fk_simple_examples中查看与IK和FK相关的各种简单示例场景。
本教程分为四个部分:
简单的仿真模型
对于本教程,我们将构建一个非动态操纵器,该操纵器仅使用逆运动学,而不使用任何物理引擎功能。 与本教程相关的CoppeliaSim CAD数据(redundantManipulator.stl)位于CoppeliaSim的安装文件夹cadFiles中。 与本教程相关的CoppeliaSim场景可以在CoppeliaSim的安装文件夹tutorials / InverseKinematics中找到。 单击[菜单栏->文件->导入->网格…],然后选择要导入的文件。 另请参阅有关如何导入/导出形状的部分。 弹出对话框,其中包含各种导入选项。 单击导入。 导入了一个简单的形状,该形状位于场景的中间。 该形状也出现在主窗口左侧的场景层次中。 根据原始CAD数据的导出方式,导入的CAD数据可以处于不同的比例,位于不同的位置,甚至可以细分为多种形状。 下图显示了导入的形状:
如您所见,导入操作给我们留下了一个形状,这是我们预期的几个形状。 这意味着我们必须自己划分操纵器对象:选择对象(只需在场景或场景层次中单击它),然后单击[菜单栏–>编辑–>分组/合并–>分割选定的形状]。 以下是您应该具备的条件:
原始图形被分为多个子图形(另请参见场景层次)。 形状分割算法通过对由公共边连接的所有三角形进行分组来操作。 根据原始网格的创建或导出方式,无法执行此类分割过程。 在这种情况下,您必须在三角形编辑模式或外部编辑器中手动提取形状。
接下来,我们将更改各种对象的颜色,以获得良好的视觉外观。 首先双击场景层次中的形状图标。 此时将打开形状属性对话框。 选择形状时,单击对话框中的调整颜色:这将允许您调整所选形状的各种颜色分量。 现在,只需调整形状的环境光/漫反射颜色分量即可。 要将一个形状的颜色传递到另一个形状,请选择这两个形状,并确保最后选择的形状(用白色边界框指示)是您要从中提取颜色的形状,然后只需单击形状对话框的颜色部分中的应用于选定内容按钮。 上色完成后,可能会出现以下情况:
在下一步中,我们将添加操纵器的7个关节。 执行此操作的一种方法是将关节添加到场景中,然后指定它们的适当位置和方向(通过位置对话框和方向对话框)。 但是,当您不知道关节的确切位置(如我们的示例)时,这是不可能的,因此我们必须从我们拥有的形状中提取它们:
选择所有导入的形状,并点击[Menu bar–>Edit–>Reorient Bound Box–>With Reference Frame of World]。 此操作可确保我们的边界框与绝对参考帧对齐,并且在给定当前操纵器配置的情况下,表示最小的边界框。 点击[菜单栏–>添加–>关节–>旋转]将旋转关节插入到场景中。 默认位置为(0,0,0),其默认方向为垂直,因此关节被操纵器的基础圆柱体隐藏。 在关节仍处于选定状态时,按住Ctrl键并选择基础圆柱体,然后打开位置选项卡上的位置对话框,并单击应用于选择。 这只是将关节定位在与基础圆柱体完全相同的坐标上(但是,此操作仅略微调整了关节的垂直位置,因为它几乎已经就位)。 现在对操纵器中的所有其他关节重复该过程(记住总共应该有7个关节)。 现在所有关节都已就位,但有些关节方向错误。 选择应与世界Y轴对齐的所有关节,然后输入(90,0,0)作为方向对话框中Alpha、Beta和Gamma项的值,然后在方向选项卡上单击应用于选择按钮。 接下来,选择应该与世界X轴对齐的关节,然后为Alpha、Beta和Gamma输入(0,90,0)。 现在,所有关节都具有正确的位置和方向。
现在可以在关节属性对话框(可以通过双击场景层次中的关节图标打开该对话框)中调整关节大小(检查长度和直径项)。 确保所有关节都清晰可见。 这是你应该拥有的:
本教程的下一步是对属于同一刚性实体的形状进行分组。 选择属于链接1的5个形状(基础圆柱体为链接0),然后单击[菜单栏–>编辑–>分组/合并–>分组选定的形状]。 将形状分组为复合形状后,可以将其边界框与世界重新对齐,但此步骤不是必需的(并且仅具有视觉效果)。 对逻辑上属于一起的所有形状重复相同的过程。 在本教程中,我们不会启动手爪的手指,因此只需将它们与最后一个链接进行严格的分组。 当所有要分组的形状共享相同的视觉属性时,请尝试将它们合并在一起:[菜单栏–>编辑–>分组/合并–>合并选定的形状]。
此时,您可以按以下方式重命名场景中的所有对象(从基础到末端):redundantRobot-redundantRob_joint1-redundantRob_link1-redundantRob_joint2等。只需双击场景层次中的对象名称即可对其进行编辑。
现在,我们可以构建运动链,从末端到基础:选择对象redundantRob_link7,然后按住Ctrl键选择对象redundantRob_link7,并单击[菜单栏–>编辑–>使最后选定的对象为父项]。 或者,可以将一个对象拖动到场景层次中的另一个对象上,以实现类似的操作。 接下来,对object redundantRob_joint7和object redundantRob_link6执行相同的操作。 以相同的方式继续,直到构建完操纵器的整个运动链。 这是您应该拥有的(请注意场景层次结构):
选择所有关节,然后在关节对话框中选择被动模式,然后单击应用于选择。 保持关节处于选定状态,然后打开对象通用属性,并在可见性层区域中,禁用层2并启用层10,然后单击相关的应用于选择按钮。 这只是将所有关节发送到可见层10,从而有效地使它们不可见。 如果您希望临时启用/禁用某些层,请查看层选择对话框。
在CoppeliaSim中,IK任务至少需要指定以下元素:
我们已经有了基础对象(Object RedundantRobot)。 添加一个虚拟对象,将其重命名为redundantRob_Tip,并使用坐标和变换对话框将其位置设置为(0.324,0,0.62)。 接下来,将虚拟对象附加到RedundantRob_Link7(选择RedundantRob_Tip,然后选择RedundantRob_Link7,然后选择[菜单栏–>编辑–>使最后选择的对象为父项])。
现在让我们准备目标虚拟对象:复制并粘贴redundantRob_Tip,并将副本重命名为redundantRob_target。 目标虚拟对象已准备好。
现在,我们将添加一种轻松操作机器人的方法,而不必担心移动错误的对象会将其弄坏。 因此,我们将其定义为模型。 首先,将redundantRob_Tip和redundantRob_target移到第11层,使两个虚拟对象都不可见。 然后按住Shift键并选择场景视图中的所有可见对象,按住Ctrl键并单击场景层次中的对象redundantRobot以将其从选择中移除,然后打开对象公用属性对话框。 选中Select base of model而不是Item,然后选中Related Apply to Selection按钮。 使用
单击操纵器上的任何对象,请注意如何始终选择基础对象RedundantRobot。
接下来,添加一个操作球,使用它来操作机器人的抓手位置/方向。 点击[Menu bar–>Add–>Primitive Shape–>Sphere]打开Primitive Shape对话框,X-Size、Y-Size和Z-Size指示0.05,然后取消选中Create Dynamic and Responsible Shape项并点击OK。 将新添加的球体的位置调整为与redundantRob_target相同(使用坐标和变换对话框)。 球体现在显示在操纵器的尖端。 将球体重命名为redundantRob_ManipSphere,然后使其成为redundantRob_target的父对象。 使redundantRob_ManipSphere成为rendundantRob_ManipSphere的冗余Robot父对象:目标虚拟对象和操纵球现在也是机器人模型的一部分。 折叠场景层次中的RedundantRobot树。 冗余机械手模型准备好了!
IK via method 1
向机器人模型添加反向运动学功能的推荐方法是完全通过脚本调用适当的API命令来完成此操作:其思想是通过运动学插件提供的函数构建等效的运动学模型。 该方法使用IK组和IK元素的概念和术语。
选择Object RedundantRobot,然后选择[菜单栏–>添加–>关联子脚本–>非线程化],将非线程化的子脚本附加到该对象。 双击对象名称旁边显示的脚本图标,并将脚本内容替换为以下代码:
function sysCall_init()
-- Take a few handles from the robot:
simBase=sim.getObjectHandle('redundantRobot')
simTip=sim.getObjectHandle('redundantRob_tip')
simTarget=sim.getObjectHandle('redundantRob_target')
simJoints={}
for i=1,7,1 do
simJoints[i]=sim.getObjectHandle('redundantRob_joint'..i)
end
-- Now build a kinematic chain and 2 IK groups (undamped and damped) inside of the IK plugin environment,
-- based on the kinematics of the robot in the scene:
ikJoints={}
-- create an IK environment:
ikEnv=simIK.createEnvironment()
-- create a dummy in the IK environemnt:
ikBase=simIK.createDummy(ikEnv)
-- set that dummy into the same pose as its CoppeliaSim counterpart:
simIK.setObjectMatrix(ikEnv,ikBase,-1,sim.getObjectMatrix(simBase,-1))
local parent=ikBase
-- loop through all joints:
for i=1,#simJoints,1 do
-- create a joint in the IK environment:
ikJoints[i]=simIK.createJoint(ikEnv,simIK.jointtype_revolute)
-- set it into IK mode:
simIK.setJointMode(ikEnv,ikJoints[i],simIK.jointmode_ik)
-- set the same joint limits as its CoppeliaSim counterpart joint:
local cyclic,interv=sim.getJointInterval(simJoints[i])
simIK.setJointInterval(ikEnv,ikJoints[i],cyclic,interv)
-- set the same joint position as its CoppeliaSim counterpart joint:
simIK.setJointPosition(ikEnv,ikJoints[i],sim.getJointPosition(simJoints[i]))
-- set the same object pose as its CoppeliaSim counterpart joint:
simIK.setObjectMatrix(ikEnv,ikJoints[i],-1,sim.getObjectMatrix(simJoints[i],-1))
-- set its corresponding parent:
simIK.setObjectParent(ikEnv,ikJoints[i],parent,true)
parent=ikJoints[i]
end
-- create the tip dummy in the IK environment:
ikTip=simIK.createDummy(ikEnv)
-- set that dummy into the same pose as its CoppeliaSim counterpart:
simIK.setObjectMatrix(ikEnv,ikTip,-1,sim.getObjectMatrix(simTip,-1))
-- attach it to the kinematic chain:
simIK.setObjectParent(ikEnv,ikTip,parent,true)
-- create the target dummy in the IK environment:
ikTarget=simIK.createDummy(ikEnv)
-- set that dummy into the same pose as its CoppeliaSim counterpart:
simIK.setObjectMatrix(ikEnv,ikTarget,-1,sim.getObjectMatrix(simTarget,-1))
-- link the two dummies:
simIK.setLinkedDummy(ikEnv,ikTip,ikTarget)
-- create an IK group:
ikGroup_undamped=simIK.createIkGroup(ikEnv)
-- set its resolution method to undamped:
simIK.setIkGroupCalculation(ikEnv,ikGroup_undamped,simIK.method_pseudo_inverse,0,6)
-- make sure the robot doesn't shake if the target position/orientation wasn't reached:
simIK.setIkGroupFlags(ikEnv,ikGroup_undamped,1+2+4+8)
-- add an IK element to that IK group:
local ikElementHandle=simIK.addIkElement(ikEnv,ikGroup_undamped,ikTip)
-- specify the base of that IK element:
simIK.setIkElementBase(ikEnv,ikGroup_undamped,ikElementHandle,ikBase)
-- specify the constraints of that IK element:
simIK.setIkElementConstraints(ikEnv,ikGroup_undamped,ikElementHandle,simIK.constraint_pose)
-- create another IK group:
ikGroup_damped=simIK.createIkGroup(ikEnv)
-- set its resolution method to damped:
simIK.setIkGroupCalculation(ikEnv,ikGroup_damped,simIK.method_damped_least_squares,1,99)
-- add an IK element to that IK group:
local ikElementHandle=simIK.addIkElement(ikEnv,ikGroup_damped,ikTip)
-- specify the base of that IK element:
simIK.setIkElementBase(ikEnv,ikGroup_damped,ikElementHandle,ikBase)
-- specify the constraints of that IK element:
simIK.setIkElementConstraints(ikEnv,ikGroup_damped,ikElementHandle,simIK.constraint_pose)
end
function sysCall_actuation()
-- reflect the pose of the target dummy to its counterpart in the IK environment:
simIK.setObjectMatrix(ikEnv,ikTarget,ikBase,sim.getObjectMatrix(simTarget,simBase))
-- try to solve with the undamped method:
if simIK.handleIkGroup(ikEnv,ikGroup_undamped)==simIK.result_fail then
-- the position/orientation could not be reached.
-- try to solve with the damped method:
simIK.handleIkGroup(ikEnv,ikGroup_damped)
if not ikFailedReportHandle then
-- We display a IK failure report message:
ikFailedReportHandle=sim.displayDialog("IK failure report","IK solver failed.",
sim.dlgstyle_message,false,"",nil,{1,0.7,0,0,0,0})
end
else
if ikFailedReportHandle then
-- We close any report message about IK failure:
sim.endDialog(ikFailedReportHandle)
ikFailedReportHandle=nil
end
end
for i=1,#simJoints,1 do
-- apply the joint values computed in the IK environment to their CoppeliaSim joint counterparts:
sim.setJointPosition(simJoints[i],simIK.getJointPosition(ikEnv,ikJoints[i]))
end
end
function sysCall_cleanup()
-- erase the IK environment:
simIK.eraseEnvironment(ikEnv)
end
上面的脚本从CoppeliaSim模型创建等效运动学模型,然后在每个仿真步骤中,读取CoppeliaSim目标的位置/方向,将其应用于等效运动学模型的目标,运行IK解算器,最后读取等效运动学模型的关节角度,并将它们应用于CoppeliaSim模型的关节。 要处理单个配置和目标无法到达的情况,我们首先尝试使用非阻尼解算器,如果它失败,我们将恢复到阻尼解算器(使用阻尼解算器,当阻尼较大时,分辨率会变得更稳定,但收敛速度会更慢)。
解算IK的方法1很优雅,因为它允许很好地将IK功能与CoppeliaSim模型分开(例如,IK解算器也可以在空场景中运行)。 但是,这并不总是那么方便,特别是当IK任务很复杂并且涉及几十个关节时(例如,在并行运动学计算机的情况下),在这种情况下,方法2可能更好:
IK via method 2
向机器人模型添加反向运动学功能的推荐方法是通过方法1完全通过编程进行处理。但是,这并不总是那么方便,特别是当IK任务很复杂且涉及数十个关节时(例如,在并行运动学机器的情况下)。 在这种情况下,方法2会派上用场:它包括适当地准备CoppeliaSim模型,然后通过GUI创建运动学任务。 此方法也使用IK组和IK元素的概念和术语。
选择所有关节,然后在关节对话框中选择反向运动学模式,然后单击应用于选择。 接下来,通知CoppeliaSim、RedundantRob_Tip和redundantRob_target是用于逆运动学解析的尖端-目标对。 打开虚拟对象属性对话框,并在虚拟对象链接部分中,将redundantRob_target指定为链接的虚拟对象。 在同一对话框中,链接类型已经是默认值IK,TIP-TARGET。 这就是你现在应该显示的东西:
在此阶段,定义反向运动学任务的所有元素都已准备就绪,我们只需要将任务注册为IK组。 打开反向运动学对话框,然后单击添加新的IK组。 IK组列表中将显示一个新项目:IK_Group。 双击它将其重命名为RedundantRobot_UnDamping。 选中该项目后,单击编辑IK元素以打开IK元素对话框。 在添加新IK元素WITH TIP按钮旁边,在下拉框中选择redundantRob_TIP,然后单击Add new IK Element With TIP按钮。 这只是添加了一个显示在列表中的IK元素。 再往下,将RedundantRobot指示为基础。 最后,确保约束部分中的所有项都已选中(选中ALS Alpha-Beta和Gamma)。 实际上,我们希望尖端虚拟对象在位置和方向上跟随目标虚拟对象:
关闭IK元素对话框。 它应该是这样的:
在这个阶段,IK分辨率不会那么强,因为围绕单个配置的计算,或者不可能达到目标的情况,都会导致不稳定。 若要避免这种情况,请添加第二个IK组,将其重命名为redundantRobot_Damping,然后选择DLS作为计算方法,阻尼为1.0,最大迭代次数为99(使用阻尼解算器,当阻尼较大时,分辨率会变得更稳定,但收敛速度会更慢)。 单击编辑IK元素,然后添加与前面相同的IK元素。 选择未阻尼IK组,然后单击编辑条件参数。 在修道院如果…。 部分中,选中这两个复选框。 然后选择阻尼IK组,单击编辑条件参数并调整Perform If…。 执行了到redundantRobot_UNDAMPED的部分,但失败。
两个IK组的显式处理标志均未选中:这意味着它们将通过主脚本进行处理,即在每个模拟步骤中将自动计算这两个IK组。 如果希望在您希望的时间手动处理计算,请选中这两个IK组的显式处理复选框:从现在开始,只有在显式调用sim.handleIkGroup时才会计算这些IK组。
运行仿真
我们的逆运动学任务准备好了! 来测试一下吧。 运行仿真,然后选择绿色操纵球体。 接下来,选择对象翻译工具栏按钮:
现在用鼠标拖动对象:操纵器应该跟随。 另请尝试对象旋转工具栏按钮:
在操作过程中也尝试按住CTR-或SHIFT-键。 切换回对象平移工具栏按钮,并尝试将对象拖动到尽可能远的位置,请注意反向运动学任务是如何非常健壮的,这要归功于阻尼组件。 停止模拟,然后禁用阻尼IK组并重试。 另请尝试禁用相应IK元素中的各个约束,并注意操纵器在模拟期间的行为。
运行仿真,在操纵器上复制粘贴几次,然后左右移动/旋转副本,还可以通过拖动其操纵球体来更改其配置。 请注意,关于IK,每个操纵器实例是如何完全起作用的。
在本教程中,我们将构建一个六足步行机器人。 在开始学习本教程之前,请确保已阅读BubbleRob教程和有关导入和准备刚体的教程。 由Lyall Randell提供的与本教程相关的CoppeliaSim CAD数据(“heapod.dxf”)位于CoppeliaSim的安装文件夹的“cadFiles”文件夹中。 与本教程相关的已完成模型可以在CoppeliaSim的模型浏览器中找到。 单击[菜单栏–>文件–>导入–>网格…]。 然后选择要导入的文件。 另请参阅有关如何导入/导出形状的部分。 将弹出一个对话框,询问有关网格缩放和网格方向的信息。 单击确定。 导入了几个形状,它们位于场景的中间。 这些形状还会显示在主窗口左侧的场景层次中。 根据原始CAD数据的导出方式,导入的CAD数据可能具有不同的比例、不同的位置,甚至可以分组为单个形状。 导入形状的指定颜色是随机的。 下图显示了导入的形状:
如你所见,这个六足机器人有6条完全相同的腿。 因此,我们将搭建一条腿,而不是分别搭建每条腿,当它完成后,只需将其复制并粘贴到正确的位置! 在场景中选择要删除的图形(按住Ctrl键和/或Shift键并单击以执行选择过程),然后按Delete键(确保腿部指向世界x轴):
导入操作给我们留下了每条腿3个形状,因此我们需要进一步细分这3个形状,以便为腿部连杆和腿部伺服电机提供不同的形状。 选择3个形状,然后单击[菜单栏–>编辑–>分组/合并–>分割所选形状]。 您可以看到,此操作已将距离机器人身体最近的形状划分为3个子形状,其中我们只预期有2个子形状。 要更正此问题,请选择左链接元素和右链接元素,如下图所示:
在两个形状仍处于选中状态时,单击[菜单栏–>编辑–>分组/合并–>合并所选形状],将它们合并为一个形状。
在下一步中,我们将添加腿部机构所需的关节。 执行此操作的一种方法是将关节添加到场景中,然后指定它们的适当位置和方向(通过位置对话框或方向对话框)。 但是,当您不知道关节的确切位置(如我们的示例)时,这是不可能的,因此我们必须从我们拥有的形状中提取它们:
选择代表伺服电机的3个形状,复制它们(ctrl-c或[菜单栏–>编辑–>复制所选对象]),然后用[菜单栏–>文件–>新建场景]创建一个新场景,然后粘贴形状(ctrl-v或[菜单栏–>编辑–>粘贴缓冲区])。 我们现在是在另一个场景(场景2),无论我们在这里做什么都不会影响我们原来的场景。 现在保持选中3个伺服电机,点击[菜单栏–>编辑–>分组/合并–>合并所选形状],将它们合并为一个形状。 然后进入三角形编辑模式。 现在,构成我们形状的所有三角形都会显示出来,并且可以对其进行操作。
切换到第2页,使用Fit-to-view工具栏按钮将相机移动到离形状更近的位置:
按住Shift键并选择组成伺服电机输出轴之一的所有三角形,如下图所示:
然后单击形状编辑模式对话框中的提取形状按钮。 对另外两个伺服电机输出轴执行相同的操作(对于第三个伺服电机,您必须切换到第3页才能按住Shift键选择其输出轴):
切换回第1页,离开编辑模式,然后删除代表3个伺服电机的形状。 现在,提取的竖井变得清晰可见:
我们可以使用提取的形状来精确定位关节。 点击[菜单栏–>添加–>关节–>旋转]将旋转关节插入到场景中。 默认位置为(0,0,0),默认方向为垂直。 在运动类型仍处于选定状态时,按住Ctrl键并选择表示垂直输出轴的形状,然后打开位置对话框,移动到位置选项卡,然后单击应用于选择按钮。
这只是将关节定位在与垂直输出轴完全相同的坐标上! 现在,对另外两个关节和两个水平轴重复该过程。 所有关节现在都已就位,但是,只有第一个添加的关节具有正确的方向。 选择最后添加的两个关节,然后输入(-90,0,0)作为方向对话框中Alpha、Beta和Gamma项的值,然后在方向选项卡上单击应用于选择按钮。 现在,所有关节都具有正确的位置和方向。 选择所有3个关节,将它们复制到缓冲区(ctrl-c),切换回初始场景(场景1),然后粘贴缓冲区(ctrl-v)。 这是你应该得到的:
现在可以在关节属性对话框(可以通过双击场景层次中的关节图标打开该对话框)中调整关节大小(检查关节长度和关节直径项)。 此外,我们希望两个水平运动类型位于y坐标0,而垂直运动类型位于与其伺服电机类似的z坐标:
将关节重命名为“HEXA_joint1”、“HEXA_joint2”和“HEXA_joint3”(从身体到脚)。 可以通过双击场景层次中的关节名称来执行此操作。 现在,让我们设置初始关节值,以便当腿部水平拉伸时,所有关节值都为零:在关节对话框的项目位置中,为“hexa_joint2”和“hexa_joint3”分别设置-30和+120。 请注意关节是如何处于力矩/力模式的。 在我们的示例中,我们希望能够在反向运动学模式下控制这些关节,但也希望反向运动学结果作为动力学位置控制值应用。 为此,我们将在反向运动学模式下设置关节(在关节模式部分中选择“关节处于反向运动学模式”)。 除此之外,我们还激活了该关节的混合操作(这告诉CoppeliaSim反向运动学计算结果将作为动态目标值应用于关节)。 单击“应用于选择”以将刚刚执行的更改应用到其他两个选定的关节。
此时,我们应该准备图形的视觉外观,如果需要将它们分组,并为运动模拟准备相应的纯图形。 此处不会详细说明如何完成此操作,因为该过程与教程中有关导入和准备刚体的操作非常相似。 完成此步骤后,您应该得到以下内容(可见部分和隐藏部分):
上面的场景是“tutorials\Hexapod\Sixapod intermediate step.ttt”。 请注意以下事项:
接下来,我们将链接该机制的当前元素。 选择“HEXA_Link3Responsible”,然后选择“HEXA_joint3”,然后单击[Menu Bar–>Edit–>Make Last Selected Object Parent]。 同样的操作包括:“HEXA_joint3”和“HEXA_Link2Responsible”、“HEXA_Link2Responsible”和“HEXA_joint2”、“HEXA_joint2”和“HEXA_Link1Respondable”、“HEXA_Link1Responsible”和“HEXA_joint1”,最后是“HEXA_joint1”和“Hexapod”。 我们已经组装好了第一条腿。 开始模拟并观察刚性对象是如何受关节约束的。 另请注意,关节不是保持固定位置,而是缓慢漂移:这是因为我们尚未为表示腿部的运动链定义反向运动学任务。 这就是我们下一步要做的。
使用[Menu Bar–>Add–>Dummy]添加虚拟对象。 将其重命名为“HEXA_FootTip”。 现在将虚拟人放置在腿部尖端,即腿部与地板接触的位置。 您可以通过使用对象操纵工具栏按钮或使用位置对话框来实现这一点。 在前一种情况下,通过切换到第2页、第3页、第4页、第5页或第6页上的正交投影视图,可以逐步调整位置。完成后,虚拟人的绝对位置应接近(0.204,0,0)。 接下来,将虚拟对象附加到“HEXA_Link3”(将虚拟对象拖动到场景层次中的“HEXA_Link3”上)。 然后复制并粘贴“HEXA_FootTip”,并将副本重命名为“HEXA_FootTarget”。
在此阶段,我们想要的是在使附加机构自动调整(即自动计算新关节位置)的同时,让“HEXA_FootTip”跟在“Hexa_FootTarget”之后。 我们必须定义逆运动学任务:
首先,让我们通知这两个虚拟人,它们是用于反向运动学解析的尖端-目标对。 双击场景层次中的虚拟图标“HEXA_FootTip”:这将打开虚拟属性对话框。 在Dummy-Dummy链接部分中,将“HEXA_FootTarget”指定为链接的Dummy。 请注意两个虚拟对象是如何通过场景层次中的红色点划线链接的(两个虚拟对象在场景中也通过红线链接,但由于两个虚拟对象重合,因此无法看到该线)。 在同一对话框中,链接类型已经是默认值“IK,TIP-TARGET”。 这就是你现在应该拥有的东西:
现在打开反向运动学对话框,然后单击添加新的IK组。 IK组列表中将显示一个新项目:“IK_Group”。 选中该项目后,打开IK元素对话框(单击编辑IK元素),并在右侧的下拉框中指示“HEXA_FootTip”,以添加新的IK元素和提示。 然后单击Add new IK Element with Tip。 关闭对话框,并将“HEXA_FootTarget”附加到“六脚架”。 我们的反向运动学任务已经为这条腿做好了准备! 我们来测试一下吧。
首先,通过打开常规动力学属性对话框临时禁用动力学,然后取消选中启用动力学。 接下来,开始仿真,并在场景层次中选择“HEXA_FootTarget”。 使用鼠标四处移动“HEXA_FootTarget”:腿部应该紧随其后。 停止仿真并再次启用动力学。
我们将通过生成一条腿的运动来控制六足机器人,并以延迟的方式将其应用于所有6条腿,其中每条腿都有不同的延迟。 我们通过让一个子脚本为一条腿生成移动序列,并让另外6个子脚本以延迟的方式应用该移动序列来实现这一点。 选择“HEXA_joint1”并点击[菜单栏–>添加–>关联子脚本–>非线程化]。 我们刚刚将一个子脚本附加到“HEXA_JOINT1”对象。
现在我们将重复这条腿5次。 选择组成腿部的所有对象(也包括隐藏对象):从“HEXA_joint1”到“HEXA_FootTip”。 同时选择“HEXA_FootTarget”。 确保没有选择“HEXA_bodyRespondable”和“Hexa_Body”,然后复制并粘贴对象。 打开方向对话框,移动到方向选项卡,并为环绕Z项目输入“60”。 确保变换将相对于世界,然后单击Z旋转当前选择。 再次粘贴初始腿部,然后将围绕Z轴项目调整为120,然后再次单击Z轴旋转当前选择。 对其余3条腿重复该过程(确保将旋转角度调整为180、240,最后调整为300度)。 这是你应该拥有的:
让我们把所有的腿都连接到身体上。 选择“HEXA_joint1#0”至“HEXA_JOINT1#4”,然后选择“六脚架”,点击[菜单栏–>编辑–>使最后选择的对象为父对象]。 在场景层次中,通过单击腿部树的“-”图标来折叠所有腿部树。 然后选择“Hexa_FootTarget#0”到“Hexa_FootTarget#4”,然后选择“六脚架”,然后点击[菜单栏–>编辑–>将最后选择的对象设为父对象]。
打开反向运动学对话框。 请注意反向运动学任务也是如何复制的。 这是你应该拥有的:
关闭反向运动学对话框。 使用[Menu Bar–>Tools–>Selection]或按相应的工具栏按钮打开对象选择对话框。 单击清除当前选择,然后单击Dummies:0/13。场景中的所有虚拟对象都已被选中。 取消选择(按住Ctrl键并单击)不属于六足机器人的虚拟对象。 现在我们已经选择了六足动物上的12个假人。 现在打开对象的公共属性。 在Visibility Layers区域中,禁用Layer 3并启用Layer 11,然后单击相关的Apply to Selection按钮。 这只是将所有虚拟对象发送到可见层11,有效地使它们不可见。 如果您希望临时启用/禁用某些层,请查看层选择对话框。
接下来,按照与上面相同的步骤将所有关节发送到可见层10。
然后将新虚拟对象添加到场景中,并将其重命名为“HEXA_BASE”。 同时将其发送到可见层11,然后将其设置为“六足”的子级。 “HEXA_BASE”表示我们选择为(0,0,0)的六足机器人的位置。
现在让我们定义六足动物模型。 正如您可能已经注意到的,当您单击六足机器人上的对象时,只有该对象会被选中。 但我们现在想要的是保护单个对象不被修改,而是选择整个机器人。 对于组成六足机器人的每个对象(对象“Sixpod”除外),启用对象通用属性对话框中的Select base of model,而不是项。 清除选择,然后选择“六脚架”。 在同一对话框中,启用Object is model base item。 现在单击六足机器人上的任何对象:现在将选择整个机器人:
现在将一个非线程子脚本附加到“六足”。 创建另一个场景(场景3),并打开“heapod.ttm”模型文件。 双击对象“heatapod”的子脚本图标以打开脚本编辑器。 复制脚本,切换回原始场景(场景1),双击对象“六脚架”的子脚本图标,粘贴脚本。 对与每个分支相关联的子脚本重复相同的过程。 请注意,与六足动物腿相关联的所有子脚本是如何完全相同的。 最后一个元素仍然缺失:我们需要每条腿应用具有不同时间延迟的运动序列。 如果仔细查看分支的子脚本,它们每个都使用以下指令从其用户参数中读取延迟值:
modulePos=sim.getUserParameter(sim.handle_self,'modulePosition')
通过双击子脚本或自定义脚本右侧的图标,可以打开用户参数对话框。 添加与其他场景中的六足完全相同的参数。 运行仿真。 六足机器人现在应该可以行走了。
使用脚本控制机器人或模型只是一种方法。 CoppeliaSim提供了许多不同的方式(也是组合的),看看外部控制器教程,或者插件教程。
在CoppeliaSim中有几种方法可以控制机器人或仿真:
与本教程相关的场景文件有8个:
在所有8种情况下,都使用子脚本,主要用于建立与外部世界的链接(例如,启动正确的客户端应用程序,并将正确的对象句柄传递给它)。 还有另外两种方法可以控制机器人、模拟器或模拟器本身:使用定制脚本或附加组件。 但是,不建议将它们用于控制,而应在模拟不运行时使用它们来处理功能。
例如,链接到场景控制中的机器人ViaB0RemoteApi.ttt的子脚本具有以下主要任务:
作为另一个示例,链接到场景ControledViaRos.ttt中的机器人的子脚本具有以下主要任务:
作为另一个示例,链接到场景控制中的机器人ViaTcp.ttt的子脚本具有以下主要任务:
运行仿真,并复制和粘贴机器人:您将看到复制的机器人将直接可操作,因为它们附加的子脚本负责启动各自外部应用程序的新实例,或调用适当的插件函数。
本教程将尝试解释如何为CoppeliaSim编写插件。 与本教程相关的CoppeliaSim场景文件位于CoppeliaSim的安装文件夹的tutorials/BubbleRobExt中。 本教程的插件项目文件可以在这里找到。
在程序启动时,CoppeliaSim会自动加载其文件夹(即安装文件夹,或与包含coppeliaSim.exe的文件夹相同的文件夹)中可以找到的所有插件。 CoppeliaSim使用以下掩码识别插件文件:Windows上的“simExt*.dll”、Mac OS上的“libsimExt*.dylib”和Linux上的“libsimExt*.so”。 另外,插件的文件名不应该包含任何下划线(显然除了开头的那个)。 本教程的插件文件是simExtBubbleRob.dll。 测试时,请确保在CoppeliaSim启动时正确加载:通过取消选中用户设置对话框([菜单栏–>工具–>设置])中的隐藏控制台窗口项,将控制台窗口切换为可见。 此选项仅在Windows版本中可用。 在Mac上,查看系统的控制台,在Linux上,尝试从控制台中启动CoppeliaSim。 控制台窗口应显示与以下内容类似的内容:
正如您已经了解的,这个插件是在BubbleRob教程中为BubbleRob编写的。 加载相关场景文件(Tutorials\BubbleRobExt\BubbleRobExt.ttt)。 BubbleRob插件添加了4个新的Lua命令(自定义Lua命令应遵循约定:“simXXX.YYY”作为名称,例如simRob.start):
现在,在场景中打开附加到BubbleRob模型的线程子脚本(例如,双击场景层次中对象bubbleRob旁边的脚本图标)。 检查代码:
function sysCall_threadmain()
-- Check if the required plugin is there:
moduleName=0
moduleVersion=0
index=0
bubbleRobModuleNotFound=true
while moduleName do
moduleName,moduleVersion=sim.getModuleName(index)
if (moduleName=='BubbleRob') then
bubbleRobModuleNotFound=false
end
index=index+1
end
if (bubbleRobModuleNotFound) then
sim.displayDialog('Error','BubbleRob plugin was not found. (simExtBubbleRob.dll)&&nSimulation will not run properly',
sim.dlgstyle_ok,true,nil,{0.8,0,0,0,0,0},{0.5,0,0,1,1,1})
else
local jointHandles={sim.getObjectHandle('leftMotor'),sim.getObjectHandle('rightMotor')}
local sensorHandle=sim.getObjectHandle('sensingNose')
local robHandle=simBubble.create(jointHandles,sensorHandle,{0.5,0.25}) -- create a BubbleRob instance
if robHandle>=0 then
simBubble.start(robHandle,20) -- control happens here
simBubble.stop(robHandle)
simBubble.destroy(robHandle) -- destroy the BubbleRob instance
end
end
end
代码的第一部分负责检查运行此脚本所需的插件(即simExtBubbleRob.dll)是否可用(即是否找到并成功加载)。 如果没有,则会显示一条错误消息。 否则,将检索关节和传感器句柄,并将其提供给自定义Lua函数,该函数在插件中创建BubbleRob的控制器实例。 如果调用成功,则可以调用simBubble.moveAndAvoid。 该函数指示插件在避开障碍物的同时移动BubbleRob模型,持续时间为20秒。 该函数是阻塞的(缺省情况下,省略的第三个参数为false),这意味着在函数完成之前(即20秒之后),调用才会返回。 运行仿真:BubbleRob移动20秒,然后如预期的那样停止。 现在离开CoppeliaSim。 暂时将插件重命名为temp_simExtBubbleRob.dll,以便CoppeliaSim不再加载它,然后再次启动CoppeliaSim。 加载上一个场景并运行模拟:现在将显示一条错误消息,指示找不到所需的插件。 再次离开CoppeliaSim,将插件重新命名为simExtBubbleRob.dll,然后再次启动CoppeliaSim。
让我们来看看插件是如何注册和处理上述4个自定义Lua函数的。 打开BubbleRob插件项目,查看文件simExtBubbleRob.cpp;
注意3个必需的插件入口点:simStart、simEnd和simMessage;simStart在插件加载(初始化)时调用一次,simEnd在插件卸载(清理)时调用一次,simMessage使用几种类型的消息定期调用。
在初始化阶段,插件加载CoppeliaSim库(以便访问CoppeliaSim的所有API函数),然后注册4个自定义Lua函数。 通过指定以下内容注册自定义Lua函数:
当脚本调用指定的函数名时,CoppeliaSim将尝试将提供的参数转换为回调所需的参数,然后调用回调地址。 回调函数中最困难的任务是正确读取输入参数,并正确写入输出值。 为了简化任务,使用了两个帮助器类,它们将负责该任务:CLuaFunctionData和CLuaFunctionDataItem,它们位于Programming/common和Programming/Include中。
在编写您自己的自定义Lua函数时,请尝试使用与文件simExtBubbleRob.cpp中相同的代码布局/框架。
BubbleRob实例的控制不会发生在4个自定义Lua函数回调中的任何一个中:回调只是初始化/销毁/更新数据结构。 控制发生在simMessage中,消息为sim_message_eventcallback_module eHandle;当主脚本调用sim.handleModule(sim.handleall,false)时,所有插件都会调用该消息,每次模拟过程都会发生一次。
通常,回调例程应该尽可能快地执行,然后应该将控制权交还给CoppeliaSim,否则整个仿真器将暂停。
CoppeliaSim的功能大部分时间需要开发插件。 在继续学习本教程之前,请确保您已经阅读了关于插件的教程和关于外部控制器的教程。
与本教程相关的CoppeliaSim场景文件位于CoppeliaSim的安装文件夹Scenes\robotLanguageControl.ttt中。 您可以在这里找到插件项目文件,在这里可以找到服务器应用程序项目文件。
首先,我们先加载相关场景文件Scenes\robotLanguageControl.ttt:
MTB机器人是一个假想的机器人(MTB代表机器类型B),它将用一种假想的机器人语言进行控制。
如前所述,所使用的机器人语言是假想的,而且非常非常简单。 支持以下命令(每行一个命令,输入区分大小写):
"REM" starts a comment line
"SETLINVEL v": sets the prismatic joint velocity for next movements (v is in m/s)
"SETROTVEL v": sets the revolute joint velocity for next movements (v is in degrees/s)
"MOVE p1 p2 p3 p4": moves to joint positions (p1;p2;p3;p4) (in degrees except for p3 in meters)
"WAIT x": waits x milliseconds
"SETBIT y": sets the bit at position y (1-32) in the robot output port
"CLEARBIT y": clears the bit at position y (1-32) in the robot output port
"IFBITGOTO y label": if bit at position y (1-32) in the robot input port is set, jump to "label"
"IFNBITGOTO y label": if bit at position y (1-32) in the robot input port is not set, jump to "label"
"GOTO label": jumps to "label"
任何不同于“REM”、“SETLINVEL”、“SETROTVEL”、“MOVE”、“WAIT”、“SETBIT”、“CLEARBIT”、“IFBITGOTO”、“IFNBITGOTO”和“GOTO”的单词都被认为是标签。 现在运行模拟。 如果未找到相关插件,将显示以下消息(消息的显示在附加到对象MTB_Robot和MTB_Robot#0的子脚本中处理):
如果找到相关插件,则MTB插件将启动一个服务器应用程序(即mtbServer),该应用程序基本上代表机器人语言解释器和控制器。 不直接需要服务器应用程序,mtbServer功能也可以直接在MTB插件中运行。 在服务器应用程序中使用该功能的主要优势是:
目前,MTB服务器主要负责两项任务:
如果MTB服务器在编译程序代码期间检测到错误,它将向插件返回一条错误消息,并将其移交给调用子脚本(即,在我们的示例中,是附加到对象MTB_Robot和MTB_Robot#0的子脚本),该脚本将显示(例如):
如果编译成功,则机器人开始执行它们各自的程序。 仿真是最高速度仿真,但可以通过切换相关工具栏按钮切换到实时仿真:
通过多次按下相应的工具栏按钮,可以进一步加快执行速度:
每个MTB机械手程序都可以通过其显示的自定义对话框(即自定义用户界面)随时单独暂停、停止或重新启动:
上面的自定义UI是MTB机器人的用户界面,可以完全定制。 如果MTB机器人被复制,那么它的自定义UI也将被复制。 除了能够控制程序执行状态之外,自定义UI还显示当前程序行(命令)和MTB的当前连接值。 用户还可以更改机器人的输入端口位,并读取机器人的输出端口位。 输入和输出端口可由机器人语言程序分别读取和写入。 输入和输出端口也可以由外部设备(例如,机器人的抓取器或吸盘)通过使用适当的函数调用来写入和读取(见下文)。
有两个子脚本附加到MTB_Robot和MTB_Robot#0对象。 他们负责处理自定义对话框并与MTB插件通信。 子脚本中的大多数代码也可以由插件处理。 打开附加到两个MTB机器人之一的子脚本(例如,双击场景层次中机器人模型旁边的脚本图标)。在脚本的顶部,您将看到机器人语言代码。
尝试修改MTB机器人的程序,使其执行不同的运动序列。 做一点实验。
MTB机器人的操作方式如下:
MTB机器人及其简单的机器人语言是一个简单的原型,旨在演示如何将机器人语言解释器集成到CoppeliaSim中。 将当前的功能扩展到更复杂的机器人或机器人语言非常容易。 所需要的只是:
现在我们来看一下MTB的插件项目。 在CoppeliaSim中嵌入机器人语言解释器(或其他仿真器)有一个先决条件:
在编写任何插件时,请确保插件只能从主线程(或从由CoppeliaSim创建的线程)访问CoppeliaSim的常规API! 插件可以启动新线程,但在这种情况下,这些新线程不应该用于访问CoppeliaSim(但是,它们可以用于与服务器应用程序通信、与某些硬件通信、执行后台计算等)。
现在让我们看一下附加到MTB机器人的子脚本。 代码可能看起来相当长或很复杂。 但是,在子脚本中处理的大多数功能也可以在插件中直接处理,从而使子脚本变得更小/更整洁。 处理子脚本中的大多数功能的优势在于,无需重新编译插件即可执行修改!
以下是MTB机械手的子脚本主要功能:
以下3个自定义Lua函数是主要感兴趣的(其他函数由插件导出):
number mtbServerHandle,string message=simMTB.startServer(string mtbServerExecutable,
number portNumber,charBuffer program,table_4 jointPositions, table_2 velocities)
number result,string message=simMTB.step(number mtbServerHandle,number timeStep)
table_4 jointValues=simMTB.getJoints(number mtbServerHandle)
您还可以设想稍微修改STEP函数,并添加一个额外的函数,以便能够处理由机器人语言程序执行触发的中间事件。 在这种情况下,每个模拟步骤都必须执行以下脚本代码(在非线程化的子脚本中):
local dt=sim.getSimulationTimeStep()
while (dt>0) do
result,dt,cmdMessage=simMTB.step(mtbServerHandle,dt) -- where the returned dt is the remaining dt
local event=simMTB.getEvent()
while event~=-1 do
-- handle events here
event=simMTB.getEvent()
end
end
在本教程中,我们将从A到Z构建传送带(或履带,在这种情况下,请查看本教程的最后部分)。下图说明了我们将设计的仿真场景:
我们将构建一个传送带,它的行为应该与真实传送带完全一样,其中每个传送带垫都是单独动态模拟的。 例如,这意味着较小的物体可能会被困在两个相邻的垫子之间。 这种类型的模拟可能计算非常密集,并会减慢整个模拟过程。 还有一种替代的、简化的方法来模拟传送带,该方法也将在本教程中进行说明,并明确标记为方法B(与模拟单个垫的方法A形成对比)。
首先,启动CoppeliaSim。 上图中的传送带基本上由沿其轨迹驱动多个垫的路径对象构成。 用[Popup Menu–>Add–>Path–>Circle type]添加循环路径。 要从上方查看路径,请切换到第6页。使用布满视图工具栏按钮将摄像机拉近:
选择路径对象后,请注意路径是如何由蓝点定义的,在蓝点之间执行Bezier插值。 您还可以区分表示路径位置的红色球体,它不是路径位置,而是沿路径的位置。
在将焊盘连接到路径之前,让我们准备正确的路径大小和形状。 您可以导入路径,也可以修改和编辑现有路径,我们将选择第二种方法。 选择路径时,通过单击路径编辑模式工具栏按钮进入路径编辑模式:
现在处于路径编辑模式。 我们想设计一条厚10厘米,宽20厘米,长1米的传送带。 组成传送带的单个垫厚度为5毫米。
在路径编辑模式对话框中,检查路径是否平坦,并保持x向上。 选择所有路径点,然后打开位置对话框,在位置缩放选项卡上,在右侧输入比例因子“0.19”的3倍,然后单击“缩放位置”。 这只是适当地缩放了路径。 使用鼠标滚轮靠近路径。 选择最上面的路径点。 使用ctrl-c复制它。 然后再次选择它,并使用ctrl-v将缓冲区粘贴到所选位置之后。 我们刚刚创建了一个与最上面的路径点重合的路径点:我们复制了路径点#13,并在随后粘贴了它的副本。 新路径点是路径点#14,如下图所示:
现在,对最低的路径点重复相同的过程。 现在我们已经复制了两个中间路径点,我们可以拉伸路径,即将左侧和右侧部分分开:在场景层次中选择路径点#6到路径点#14,然后在位置对话框的平移选项卡上,对于沿X的项目,输入“-0.5”,然后单击X-Translate Selection。 现在稍微缩小一点。 这是应该得到的:
现在选择左侧的路径点,并以类似的方式将它们朝正x坐标偏移0.5米。 路径已就绪:
离开路径编辑模式,选择路径,并在主窗口的信息文本部分注意到行“Last Selected Object type:Path(Bezier曲线点计数=270,总长度=2.2985,p=+0.0000,vn=+0.0000)”。 这告诉我们这条小路的长度是2.2985米。 现在我们可以计算出我们需要多少个焊盘,它们的宽度,以及焊盘之间的距离应该是多少。 我们有40个焊盘,宽度为5厘米,因此焊盘之间的距离是0.75厘米。
单击[弹出菜单–>添加–>基本体形状–>长方体]。 此时将显示基本体形状对话框,您可以在其中调整各种参数。 输入(0.05,0.005,0.18)作为x、y和z大小。
方法A。
只需单击“确定”即可。 这添加了一个动态的、可响应场景的纯形状。 切换到第1页。现在您可以看到添加的形状。 将其重命名为“pad0”(可以通过在场景层次中双击任何对象的名称来重命名该对象)。 使用位置对话框将焊盘的绝对z坐标设置为0。 双击场景层次中的PAD图标以打开图形属性对话框。 调整其颜色(单击调整外部颜色项)。 然后,在形状动态属性对话框中,检查静态项,以便在模拟过程中不会使垫子掉落。 在对象通用属性中,选中以下项目:改为选择模型基础、可碰撞、可测量、可渲染和所有可检测属性。 在Visibility Layers部分中,同时启用Layer 9(但同时保持Layer 1处于启用状态)。
方法B。
取消选中Create Dynamic and Responsible Shape项,然后单击OK。 这将为场景添加一个静态的纯形状。 切换到第1页。现在您可以看到添加的形状。 将其重命名为“pad0”(可以通过在场景层次中双击任何对象的名称来重命名该对象)。 使用位置对话框将焊盘的绝对z坐标设置为0。 双击场景层次中的PAD图标以打开图形属性对话框。 调整其颜色(单击调整外部颜色项)。 在对象通用属性中,选中以下项目:改为选择模型基础、可碰撞、可测量、可渲染和所有可检测属性。 在方法B中,传送带垫没有动力功能!
接下来,如果使用sim.setPathPosition修改了路径的固有位置,我们希望将焊盘附加到路径,以便它自动跟随路径的轨迹。 对于此任务,我们需要一个辅助对象:一个虚拟对象。 单击[弹出菜单–>添加–>虚拟]。 将虚拟对象重命名为“padLink0”。 通过方向选项卡上的方向对话框将虚拟对象的方向调整为(0,-90,0)。 通过点击焊盘,然后点击虚拟对象,然后点击[Popup Menu–>Edit–>Make Last Selected Object Parent],将焊盘连接到虚拟对象。 接下来,以类似的方式将虚拟对象附加到路径(也可以通过在场景层次中拖放来实现为父对象)。 双击场景层次中的虚拟图标以打开虚拟属性对话框。 检查Follow Parent Path(Only Direct Parent)项:注意虚拟对象和焊盘是如何跳到路径的红色球体位置的。 在虚拟人仍处于选中状态时,将复制增量项设置为“0.0575”。 这表示如果复制虚拟对象,则其在路径上的偏移将自动递增0.0575米,即焊盘宽度加上焊盘间距离。 在对象公用属性对话框中,隐藏层11中的虚拟对象(取消激活层3并激活层11)。
现在我们将添加剩余的39个焊盘。 选择虚拟对象和焊盘,然后使用ctrl-c复制所选内容。 然后按ctrl-v键39次,准确粘贴缓冲区39次。 用Esc-键清除选择,然后在场景层次结构中选择“padLink1”到“padLink39”(确保您没有选择焊盘,而只选择了Dummies!),然后选择路径,然后点击[Popup Menu–>Edit–>Make Last Selected Object Parent]。 这是你应该拥有的:
下一步,我们将为传送带添加一个简化的外壳。 将尺寸为(0.12,0.12,0.2)的纯圆柱体添加到场景中。 将其z位置设置为0,将x位置设置为0.5,然后调整其颜色。 复制并粘贴,然后将副本移动到x坐标-0.5米。 添加一个带有维度(1.0,0.09,0.18)的纯长方体。 将其z位置设置为0,并调整其颜色。 选择刚才添加的两个圆柱体和长方体,然后单击[弹出菜单–>编辑–>分组/合并–>分组选定的形状]。 将生成的形状重命名为“传送带”。 在图形动态属性对话框中,将“传送带”设为静态,并在对象的通用属性中,选中“Colliable”、“Measable”、“Renderable”和“All Detect”属性。 同时按可见层按钮9。然后将路径连接到“传送带”上。
方法B。
添加一个带有维度(1.0,0.1,0.18)的纯长方体。 将其z位置设置为0。 将生成的形状重命名为“transportorForwarder”。 在Shape对话框中,将“TransportorForwarder”设为静态,然后在Object通用属性对话框中将对象发送到可见层9(禁用按钮1和启用按钮9)。 然后在“传送带”上贴上“传送带”。 “ConveyorForwarder”是一个物体,它将移动躺在上面的其他物体和一个小把戏(见下文)。
现在将传送带主体绕绝对x轴旋转90度,并将其坐标设置为(0.0,0.0,0.5)。 选择路径,然后在路径属性中取消选中显示路径线、显示点方向和显示路径上的当前位置。 选择“传送带”,在对象通用属性对话框中,检查对象是否为模型基项。 单击编辑模型属性,然后在模型内容确认/信息部分中,添加您希望在每次加载传送带模型时显示的一些文本。 最后从“传送带”开始折叠层次树。 我们的模型差不多准备好了:
请注意,单击传送带模型上的任何对象,整个模型都会被选中。 如果要选择单个对象,仍可以在场景层次中或通过按住Shift和Ctrl键(同时使用这两个键!)。 单击对象时。
还可以通过API函数sim.setPathPosition修改路径的固有位置(因此,路径的移动)。 有关如何执行此操作的示例,请查看模型浏览器中的其他传送带模型。
方法A。
选择“传送带”对象,点击[菜单栏–>添加–>关联子脚本–>非线程化]。 这只是将一个非线程化的子脚本附加到模型库。 双击场景层次中的子脚本图标以打开子脚本。 将该脚本替换为以下代码:
function sysCall_init() pathHandle=sim.getObjectHandle("Path") sim.setPathTargetNominalVelocity(pathHandle,0) -- for backward compatibility end function sysCall_cleanup() end function sysCall_sensing() end function sysCall_actuation() beltVelocity=0.1 -- in meters/seconds local dt=sim.getSimulationTimeStep() local pos=sim.getPathPosition(pathHandle) pos=pos+beltVelocity*dt sim.setPathPosition(pathHandle,pos) -- update the path's intrinsic position end
上述代码有效地完成了以下工作:在每个模拟通道中,修改路径的固有位置,以生成传送带的固有运动。
方法B。
选择“传送带”对象,点击[菜单栏–>添加–>关联子脚本–>非线程化]。 这只是将一个非线程化的子脚本附加到模型库。 双击场景层次中的子脚本图标以打开子脚本。 将该脚本替换为以下代码:
function sysCall_init() pathHandle=sim.getObjectHandle("Path") forwarder=sim.getObjectHandle('conveyorForwarder') sim.setPathTargetNominalVelocity(pathHandle,0) -- for backward compatibility end function sysCall_cleanup() end function sysCall_sensing() end function sysCall_actuation() beltVelocity=0.1 -- in meters/seconds local dt=sim.getSimulationTimeStep() local pos=sim.getPathPosition(pathHandle) pos=pos+beltVelocity*dt sim.setPathPosition(pathHandle,pos) -- update the path's intrinsic position -- Here we "fake" the transportation pads with a single -- static cuboid that we dynamically reset at each -- simulation pass (while not forgetting to set its initial -- velocity vector) : local relativeLinearVelocity={-beltVelocity,0,0} -- Reset the dynamic cuboid from the simulation -- (it will be removed and added again): sim.resetDynamicObject(forwarder) -- Compute the absolute velocity vector: local m=sim.getObjectMatrix(forwarder,-1) m[4]=0 -- Make sure the translation component is discarded m[8]=0 -- Make sure the translation component is discarded m[12]=0 -- Make sure the translation component is discarded local absoluteLinearVelocity=sim.multiplyVector(m,relativeLinearVelocity) -- Now set the initial velocity of the dynamic cuboid: sim.setObjectFloatParameter(forwarder,3000,absoluteLinearVelocity[1]) sim.setObjectFloatParameter(forwarder,3001,absoluteLinearVelocity[2]) sim.setObjectFloatParameter(forwarder,3002,absoluteLinearVelocity[3]) end
上述代码有效地完成了以下工作:在每个模拟通道中,修改路径的固有位置,以生成传送带的固有运动。 同时,在每个模拟过程中,都会动态重置(从动力学模拟中移除,然后再次直接将其直接添加到动力学模拟中)和初始速度集。 初始速度就是使躺在上面的其他物体移动的东西! 这比单独模拟每个传送带衬垫的计算强度要小得多。
为结束本教程,我们将保存刚刚创建的模型,以便它将显示在CoppeliaSim的模型浏览器中。 选择模型,然后单击[菜单栏–>文件–>模型另存为…]。 此时将显示一个对话框,允许调整模型缩略图。 一旦您对缩略图感到满意,单击OK并导航到CoppeliaSim的“Models/Equipment”文件夹并保存模型。
制作履带
在CoppeliaSim中,履带是通过沿着履带使用几个动态圆柱体来模拟的,以给人一种履带正在移动车辆的印象,而实际上是这些圆柱体在移动! 这意味着履带垫只是“眼花缭乱”,动态不启用(它们应该是静态的和无响应的)。 创建履带的方法实际上非常类似于使用方法B制作传送带。下图说明了CoppeliaSim中的履带概念:
本教程将尝试以一种简单的方式来说明如何基于ROS Melodic和Catkin build来启用CoppeliaSim ROS。
首先,您应该确保已学过官方ROS教程(至少是初学者部分),并且已安装Catkin工具。然后,我们假设您正在运行最新的Ubuntu,已安装ROS,并且已设置工作空间文件夹。在此还请参阅有关ROS安装的官方文档。
通过ROS接口(libsimExtROSInterface.so)支持CoppeliaSim中的常规ROS功能。 Linux发行版应包括已经在CoppeliaSim / compiledROSPlugins中编译的文件,但首先需要将其复制到CoppeliaSim /,否则将不会被加载。但是,根据系统的特性,您可能会遇到插件加载问题:请确保始终检查CoppeliaSim的终端窗口以获取有关插件加载操作的详细信息。启动CoppeliaSim时将加载插件。ROS插件只有在roscore正在运行的情况下才能成功加载和初始化。(roscore是ROS 主控程序)。另外,在运行CoppeliaSim之前,请确保获取ROS环境。
如果无法加载该插件,则应自己重新编译。它是开源的,可以根据需要进行任意修改,以支持特定功能或扩展其功能。如果需要支持特定的消息、服务等,请确保在重新编译之前编辑simExtROSInterface/meta/中的文件。有2个软件包:
controlTypeExamples/controlledViaRos.ttt
以上软件包应复制到您的catkin_ws / src文件夹中。确保ROS知道这些软件包,即可以使用以下命令切换到以上软件包文件夹:
$ roscd sim_ros_interface
$ roscd ros_bubble_rob
为了生成软件包,请打开catkin_ws文件夹并输入:
$ export COPPELIASIM_ROOT_DIR=~/path/to/coppeliaSim/folder
$ catkin build --cmake-args -DCMAKE_BUILD_TYPE=Release -DLIBPLUGIN_DIR=$COPPELIASIM_ROOT_DIR/programming/libPlugin
就这样,软件包应该已经生成并编译为可执行文件或库。 将创建的文件复制并粘贴到CoppeliaSim安装文件夹。 插件现在可以使用了!
现在打开一个终端,使用以下命令启动ROS主机:
$ roscore
打开另一个终端,移动到CoppeliaSim安装文件夹并启动CoppeliaSim。显示如下:
$ ./coppeliaSim.sh
...
Plugin 'ROSInterface': loading...
Plugin 'ROSInterface': load succeeded.
...
成功加载ROS接口后,检查可用节点将得到以下信息:
$ rosnode list
/rosout
/sim_ros_interface
在空的CoppeliaSim场景中,选择一个对象,然后使用[菜单栏->添加->关联的子脚本->非线程]将非线程的子脚本附加到该对象。 打开该脚本的脚本编辑器,并将内容替换为以下内容:
function subscriber_callback(msg)
-- This is the subscriber callback function
sim.addStatusbarMessage('subscriber receiver following Float32: '..msg.data)
end
function getTransformStamped(objHandle,name,relTo,relToName)
-- This function retrieves the stamped transform for a specific object
t=sim.getSystemTime()
p=sim.getObjectPosition(objHandle,relTo)
o=sim.getObjectQuaternion(objHandle,relTo)
return {
header={
stamp=t,
frame_id=relToName
},
child_frame_id=name,
transform={
translation={x=p[1],y=p[2],z=p[3]},
rotation={x=o[1],y=o[2],z=o[3],w=o[4]}
}
}
end
function sysCall_init()
-- The child script initialization
objectHandle=sim.getObjectAssociatedWithScript(sim.handle_self)
objectName=sim.getObjectName(objectHandle)
rosInterfacePresent=simROS
-- Prepare the float32 publisher and subscriber (we subscribe to the topic we advertise):
if rosInterfacePresent then
publisher=simROS.advertise('/simulationTime','std_msgs/Float32')
subscriber=simROS.subscribe('/simulationTime','std_msgs/Float32','subscriber_callback')
end
end
function sysCall_actuation()
-- Send an updated simulation time message, and send the transform of the object attached to this script:
if rosInterfacePresent then
simROS.publish(publisher,{data=sim.getSimulationTime()})
simROS.sendTransform(getTransformStamped(objectHandle,objectName,-1,'world'))
-- To send several transforms at once, use simROS.sendTransforms instead
end
end
function sysCall_cleanup()
-- Following not really needed in a simulation script (i.e. automatically shut down at simulation end):
if rosInterfacePresent then
simROS.shutdownPublisher(publisher)
simROS.shutdownSubscriber(subscriber)
end
end
上面的脚本将发布模拟时间,并同时订阅它。 它还将发布脚本附加到的对象的转换。 您应该可以使用以下内容查看仿真时间话题:
$ rostopic list
要查看消息内容,可以键入:
$ rostopic echo /simulationTime
现在,加载演示场景rosInterfaceTopicPublisherAndSubscriber.ttt,并运行模拟。 附加到Vision_sensor的子脚本中的代码将使发布者可以流式传输视觉传感器的图像,还可以使订阅者收听相同的流。 订阅者将读取的数据应用于被动视觉传感器,该被动视觉传感器仅用作数据容器。 因此,CoppeliaSim在监听相同数据的同时传输数据! 这是正在发生的事情:
尝试以下代码。 您还可以使用以下命令可视化CoppeliaSim流式传输的图像:
$ rosrun image_view image_view image:=/visionSensorData
如果您正在流传输更简单的数据,那么您还可以通过以下方式将其可视化:
$ rostopic echo /visionSensorData
现在停止模拟并加载演示场景controlTypeExamples/controlledViaRos.ttt,然后运行模拟。 机器人是简单化的,并且出于简化目的也以简单的方式表现。 它是通过ROS接口控制的:
附加到机器人的子脚本以非线程方式运行,它负责以下工作:
运行模拟时,复制并粘贴几次机器人。请注意,每个副本都是直接可操作且独立的。这是CoppeliaSim的众多优势之一。
现在停止模拟并打开一个新场景,然后将以下模型拖入其中:Models / tools / rosInterface_helper_tool.ttm。此模型由提供以下主题发布者和订阅者的单个定制脚本构成:
查看定制脚本的内容,该脚本可以针对各种目的进行完全定制。尝试从命令行生成主题消息,例如:
$ rostopic pub /startSimulation std_msgs/Bool true --once
$ rostopic pub /pauseSimulation std_msgs/Bool true --once
$ rostopic pub /stopSimulation std_msgs/Bool true --once
$ rostopic pub /enableSyncMode std_msgs/Bool true --once
$ rostopic pub /startSimulation std_msgs/Bool true --once
$ rostopic pub /triggerNextStep std_msgs/Bool true --once
$ rostopic pub /triggerNextStep std_msgs/Bool true --once
$ rostopic pub /triggerNextStep std_msgs/Bool true --once
$ rostopic pub /stopSimulation std_msgs/Bool true --once
为了显示当前的模拟时间,您可以输入:
$ rostopic echo /simulationTime
最后,确保查看一下CoppeliaSim中的远程API功能和BlueZero框架:与ROS相似,它允许远程执行功能,来回快速流数据,易于使用,重量轻且跨平台。 远程API功能可用于7种不同的语言。 在某些情况下,远程APi和BlueZero框架都是ROS的有趣替代品。
本教程将尝试以一种简单的方式来说明如何基于ROS 2 Dashing来启用CoppeliaSim ROS 2。
首先,您应确保至少已经学习过了ROS官方教程,至少是初学者部分。然后,我们假设您正在运行最新的Ubuntu,已安装ROS,并且已设置工作空间文件夹。在此还请参阅有关ROS 2安装的官方文档。
通过ROS接口(libsimExtROS2Interface.so)支持CoppeliaSim中的常规ROS功能。 Linux发行版应包括已经在CoppeliaSim / compiledROSPlugins中编译过的文件,但是首先需要将其复制到CoppeliaSim /中,否则将不会被加载。但是,您可能会遇到插件加载问题,具体取决于您的系统特性:请确保始终检查CoppeliaSim的终端窗口以获取有关插件加载操作的详细信息。启动CoppeliaSim时将加载插件。另外,在运行CoppeliaSim之前,请确保获取ROS环境。
如果无法加载该插件,则应自己重新编译。它是开源的,可以根据需要进行任意修改,以支持特定功能或扩展其功能。如果需要支持特定的消息、服务等,请确保在重新编译之前编辑simExtROSInterface / meta /中的文件。有2个软件包:
以上软件包应复制到ros2_ws / src文件夹中。
为了生成软件包,请打开ros2_ws文件夹并键入:
$ export COPPELIASIM_ROOT_DIR=~/path/to/coppeliaSim/folder
$ ulimit -s unlimited #otherwise compilation might freeze/crash
$ colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release -DLIBPLUGIN_DIR=$COPPELIASIM_ROOT_DIR/programming/libPlugin
就这样,软件包应该已经生成并编译为可执行文件或库。 将创建的文件复制并粘贴到CoppeliaSim安装文件夹。 插件现在可以使用了!
现在打开一个终端,移至CoppeliaSim安装文件夹并启动CoppeliaSim。 这是您应该拥有的(或类似的):
$ ./coppeliaSim.sh
...
Plugin 'ROS2Interface': loading...
Plugin 'ROS2Interface': load succeeded.
...
成功加载ROS2接口后,检查可用节点将得到以下信息:
$ ros2 node list
/sim_ros2_interface
在CoppeliaSim的空场景中,选择一个对象,然后使用[菜单栏->添加->关联的子脚本->非线程]将非线程的子脚本附加到该对象。 打开该脚本的脚本编辑器,并将内容替换为以下内容:
function subscriber_callback(msg)
-- This is the subscriber callback function
sim.addStatusbarMessage('subscriber receiver following Float32: '..msg.data)
end
function getTransformStamped(objHandle,name,relTo,relToName)
-- This function retrieves the stamped transform for a specific object
t=sim.getSystemTime()
p=sim.getObjectPosition(objHandle,relTo)
o=sim.getObjectQuaternion(objHandle,relTo)
return {
header={
stamp=t,
frame_id=relToName
},
child_frame_id=name,
transform={
translation={x=p[1],y=p[2],z=p[3]},
rotation={x=o[1],y=o[2],z=o[3],w=o[4]}
}
}
end
function sysCall_init()
-- The child script initialization
objectHandle=sim.getObjectAssociatedWithScript(sim.handle_self)
objectName=sim.getObjectName(objectHandle)
ros2InterfacePresent=simROS2
-- Prepare the float32 publisher and subscriber (we subscribe to the topic we advertise):
if ros2InterfacePresent then
publisher=simROS2.advertise('/simulationTime','std_msgs/Float32')
subscriber=simROS2.subscribe('/simulationTime','std_msgs/Float32','subscriber_callback')
end
end
function sysCall_actuation()
-- Send an updated simulation time message, and send the transform of the object attached to this script:
if ros2InterfacePresent then
simROS2.publish(publisher,{data=sim.getSimulationTime()})
simROS2.sendTransform(getTransformStamped(objectHandle,objectName,-1,'world'))
-- To send several transforms at once, use simROS2.sendTransforms instead
end
end
function sysCall_cleanup()
-- Following not really needed in a simulation script (i.e. automatically shut down at simulation end):
if rosInterfacePresent then
simROS.shutdownPublisher(publisher)
simROS.shutdownSubscriber(subscriber)
end
end
上面的脚本将发布仿真时间,并同时订阅它。 它还将发布脚本附加到的对象的转换。 您应该可以使用以下内容查看仿真时间话题:
$ ros2 topic list
要查看消息内容,可键入:
$ ros2 topic echo /simulationTime
现在,加载演示场景ros2InterfaceTopicPublisherAndSubscriber.ttt,并运行仿真。 附加到Vision_sensor的子脚本中的代码将使发布者可以流式传输视觉传感器的图像,还可以使订阅者收听相同的流。 订阅者将读取的数据应用于被动视觉传感器,该被动视觉传感器仅用作数据容器。 因此,CoppeliaSim在监听相同数据的同时传输数据! 这是正在发生的事情:
尝试以下代码。 您还可以使用以下命令可视化CoppeliaSim流式传输的图像:
$ ros2 run image_view image_view image:=/visionSensorData
如果您正在流传输更简单的数据,那么您还可以通过以下方式将其可视化:
$ ros2 topic echo /visionSensorData
现在停止模拟并加载演示场景controlTypeExamples/controlledViaRos2.ttt,然后运行模拟。 机器人是简单化的,并且出于简化目的也以简单的方式表现。 它是通过ROS2接口控制的:
附加到机器人的子脚本以非线程方式运行,它负责以下工作:
运行模拟时,复制并粘贴几次机器人。请注意,每个副本都是直接可操作且独立的。这是CoppeliaSim的众多优势之一。
现在停止模拟并打开一个新场景,然后将以下模型拖入其中:Models / tools / ros2Interface_helper_tool.ttm。此模型由提供以下主题发布者和订阅者的单个定制脚本构成:
查看定制脚本的内容,该脚本可以针对各种目的进行完全定制。尝试从命令行生成主题消息,例如:
$ ros2 topic pub /startSimulation std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /pauseSimulation std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /stopSimulation std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /enableSyncMode std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /startSimulation std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /triggerNextStep std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /triggerNextStep std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /triggerNextStep std_msgs/Bool '{data: true}' --once
$ ros2 topic pub /stopSimulation std_msgs/Bool '{data: true}' --once
为了显示当前的仿真时间,你可以键入:
$ ros2 topic echo /simulationTime
最后,确保查看一下CoppeliaSim中的远程API功能和BlueZero框架:与ROS相似,它允许远程执行功能,来回快速流数据,易于使用,重量轻且跨平台。 远程API功能可用于7种不同的语言。 在某些情况下,远程APi和BlueZero框架都是ROS的有趣替代品。
可以在此处找到CoppeliaSim的完整源代码(包括大多数插件源代码和其他)。虽然CoppeliaSim库(coppeliaSimLib)是GNU GPL许可的,但是几何插件(simExtGeometric)附带了特定的许可证。简而言之,未经明确许可,只有教育实体(学生、教师、教授、学校或大学)可以下载和使用该插件。在进行此操作之前,请确保您了解并遵守许可条件。
其他源代码项(例如,各种项目、插件、接口等)不在此处讨论。
CoppeliaSimLib和几何插件是Qt项目,需要在计算机上安装Qt。尝试遵守以下要求列表,以便在编译过程中最少遇到问题:
下载并安装CoppeliaSim Edu。然后将CoppeliaSimLib库源代码(以及可选的插件源代码)下载到CoppeliaSim Edu安装文件夹中。您应该具有以下文件夹结构:
与插件一起使用CoppeliaSim时,请确保使用同一发行版中的源代码/二进制文件,以避免不兼容。最后,在能够编译CoppeliaSim项目之前,您将必须在config.pri文件中调整各种路径。
CoppeliaSimLib编译为共享库。加载和运行该库的默认客户端应用程序是coppeliaSim或coppeliaSim.exe。您可以使用预编译的版本,也可以自己重新编译(请参阅以下项目文件:coppeliaSimClientApplication)。仅在以下情况下,库的加载操作才能成功:
简单的操作是将已编译的库复制到CoppeliaSim文件夹中,然后将确保CoppeliaSim应该启动并且不会抱怨缺少依赖项。
不要混用各种Qt版本或来自各种编译器的二进制文件,这一点非常重要。如果您的主要CoppeliaSimLib库是使用Qt X和编译器Y编译的,则所有与CoppeliaSim相关的插件也应该已经使用Qt X和编译器Y编译,否则,您将遇到奇怪的行为(库无法加载,突然崩溃等)。如果您的插件未使用任何Qt函数,则可以放宽此要求。