一步步製作下棋機器人之 coppeliasim進行Scara機械臂模擬與python控制

2023-04-06 06:00:43

稚暉君又釋出了新的機器人,很是強大。
在編寫時看到了稚暉君的招聘資訊,好想去試試啊!

小時候都有一個科幻夢,如今的職業也算與夢想有些沾邊了。但看到稚暉君這種閃著光芒的作品,還是很是羨慕。

以前就想做一個機械臂,實現遠端象棋對戰等功能,看到稚暉君的作品,更加心動了。心動不如行動,下面就一步一步模擬一個簡單的機器人,最終移植控制現實的機械臂,實現真正的下象棋,甚至能遠端象棋對戰。

說明

使用【FreeCAD軟體】繪製一個簡單的Scara機械臂,使用 【coppeliasim模擬軟體】模擬前面繪製的機械臂,使用python進行模擬控制,力求實現一個能夠移動棋子的模擬程式。後續目標是,將機械臂實物話,將模擬程式與實物實現通訊控制,最終實現一個移動象棋的機械臂。然後在它的基礎上新增視覺和象棋對戰演演算法,實現能夠自主象棋對戰(淘寶上已經有象棋對戰機械臂了)。當然,後面幾項需要花時間實現,現在先實現模擬控制。

學習機械臂的操作需要學習正逆運動學模型及解算、座標系等知識,本文由於涉及的較淺,暫不做說明,後續深入使用過程中涉及到時,再做說明。

簡介

FreeCAD軟體

軟體簡介

  • 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模擬軟體

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機械臂

  • SCARA是Selective Compliance Assembly Robot Arm的縮寫,意思是一種應用於裝配作業的機器人手臂。它有3個旋轉關節,最適用於平面定位。比如自帶的【MTB】機械臂:

  • 選擇Scara機械臂,一方面是因為簡單好實現,另一方面是公司之前有設計過Scara機械臂,我層深度參與過底層程式的編寫。但是由於種種原因暫時擱置,後續會繼續開發。另外目前淘寶已經有了下象棋的機器人(可自行搜尋下象棋機器人),也是類似Scara的設計。由於結構簡單,對於電機的要求也不是很高,所以實現一個Scara機械臂是入門機械臂的優先選擇。

  • 對於像稚暉君那種機械臂,我相信,在完成Scara機械臂的製作後,實現起來也會更加容易。雖然不推薦重複造輪子,但是很多事情只有從底層一步一步趟過去,才能有到達頂峰的實力。

開始建模

  • 由於是用於測試的,所以我認為,只要有個簡單的樣子,就能進行最簡單的模擬。後續完成基本模擬後,開始3D列印實裝時進行細化建模。
  • 所以此處只建立了四個簡單的模型,臂長等引數也沒有細緻優化。後續寫程式碼過程中根據需要進行優化。模型如下:
    • 一個用於支撐的支撐座:

    • 一個上下移動的短臂:

    • 第一長臂:

    • 第二短臂:

    • 合起來就是上面的簡化模型:

  • 後續會在短臂末端增加一個夾爪用於夾取物體。
  • 建模完成後,在左側工程樹中選中要匯出的零件實體,選擇【檔案->匯出】,匯出為.stl格式(STL MESH)。四個部分恩都要匯出。

匯入到Compliance

  • 教學見【機器人系統設計-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

本文水平有限,內容很多詞語由於知識水平問題不嚴謹或很離譜,但主要作為記錄作用,希望以後的自己和路過的大神對必要的錯誤提出批評與指點,對可笑的錯誤請指出來,我會改正的。

另外,轉載使用請註明作者和出處,不要刪除檔案中的關於作者的註釋。

隨夢,隨心,隨願,恆執念,為夢執戰,執戰蒼天! ------------------執念執戰