《機器人操做系統(ROS)淺析》肖軍浩譯

PDF下載地址:node

連接:https://pan.baidu.com/s/1l_FAbn_g04q-Z0lKSxx8tw
提取碼:8lbp git

注:英文原文爲《A Gentle Introduction to ROS》 【美】Jason M. O'Kane著shell

1、緒論編程

2、入門概述bash

1. 安裝:參考 http://wiki.ros.org/cn服務器

(1)sudo rosdep init  初始化rosdep,rosdep用於檢查和安裝軟件包的依賴網絡

(2)rosdep update  初始化rosdep,在根目錄下保存一些文件,文件夾名爲.rosapp

(3)source /opt/ros/ros版本/setup.bashless

  • 設置ROS_PACKAGE_PATH等環境變量,ROS根據這些環境變量來定位文件  // 輸入 export | grep ROS 判斷是否配置完成
  • setup.bash還定義了一些ros系統的bash函數,如roscd和rosls  //這些函數定義在rosbash軟件包

(3)測試ROS是否正確安裝編程語言

  • roscore
  • rosrun turtlesim turtlesim_node
  • rosrun turtlesim turtle_teleop_key

注:三個終端分別執行指令

2. 經常使用術語和命令

(1)ROS功能包/軟件包:一組用於實現特定功能的相關文件的集合,包括可執行文件和其餘支持文件  //任何能找到且包含package.xml文件的目錄

注:使用catkin編譯生成的可執行文件放在外部標準目錄devel中(源碼外編譯

(2)節點node:ROS程序的運行實例 running instance

(3)rosrun命令:啓動ROS節點  rosrun  功能包名  可執行文件名  //節點名不必定與可執行文件名相同,可使用 __name:=節點名 顯式設置節點名稱

  • rosrun本質上是一個shell腳本,能夠根據ROS的文件組織結構自動找到相應可執行文件  //能夠直接輸入可執行文件的路徑啓動節點(等價的普通程序執行方式
  • 經過節點管理器master註冊成爲ROS節點發生在程序的源碼內部,而不是經過rosrun命令

注:節點名稱必須惟一,不可多個節點擁有相同名稱

(4)消息傳遞機制:節點管理器負責確保發佈節點和訂閱節點能找到對方,隨後消息從發佈節點直接傳遞到訂閱節點,不通過節點管理器轉交

注:節點只管發,無論收,有助於減小節點之間的耦合度;話題可共享,話題和消息的通訊機制是多對多的;服務是一對一的通訊機制

(5)rosmsg show 消息類型名  //輸出消息類型的詳細信息

(6)rostopic pub 話題名 消息類型 消息內容  //命令行手動發佈消息

注:消息內容能夠按照rosmsg show命令的顯示結果進行輸入,也能夠以YAML字典的形式輸入顯式指明結構域中變量名和值的映射關係)

(7)每一個消息類型均屬於一個特定的包,如turtlesim/Color屬於turtlesim包  //消息類型斜槓/前的是包名,斜槓/後是類型名

注:把包名包含在消息類型中的目的

  • 避免命名衝突  //  geometry_msgs/Pose 和 turtlesim/Pose
  • 在用到其餘包的消息類型時,更容易代表包之間的依賴關係
  • 有助於理解消息類型的含義

3、編寫ROS程序

1. catkin編譯系統試圖一次性編譯同一個工做區中的全部功能包

2. 建立功能包:catkin_create_pkg 包名  //包名只容許使用小寫字母、數字和下劃線,且首字符必須爲字母

3. 清單文件package.xml中聲明的依賴庫並不會在編譯過程當中進行檢查,可是會在將包發佈給他人時,他人會由於沒有安裝依賴庫而編譯報錯

4. CMakeLists.txt中的${catkin_LIBRARIES}變量由find_package(catkin REQUIRED COMPONENTS ...)語句定義

5. source devel/setup.bash  //專門爲當前的工做區進行環境變量設置

6. 每一個話題都有一個消息類型,而每一個消息類型都有相應的C++頭文件  //包含消息類型頭文件  #include<包名/類型名.h>  如#include<geometry_msgs/Twist.h>

注:頭文件定義了一個與消息類型對應的C++類,且該類定義在以包名命名的域名空間中,如geometry_msgs::Twist類

