一步步制作下棋机器人之 coppeliasim进行Scara机械臂仿真与python控制
阅读原文时间:2023年07月09日阅读:4

稚晖君又发布了新的机器人,很是强大。

在编写时看到了稚晖君的招聘信息,好想去试试啊!

小时候都有一个科幻梦,如今的职业也算与梦想有些沾边了。但看到稚晖君这种闪着光芒的作品,还是很是羡慕。

以前就想做一个机械臂,实现远程象棋对战等功能,看到稚晖君的作品,更加心动了。心动不如行动,下面就一步一步仿真一个简单的机器人,最终移植控制现实的机械臂,实现真正的下象棋,甚至能远程象棋对战。

说明

使用【FreeCAD软件】绘制一个简单的Scara机械臂,使用 【coppeliasim仿真软件】仿真前面绘制的机械臂,使用python进行仿真控制,力求实现一个能够移动棋子的仿真程序。后续目标是,将机械臂实物话,将仿真程序与实物实现通讯控制,最终实现一个移动象棋的机械臂。然后在它的基础上添加视觉和象棋对战算法,实现能够自主象棋对战(淘宝上已经有象棋对战机械臂了)。当然,后面几项需要花时间实现,现在先实现仿真控制。

学习机械臂的操作需要学习正逆运动学模型及解算、坐标系等知识,本文由于涉及的较浅,暂不做说明,后续深入使用过程中涉及到时,再做说明。

简介

软件简介

  • FreeCAD是一个基于OpenCASCADE的开源CAD/CAE工具。 OpenCASCADE是一套开源的CAD/CAM/CAE几何模型核心,来自法国Matra Datavision公司,是著名的CAD软件EUCLID的开发平台。

  • 我个人的感觉是,FreeCAD界面操作、功能特性、实时画面和渲染画面等还是不如犀牛、3DMAX等软件的,但是上手构建简单的三维模型很快,软件体积小,免费,学习成本也很低,对于非专业人士进行简单的建模很方便,建模完成后如果需要可以去犀牛等软件进行进一步精细处理。而且进阶学习可以使用python进行操作,由于是开源的,你甚至可以集成在自己的python程序中。

  • FreeCAD 官网

  • FreeCAD 官网介绍中文版

  • FreeCAD Git网站

  • 推荐B站UP 灰大柱 的额视频教程 freecad基础教程全套-灰大柱 ,他的软件版本较老,但是基本功能一致,跟着学习完成后简单使用没有问题。

FreeCAD进行机械臂仿真

  • 其实FreeCAD也是可以进行机器仿真的,它有很多插件,而且自带 Robot 插件,就可以进行仿真。比如可以直接打开起始页的机械臂的例子:

  • 然后在【工作台】中选择 【Robot】工作台:

  • 用【shift】同时选中左侧的轨迹

  • 然后点击【任务】进入任务界面,就会出现机器人控制台:

  • 此时点击【模拟轨迹】,进入运动模拟界面,点击左侧的运行右箭头就可以进行轨迹运动了(如果有轨迹的话),机械臂就会自己动起来:

  • 也可以自己进行示教运动轨迹录入,具体方法可以参考官网说明,也可以查看B站的视频教程:【FreeCAD让机器人动起来】,经过简单的示教录入,就能实现机械臂运动仿真。

  • 对于FreeCAD的机械臂仿真,我认为简单玩一玩还是可以的,但功能肯定是没有【coppeliasim仿真软件】这种专业软件完善的。所以此处不做深入学习。

