ROS时钟仿真的初探

ros时钟仿真的初探

如果设置了ros时钟仿真参数,发布一个时间的clock话题,就可以控制ros系统的时间。
但是当发布clock话题的节点没有运行,会出现什么反应?
我得出的答案是
当其他所有节点线程遇到延时,执行rospy.sleep,rate.sleep等函数时,会一直卡住,while循环不跳出。一直等待 ,直到时钟初始化。

让我们顺腾摸瓜一起看看ros 的python代码是如何运作的。
过程中对代码进行逻辑分析,增加注释,这样更好的理解ros系统底层组织结构。
甚至照猫画虎依葫芦画瓢,开发自己的机器人系统。哈哈,别吹了。
赶紧用咱们程序猿的透视眼,再开启一目十行的代码浏览模式,行动吧!

先附上官方API文档
https://docs.ros.org/api/rospy/html/rospy-module.html#sleep
可以对照在线说明,在本地找到相关的py文件。以win10+melodic为例。


从包含判断仿真参数的python文件开始:
C:\opt\ros\melodic\x64\lib\site-packages\rospy\impl
simtime.py
注释内部使用 支持时钟仿真

# ROS clock topics and parameter config 参数配置
_ROSCLOCK = '/clock'
_USE_SIMTIME = '/use_sim_time'

# Subscriber handles for /clock and /time订阅者
_rostime_sub = None
_rosclock_sub = None

#判断是否使用仿真
#请求master获取参数 返回
def _is_use_simtime():
    # in order to prevent circular dependencies, this does not use the
    # builtin libraries for interacting with the parameter server, at least
    # until I reorganize the client vs. internal APIs better.
    master_uri = rosgraph.get_master_uri()
    m = rospy.core.xmlrpcapi(master_uri)
    code, msg, val = m.getParam(rospy.names.get_caller_id(), _USE_SIMTIME)
    if code == 1 and val:
        return True
    return False

from rospy.rostime import _set_rostime
def _set_rostime_clock_wrapper(time_msg):#这里是订阅时钟的callback
    _set_rostime(time_msg.clock)#收到了就进行设置,这个设置函数定义在哪里呢??------->_set_rostime问题
def _set_rostime_time_wrapper(time_msg):
    _set_rostime(time_msg.rostime)
    
def init_simtime():
    """
    Initialize the ROS time system by connecting to the /time topic
    IFF the /use_sim_time parameter is set.
    如果使用仿真参数设置了,ros系统时钟是 发布的/time话题 初始
    """    
    import logging
    logger = logging.getLogger("rospy.simtime")
    try:
        if not _is_use_simtime():
            logger.info("%s is not set, will not subscribe to simulated time [%s] topic"%(_USE_SIMTIME, _ROSCLOCK))
        else:
            global _rostime_sub, _clock_sub
            if _rostime_sub is None:
                logger.info("initializing %s core topic"%_ROSCLOCK)
#使用仿真这里订阅
                _clock_sub = rospy.topics.Subscriber(_ROSCLOCK, Clock, _set_rostime_clock_wrapper, queue_size=1)
                logger.info("connected to core topic %s"%_ROSCLOCK)
#初始为0,。。。等待订阅执行callback进行设置
                _set_rostime(rospy.rostime.Time(0, 0))
#设置时间已经初始化的标记
        rospy.rostime.set_rostime_initialized(True)#1111111111111
        return True
    except Exception as e:
        logger.error("Unable to initialize %s: %s\n%s", _ROSCLOCK, e, traceback.format_exc())
        return False


从目录在C:\Users\Administrator\.ros\log的log文件可以看到时钟是否是仿真的状态。

master的log显示增加/use_sim_time参数

[rosmaster.master][INFO] 2019-11-17 23:00:06,096: +PARAM [/use_sim_time] by /roslaunch


是仿真状态时节点log输出

[rospy.simtime][INFO] 2019-11-17 23:47:16,391: initializing /clock core topic
[rospy.simtime][INFO] 2019-11-17 23:47:16,401: connected to core topic /clock


master的log显示订阅了 clock话题

[rosmaster.master][INFO] 2019-11-17 23:47:16,401: +SUB [/clock] /serial_node http://SC-201904021108:59930/


如果不是仿真节点输出如下

[rospy.simtime][INFO] 2019-11-19 17:22:26,328: /use_sim_time is not set, will not subscribe to simulated time [/clock] topic

_set_rostime定义在
C:\opt\ros\melodic\x64\lib\site-packages\rospy
rostime.py

## /time support. This hooks into the rospy Time representation and
## allows it to be overriden with data from the /time topic.