7. ros::NodeHandle類維護一個引用計數,僅在第一個NodeHandle對象建立時纔會在節點管理器註冊新的節點  //使用標準的roscpp接口,在單個程序中沒法運行多個節點

8. 建立發佈者ros::Publisher屬於耗時操做,應該每一個話題建立一個發佈者,在程序執行過程當中一直使用相應發佈者  //發佈者建立不要放在循環內,避免爲每條消息生成新的發佈者

9. ros::ok()返回false條件:

  • 使用rosnode kill命令終止節點
  • 使用Ctrl+C發送終止信號  //須要採用rosnode cleanup從節點管理器的列表中刪除節點
  • 調用ros::shutdown()  //在代碼中顯式代表節點工做已完成
  • 以相同節點名啓動新的節點  //新的節點將正常運行,而舊節點會被終止

10. 訂閱者回調函數:void function_name(const package_name::type_name &msg){ ... }

  • 調用回調函數是ROS執行,返回值也交給ROS,用戶程序沒法得到返回值,所以設置爲void
  • 回調函數指針爲 &function_name,如程序中的 &poseMessageReceived  // 取地址符&是可選的,但建議使用,編譯器會根據函數名後面是否有括號,自動判斷是指針仍是函數調用 

11. 顯式調用 ros::spin()或ros::spinonce()讓ROS執行回調函數

4、日誌消息  //ROS使用log4cxx庫實現日誌功能

1. ROS日誌系統的核心思想:使程序生成一些簡短的文本字符流,即日誌消息

2. 嚴重級別(遞增):DEBUG、INFO、WARN、ERROR、FATAL

3. 產生日誌消息的基本C++宏

  • ROS_DEBUG_STREAM(message)
  • ROS_INFO_STREAM(message)
  • ROS_WARN_STREAM(message)
  • ROS_ERROR_STREAM(message)
  • ROS_FATAL_STREAM(message)

注:日誌系統是面向行的,調用任意宏會生成完整的一行日誌消息  //無需使用std::endl

4. 日誌消息的輸出  

(1)控制檯

  • DEBUG和INFO消息被送至標準輸出,WARN、ERROR和FATAL消息被送至標準錯誤
  • 設置ROSCONSOLE_FORMAT環境變量調整日誌消息打印到控制檯的格式
  • roslaunch工具默認不會將節點的標準輸出和標準錯誤導入至本身的輸入流,需顯式使用 output="screen"屬性或者--screen參數

(2)/rosout 話題:消息類型爲rosgraph_msgs/Log

  • 包含系統中全部節點的日誌消息
  • 查看消息內容:rostopic echo /rosout 或者 rqt_console

  • rqt_console節點訂閱/rosout_agg話題  //後綴_agg表示消息是通過rosout節點聚合的結果
  • /rosout話題發佈的消息都經過rosout節點輸出到/rosout_agg話題
  • rosout是惟一一個訂閱/rosout話題的節點,也是/rosout_agg話題惟一的發佈者  //減小調試代價

注:rosout即表示話題,也表示節點

(3)日誌文件:做爲/rosout話題回調函數的一部分,由rosout節點生成

  • 文件名相似於:~/.ros/log/run_id/rosout.log  //純文本文件,用less、head、tail等命令查看
  • 查看運行標識碼run_id  //節點管理器開始運行時基於MAC地址和當前時間生成
  1. 查看roscore的輸出結果:setting /run_id to run_id
  2. 向節點管理器查詢:rosparam get /run_id  //run_id存放在參數服務器中

檢查日誌文件大小:rosclean check

刪除日誌文件:rosclean purge

5. 設置日誌級別

(1)經過命令行設置:rosservice call /node-name /set_logger_level ros.包名 level  // level參數包括DEBUG、INFO、WARN、ERROR、FATAL

注:參數ros.包名指按期望配置的日誌記錄器logger名稱

(2)經過圖形界面設置:rqt_logger_level

(3)經過C++代碼設置:調用ROS實現日誌功能的log4cxx提供的接口  #include<log4cxx/logger.h>

5、計算圖源命名

1. 計算圖源graph resource:節點、話題、服務和參數的統稱,由短字符串表示的計算圖源名稱進行標識