coppeliasim仿真软件简介

  • 瑞士CoppeliaRobotics公司的CoppeliaSim软件是一款基于分布式控制架构,具有集成开发环境的基于物理引擎的动力学机器人模拟器。CoppeliaSim软件曾叫Vrep(VirtualRobotExperimentationPlatform虚拟机器人实验平台),于2019年底正式更名为CoppeliaSim。CoppeliaSim与Vrep完全兼容,并丰富的功能,特别是增加了对ROS2的支持。

  • CoppeliaSim 原生支持LUA脚本语言控制和python语言控制,默认为LUA语言。且文档主要以LUA语言为主。也支持C++等语言控制。由于使用python语言,可以很方便的调用一些库,包括图像处理等方面的库,所以此处选用python语言进行控制。感兴趣的可以学习lua语言进行控制。

  • lua语言:Lua 是一个诞生于巴西的小巧的脚本语言,纯C语言编写,体积小巧,速度高效,其执行速度较python更快,方便移植,可裸机运行于小资源的单片机上。而python只有第三方的microPython和pikapython才能运行在小资源的单片机上,兼容性不如lua。但lua的库没有python丰富,因为它更多的被用作胶水语言,所以在程序界没有python普及。

  • 菜鸟教程 lua

  • lua Git

  • lua语法简单,学过C和python的很快就能入手lua。但是后面不用它,所以不多做介绍,感兴趣的可以自行了解。

  • coppeliasim官网

  • coppeliasim 有几个版本,推荐下载 EDU 教育版。player版本的功能限制太多,模型也不全。

  • coppeliasim英文文档

  • coppeliasim文档翻译

  • 关于教程,肯定是外网的教程较多,可自行查找,此处不做推荐。内网的很多较乱,但跟着走也是可以的。

  • 想快速入门的可以看 B站UP的视频【听尘listener

  • 详细视频教程可以看 B站搬运的【Vrep / CoppeliaSim 教程(4.3版本)】(没看过,不知道质量如何)

  • 文本教程可以看 【CoppeliaSim(原V-REP)新手上路】 ,跟着走一遍就能搭建基本的python控制程序。

安装完后,可以拖拽即个示例模型进行测试。很多机械臂都是现实中存在的,比如和公司的那台大机械臂,这里面就有一款应该是同一家的很相似的产品【FrankaEmikaPanda】,拖拽后点击【start】就可以仿真自带的程序:

当然,还提供了很多带程序的模型,比如小车等,还有不带程序的模型,比如墙体、椅子等物品,可用于搭建碰撞环境。

  • 示例中是有一款Scara的【MTB】机械臂的,可以自行运行查看。

    学习机械臂控制还需要了解和学习 正逆运动学求解 、坐标系、编程甚至电子电路、建模等知识,此处不做讲解,请根据实际需要进行学习。

绘制简单的机械臂模型

  • SCARA是Selective Compliance Assembly Robot Arm的缩写,意思是一种应用于装配作业的机器人手臂。它有3个旋转关节,最适用于平面定位。比如自带的【MTB】机械臂:

  • 选择Scara机械臂,一方面是因为简单好实现,另一方面是公司之前有设计过Scara机械臂,我层深度参与过底层程序的编写。但是由于种种原因暂时搁置,后续会继续开发。另外目前淘宝已经有了下象棋的机器人(可自行搜索下象棋机器人),也是类似Scara的设计。由于结构简单,对于电机的要求也不是很高,所以实现一个Scara机械臂是入门机械臂的优先选择。

  • 对于像稚晖君那种机械臂,我相信,在完成Scara机械臂的制作后,实现起来也会更加容易。虽然不推荐重复造轮子,但是很多事情只有从底层一步一步趟过去,才能有到达顶峰的实力。

  • 由于是用于测试的,所以我认为,只要有个简单的样子,就能进行最简单的仿真。后续完成基本仿真后,开始3D打印实装时进行细化建模。

  • 所以此处只建立了四个简单的模型,臂长等参数也没有细致优化。后续写代码过程中根据需要进行优化。模型如下:

    • 一个用于支撑的支撑座:

    • 一个上下移动的短臂:

    • 第一长臂:

    • 第二短臂:

    • 合起来就是上面的简化模型:

  • 后续会在短臂末端增加一个夹爪用于夹取物体。

  • 建模完成后,在左侧工程树中选中要导出的零件实体,选择【文件->导出】,导出为.stl格式(STL MESH)。四个部分恩都要导出。

  • 教程见【机器人系统设计-coppeliasim仿真

  • 在【coppeliasim】中 选择【File->Import->Mesh】导入刚才用FreeCAD导出的几个零件。

  • 在跳出的界面要将缩放比例填为0.001,因为绘图时使用的单位为mm,而Compliance 默认的为m,然后点击import就能导入了:

  • 导入后是没有先后逻辑的。我们先添加几个关节。分析机械臂,可以得出我们需要两个转动关节,一个移动关节。在【add->Joint】中分别选择 Revolute(旋转关节)、Prismatic(移动关节)进行添加。

  • 然后对各个部件进行命名:

