工具使用-rviz 中可交互的markers(InteractiveMarker)

发布 : 2019-06-26 浏览 :

我们将通过一个简单的例子来了解interactive marker如何使用在rviz中实现交互的。大多数教程都只是介绍了官方的例子,但并没有分析交互是怎么实现的,消息流是如何传递的。官方给的交互网络:

interactive_framework

并给了一段介绍:

If you want to create a node providing a set of interactive markers, you need to instantiate an InteractiveMarkerServer object. This will handle the connection to the client (usually RViz)
and make sure that all changes you make are being transmitted and that your application is being notified of all the actions the user performs on the interactive markers.

听的云里雾里的,并没有牵涉到本质。所以我们这里来分析下教程中simple_marker这个例子。

interactive_marker_server.py

要理解消息流是怎么流动的,我们首先要看看InteractiveMarkerServer内部定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import rospy
from std_msgs.msg import Header
from visualization_msgs.msg import InteractiveMarker
from visualization_msgs.msg import InteractiveMarkerFeedback
from visualization_msgs.msg import InteractiveMarkerInit
from visualization_msgs.msg import InteractiveMarkerPose
from visualization_msgs.msg import InteractiveMarkerUpdate
from threading import Lock
# 这几个marker msg的定义差别不大,
# InteractiveMarker是最基本的定义, 包含header, pose, MenuEntry[], Controls[]
# InteractiveMarkerInit 相对于InteractiveMarker更高一层, 它有一个InteractiveMarker List
# InteractiveMarkerPose 包含header, pose
# InteractiveMarkerUpdate 包含InteractiveMarker List和 pose List
# InteractiveMarkerFeedback 包含一些操作值,比如鼠标选择等还有一个pose

# Represents a single marker
class MarkerContext:
def __init__(self):
self.last_feedback = rospy.Time.now()
self.last_client_id = ""
self.default_feedback_cb = None
self.feedback_cbs = dict()
self.int_marker = InteractiveMarker()

# Represents an update to a single marker
class UpdateContext:
FULL_UPDATE = 0
POSE_UPDATE = 1
ERASE = 2
def __init__(self):
self.update_type = self.FULL_UPDATE
self.int_marker = InteractiveMarker()
self.default_feedback_cb = None
self.feedback_cbs = dict()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Note: Keep in mind that changes made by calling insert(), erase(), setCallback() etc.
# are not applied until calling applyChanges().
class InteractiveMarkerServer:
DEFAULT_FEEDBACK_CB = 255
## @brief Create an InteractiveMarkerServer and associated ROS connections
## @param topic_ns The interface will use the topics topic_ns/update and
## topic_ns/feedback for communication.
## @param server_id If you run multiple servers on the same topic from
## within the same node, you will need to assign different names to them.
## Otherwise, leave this empty.
def __init__(self, topic_ns, server_id="", q_size=100):
self.topic_ns = topic_ns
self.seq_num = 0
self.mutex = Lock()
self.server_id = rospy.get_name() + server_id
# 当前markers的当前状态
self.marker_contexts = dict()
# 下一次publish需要发布的Markers,也是发生update的markers
self.pending_updates = dict()
self.init_pub = rospy.Publisher(topic_ns+"/update_full", InteractiveMarkerInit, latch=True, queue_size=100) # 初始的发布器,发布所有的InteractiveMarker
self.update_pub = rospy.Publisher(topic_ns+"/update", InteractiveMarkerUpdate, queue_size=100) # 只发布发生改变的InteractiveMarker, 注意与init_pub发布消息类型不一样
rospy.Subscriber(topic_ns+"/feedback", InteractiveMarkerFeedback, self.processFeedback, queue_size=q_size) # 订阅器,订阅哪些marker发生了变化
rospy.Timer(rospy.Duration(0.5), self.keepAlive) # 保持node发布订阅从而一直alive
self.publishInit() # 当前其实没有任何markers要发布,只是发布了个msg头
## @brief 析构函数接口 will lead to all managed markers being cleared.
def __del__(self):
self.clear()
self.applyChanges()
1
2
3
4
5
6
7
8
9
10
11
# send an empty update to keep the client GUIs happy
def keepAlive(self, msg):
empty_msg = InteractiveMarkerUpdate()
empty_msg.type = empty_msg.KEEP_ALIVE
self.publish(empty_msg) # 这个就是发布一个空的UpdateMarker, 没有任何改变,只是保持连接

# increase sequence number & publish an update
def publish(self, update):
update.server_id = self.server_id
update.seq_num = self.seq_num
self.update_pub.publish(update)
1
2
3
4
5
6
7
8
9
10
11
12
# update pose, schedule update without locking
def publishInit(self):
with self.mutex:
init = InteractiveMarkerInit()
init.server_id = self.server_id
init.seq_num = self.seq_num

for name, marker_context in self.marker_contexts.items():
rospy.logdebug("Publishing " + name)
init.markers.append(marker_context.int_marker)