2. 全局名稱:/turtle1/cmd_vel  //優勢:任何地方均可以使用;缺點:須要完整列出其所屬的命名空間

  • 由前斜槓"/"做爲首字符
  • 由斜槓分開一系列命名空間,每一個斜槓表明一級命名空間,如turtle1命名空間
  • 命名空間用於將相關的計算圖源歸類在一塊兒,ROS容許多層命名空間嵌套
  • 最末爲基本名稱base name,如cmd_vel

3. 相對名稱  //利用ROS提供的默認命名空間

(1)典型特徵:缺乏全局名稱帶有的前斜槓"/"

(2)解析相對名稱:將當前的默認命名空間名稱加在相對名稱以前,生成全局名稱,如 /turtle1 + cmd_vel => /turtle1/cmd_vel

(3)設置默認命名空間:單獨爲每一個節點設置  //不設置,則使用全局命名空間"/"做爲默認命名空間

  • 在啓動文件中使用命名空間ns屬性
  • 利用命令行參數 __ns:=default-namespace
  • 利用環境變量設置shell:export ROS_NAMESPACE=default-namespace  //僅當未設置__ns參數時纔有效

優勢:避免在移植和整合來自不一樣來源的節點時發生名稱衝突

4. 私有名稱  //用於節點內部僅與本節點有關的資源,如參數

(1)典型特徵:以一個波浪字符(~)開始

(2)與相對名稱一致,不能徹底肯定自身所在命名空間,須要利用ROS客戶端庫進行名稱解析

(3)不使用當前的默認命名空間,而採用節點名稱做爲命名空間,如  /sim1/pubvel + ~max_vel => /sim1/pubvel/max_vel

(4)私有名稱可用於管理節點的服務,話題不能命名爲私有名稱

注:私有名稱的關鍵字"private"僅代表不使用所在的命名空間,其餘節點能夠經過私有名稱解析後的全局名稱進行訪問  //僅在命名空間層面上有意義,注意與其餘編程語言的不一樣

5. 匿名名稱:非用戶指定的無語義信息的名字

(1)通常用於爲節點命名,使節點的命名保存惟一性

(2)調用ros::init方法時請求一個自動分配的惟一名稱

  • ros::init(argc, argv, base_name, ros::init_options::AnonymousName);

注:ros::init使用處理器時間在節點的基本名稱後追加文本,保證名字的惟一性

6、啓動文件

1. 目的:利用啓動文件一次性配置和運行多個節點

2. 執行啓動文件:roslaunch 包名 啓動文件名  //若是沒有運行roscore,roslaunch會自動啓動roscore

(1)若是啓動文件不屬於任何功能包,則能夠直接以啓動文件路徑啓動 roslaunch + 啓動文件路徑

(2)啓動文件內的節點幾乎是同一時刻啓動,沒法肯定啓動順序,所以節點之間應儘可能保持獨立

(3)添加-v選項能夠輸出詳細信息:roslaunch -v 包名 啓動文件名

(4)查找啓動文件時,roslaunch工具會同時搜索每一個功能包目錄的子目錄

3. 啓動文件基本元素

(1)根元素:<launch> ... </launch>

(2)啓動節點:

<node

  pkg=包名

  type=可執行文件名

  name=節點名