关节

名称

底座

ShapeBase

固定短臂

ShapeArm1

第一长臂

ShapeArm2

第二短臂

ShapeArm3

移动关节

BaseJoint

转动关节1

ArmJoint1

转动关节2

ArmJoint2

  • 移动几个关节的位置到指定地方,使其符合移动逻辑:

  • 在左侧的树状图中按照逻辑顺序拖拽,使其符合主从关系:

  • 简化模型:

    • 选中一个模型,然后 【Edit->Decimate selected shape】,按照默认,点击OK即可:

    • 对每个模型都简化一下,简化完成的会变色:

  • 对【ShapeBase】添加程序文件,选择【ADD->Associated child script->Non threated->lua】,添加一个无线程的lua文件:

  • 打开程序文件,在第一行添加:

    simRemoteApi.start(19999)

因为python控制coppeliasim仿真软件实际上是用的远程端口。

剩余的代码可以自行理解其意思,然后对其编程操作。我们使用外部python编程,对程序不必关心,默认即可。

  • 此时我们可以点击 【Start】 按钮进行仿真,并没有什么现象,因为没有控制代码。

  • 如果想有现象,可以打开动力学属性,方法如下:对选中的机械臂,点击左侧的【scene object properties】进入配置页面,点击最下面的【Show dynamic properties dialog】,将【Body is respondable】和【Body is dynamic】勾选上。分别对三个臂进行同样处理后,再点击运行,就可以看到,机械臂一起掉到了地上,且小臂会由于重力作用乱摆。

  • 我们简单实用程序测试的话,不需要动力学属性模拟,所以都取消勾选就行了,后续深入学习时再根据需要打开。

  • 下面,打开python环境,进行简单的连接测试。此处我使用的是anaconda环境的Spyder IDE。

  • 使用python控制机械臂,需要使用官方提供好的初始化脚本和dll文件。首先,打开【coppeliasim】的安装位置,在【CoppeliaRobotics\CoppeliaSimEdu\programming】文件夹下都是各种控制环境的支持包,在本文件夹下的【legacyRemoteApi\remoteApiBindings\python\python】文件夹下,找到 sim.py和simConst.py,以及 simpleTest.py,在【legacyRemoteApi\remoteApiBindings\lib\lib\Windows】下找到remoteApi.dll(其他环境请自行按要求配置),这四个文件,将他们复制到自己的工作区文件夹,然后在Spyder中打开simpleTest.py。

  • 注意:在运行python程序前,需要先在【 coppeliasim】中点击【start】运行 ,在coppeliasim中开启运行后,开始运行python程序,如果python的控制台输出鼠标的X轴坐标,说明一切正常,python控制成功。

  • 新建一个.py文件,开始编写自己的代码。

  • 写之前,先构思一下代码需求,再设定一下代码框架。

  • 因为是测试代码,主要用于实现机械臂的移动旋转控制,上下运动控制,这两个基本功能,以及理解基本的控制代码和逻辑。如果时间足够,后续拓展实现(X,Y)坐标点自行运动,需要添加界面控制环境,预计使用pyqt环境。

  • 基本框架就是将运动控制部分整合为一个类,方便以后拓展串口通讯、UI控制等功能。

机械臂基本的操作类如下:

# -*- coding: utf-8 -*-
"""
Created on Wed Apr  5 13:11:11 2023

@author: ZNZZ
"""
# Make sure to have the server side running in CoppeliaSim:
# in a child script of a CoppeliaSim scene, add following command
# to be executed just once, at simulation start:
#
# simRemoteApi.start(19999)
#
# then start simulation, and run this program.
#
# IMPORTANT: for each successful call to simxStart, there
# should be a corresponding call to simxFinish at the end!