self.init_pub.publish(init) # 发布初始的markers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
## @brief Add or replace a marker.
## Note: Changes to the marker will not take effect until you call applyChanges().
## The callback changes immediately.
## @param marker The marker to be added or replaced
## @param feedback_cb Function to call on the arrival of a feedback message.
## @param feedback_type Type of feedback for which to call the feedback.
def insert(self, marker, feedback_cb=-1, feedback_type=DEFAULT_FEEDBACK_CB):
#插入marker,其实也是一种update
with self.mutex:
try: # 首先看看要insert的marker是不是在等待update的dict中
update = self.pending_updates[marker.name]
except:# 不在的话,就新建一个UpdateContext类型,内容是marker
update = UpdateContext()
self.pending_updates[marker.name] = update
update.update_type = UpdateContext.FULL_UPDATE # 更新的类型,包含pose和颜色等
update.int_marker = marker
if feedback_cb != -1: # 是否给该marker添加反馈回调函数,添加的话,当订阅器收到对应marker的feedback之后就会调用回调函数处理。
self.setCallback(marker.name, feedback_cb, feedback_type)

def setCallback(self, name, feedback_cb, feedback_type=DEFAULT_FEEDBACK_CB):
# 判断marker。name的对应marker,然后修改其回调函数
with self.mutex:
try:
marker_context = self.marker_contexts[name]
except:
marker_context = None
try:
update = self.pending_updates[name]
except:
update = None
if marker_context is None and update is None:
return False

# we need to overwrite both the callbacks for the actual marker
# and the update, if there's any
if marker_context:
# the marker exists, so we can just overwrite the existing callbacks
if feedback_type == self.DEFAULT_FEEDBACK_CB:
marker_context.default_feedback_cb = feedback_cb
else:
if feedback_cb: # 可以定义多个回调函数
marker_context.feedback_cbs[feedback_type] = feedback_cb
else:
del marker_context.feedback_cbs[feedback_type]
if update:
if feedback_type == self.DEFAULT_FEEDBACK_CB:
update.default_feedback_cb = feedback_cb
else:
if feedback_cb:
update.feedback_cbs[feedback_type] = feedback_cb
else:
del update.feedback_cbs[feedback_type]
return True
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# update marker pose & call user callback.  回调函数的处理过程
def processFeedback(self, feedback):
with self.mutex:
# ignore feedback for non-existing markers
try: # 如果该InteractiveMarkerFeedback指定的Marker不存在则返回,否则找到marker
marker_context = self.marker_contexts[feedback.marker_name]
except:
return
# 不能多个client同时请求处理同个marker,会发生冲突,相当于互斥锁
if marker_context.last_client_id != feedback.client_id \
and (rospy.Time.now() - marker_context.last_feedback).to_sec() < 1.0:
rospy.logdebug("Rejecting feedback for " +
feedback.marker_name +
": conflicting feedback from separate clients.")
return

marker_context.last_feedback = rospy.Time.now()
marker_context.last_client_id = feedback.client_id

if feedback.event_type == feedback.POSE_UPDATE:
if marker_context.int_marker.header.stamp == rospy.Time():
# keep the old header
try:
self.doSetPose(self.pending_updates[feedback.marker_name],
feedback.marker_name,
feedback.pose,
marker_context.int_marker.header)
except:
self.doSetPose(None,
feedback.marker_name,
feedback.pose,
marker_context.int_marker.header)
else:
try:
self.doSetPose(self.pending_updates[feedback.marker_name],
feedback.marker_name,
feedback.pose,
feedback.header)
except:
self.doSetPose(None, feedback.marker_name, feedback.pose, feedback.header)

# call feedback handler
try:
feedback_cb = marker_context.feedback_cbs[feedback.event_type]
feedback_cb(feedback)
except KeyError:
#try:
marker_context.default_feedback_cb(feedback)
#except:
#pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	def setPose(self, name, pose, header=Header()):
with self.mutex:
try:
marker_context = self.marker_contexts[name]
except:
marker_context = None
try:
update = self.pending_updates[name]
except:
update = None
# if there's no marker and no pending for it, we can't update the pose
if marker_context is None and update is None:
return False
if update is not None and update.update_type == UpdateContext.FULL_UPDATE:
return False

if header.frame_id is None or header.frame_id == "":
# keep the old header
self.doSetPose(update, name, pose, marker_context.int_marker.header)
else:
self.doSetPose(update, name, pose, header)
return True

def doSetPose(self, update, name, pose, header):
if update is None:
update = UpdateContext()
update.update_type = UpdateContext.POSE_UPDATE
self.pending_updates[name] = update
elif update.update_type != UpdateContext.FULL_UPDATE:
update.update_type = UpdateContext.POSE_UPDATE