_rostime_initialized = False  #初始化标记
_rostime_current = None
_rostime_cond = threading.Condition()

# subclass genpy to provide abstraction layer
class Duration(genpy.Duration):#继承自genpy.Duration
class Time(genpy.Time):#继承自genpy.Time

def _set_rostime(t):
    print("---------------------_set_rostime(",t,")")
    """Callback to update ROS time from a ROS Topic"""
    if isinstance(t, genpy.Time):
        t = Time(t.secs, t.nsecs)
    elif not isinstance(t, Time):
        raise ValueError("must be Time instance: %s"%t.__class__)
    global _rostime_current
    _rostime_current = t
    try:
        _rostime_cond.acquire()
        _rostime_cond.notifyAll()
    finally:
        _rostime_cond.release()
    
def get_rostime():
    """
    Get the current time as a L{Time} object    
    @return: current time as a L{rospy.Time} object
    @rtype: L{Time}
    """
    if not _rostime_initialized:#先判断初始化标记_rostime_initialized,Have you called init_node()?看来这个标记是在init_node中设置的。
        raise rospy.exceptions.ROSInitException("time is not initialized. Have you called init_node()?")
    if _rostime_current is not None:
        # initialize with sim time初始化仿真时间
        #rospy.loginfo('notnone get_rostime=%s'%_rostime_current  )
        print("notnone get_rostime=",_rostime_current) 
        return _rostime_current
    else:
        print("--------------------none get_rostime=", time.time()) #非仿真时的设置
        # initialize with wallclock
        float_secs = time.time()
        secs = int(float_secs)
        nsecs = int((float_secs - secs) * 1000000000)
        #rospy.loginfo('none get_rostime=%s'%secs  )
        return Time(secs, nsecs)

def set_rostime_initialized(val):#init_node中调用这个函数。------------------------>找找 init_node
    """
    Internal use.
    Mark rostime as initialized. This flag enables other routines to
    throw exceptions if rostime is being used before the underlying
    system is initialized.
    @param val: value for initialization state
    @type  val: bool
    """
    print("--------------------set_rostime_initialized",val) 
    global _rostime_initialized
    _rostime_initialized = val

def is_rostime_initialized():
   """
    Internal use.
    @return: True if rostime has been initialized
    @rtype: bool
    """
    return _rostime_initialized    

def get_rostime_cond():
    """
    internal API for helper routines that need to wait on time updates
    @return: rostime conditional var
    @rtype: threading.Cond
    """
    return _rostime_cond

def is_wallclock():
    """
    Internal use for ROS-time routines.
    @return: True if ROS is currently using wallclock time
    @rtype: bool
    """
    return _rostime_current == None
    
def switch_to_wallclock():
    """
    Internal use.
    Switch ROS to wallclock time. This is mainly for testing purposes.
    """
    global _rostime_current
    _rostime_current = None
    try:
        _rostime_cond.acquire()
        _rostime_cond.notifyAll()
    finally:
        _rostime_cond.release()

def wallsleep(duration):
    """
    Internal use.
    Windows interrupts time.sleep with an IOError exception
    when a signal is caught. Even when the signal is handled
    by a callback, it will then proceed to throw IOError when
    the handling has completed. 

    Refer to https://code.ros.org/trac/ros/ticket/3421.

    So we create a platform dependant wrapper to handle this
    here.
    """
    if sys.platform in ['win32']: # cygwin seems to be ok
        try:
            time.sleep(duration)
        except IOError:
            pass
    else:
        time.sleep(duration) 


父类
genpy.Duration
genpy.Time

的定义
C:\opt\ros\melodic\x64\lib\site-packages\genpy
rostime.py

其中主要的内容

class TVal(object):
class Time(TVal):
class Duration(TVal):

 init_node
创建节点初始化节点必然调用
C:\opt\ros\melodic\x64\lib\site-packages\rospy
client.py

_init_node_args = None