/>

  • 標籤末尾的斜槓"/"不可少,另外一種方式爲 <node pkg="..." type="..." name="..."></node>  //顯式給出結束標籤
  • 必需屬性pkg,type,name(會覆蓋ros::init設置的名稱
  • 默認狀況下,啓動文件啓動節點的標準輸出被重定向至日誌文件~/.ros/log/run_id/node_name-number-stout.log
  • 在節點元素中配置屬性output="screen"能夠在控制檯終端輸出信息
  • 使用--screen 命令行選項在控制檯顯式全部節點輸出:roslaunch --screen 包名 啓動文件名

(3)請求復位:respawn="true"  //當節點中止時,roslaunch會從新啓動該節點,可用於應對軟件崩潰、硬件故障等引發的節點停止

(4)必要節點:required="true"  //當必要節點停止時,roslaunch會終止全部其餘活躍節點,並退出;與respawn做用相互矛盾,不能同時配置

(5)roslaunch全部節點共享一個終端,針對依賴控制檯輸入的節點來講,須要維護獨立的終端:使用啓動前綴屬性  launch-prefix="命令行前綴"

  • roslaunch啓動節點時調用相應的命令行工具rosrun
  • 啓動前綴至關於在rosrun前添加前綴,即示例中的launch-prefix="xterm -e" 等價於 xterm -e rosrun turtlesim turtle_teleop_key

3. 在命名空間內啓動節點  //經過配置節點元素的ns屬性,壓入命名空間

(1)儘管兩個節點相對名稱相同,可是因爲命名空間不一樣,因此兩個節點相互獨立

(2)因爲話題名稱定義時採用的也是相對名稱,因此話題也分別位於獨立的命名空間

4. 名稱重映射remapping names  //從更精細的層面控制節點名稱的修改

(1)基於替換的思想:每一個重映射包含一個原始名稱和一個新名稱,每當節點使用原始名稱時,ROS客戶端庫會將它自動替換爲新名稱

(2)建立重映射

  • 命令行啓動時,分別給出原始名稱和新名稱  原始名稱:=新名稱  //rosrun turtlesim turtlesim_node turtl1/pose:=tim
  • 啓動文件中,使用重映射元素:  <remap from="original-name" to "new-name"/>  //若屬性做爲launch元素的子元素出如今頂層,則應用到全部後續節點

  注:重映射元素也能夠做爲節點的子元素:<node ...> <remap from="original-name" to "new-name"/> </node> 此時只應用於所在節點

注:

  • ROS在應用任何重映射以前,全部的名稱須要先解析爲全局名稱
  • 應用示例:反向海龜  //將原始速度話題重映射爲處理後的反向速度話題

5. 其餘元素

(1)啓動文件的嵌套:include元素

  • <include file="啓動文件完整路徑"/>  //一般使用find命令搜索功能包位置,避免路徑輸入錯誤,如<include file="$(find package-name)/launch-file-name"/>
  • 支持命名空間屬性,能夠將包含內容壓入指定命名空間:<include file="..." ns="namespace"/>

(2)啓動參數argument:便於配置啓動文件,相似可執行程序中的局部變量(僅在聲明的當前啓動文件內有效,不可被包含的啓動文件繼承)

  • 聲明參數:<arg name="arg-name"/>  //聲明是可選的,有助於明確啓動文件的參數有哪些
  • 參數賦值
    • 命令行賦值  roslaunch 包名 啓動文件名 arg-name:=arg-value
    • 啓動文件內聲明時賦值
    1. <arg name="arg-name" default="arg-value"/>  //可被命令行參數覆蓋
    2. <arg name="arg-name" value="arg-value"/>  //不可被命令行參數覆蓋
  • 獲取參數值:$(arg arg-name)
  • 向包含的次級啓動文件中發送參數值
    • 將arg元素做爲include元素的子元素  <include ...> <arg name="arg-name" value="arg-value"/> </include>
    • 可以使用<arg name="arg-name" value="$(arg arg-name)"/> 保證內外同名參數具備相同的參數值

注:在ROS中,parameter和argument是有區別的

  • parameter是ROS系統運行過程當中使用的數值,存儲在參數服務器parameter server中,能夠被節點和用戶獲取(ros::param::get或rosparam)
  • argument只在啓動文件內有意義,不能被節點直接獲取

(3)建立組group:大型啓動文件內管理節點的快捷方式

  • 把若干節點放入同一命名空間中:<group ns="namespace"> ... </group>
  • 能夠有條件地啓動或禁用一個節點:<group if="0 or 1"> ... </group>  //若屬性爲1,則正常啓動,不然忽略組標籤內元素;若 if 改成 unless 則用法相反

注:僅有0和1爲合法取值,且不能使用布爾運算;也能夠不使用組元素,單獨爲每一個節點設置ns、if和unless

7、參數parameter  //配置節點信息的集中式方法

1. 主要思想:使用集中的參數服務器維護一個變量集的值(字典),適用於不會隨時間頻繁變動的信息

2. 經過命令行獲取參數

(1)rosparam list  //查看參數列表,輸出 /rosdistro全局計算圖源的名稱字符串

(2)rosparam get parameter_name  //查詢參數,如 rosparam get /rosdistro 獲得ROS版本

(3)rosparam get namespace  //檢索給定命名空間每一個參數的值,如 rosparam get / 查詢全局命名空間/

(4)rosparam set parameter_name parameter_value  //設置參數,修改已有參數的值或建立新參數

(5)rosparam set namespace values  //設置同一命名空間的多個參數,其中值values要以YAML字典的形式給出參數和值的對應關係

(6)建立和加載參數文件(YAML形式)  //可用於場景復現

  • rosparam dump filename namespace  //存儲命名空間中的全部參數
  • rosparam load filename namespace  //讀取參數至參數服務器

  注:命名空間參數可選,默認爲全局命名空間/

注:

  • 全部的參數都屬於參數服務器,而不是任何特定節點,即便節點終止時參數仍然存在
  • 更新的參數值不會自動「推送」到節點,節點須要主動顯式地向參數服務器查詢參數值

3. 使用C++接口獲取參數  //參數名能夠是全局的、相對的或者私有的

void ros::param::set(parameter_name, input_value);

bool ros::param::get(parameter_name, output_value);  //讀取成功返回true,不然代表參數還未指定值

注:在命令行中實現私有參數賦值 rosrun agitr pubvel_with_max _max_vel:=1  //參數max_vel前面添加下劃線_前綴  _param-name:=param-value

 4. 在啓動文件中設置參數

(1)<param name="param-name" value="param-value"/>  //設置參數,參數名是相對名稱

(2)<node ...> <param name="param-name" value="param-value"/>  </node>  //在節點元素內包含param元素,不管是否以~或/開頭,參數均爲節點私有參數

(3)<rosparam command="load" file="path-to-param-file"/>  //一次性從文件中加載多個參數,等價於rosparam load

注:一般採用 file="$(find package-name)/param-file" 指定文件路徑

 8、服務service

1. 與話題消息的區別

  • 服務調用是雙向
  • 服務調用是一對一通訊

2. 執行過程:客戶端節點發送請求數據到服務器節點,而且等待迴應;服務器節點收到請求後,執行相應活動,隨後發送響應數據給客戶端節點;

3. 與話題內容由消息數據類型肯定相似,服務的內容也由服務數據類型決定

4. 從命令行查看和調用服務

(1)rosservice list  //列出全部服務

(2)服務通常能夠劃分爲兩類:

  • 特定節點獲取或向其傳遞消息  //一般採用節點名做爲命名空間以防止命名衝突,如採用私用名稱的~get_loggers服務和~set_logger_level服務會被解析爲/turtlesim/get_logger和/turtlesim/set_logger_level
  • 不針對某些特定節點的服務  //例如/spawn服務用於生成一個新的仿真海龜,儘管由turtlesim節點提供,可是不屬於任何特定節點(只須要完成相應功能,而不關心是哪一個節點在起做用

5. rosnode info node-name  //查看某個節點的服務類型

6. rosservice node service-name  //查找提供服務的節點(反向查詢

7. rosservice info service-name  //肯定服務的數據類型,一般也是由包名和類型名組成「包名/類型名」,如turtlesim/Spawn

8. rossrv show 服務數據類型名  //獲取服務數據類型的詳細信息(一系列數據域),注意rosservice和rossrv的區別

注:

  • 短橫線---以前爲請求的數據項,以後的字段爲響應項
  • 服務的請求和響應字段均可覺得空,如turtlesim_node提供的/reset服務的數據類型爲std_srvs/Empty,其請求和響應字段均爲空

9. rosservice call service-name request-content  //調用服務,提供請求域的全部值,如rosservice call /spawn 3 3 0 Mikey 在位置(3, 3)處建立一個名爲"Mikey"的新海龜,朝向爲0

注:

  • 新海龜有本身的資源集,且位於新命名空間Mikey中  //避免命名衝突
  • rosservice call的輸出爲服務器節點的響應數據,如上例爲 name: Mikey  //調用成功或失敗
  • rosservice call /clear 從參數服務器讀取參數值,刷新當前參數

 10. 客戶端程序

(1)聲明請求和響應類型:#include<turtlesim/Spawn.h>  //包含頭文件,定義turtlesim::Spawn類(服務數據類型

(2)建立客戶端對象:ros::ServiceClient client = node_handle.serviceClient<service_type>(service_name)  //服務名稱通常應當爲相對名稱,如"spawn"

注:與話題發佈者不一樣,無需緩衝隊列,會阻塞等待直至服務調用完成

(3)建立請求和響應對象  //上述頭文件中定義了請求和響應的類,經過包名和服務類型引用,如turtlesim::Spawn::

  • package_name::service_type::Request  //請求對象類,應作賦值操做
  • package_name::service_type::Response  //響應對象類,接收服務器響應消息,無需賦值

注:服務類型的頭文件中還定義了一個單獨類package_name::service_type同時擁有Request和Response做爲數據成員;能夠定義單獨對象package_name::service_type srv,而不使用獨立的Request和Response對象

(4)調用服務:bool success = service_client.call(request, response)

  • 完成定位服務器節點、傳輸請求數據、等待響應和存儲響應數據等一系列工做
  • 調用方法返回布爾值來確認調用服務是否成功完成

注:

  • 不要忘記對調用返回值進行判斷,可經過ROS_ERROR_STREAM輸出可能的錯誤信息
  • 默認狀況下,調用返回後,會關閉與服務器的鏈接
  • 可經過ros::ServiceClient client = node_handle.serviceClient<service_type>(service_name, true)創建持續鏈接的客戶端  //提升了節點耦合性,系統魯棒性變差,不建議

11. 服務器程序

(1)編寫回調函數 bool function_name(package_name::service_type::Request &req, package_name::service_type::Response &resp)

注:本例中請求和響應的數據類型均爲std_srvs/Empty(空字符串),無需進行數據處理

(2)與話題訂閱者相似,須要建立服務器對象ros::ServiceServer server=node_handle.advertiseService(service_name, pointer_to_callback_function);

  • service_name 建議使用局部名稱,但不嚴格限制使用全局名稱
  • ros::NodeHandle::advertiseService拒絕接受私有名稱,即以波浪號~開頭的名稱
  • 能夠利用節點自身的缺省命名空間建立私有名稱服務
  1. ros::NodeHandle nhPrivate("~");  //發送給此節點句柄的任意局部名稱的缺省命名空間與節點名稱一致 (此句柄+相對名稱 效果等價於 私有名稱)
  2. ros::ServiceServer server=nhPrivate.advertiseService("baz", Callback);  //與廣播名爲~baz的服務效果相同

注:本例使用ros::spinOnce()而非ros::spin(),緣由在於沒有服務調用時還須要執行其餘工做,即發佈速度指令  //與第三章 表3.5 進行對比

(3)解決ros::spinOnce() + sleep()引發的延遲問題

  1. 採用雙線程:一個負責發佈消息,一個負責處理服務器回調
  2. 採用ros::spin(),並利用計數器回調函數(timer callback)發佈消息  // http://wiki.ros.org/roscpp/Overview/Timers

 9、消息錄製與回放

1. rosbag:將發佈在一個或多個話題上的消息錄製到一個包文件中,可用於過後回放

注:術語包文件bag files指用於存儲帶時間戳的ROS消息的特殊文件格式

2. 命令行工具

(1)錄製包文件:rosbag record -O filename.bag topic-names  //不指定文件名時,rosbag基於當前的日期和時間自動生成文件名;會建立新節點 /record_...

  • rosbag record -a 記錄當前發佈的全部話題消息
  • rosbag record -j 啓動包文件的壓縮

(2)回放包文件:rosbag play filename.bag  //保持與原始發佈相同的順序和時間間隔

注:儘可能避免系統中rosbag和「真實」節點向同一個話題發佈消息

(3)檢查文件包:rosbag info filename.bag  //提供包文件的豐富信息

3. rosbag功能包

(1)rosrun rosbag record -O filename.bag topic-names;rosrun rosbag play filename.bag  //利用record和play可執行文件

(2)在啓動文件中使用包文件

  • 錄製節點:<node pkg="rosbag" name="record" type="record" args='-O filename.bag topic-names"/>
  • 回放節點:<node pkg="rosbag" name="play" type="play" args="filename.bag"/>

注:須要提供參數

第十章 總結

1. 在網絡環境中運行ROS:分佈式機器人控制模式

(1)網絡層配置:確保計算機之間可以互相通訊

(2)ROS層配置:確保全部節點都能與節點管理器通訊

2. 編寫更規範的程序,如使用ros::Timer的回調函數替代ros::Rate對象

3. 使用rviz是數據可視化:訂閱用戶話題以顯示消息

4. 建立消息和服務類型

5. 使用tf工具來管理多個座標系:幫助節點完成座標轉換

6. 使用Gazebo仿真:高保真的機器人仿真器

相關文章
相關標籤/搜索