try:
    import sim
except:
    print ('--------------------------------------------------------------')
    print ('"sim.py" could not be imported. This means very probably that')
    print ('either "sim.py" or the remoteApi library could not be found.')
    print ('Make sure both are in the same folder as this file,')
    print ('or appropriately adjust the file "sim.py"')
    print ('--------------------------------------------------------------')
    print ('')

import time
import math 

class MyArmBasicClass():

    def __init__(self):
        print('MyArmTest Program started')
        self.clientID = 0
        self.Handle={  #字典,用于保存各个模块的句柄,方便拓展和查找;key值要和CoppeliaSim中的模块命名一致
            "ShapeBase":0,
            "ShapArm1":0,
            "ShapArm2":0,
            "ShapArm3":0,
            "BaseJoint":0,
            "ArmJoint1":0,
            "ArmJoint2":0,
            }
        self.HandleOrder=(   #元组有固定得的顺序,所以用元组作为顺序记录,要和上面的Handle的一致
            "ShapeBase",
            "ShapArm1",
            "ShapArm2",
            "ShapArm3",
            "BaseJoint",
            "ArmJoint1",
            "ArmJoint2",
            )

    def ConnectedStart(self):
        '''
        连接初始化。
        需要首先在 CoppeliaSimEdu 中点击运行,再运行本python程序,才能正确连接。
        返回-1表示连接失败

        Returns
        -------
        TYPE
            DESCRIPTION.

        '''
        sim.simxFinish(-1) # just in case, close all opened connections
        self.clientID=sim.simxStart('127.0.0.1',19999,True,True,5000,5) # Connect to CoppeliaSim
        if self.clientID!=-1:
            print ('Connected to remote API server')
            return 0
        else:
            print ('Connected faile,please check!')
            return -1

    def GetArmHandle(self):
        '''
        获取各个模块的句柄,用于操控
        有些模块的句柄获取不到

        '''
        for i in  self.HandleOrder:
            self.Handle[i]  = sim.simxGetObjectHandle(self.clientID, i, sim.simx_opmode_blocking) #获取句柄,返回两个值,一个是ret,用于判断是否获取成功,一个是obj,表示句柄号,两个值以元组的方式存到Handle中
            print(self.Handle[i][0])
            if self.Handle[i][0] != sim.simx_return_ok:
                print("Get "+i+" Handle Error!!")
            else:
                print("Get "+i+" Handle OK!!")
                   #实测可以看到,只获取了 底座和三个关节的句柄,所以能操控底座的位置,能操控三个关节的角度,但是不能操作三个臂

    def GetShapeBasePosition(self):
        '''
        获取底座的位置

        '''
        if self.clientID!=-1:
            if self.Handle[self.HandleOrder[0]][0] != sim.simx_return_ok:
                print("Get "+self.HandleOrder[0]+" Handle Error!!")
            else:
                ret, arr = sim.simxGetObjectPosition(self.clientID, self.Handle[self.HandleOrder[0]][1], -1, sim.simx_opmode_blocking)
                print(ret,arr)
                return ret, arr
        else:
            print("Something Error!!")

    def SetShapeBasePosition(self,X,Y,Z):
        '''
        设置底座的位置

        Parameters
        ----------
        (X,Y,Z) : TYPE
            DESCRIPTION:目标坐标(世界坐标系)

        Returns
        -------
        None.

        '''
        if self.clientID!=-1:
            if self.Handle[self.HandleOrder[0]][0] != sim.simx_return_ok:
                print("Get "+self.HandleOrder[0]+" Handle Error!!")
            else:
                sim.simxSetObjectPosition(self.clientID, self.Handle[self.HandleOrder[0]][1],-1,(X,Y,Z), sim.simx_opmode_blocking)
                print("Set ShapeBase Pos to X:"+str(X)+" Y:"+str(Y)+"  Z:"+str(Z))

        else:
            print("Something Error in SetShapeBasePosition!!")

    def GetJointAngle(self,num):
        '''
        获取旋转关节的角度

        Parameters
        ----------
        num : TYPE:控制哪个joint关节
            DESCRIPTION:可以输入 0 1 2  分分别表示 ShapeBase ArmJoint1 ArmJoint2
                        也可以直接输入字符串 ShapeBase ArmJoint1 ArmJoint2 

        Returns
        -------
        None.

        '''
        if self.clientID!=-1:
            if self.Handle[self.HandleOrder[0]][0] != sim.simx_return_ok:
                print("Get "+self.HandleOrder[0]+" Handle Error!!")
            else:
                if str(type(num)) == "<class 'int'>":  #先判断输入的num类型
                    if num==1:
                        targetObj_Revolute_joint = "ArmJoint1"
                    elif num == 2:
                        targetObj_Revolute_joint = "ArmJoint2"
                    elif num == 0:
                        targetObj_Revolute_joint = "ShapeBase"
                    else:
                        print("Joint num Error !!")
                        return
                elif str(type(num)) == "<class 'str'>":
                    targetObj_Revolute_joint = num

                else:
                    print("Joint num type Erroe,Pleace give 1 or 2 or stringName")

                position = sim.simxGetJointPosition(self.clientID, self.Handle[targetObj_Revolute_joint][1], sim.simx_opmode_blocking)

                print("Joint "+targetObj_Revolute_joint + " Angle is "+str(position))
                return position

        else:
            print("Something Error in GetJointAngle!!")

    def SetJointAngle(self,num,angle):
        '''
        设置关节角度/位置
        对于旋转关节,是设置角度值,内部需要转换为弧度;对于移动关节,我还没搞明白其单位,推测是米,所以mm需要除以1000

        Parameters
        ----------
        num : TYPE:控制哪个joint关节
            DESCRIPTION:可以输入 0 1 2  分分别表示 ShapeBase ArmJoint1 ArmJoint2
                        也可以直接输入字符串 ShapeBase ArmJoint1 ArmJoint2
        angle : TYPE  对于  ArmJoint1 ArmJoint2 ,为旋转得到角度值,对于ShapeBase,就是拉伸,暂时没注意具体拉伸多少
            DESCRIPTION.

        Returns
        -------
        None.

        '''
        if self.clientID!=-1:
            if self.Handle[self.HandleOrder[0]][0] != sim.simx_return_ok:
                print("Get "+self.HandleOrder[0]+" Handle Error!!")
            else:
                if str(type(num)) == "<class 'int'>":  #先判断输入的num类型
                    if num==1:
                        targetObj_Revolute_joint = "ArmJoint1"
                    elif num == 2:
                        targetObj_Revolute_joint = "ArmJoint2"
                    elif num == 0:
                        targetObj_Revolute_joint = "ShapeBase"                        

                    else:
                        print("Joint num Error !!")
                        return -1
                elif str(type(num)) == "<class 'str'>":
                    targetObj_Revolute_joint = num

                else:
                    print("Joint num type Erroe,Pleace give 1 or 2 or stringName")
                    return -1

                if  targetObj_Revolute_joint ==    "ArmJoint1" or  targetObj_Revolute_joint ==    "ArmJoint2":
                    setangle = angle*math.pi/90#角度要转为弧度,但是弧度计算不是 A*π/180 吗,此处90才是正常的?
                else:
                    #ShapeBase 关节不是角度,是运行,目前还没弄清数值与实际运动的关系
                    setangle = angle

                sim.simxSetJointPosition(self.clientID, self.Handle[targetObj_Revolute_joint][1], setangle, sim.simx_opmode_blocking)
                print("Set " + targetObj_Revolute_joint + "Angle to "+str(angle))

        else:
            print("Something Error in GetJointAngle!!")    

    def ConnectedStop(self):
        '''
        断开操控连接

        Returns
        -------
        None.

        '''
        if self.clientID != -1:

            # Before closing the connection to CoppeliaSim, make sure that the last command sent out had time to arrive. You can guarantee this with (for example):
            sim.simxGetPingTime(self.clientID)

            sim.simxStopSimulation(self.clientID, sim.simx_opmode_oneshot)

            # Now close the connection to CoppeliaSim:
            sim.simxFinish(self.clientID)

            print("Connecte Stoped!!")

        else:
            print("Pleace check clientID !")
  • 整体流程如下:

    • 远程连接到软件端口,并判断是否连接成功,成功后会返回正确的 clientID ,这个ID需要全局使用。即 ConnectedStart 函数的内容
    • 连接成功后就需要可控部件的句柄了,用于操控。即 GetArmHandle 函数的内容。此处我获取了所有部件的句柄,运行时可以看到,有些部件是获取不到的,也就没办法操控。而我们需要操控的部件,都是可以获取到的,比如底座的坐标位置,可获取可设置;三个关节的角度或拉伸值,可获取可控制,而对于机械臂零件,则是不能获取也不能控制。 需要注意区分不同关节的意思,虽然他们用同一个函数就能操作。
    • 获取到句柄后,就能对其进行获取信息或是控制了。