def init_node(name, argv=None, anonymous=False, log_level=None, disable_rostime=False, disable_rosout=False, disable_signals=False, xmlrpc_port=0, tcpros_port=0):
    """
    Register client node with the master under the specified name.
    This MUST be called from the main Python thread unless
    disable_signals is set to True. Duplicate calls to init_node are
    only allowed if the arguments are identical as the side-effects of
    this method are not reversible.

    @param name: Node's name. This parameter must be a base name,
        meaning that it cannot contain namespaces (i.e. '/')
    @type  name: str
    
    @param argv: Command line arguments to this program, including
        remapping arguments (default: sys.argv). If you provide argv
        to init_node(), any previously created rospy data structure
        (Publisher, Subscriber, Service) will have invalid
        mappings. It is important that you call init_node() first if
        you wish to provide your own argv.
    @type  argv: [str]

    @param anonymous: if True, a name will be auto-generated for the
        node using name as the base.  This is useful when you wish to
        have multiple instances of the same node and don't care about
        their actual names (e.g. tools, guis). name will be used as
        the stem of the auto-generated name. NOTE: you cannot remap
        the name of an anonymous node.  
    @type anonymous: bool

    @param log_level: log level for sending message to /rosout and log
        file, which is INFO by default. For convenience, you may use
        rospy.DEBUG, rospy.INFO, rospy.ERROR, rospy.WARN, rospy.FATAL
    @type  log_level: int
    
    @param disable_signals: If True, rospy will not register its own
        signal handlers. You must set this flag if (a) you are unable
        to call init_node from the main thread and/or you are using
        rospy in an environment where you need to control your own
        signal handling (e.g. WX). If you set this to True, you should
        call rospy.signal_shutdown(reason) to initiate clean shutdown.

        NOTE: disable_signals is overridden to True if
        roslib.is_interactive() is True.
        
    @type  disable_signals: bool
    
    @param disable_rostime: for internal testing only: suppresses
        automatic subscription to rostime-----------------------------抑制rostime(仿真)的自动订阅。false 默认不抑制仿真。false==仿真
    @type  disable_rostime: bool

    @param disable_rosout: for internal testing only: suppress
        auto-publication of rosout
    @type  disable_rostime: bool

    @param xmlrpc_port: If provided, it will use this port number for the client
        XMLRPC node. 
    @type  xmlrpc_port: int

    @param tcpros_port: If provided, the TCPROS server will listen for
        connections on this port
    @type  tcpros_port: int

    @raise ROSInitException: if initialization/registration fails
    @raise ValueError: if parameters are invalid (e.g. name contains a namespace or is otherwise illegal)
    """
    if argv is None:
        argv = sys.argv
    else:
        # reload the mapping table. Any previously created rospy data
        # structure does *not* reinitialize based on the new mappings.
        rospy.names.reload_mappings(argv)

    if not name:
        raise ValueError("name must not be empty")

    # this test can be eliminated once we change from warning to error in the next check
    if rosgraph.names.SEP in name:
        raise ValueError("namespaces are not allowed in node names")

    global _init_node_args

    # #972: allow duplicate init_node args if calls are identical
    # NOTE: we don't bother checking for node name aliases (e.g. 'foo' == '/foo').
    if _init_node_args:
        if _init_node_args != (name, argv, anonymous, log_level, disable_rostime, disable_signals):
            raise rospy.exceptions.ROSException("rospy.init_node() has already been called with different arguments: "+str(_init_node_args))
        else:
            return #already initialized

    # for scripting environments, we don't want to use the ROS signal
    # handlers
    disable_signals = disable_signals or roslib.is_interactive()
    _init_node_args = (name, argv, anonymous, log_level, disable_rostime, disable_signals)
        
    if not disable_signals:
        # NOTE: register_signals must be called from main thread
        rospy.core.register_signals() # add handlers for SIGINT/etc...
    else:
        logdebug("signal handlers for rospy disabled")

    # check for name override
    mappings = rospy.names.get_mappings()
    if '__name' in mappings:
        name = mappings['__name']
        if anonymous:
            logdebug("[%s] WARNING: due to __name setting, anonymous setting is being changed to false"%name)
            anonymous = False
        
    if anonymous:
        # not as good as a uuid/guid, but more readable. can't include
        # hostname as that is not guaranteed to be a legal ROS name
        name = "%s_%s_%s"%(name, os.getpid(), int(time.time()*1000))

    # check for legal base name once all changes have been made to the name
    if not rosgraph.names.is_legal_base_name(name):
        import warnings
        warnings.warn("'%s' is not a legal ROS base name. This may cause problems with other ROS tools."%name, stacklevel=2)

    # use rosgraph version of resolve_name to avoid remapping
    resolved_node_name = rosgraph.names.resolve_name(name, rospy.core.get_caller_id())
    rospy.core.configure_logging(resolved_node_name)
    # #1810
    rospy.names.initialize_mappings(resolved_node_name)
    
    logger = logging.getLogger("rospy.client")
    logger.info("init_node, name[%s], pid[%s]", resolved_node_name, os.getpid())
            
    # node initialization blocks until registration with master-------------------向master注册
    node = rospy.impl.init.start_node(os.environ, resolved_node_name, port=xmlrpc_port, tcpros_port=tcpros_port) 
    rospy.core.set_node_uri(node.uri)
    rospy.core.add_shutdown_hook(node.shutdown)    
          
    if rospy.core.is_shutdown():
        logger.warn("aborting node initialization as shutdown has been triggered")
        raise rospy.exceptions.ROSInitException("init_node interrupted before it could complete")

    # upload private params (set via command-line) to parameter server
    _init_node_params(argv, name)

    rospy.core.set_initialized(True)

    if not disable_rosout:#非 禁止rosout
        rospy.impl.rosout.init_rosout()#创建/rosout话题的发布者
        rospy.impl.rosout.load_rosout_handlers(log_level)
    #zzz time.sleep(3) 
    
    if not disable_rostime: #disable_rostime名字有点异议,注释说 disable_rostime代表抑制仿真。
        #仿真。
        #订阅clock话题再 set_rostime_initialized(True)标记
        #rospy.loginfo("zzzinit_node1" )
        #---------------------看看前文就知道rospy.impl.simtime.init_simtime()是不是做了这些工作。
        if not rospy.impl.simtime.init_simtime():
            raise rospy.exceptions.ROSInitException("Failed to initialize time. Please check logs for additional details")
    else:
        #不仿真直接标记
        rospy.rostime.set_rostime_initialized(True)#1111111111111
        
    logdebug("init_node, name[%s], pid[%s]", resolved_node_name, os.getpid())    
    # advertise logging level services
    Service('~get_loggers', GetLoggers, _get_loggers)
    Service('~set_logger_level', SetLoggerLevel, _set_logger_level)


