ROS Learning-032 (提高篇-010 Launch)Launch 深入研究 --- (啟動檔案程式設計)ROS 的 XML語法簡介
ROS 提高篇 之 Launch 深入研究 - 01 — 啟動檔案的程式設計 — ROS 的 XML語法簡介
我使用的虛擬機器軟體:VMware Workstation 11
使用的Ubuntu系統:Ubuntu 14.04.4 LTS
ROS 版本:ROS Indigo
注意:
1 . ROS 提高篇這個專欄的教學有門檻。
2 . 如果你沒有學習前面的教程,請想學習前面的 beginner_Tutorials 和 learning_tf 的ROS 相關教程。
1 . 前言
在之前的 beginner_Tutorials 教程裡面,我們已經初步學習了 Launch檔案
這一篇部落格和接下來的幾篇部落格,我們將詳細的講解 Launch 檔案要如何編寫。
使用 launch檔案,我們可以一次性配置並且執行很多節點。
至今,如果你按照我們的教程(ROS Learning)學習到這裡,想必你一定運行了許多的例子。你可能會對在許多的終端中執行不同的節點感到麻煩,並且你會因為不瞭解我們執行過的例子中的 launch檔案 都做了什麼事情會感到困擾。我們在ROS Learning教程中 的 beginner_Tutorials 和 learning_tf 的ROS 相關教程 中已經學習簡單的 launch啟動指令碼
說了一堆廢話,接下來進入正題。
2 . 使用launch啟動指令碼檔案:
<launch>
<node
pkg="turtlesim"
type="turtlesim_node "
name="turtlesim "
respawn="true "
/>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
required="true"
launch−prefix="xterm −e"
/>
<node
pkg="agitr"
type="subpose"
name="pose_subscriber"
output="screen"
/>
</launch>
roslaunch 是如何允許我們一次啟動多個節點”?
基本的實現思想是列表,在一個指定的XML格式,一組節點需要在同一時間被啟動。你會很熟悉下面的列表,它展示啟動了海龜(turtlesim)模擬器的一個簡單的launch檔案。
再我們深入研究launch檔案的格式的細節前,讓我們看看這些檔案都是如何使用的:
roslaunch
命令的使用格式:
# roslaunch package-name launch-file-name
在 編寫簡單的啟動指令碼檔案 的部落格中最後的小練習中,我們自己編寫了一個launch檔案,如果一切工作正常的話,這個命令將會啟動3個節點。在開始任何一個節點前,roslaunch
將會確定 roscore節點 是否已經在執行,如果沒有,自動啟動它。
注意: 要小心,不要與
rosrun
命令 混淆,rosrun
只能啟動一個節點。roslaunch
命令可以同時啟動多個節點。
擴充套件 :你也可以在使用 roslaunch
啟動launch檔案的時候沒有package-name
這部分。要想做到這一定,launch-file-name
檔案需要寫成絕對路徑的形式。只要可以讓 roalaunch
命令找到唯一的一個 launch
檔案,你就可以不提供 package-name
部分。舉例:
$ roslaunch ~/catkin_ws/src/beginner_turtorls/launch/xxx.launch
關於
roslaunch
的一個重要的事實,這個事實很容易被忽略,就是在launch檔案中的所有節點幾乎被同一時間啟動。結果,我們不能確定這些節點初始化的順序。好在 ROS節點 不關係彼此的啟動順序。為什麼?
這個行為體現了ROS哲學:每一個節點與其他的節點都應該儘可能的獨立、不相關。
如果你想請求 roslaunch
命令 請求詳細輸出( Requesting verbosity )資訊的話。就像其他的命令列工具一樣,roslaunch
也有請求詳細輸出的選項。在你進行除錯程式的時候,使用 -v
引數對你是有幫助的,你可以看到 roslaunch
是如何解讀你的 launch檔案 的詳細解釋。
# roslaunch -v package-name launch-file-name
3 . 建立launch啟動指令碼檔案:
3-1 . 基本的成分:
3-1-1 . <launch>
最簡單的啟動檔案是由一個包含了幾個節點元素的根元素(root element)組成。
根元素:launch檔案是XML文件,每一個XML文件必須有且只有一個跟元素(root element)。對於ROS launch檔案,根元素(root element)是由一對 launch
標籤定義的。
<launch>
...
</launch>
其他的所有元素標籤都要寫在 <launch>...</launch>
便籤之間。
3-1-2 . <node>
啟動的節點:任何一個launch檔案的重點都是:節點(node)元素的集合。啟動的每一個節點(node)都要有自己獨一無二的名字(name)。一個節點(node)元素看上去就像下面這個樣子:
<node
pkg="package-name"
type="executable-name"
name="node-name"
/>
注意: 在節點(node)標籤的末尾加上’ /
‘斜槓是重要的,並且也容易被忘記。’ /
’ 表明:這裡不會再有”</node>
“出現,並且node元素定義完成。
XML解析器非常嚴格,如果你忘記了加’ /
‘,你要對這樣的錯誤做好準備:
Invalid roslaunch XML syntax: mismatched tag
你也可以寫成顯式關閉標籤的形式:
<node pkg=". . . " type=". . . " name=". . . "></node>
事實上,如果node(節點)元素有children,就需要顯式標籤來定義,children比如是:remap
元素或者 param
元素。這些元素會在下面介紹。
3-1-2-1 . 一個node(節點)元素必需的三個屬性:pkg
、 type
和 name
屬性:
pkg
和 type
屬性確定了:啟動此節點,ROS應該執行哪個程式。它們與 rosrun
的兩個命令列引數一樣,它們分別是:程式包名字 和 可執行檔案的名字。
name
屬性指定了一個節點的名字。這個節點程式會呼叫 ros::init()
函式內有一個這個節點的 name
引數,導致這個節點的 name
被改寫,所以這個 name
屬性容易被覆蓋。
擴充套件:
ros::init()
函式 提供的name
資訊將會全面的覆蓋命名資訊(launch檔案中<node>
標籤裡面的name
屬性),就算這個節點已經以匿名name被建立。如果你想在 launch檔案 中使用匿名name,使用anon
屬性 來替代name
屬性:
name="$(anon base_name)"
但是請注意,多次使用同樣的基本名稱,將生成同樣的匿名name。這意味著,(一)我們參考launch檔案中其他部分的name,但(二)我們必須小心的為每一個我們想要匿名的節點使用不同的base names。
3-1-2-2 . 查詢節點(node)的日誌檔案(log files):
使用 roslaunch
命令 和 使用 rosrun
命令 單獨執行每個節點 之間的重要區別是:預設情況下,roslaunch
命令 從啟動節點開始,標準輸出資訊會重定向到一個日誌檔案中,而不會像 rosrun
命令那樣,將 log 資訊顯示在終端(console)上。
日誌檔案所在路徑: ∼/.ros/log/run_id/node_name-number-stdout.log
當主機( master )被啟動,run_id
是被生成的唯一的識別符號。在log檔名中的number是一個小整數,表示節點的數量。例如:執行上面的launch檔案,2個節點的標準輸出資訊將會傳給這些節點的日誌檔案(log files):
turtlesim-1-stdout.log
telep_key-3-stdout.log
這些日誌檔案可以被任何的文字編輯器(比如:gedit、vim、cat)開啟。
3-1-2-3 . 將標準輸出資訊顯示在終端(console)上
Q: 如何將標準輸出資訊顯示在終端(console)上?
A: 在 node
元素中使用 output
屬性:
output="screen"
帶這個屬性啟動的節點會將標準輸出資訊顯示在終端的視窗中,而不會儲存在日誌檔案中。這也解釋了為什麼這個帶有output="screen"
的節點(node
) 的日誌檔案在上面日誌檔案列表中丟失的原因。
擴充套件:
node
元素的output
屬性只能影響這個節點自己。除了output
屬性,我們可以使用roslaunch
命令列工具的--screen
命令列選項強制性的在終端的視窗中顯示所有節點的輸出資訊。
roslaunch --screen package-name launch-file-name
3-1-2-4 . 請求重生(respawning) :
啟動完所有請求啟動的節點之後,roslaunch
監測每一個節點,讓它們保持正常的執行狀態。對於每一個節點(node
),當它終止( terminates)時,我們可以要求 roslaunch
重新啟動它,這就是 respawn
屬性做的事情:
respawn="true"
-- This can be useful, for example, for nodes that might terminate prematurely, due to software crashes, hardware problems, or other reasons.
The respawn attribute is not really necessary in our example—All three programs are
quite reliable—but we include it for the turtlesim_node to illustrate how respawning
works. If you close the turtlesim window, the corresponding node will terminate. ROS
quickly notices this and, since that node is marked as a respawn node, a new turtlesim
node, with its accompanying window, appears to replace the previous one.
3-1-2-5 . 必需節點(Requiring):
讓一個節點可以重生的另一種方法是:宣佈這個節點是必需的(require
):
required="true"
當一個必需的節點終止時,roslaunch
會做出響應,終止其他所有的節點並退出它自己。這種行為機制是有用的。例如,對於一些節點(一)它們非常的重要,如果它們失敗了它,那麼整個會話應該被放棄,或者這些節點(二)不能使用重生( respawn
)屬性完美的重新啟動,在這些情況下,我們都需要對這個 節點(node
)使用 required="true"
屬性。上面 roslaunch
例子檔案中,我們給 turtle_teleop_key
節點使用了這個 required
屬性。如果你關閉了執行 teleoperation
節點的視窗,roslaunch
將會殺死其他的2個節點退出。
注意: 由於
required
屬性和respawn
屬性的含義,所以如果你給單個的一個節點同時設定了這2個屬性,roslaunch
命令會抱怨。所以不要這樣設定。
3-1-2-6 . 讓每一個節點在單獨的終端視窗中啟動它們自己:
使用 roslaunch
命令 的一個潛在的缺點:相比我們原來對每個節點在單獨的終端使用 rosrun
命令啟動的做法,roslaunch
則是讓所有的節點共享同一個終端。 那些只需要生產簡單的日誌訊息檔案而不需要終端(console)輸入的節點是容易管理的,而那些依賴終端輸入的節點,比如 turtle_teleop_key
節點,它可能要優先的保留在獨立的終端上。
慶幸的是,roslaunch
提供了一個簡單的屬性去實現這一點,在 node
元素裡使用 launch-prefix
屬性:
launch-prefix="command-prefix"
在例子launch檔案中,我們給 teleoperation
節點使用了這個屬性:
launch-prefix="xterm -e"
因為這個屬性,啟動這個 node
元素的 rosrun
命令大致相當於:
xterm -e rosrun turtlesim turtle_teleop_key
正如我們所知道的,xterm
命令會開一個新的終端視窗。 -e
引數告訴 xterm
:執行其命令列剩餘部分(rosrun turtlesim turtle_teleop_key
)。
擴充套件:
launch-prefix
屬性不是隻能使用xterm
。它還可以使用gdb
或者valgrind
或者nice
命令。
3-1-2-7 . 在一個名稱空間內啟動一個節點:
<launch>
<node
name="turtlesim_node "
pkg="turtlesim"
type="turtlesim_node "
ns="sim1"
/>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
required="true"
launch −prefix="xterm −e"
ns="sim1"
/>
<node
name="turtlesim_node "
pkg="turtlesim"
type="turtlesim_node "
ns="sim2"
/>
<node
pkg="a gitr "
type="pubvel"
name="velocity_publisher "
ns="sim2"
/>
</launch>
在 node
元素中指定 ns
屬性:
ns="namespace"
在例子launch檔案中,使用這個屬性建立了兩個無關的 turtlesim 模擬器。
turtlesim 話題名字(turtle1/cmd_vel
、turtle1/color_sensor
和 turtle1/pose
)被從全域性名稱空間移動到 /sim1
和 /sim2
的單獨名稱空間裡。
3-1-2-8 . 重對映名字:
在啟動一個節點的時候,有兩種方法建立重對映:
在終端命令列中啟動一個節點時,要重新給這個節點命名:給出一個節點原來的名字和新的名字,中間用:=分開。
original-name:=new-name
例如,在執行turtlesim例項時,我們現在想把釋出姿態資料的話題/turtle1/pose
名稱改為:/tim
,那麼命令就是這樣的:
rosrun turtlesim turtlesim_node turtle1/pose:=tim
在launch檔案中重新命名:使用 remap
元素:
<remap from="original-name" to="new-name" />
如果這個 remap
是 launch
元素的一個child(子類),與 node
元素同一層級, 並在 launch
元素內的最頂層。那麼這個 remapping 將會作用於後續所有的節點。
這個 remap
元素也可以作為 node
元素的一個child(子類)出現。下面這個就是使用模板:
<node node-attributes >
<remap from="original-name" to="new-name" />
. . .
</node>
例如:上面命令列命令如果在launch檔案中,就是下面這個樣子的:
<node pkg="turtlesim" type="turtlesim_node"
name="turtlesim" >
<remap from="turtle1/pose" to="tim" />
</node>
3-2 . 其他launch檔案元素:
這一節介紹幾個額外的 roslaunch
結構元素。
3-2-1 . <include>
元素
3-2-1-1 . file
屬性
包含(including)其他檔案:
<include file="path-to-launch-file" />
這個 file
屬性期望我們新增想要包含的檔案的完整路徑。但是大多數時候,include
元素使用一個 find
命令來搜尋一個程式包,代替一個明確的完整路徑:
<include file="$(find package-name)/launch-file-name" />
<launch>
<node
pkg="turtlesim"
type="turtlesim_node "
name="turtlesim "
>
<remap
from="turtle1 /cmd_vel"
to="turtle1 /cmd_vel_reversed"
/>
</node>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
launch−prefix="xterm −e"
/>
<node
pkg="a gitr "
type="reverse_cmd_vel"
name="reverse_velocity "
/>
</launch>
注意:roslaunch
命令 將會在程式包(package)的子目錄裡搜尋launch檔案。 include
元素必須要指定檔案的特定路徑,你可以使用 find
來找到這個程式包,但是卻不能在這個程式包目錄裡面自動的找到某個子目錄裡有launch檔案。舉例:
這樣做是正確的:<include file = "find learning_tutrols"/launch/start_demo.launch" / >
這樣做是錯誤的:<include file = "find learning_tutrols"/start_demo.launch" />
你可以使用 roslaunch
命令:roslaunch learning_tutrols start_demo.launch
,這樣是可以成功執行的。但是同樣的程式包名和launch檔名使用include
元素就會失敗。
3-2-1-2 . ns
屬性
include
元素也支援 ns
屬性,可以讓這個檔案裡的內容推送到一個名稱空間裡面:
<include file=". . . " ns="namespace" />
一般我們都會給 include
元素設定一個 ns
屬性。
3-2-2 . 啟動引數(Launch arguments)
為了幫助launch檔案配置,roslaunch
支援 launch arguments(啟動引數),也叫做 arguments
或者 args
。它的功能有點像一個可執行程式的區域性變數。它的優點是,你可以通過編寫launch檔案來避免編寫重複程式碼。為少數的資訊使用arguements,可以改變程式的執行。
為了說明這個道理,我們的例子launch檔案中使用了一個argument
,叫做 use_sim3
,目的是為了確定是否啟動了3個turtlesim副本或只有2個。
Z:儘管術語argument和parameter在許多計算機環境中稍微可以互換使用,它們的含義在ROS中有很大的不同。Parameters(引數)在一個執行的ROS系統中是變數(values),它被儲存在parameter伺服器中,活動(或者叫:執行)的節點通過
ros::param::get()
函式訪問它,並且使用者可以通過rosparam
命令列工具使用它。相比之下,arguments只有在launch檔案裡合法,它們的值不是直接提供給節點。
<launch>
<include file ="$( find agitr )/doublesim.launch "/>
<arg
name="use_sim3"
default ="0"
/>
<group ns="sim3" if ="$( arg use_sim3 )" >
<node
name="turtlesim_node "
pkg="turtlesim"
type="turtlesim_node "
/>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
required="true "
launch−prefix="xterm −e"
/>
</group>
</launch>
3-2-3 . Declaring arguments(宣告引數)
在launch檔案中,要宣告一個引數的存在,我們使用arg
元素:
<arg name="arg-name" />
聲明裡面只有一個 name
是起不上什麼作用的,這就像是你在程式中定義了一個 int
型別的變數,但是你並沒有使用它一樣。(你至少還需要給 arg
元素分配 default
屬性或 value
屬性,請看下面)
3-2-3-1 . Assigning argument values(分配argument的數值)
在launch檔案中使用的每一個argument必須給它分配一個 value(賦值)。有這麼一下幾種方法來實現這一點:
在命令列中你可以這樣做:給 roslaunch
提供一個 value
:
roslaunch package-name launch-file-name arg-name:=arg-value
另外,在launch檔案中,你可以提供一個 value(賦值) 作為 arg
宣告的一部分,使用下面的兩種語法之一就可以:
<arg name="arg-name" default="arg-value" />
<arg name="arg-name" value="arg-value" />
這兩種語法的唯一不同是:命令列是argument可以覆蓋default
的值,但是不能覆蓋 value
。在例子launch檔案中,use_sim3
節點的 default
值為 0
,所以它可以通過命令列改變值,就像下面這樣:
roslaunch agitr triplesim.launch use_sim3:=1
如果我們修改了這個例子launch檔案:使用value
替換default
。那麼上面這個命令執行的時候會出現錯誤,因為使用 value
屬性 配置的argument的值是不允許改變的。
3-2-3-2 . Accessing argument values(獲取argument的數值)
一旦聲明瞭一個argument,並且使用 value
屬性給它分配值,你可以在程式中通過arg substitution來使用它的 value
屬性的值。像是這樣子:
$(arg arg-name)
$()
這個符號出現的任何地方,roslaunch
命令 都將會把它替換成給定argument 的值(value
)。在例子launch檔案中,我們使用 use_sim3
引數(argurnt),???????一個 group
元素內的 if
屬性。(下面我們將會簡短的介紹 if
和 group
)
3-2-4 . Sending argument values to included launch files(給包含(included)的launch檔案傳遞argument)
在argument的傳遞上有一個限制,就是argument不能傳遞給 include
元素裡包含的子launch檔案使用。這個問題非常重要,因為這個 argument 就像是一個區域性變數,它不能被包含的launch檔案所 “繼承” 。
解決這個問題的方法:在 include
元素中插入 arg
元素作為 include
元素的子類(children),就像是這樣:
<include file="path-to-launch-file">
<arg name="arg-name" value="arg-value"/>
. . .
</include>
注意,這裡的 arg
元素不同於我們已經知道的 arg
宣告,在 inchude
標籤內的arguments是給包含(included) 的launch檔案提供的arguments,不是為本launch檔案提供的。
一種常見的情況是,被包含(included)的launch檔案和本launch檔案會有共同的引數。在這種情況下,我們希望這些值(values)永遠不變。像這樣的元素,在這兩個地方使用相同的argument name(引數名),要這樣做:
<arg name="arg-name" value="$(arg arg-name)" />
在這種情況下,第一個 arg-name
和往常一樣。第二個 arg-name
是launch檔案中提供的。結果是,這兩個launch檔案中給定的argument具有相同的值(value)。
3-2-5 . Creating groups (建立組)
最後一個要介紹的launch檔案特性就是,再來說說:group
元素,它提供了一個方便的方法來在一個大型的launch檔案中組織節點。group
元素可以達到兩個目的:
1 . 組(group)可以把幾個節點推送到同一個名稱空間中。
<group ns="namespace" />
. . .
</group>
2 . 組(groups)裡面的每一個節點在啟動的時候都會給定預設的名稱空間。
:如果一個已經被分組的節點有自己的
ns
屬性,那麼結果:被啟動的節點的名稱空間會出現巢狀的關係:/group-namespace/ns-namespace/node-name
。
3 . 組(groups)可以通過 判別條件 來啟用或禁用節點(nodes):
<group if="0-or-1" />
. . .
</group>
如果 if
屬性的值是 1
,<group>
標籤內封閉的元素(elements)會被包含。如果 if
屬性 值是 0
,則 <group>
標籤內包含的元素會被忽略。 unless
屬性的工作方式類似 if
屬性,但是含義顛倒:
<group unless="1-or-0" />
. . .
</group>
當然了,通常我們不會給這些屬性使用簡單的 0
或 1
這樣的幅值。建議:結合 arg
的 $()
技術,它們會將你的launch檔案的配置變得非常的強大。
注意: 注意
1
和0
是唯一的合法值。具體來說,你通常會使用布林(boolean)的 AND 和 OR 操作符,而不會直接使用1
或0
。
在例子launch檔案中,有一個group
元素具有ns
和 if
這兩屬性。這個 group
元素的 ns
屬性:將這個 group
元素內的兩個節點推送到 sim3
名稱空間。if
屬性:基於use_sim3
引數的值來落實:是啟動還是禁止 group
元素內包含的節點。
擴充套件: 遺憾的是,只有這三個屬性(
ns
、if
、unless
)可以通過group
元素向下傳遞給group
元素內包含的node
(節點)元素。
例如,我們可能會想給group
元素新增一個output="screen"
來讓這個group
元素內的節點啟動後輸出的資訊都顯示在終端窗口裡,但是我們不可以這樣設定,我們必須要對每一個node
元素直接給予output="screen"
,這樣才能達到我們的要求。
完
By The Way: 可能以後,這篇部落格,我還會繼續完善。現在的這些關於:如何編寫Launch檔案的語法 ,基本上介紹的差不多了。