下面,写一个简单的控制函数,实现三个关节的循环控制:

def main():
    '''
    简易机械臂控制测试

    '''
    MyArmClass = MyArmBasicClass()
    ret = MyArmClass.ConnectedStart()
    if ret == 0: #连接成功
        MyArmClass.GetArmHandle()
        MyArmClass.GetShapeBasePosition()#获取底座坐标
        position1 = MyArmClass.GetJointAngle("ArmJoint1")  #获取三个关节的角度/位置
        position2 = MyArmClass.GetJointAngle("ArmJoint2")
        position1 = MyArmClass.GetJointAngle("BaseJoint")

        movedir = 0
        movecount = 0
        movestep = 5    #步进量,可用于调速
        moveangle = 90 #目标角度

        #让机械臂循环流畅动起来

        while(True):
            if movedir == 0:
               movecount = movecount-movestep
               if movecount < -moveangle*movestep:
                   movedir = 1

            elif movedir == 1:
                movecount = movecount+movestep
                if movecount > moveangle*movestep:
                    movedir = 0
            MyArmClass.SetJointAngle("ArmJoint1",movecount/10)
            MyArmClass.SetJointAngle("ArmJoint2",movecount/10)
            MyArmClass.SetJointAngle("BaseJoint",movecount/5000) #可以看到机械臂在上下运动

        MyArmClass.ConnectedStop() #若是需要断掉,请主动调用
    else:
        print("Connected Error!! Pleace check and retry!!")
        print("需要先在  coppeliasim 软件中开启仿真,再开始本python才能正确远程连接!!")