C:\opt\ros\melodic\x64\lib\site-packages\rospy
timer.py

class Rate(object):
    """
    Convenience class for sleeping in a loop at a specified rate
    """
    
    def __init__(self, hz, reset=False):
        """
        Constructor.
        @param hz: hz rate to determine sleeping
        @type  hz: float
        @param reset: if True, timer is reset when rostime moved backward. [default: False]
        @type  reset: bool
        """
        # #1403
        self.last_time = rospy.rostime.get_rostime()
        self.sleep_dur = rospy.rostime.Duration(0, int(1e9/hz))
        self._reset = reset

    def _remaining(self, curr_time):
        """
        Calculate the time remaining for rate to sleep.
        @param curr_time: current time
        @type  curr_time: L{Time}
        @return: time remaining
        @rtype: L{Time}
        """
        # detect time jumping backwards
        if self.last_time > curr_time:
            self.last_time = curr_time

        # calculate remaining time
        elapsed = curr_time - self.last_time
        return self.sleep_dur - elapsed

    def remaining(self):
        """
        Return the time remaining for rate to sleep.
        @return: time remaining
        @rtype: L{Time}
        """
        curr_time = rospy.rostime.get_rostime()
        return self._remaining(curr_time)

    def sleep(self):
        """
        Attempt sleep at the specified rate. sleep() takes into
        account the time elapsed since the last successful
        sleep().
        
        @raise ROSInterruptException: if ROS shutdown occurs before
        sleep completes
        @raise ROSTimeMovedBackwardsException: if ROS time is set
        backwards
        """
        curr_time = rospy.rostime.get_rostime()
        try:
            sleep(self._remaining(curr_time))
        except rospy.exceptions.ROSTimeMovedBackwardsException:
            if not self._reset:
                raise
            self.last_time = rospy.rostime.get_rostime()
            return
        self.last_time = self.last_time + self.sleep_dur

        # detect time jumping forwards, as well as loops that are
        # inherently too slow
        if curr_time - self.last_time > self.sleep_dur * 2:
            self.last_time = curr_time