update.int_marker.pose = pose
update.int_marker.header = header
rospy.logdebug("Marker '" + name + "' is now at " +
str(pose.position.x) + ", " + str(pose.position.y) +
", " + str(pose.position.z))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 最重要的函数,只有该函数调用了,所有操作才会生效
def applyChanges(self):
with self.mutex:
if len(self.pending_updates.keys()) == 0: # 没有需要更新的
return
update_msg = InteractiveMarkerUpdate()
update_msg.type = InteractiveMarkerUpdate.UPDATE
for name, update in self.pending_updates.items():
if update.update_type == UpdateContext.FULL_UPDATE:
try:
marker_context = self.marker_contexts[name]
except:
rospy.logdebug("Creating new context for " + name)
# create a new int_marker context
marker_context = MarkerContext()
marker_context.default_feedback_cb = update.default_feedback_cb
marker_context.feedback_cbs = update.feedback_cbs
self.marker_contexts[name] = marker_context
marker_context.int_marker = update.int_marker
update_msg.markers.append(marker_context.int_marker)
elif update.update_type == UpdateContext.POSE_UPDATE: # 只更新pose
try:
marker_context = self.marker_contexts[name]
marker_context.int_marker.pose = update.int_marker.pose
marker_context.int_marker.header = update.int_marker.header
pose_update = InteractiveMarkerPose()
pose_update.header = marker_context.int_marker.header
pose_update.pose = marker_context.int_marker.pose
pose_update.name = marker_context.int_marker.name
update_msg.poses.append(pose_update)
except:
rospy.logerr("""\
00265 Pending pose update for non-existing marker found. This is a bug in InteractiveMarkerInterface.""")

elif update.update_type == UpdateContext.ERASE: # erase
try:
marker_context = self.marker_contexts[name]
del self.marker_contexts[name]
update_msg.erases.append(name)
except:
pass
self.pending_updates = dict()

self.seq_num += 1
self.publish(update_msg)
self.publishInit()

Simple_marker Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python
import rospy

from interactive_markers.interactive_marker_server import *
from visualization_msgs.msg import *

def processFeedback(feedback):
p = feedback.pose.position
print feedback.marker_name + " is now at " + str(p.x) + ", " + str(p.y) + ", " + str(p.z)

if __name__=="__main__":
rospy.init_node("simple_marker") #初始化进程

# create an interactive marker server on the topic namespace simple_marker
server = InteractiveMarkerServer("simple_marker") # 创建interactive marker server类的对象, 命名空间是simple_marker
# 以下部分都是创建interactive marker消息的部分
# create an interactive marker for our server
int_marker = InteractiveMarker()
int_marker.header.frame_id = "base_link"
int_marker.name = "my_marker"
int_marker.description = "Simple 1-DOF Control"

# create a grey box marker
box_marker = Marker()
box_marker.type = Marker.CUBE
box_marker.scale.x = 0.45
box_marker.scale.y = 0.45
box_marker.scale.z = 0.45
box_marker.color.r = 0.0
box_marker.color.g = 0.5
box_marker.color.b = 0.5
box_marker.color.a = 1.0

# create a non-interactive control which contains the box
box_control = InteractiveMarkerControl()
box_control.always_visible = True
box_control.markers.append( box_marker )

# add the control to the interactive marker
int_marker.controls.append( box_control )

# create a control which will move the box
# this control does not contain any markers,
# which will cause RViz to insert two arrows
rotate_control = InteractiveMarkerControl()
rotate_control.name = "move_x"
rotate_control.interaction_mode = InteractiveMarkerControl.MOVE_AXIS

# add the control to the interactive marker
int_marker.controls.append(rotate_control);

# add the interactive marker to our collection &
# tell the server to call processFeedback() when feedback arrives for it
server.insert(int_marker, processFeedback) # 将创建的消息和回调函数注册到interactive marker server中

# 'commit' changes and send to all clients
server.applyChanges() # 调用server中的函数
rospy.spin()

总结

所以我们定义了一个interactiveMarker的node,其本质上是构造了InteractiveMarkerServer类,并初始化了InteractiveMarker和对应的回调函数。

然后InteractiveMarkerServer发布了Markers, 客户端的Rviz则订阅到对应的消息, 同时客户端RViz本省也在接收鼠标,键盘等事件消息,一旦接收到这些msg之后就封装成InteractiveMarkerFeedback的消息发布出去。

而这些消息则进一步被Server的订阅器接收到,从而将这些消息调用回调函数进行处理,修改Markers的状态,然后再将Update之后的状态发布出去。

Server的消息再次被RViz订阅到,于是对应调整Markers的显示状态。

如此往复,完成交互。

本文作者 : zhouzongwei
原文链接 : http://yoursite.com/2019/06/26/rviz-interactive-markers/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

知识 & 情怀 | 赏或者不赏,我都在这,不声不响

微信扫一扫, 以资鼓励

微信扫一扫, 以资鼓励

支付宝扫一扫, 再接再厉

支付宝扫一扫, 再接再厉

留下足迹