if __name__ == '__main__':
     #multiprocessing.freeze_support()
    main()
  • 以上测试代码简单实现了机械臂的长臂短臂的循环±90度旋转运动和上下循环运动。

总结

  • 以上,我们实现了对机械臂的简单控制,重点是理清了如何用程序控制一个自己建模设计的物体。

  • 由于时间关系,没有实现更进一步的操作逻辑,比如使用正逆运动学解算实现坐标点与机械臂运行的匹配,实现给定三维空间坐标点,机械臂就能移动过去。

  • 实际上,以前在公司做机械臂时,解算这一步已经实现了。当时还写了个简单的模拟程序,就是能根据坐标点连续移动机械臂到指定角度,实现末端的坐标点位移:

后续会进一步实现运动学解算、象棋夹起放置等步骤,还会简单实现一个对应的硬件,以实现实际控制。

以上完整的资料都放在【ChessRobot_PythonControlCoppeliasimModel

本文水平有限,内容很多词语由于知识水平问题不严谨或很离谱,但主要作为记录作用,希望以后的自己和路过的大神对必要的错误提出批评与指点,对可笑的错误请指出来,我会改正的。

另外,转载使用请注明作者和出处,不要删除文档中的关于作者的注释。

随梦,随心,随愿,恒执念,为梦执战,执战苍天! ------------------执念执战