def sleep(duration):
    rospy.loginfo('zzz  sleep')
    """
    sleep for the specified duration in ROS time. If duration
    is negative, sleep immediately returns.
    
    @param duration: seconds (or rospy.Duration) to sleep
    @type  duration: float or Duration
    @raise ROSInterruptException: if ROS shutdown occurs before sleep
    completes
    @raise ROSTimeMovedBackwardsException: if ROS time is set
    backwards
    """
    if rospy.rostime.is_wallclock():
        rospy.loginfo('zzzrospy.rostime.is_wallclock')
        if isinstance(duration, genpy.Duration):
            duration = duration.to_sec()
        if duration < 0:
            return
        else:
            rospy.rostime.wallsleep(duration)

    else:
        initial_rostime = rospy.rostime.get_rostime()
        if not isinstance(duration, genpy.Duration):
            duration = genpy.Duration.from_sec(duration)

        rostime_cond = rospy.rostime.get_rostime_cond()

        # #3123
        if initial_rostime == genpy.Time(0):
            # break loop if time is initialized or node is shutdown 
            #也就是说没初始化&&没有 shutdown一直循环等待重复设置initial_rostime。initial_rostime == genpy.Time(0)是没有初始化
            while initial_rostime == genpy.Time(0) and \
                      not rospy.core.is_shutdown():
                with rostime_cond:
                    rostime_cond.wait(0.3)
                initial_rostime = rospy.rostime.get_rostime()
                rospy.loginfo('sleep_t=%s'%initial_rostime  )

        sleep_t = initial_rostime + duration
        rospy.loginfo('sleep_t=%s'%sleep_t )
        # break loop if sleep_t is reached, time moves backwards, or
        # node is shutdown
        #sleep时间没到&&时间没有倒流&&节点没有关闭一直循环
        while rospy.rostime.get_rostime() < sleep_t and \
              rospy.rostime.get_rostime() >= initial_rostime and \
                  not rospy.core.is_shutdown():
            with rostime_cond:
                rostime_cond.wait(0.5)
                

        if rospy.rostime.get_rostime() < initial_rostime:
            time_jump = (initial_rostime - rospy.rostime.get_rostime()).to_sec()
            raise rospy.exceptions.ROSTimeMovedBackwardsException(time_jump)
        if rospy.core.is_shutdown():
            raise rospy.exceptions.ROSInterruptException("ROS shutdown request")

class TimerEvent(object):
    """
    Constructor.
    @param last_expected: in a perfect world, this is when the previous callback should have happened
    @type  last_expected: rospy.Time
    @param last_real: when the callback actually happened
    @type  last_real: rospy.Time
    @param current_expected: in a perfect world, this is when the current callback should have been called
    @type  current_expected: rospy.Time
    @param last_duration: contains the duration of the last callback (end time minus start time) in seconds.
                          Note that this is always in wall-clock time.
    @type  last_duration: float
    """
    def __init__(self, last_expected, last_real, current_expected, current_real, last_duration):
        self.last_expected    = last_expected
        self.last_real        = last_real
        self.current_expected = current_expected
        self.current_real     = current_real
        self.last_duration    = last_duration

class Timer(threading.Thread):#-----------------------------继承了线程,以一定频率触发的计时器
    """
    Convenience class for calling a callback at a specified rate
    """

    def __init__(self, period, callback, oneshot=False, reset=False):
        """
        Constructor.
        @param period: desired period between callbacks
        @type  period: rospy.Duration
        @param callback: callback to be called
        @type  callback: function taking rospy.TimerEvent
        @param oneshot: if True, fire only once, otherwise fire continuously until shutdown is called [default: False]
        @type  oneshot: bool
        @param reset: if True, timer is reset when rostime moved backward. [default: False]
        @type  reset: bool
        """
        super(Timer, self).__init__()
        self._period   = period
        self._callback = callback
        self._oneshot  = oneshot
        self._reset = reset
        self._shutdown = False
        self.daemon = True
        self.start()

    def shutdown(self):
        """
        Stop firing callbacks.
        """
        self._shutdown = True
        
    def run(self):
        r = Rate(1.0 / self._period.to_sec(), reset=self._reset)
        current_expected = rospy.rostime.get_rostime() + self._period
        last_expected, last_real, last_duration = None, None, None
        while not rospy.core.is_shutdown() and not self._shutdown:
            try:
                r.sleep()
            except rospy.exceptions.ROSInterruptException as e:
                if rospy.core.is_shutdown():
                    break
                raise
            if self._shutdown:
                break
            current_real = rospy.rostime.get_rostime()
            start = time.time()
            self._callback(TimerEvent(last_expected, last_real, current_expected, current_real, last_duration))
            if self._oneshot:
                break
            last_duration = time.time() - start
            last_expected, last_real = current_expected, current_real
            current_expected += self._period

这里就比较亲切了,是一个发布talker节点的示例

import rospy
from std_msgs.msg import String

def talker():
    pub = rospy.Publisher('chatter', String, queue_size=10)
    rospy.init_node('talker', anonymous=True)#----------------------这里init_node,每个节点必不可少。
    rate = rospy.Rate(10) # 10hz
    rospy.loginfo("---------------------talkertest")
    while not rospy.is_shutdown():
        hello_str = "hello world %s" % rospy.get_time()
        rospy.loginfo(hello_str)
        pub.publish(hello_str)
        rospy.loginfo("while")
        rate.sleep()

if __name__ == '__main__':
    try:
        talker()
    except rospy.ROSInterruptException:
        pass
发布了78 篇原创文章 · 获赞 76 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/qq_38288618/article/details/103153996