Package: rosros
rosrosSimple unified interface to ROS1 / ROS2 Python API.Auto-detects ROS version from run-time environment, and provides
equivalent functionality in either ROS version.Main use-cases:helper library for working with ROS messages and data typesreplacement for ROS1 / ROS2 Python APIdrop-in replacement forrospyto convert a ROS1 package to a ROS1/ROS2 packagedrop-in replacement forrclpyto convert a ROS2 package to a ROS1/ROS2 packageRequires the corresponding ROS Python libraries:rospyfamily for ROS1,rclpyfor ROS2.Full API documentation available athttps://suurjaak.github.io/rosros.Example usageParametersConstructor args in publishers and servicesTimersBagsROS core functionalityPatches in ROS1Patches in ROS2API helpersConverting an existing packageExisting ROS1 package to ROS1/ROS2 packageExisting ROS2 package to ROS1/ROS2 packageRunning testsDependenciesAttributionLicenseExample usageA simple node that publishes a message on receiving a service request:importrosrosdefon_trigger(req):pub.publish(True)return{"success":True,"message":"Triggered!"}rosros.init_node("mynode")params=rosros.init_params(service="/trigger",topic="/triggerings")srv=rosros.create_service(params["service"],"std_srvs/Trigger",on_trigger)pub=rosros.create_publisher(params["topic"],"std_msgs/Bool",latch=True,queue_size=2)rosros.spin()Equivalent ROS1 code using rospy:importrospy,std_msgs.msg,std_srvs.srvdefon_trigger(req):pub.publish(True)return{"success":True,"message":"Triggered!"}rospy.init_node("mynode")service=rospy.get_param("~service","/trigger")topic=rospy.get_param("~topic","/triggerings")rospy.set_param("~service",service)rospy.set_param("~topic",topic)srv=rospy.Service(service,std_srvs.srv.Trigger,on_trigger)pub=rospy.Publisher(topic,std_msgs.msg.Bool,latch=True,queue_size=2)rospy.spin()Equivalent ROS2 code using rclpy:importrclpy,rclpy.qos,std_msgs.msg,std_srvs.srvdefon_trigger(req,resp):pub.publish(std_msgs.msg.Bool(data=True))resp.success=Trueresp.message="Triggered!"returnresprclpy.init()node=rclpy.create_node("mynode")service=node.get_parameter_or("service",rclpy.Parameter("",value="/trigger")).valuetopic=node.get_parameter_or("topic",rclpy.Parameter("",value="/triggerings")).valueifnotnode.has_parameter("service"):node.declare_parameter("service",service)ifnotnode.has_parameter("topic"):node.declare_parameter("topic",topic)srv=node.create_service(std_srvs.srv.Trigger,service,on_trigger)qos=rclpy.qos.QoSProfile(depth=2,durability=rclpy.qos.DurabilityPolicy.TRANSIENT_LOCAL)pub=node.create_publisher(std_msgs.msg.Bool,topic,qos)rclpy.spin(node)Any ROS2-only extras used will be ignored under ROS1, like topic QoS reliability.Parametersrosros provides a convenient way to auto-load all node parameters,
and override or populate additional parameters from a nested dictionary.rosros.init_node("mynode")# If the given defaults were not among existing node parameters,# they will be auto-declared on the node.params=rosros.init_params({"my":{"nested":{"param1":5,"param2":True}}})Constructor args in publishers and servicesrosros provides short-hand convenience wrappers for publishing messages,
invoking services and returning service responses, without needing to
create the message or service request/response class instance explicitly.ROS1 already provides this short-hand inrospy,
but ROS2 API requires the instances to be created explicitly.rosros also supports giving message or service request as dictionary.In rosros:pub=rosros.create_publisher("/topic","std_msgs/Bool")pub.publish(True)pub.publish(data=True)pub.publish({"data":True})pub.publish()# Service callback need not accept any parameters if request object is irrelevantsrv=rosros.create_service("/service","std_srvs/SetBool",lambda:{"message":"Triggered!"})cli=rosros.create_client("/service","std_srvs/SetBool")cli.call(True)cli.call(data=True)cli.call({"data":True})cli(True)Equivalent ROS1 code using rospy:pub=rospy.Publisher("/topic",std_msgs.msg.Bool,queue_size=0)pub.publish(True)pub.publish(data=True)pub.publish(std_msgs.msg.Bool(True))pub.publish()srv=rospy.Service("/service",std_srvs.srv.SetBool,lambdareq:{"message":"Triggered!"})cli=rospy.ServiceProxy("/service",std_srvs.srv.SetBool)cli.call(True)cli.call(data=True)cli.call(std_srvs.srv.SetBoolRequest(True))cli(True)Equivalent ROS2 code using rospy:pub=mynode.create_publisher(std_msgs.msg.Bool,"/topic",0)pub.publish(std_msgs.msg.Bool(data=True))pub.publish(std_msgs.msg.Bool(data=True))pub.publish(std_msgs.msg.Bool(data=True))pub.publish(std_msgs.msg.Bool())defcallback(req,resp):resp.message="Triggered!"returnrespsrv=mynode.create_service(std_srvs.srv.SetBool,"/service",callback)cli=mynode.create_client(std_srvs.srv.SetBool,"/service")cli.call(std_srvs.srv.SetBool.Request(data=True))Timersrosros provides convenience one-shot timers in ROS2 like ROS1 does.Additionally, rosros provides an option to start invoking the timer callback
immediately, instead of waiting one period before first call.timer=rosros.create_timer(2,lambda:print("just once, after 2 sec"),oneshot=True)timer=rosros.create_timer(5,lambda:print("every 5 sec, starting now"),immediate=True)# Can take seconds, can take an explicit Duration objecttimer=rosros.create_timer(rosros.api.make_duration(5),lambda:print("every 5 sec"))Bagsrosros provides a unified interface for reading and writing ROS bags.Bag format is the custom ROS format in ROS1, and the SQLite database format in ROS2.bag=rosros.Bag("my.bag",mode="a")print(bag)# Prints bag metainfo, equivalent to "rosbag info"bag.write("/my/topic",std_msgs.msg.Bool())fortopic,message,stampinbag:print("[%s]%s:%s"%(rosros.api.to_sec(stamp),topic,message))ROS core functionalityFunctionality for creating and operating a ROS node.Objects returned are standardrospy/rclpyAPI objects, patched with
additional members for a unified interfaceconforming to ROS1 API.NameDescriptionArgumentsStartup, spin and shutdownrosros.init_nodeinitializes ROS and creates ROS node (and returns Node object in ROS2)name, args=None, namespace=None, anonymous=True, log_level=None, enable_rosout=True, multithreaded=True, reentrant=Falserosros.start_spinsets ROS node spinning forever in a background threadrosros.spinspins ROS node foreverrosros.spin_oncewaits until timeout in ROS1; executes one ROS operation or waits until timeout in ROS2timeout=Nonerosros.spin_until_future_completespins ROS until future complete or timeout reached or ROS shut downfuture, timeout=Nonerosros.okreturns whether ROS has been initialized and is not shut downrosros.on_shutdownregisters function to be called on shutdowncallback, *args, **kwargsrosros.shutdownshuts down live ROS node, if anyreason=NoneParametersrosros.init_paramssets all parameters on node from defaults dictionary, returns full parameters dictionary.defaults=None, **defaultkwsIn ROS2, auto-declares unregistered parameters.rosros.has_paramreturns whether the parameter existsnamerosros.get_paramreturns parameter value from current ROS nodename, default=None, autoset=Truerosros.get_param_namesreturns the names of all current ROS node parametersrosros.get_paramsreturns the current ROS node parameters, by default as nested dictionarynested=Truerosros.set_paramsets a parameter on the node.name, value, descriptor=NoneIn ROS2, parameter will be auto-declared if unknown so far.rosros.delete_paramdeletes parameter from the nodenameTopics, services, timers and ratesrosros.create_publisherreturns a ROS publisher instancetopic, cls_or_typename, latch=False, queue_size=0, **qosargsrosros.create_subscriberreturns a ROS subscriber instancetopic, cls_or_typename, callback, callback_args=None, queue_size=0, raw=False, **qosargsrosros.create_servicereturns a ROS service server instance, for providing a serviceservice, cls_or_typename, callback, **qosargsrosros.create_clientreturns a ROS service client instance, for invoking a serviceservice, cls_or_typename, **qosargsrosros.create_timerreturns a ROS timer instanceperiod, callback, oneshot=False, immediate=Falserosros.create_ratereturns a ROS rate instance, for sleeping at a fixed ratefrequencyrosros.destroy_entitycloses the given publisher, subscriber, service client, service server, or timer instanceitemrosros.AnyMsgrospy.AnyMsgin ROS1, stand-in class with equivalent functionality in ROS2rosros.sleepsleeps for the specified duration in ROS timedurationrosros.wait_for_publisherblocks until topic has at least one publishertopic, timeout=None, cls_or_typename=Nonerosros.wait_for_subscriberblocks until topic has at least one subscribertopic, timeout=None, cls_or_typename=Nonerosros.wait_for_serviceblocks until service is availableservice, timeout=None, cls_or_typename=NoneROS bagsrosros.BagROS bag reader and writer;rosbag.Bagin ROS1 with some additional functionality;equivalent class in ROS2, using the ROS2 default SQLite formatInformation queriesrosros.get_namespacereturns ROS node namespacerosros.get_node_namereturns ROS node full name with namespacerosros.get_nodesreturns all ROS nodes, as[node full name, ]rosros.get_topicsreturns all available ROS topics, as[(topic name, [type name, ]), ]rosros.get_servicesreturns all available ROS services, as[(service name, [type name, ]), ]rosros.remap_namereturns the absolute remapped topic/service name if mapping existsname, namespace=Nonerosros.resolve_namereturns absolute remapped name, namespaced under current node if relative or privatename, namespace=NoneMiscellaneousrosros.get_loggerreturnslogging.Loggerfor logging to ROS log handler;logger supports additional keywords__once__,__throttle__,__throttle_identical__rosros.get_ros_versionreturns ROS version information as a data dictionaryrosros.get_rostimereturns ROS time instance for current ROS clock timerosros.register_initinformsrosrosof ROS having been initialized outside ofinit_node().node=NoneGiving node as argument is mandatory in ROS2.Patches in ROS1Additional functionality patched ontorospyclasses for convenience:rospy.ServiceProxy.call_async(*args, **kwargs):makes service call in a background thread, returnsconcurrent.futures.Future-like responserospy.ServiceProxy.wait_for_service(timeout=None, timeout_sec=None):waits for service to become available, returnsTrue, orFalseon timeoutrospy.ServiceProxy.service_is_ready():returns whether service is currently availablerospy.Subscriber:callbacks are invoked with serialized message bytes if subscription was created withraw=Truerosbag.Bag.get_message_definition(msg_or_type):returns ROS1 message type definition full text from bagrosbag.Bag.get_message_class(typename, typehash=None):returns ROS1 message classrosbag.Bag.get_message_type_hash(msg_or_type):returns ROS1 message type MD5 hashrosbag.Bag.get_topic_info():returns topic and message type metainfo as{(topic, typename, typehash): count}rosbag.Bag.reindex_file(f):reindexes bag file on disk; makes a temporary copy in file directoryPatches in ROS2The following classes are patched with full conformity to their equivalents in ROS:rclpy.client.Clientrclpy.publisher.Publisherrclpy.service.Servicerclpy.subscription.Subscriptionrclpy.duration.Durationrclpy.time.Timerclpy.timer.Raterclpy.timer.TimerE.g. durations support+-*/arithmetic, and publisher / subscriber provide.md5sumfor message type hash.API helpersFunctionality for working with message types and data and metainfo.
Can be used as stand-alone library functions without initializing rosros core.NameDescriptionArgumentsMessage types and instancesrosros.api.get_message_classreturns ROS message / service class object, likestd_msgs.msg.Boolfor"std_msgs/Bool"msg_or_typerosros.api.get_message_definitionreturns ROS message or service request/response type definition text,msg_or_type, full=Trueby default including message subtype definitionsrosros.api.get_message_fieldsreturns{field name: field type name}if ROS message or service request/response, else{}valrosros.api.get_message_headerreturns messageHeader-attribute if any, elseNonevalrosros.api.get_message_typereturns ROS message / service canonical type name, like"std_msgs/Header"msg_or_clsor"*"forAnyMsgrosros.api.get_message_type_hashreturns ROS message / service type MD5 hashmsg_or_typerosros.api.get_message_valuereturns message attribute value, with numeric arrays converted to listsmsg, name, default=...rosros.api.is_ros_messagereturns whether value is a ROS message or service request/response class or instancevalMessage conversionrosros.api.deserialize_messagereturns ROS message or service request/response instantiated from serialized binaryraw, cls_or_typenamerosros.api.dict_to_messagereturns ROS message populated from Python dictionarydct, msg_or_typerosros.api.message_to_dictreturns ROS message as nested Python dictionary;msg, replace=Nonewith optional replacements for certain values like{math.nan: None}rosros.api.message_to_strreturns ROS message as an evaluatable string, e.g."std_msgs.msg.UInt8(data=0)"msg, indent=Nonerosros.api.serialize_messagereturns ROS message or service request/response as a serialized binary ofbytes()msgService classes and definitionsrosros.api.get_service_definitionreturns ROS service type definition textsrv_or_typerosros.api.get_service_request_classreturns ROS service request class objectsrv_or_typerosros.api.get_service_response_classreturns ROS service response class objectsrv_or_typerosros.api.is_ros_servicereturns whether value is a ROS service class objectvalTime and durationrosros.api.is_ros_timereturns whether value is a ROS time/durationvalrosros.api.make_durationreturns a ROS durationsecs=0, nsecs=0rosros.api.make_timereturns a ROS timesecs=0, nsecs=0rosros.api.time_categoryreturns "time" or "duration" for time/duration type, else valuemsg_or_typerosros.api.time_messagereturns ROS2 time/duration fromrclpyasbuiltin_interfacesor vice versa,val, to_message=True, clock_type=Noneor value if not convertiblerosros.api.to_datetimereturns value asdatetime.datetimeif value is ROS time/duration, else valuevalrosros.api.to_decimalreturns value asdecimal.Decimalif value is ROS time/duration, else valuevalrosros.api.to_durationreturns value as ROS duration if convertible (int/float/time/datetime/decimal), else valuevalrosros.api.to_nsecreturns value in nanoseconds if value is ROS time/duration, else valuevalrosros.api.to_secreturns value in seconds if value is ROS time/duration, else valuevalrosros.api.to_sec_nsecreturns value in (seconds, nanoseconds) if value is ROS time/duration, else valuevalrosros.api.to_timereturns value as ROS time if convertible (int/float/duration/datetime/decimal), else valuevalClass and type namesrosros.api.canonicalreturns"pkg/Type"for"pkg/subdir/Type"typenamerosros.api.get_alias_typereturns ROS built-in type for alias like"char", if any; reverse ofget_type_alias().typenameIn ROS1,byteandcharare aliases forint8anduint8; in ROS2 the reverse.rosros.api.get_type_aliasreturns alias like"char"for ROS built-in type, if any; reverse ofget_alias_type().typenameIn ROS1,byteandcharare aliases forint8anduint8; in ROS2 the reverse.rosros.api.make_full_typenamereturns"pkg/msg/Type"or"pkg/srv/Type"for"pkg/Type"typename, category="msg"rosros.api.scalarreturns scalar type from ROS message data type, like"uint8"fromuint8-array;typenamein ROS2, returns unbounded type, e.g."string"from"string<=10[<=5]".Miscellaneousrosros.api.format_param_namereturns parameter name with correct separator for ROS version, and leading sigils strippednamerosros.api.PARAM_SEPARATORseparator char between ROS parameter namespace partsrosros.api.ROS_BUILTIN_TYPESlist of ROS built-in numeric and string type names, like["string", "uint8", ..]rosros.api.ROS_NUMERIC_TYPESlist of ROS built-in numeric type names, like["bool", "int8", ..]rosros.api.ROS_STRING_TYPESlist of ROS built-in string type namesrosros.api.ROS_TIME_CLASSESROS time/duration types mapped to type namesrosros.api.ROS_TIME_TYPESlist of ROS built-in time/duration type namesConverting an existing packageExisting ROS1 package to ROS1/ROS2 packagerosros can be used as a (mostly) drop-in replacement for rospy
to make a ROS1 package equally usable under ROS2 - if the code relies only
onrospy, and does not go too deeply into using ROS1 specifics likerosgraph.rospyimports can be replaced withrosros.rospify, e.g.importthreadingfromrosrosimportrospifyasrospyfromstd_msgs.msgimportStringrospy.init_node("talker",anonymous=True)pub=rospy.Publisher("chatter",String,queue_size=10)rate=rospy.Rate(10)# 10hzthreading.Thread(target=rospy.spin).start()# Concession for ROS2: spin is always neededwhilenotrospy.is_shutdown():hello_str="hello world%s"%rospy.get_time()rospy.loginfo(hello_str)pub.publish(hello_str)rate.sleep()In ROS1 it providesrospy, and in ROS2 it provides matching API classes and functions.Main behavioral differences ofrospifyin ROS2 vs ROS1:Subscriber.get_num_connections()always returns 0, as ROS2 does not provide this informationsubscribing withAnyMsgneeds the publisher existingparameter operations always apply to node parameters, as ROS2 has no global parametersfunctions registered inon_shutdown()are called after node shutdown, not beforerosros.rospify.get_master()requires local node to be initialized, unlikerospy.get_master()rosros.rospify.SubscribeListenerdoes nothingExisting ROS2 package to ROS1/ROS2 packagerosros can also be used as a (mostly) drop-in replacement for rclpy
to make a ROS2 package equally usable under ROS1 - if the code mainly relies onrclpymodule and does not go deep into using ROS2 specifics likebuiltin_interfaces.rclpyimports can be replaced withrosros.rclify, e.g.fromrosrosimportrclifyasrclpyfromstd_msgs.msgimportStringclassMinimalPublisher(rclpy.node.Node):def__init__(self):super().__init__("minimal_publisher")self.publisher_=self.create_publisher(String,"chatter",10)timer_period=0.1# secondsself.timer=self.create_timer(timer_period,self.timer_callback)self.i=0deftimer_callback(self):msg=String()msg.data="Hello World:%d"%self.iself.publisher_.publish(msg)self.get_logger().info('Publishing: "%s"'%msg.data)self.i+=1rclpy.init()minimal_publisher=MinimalPublisher()rclpy.spin(minimal_publisher)In ROS2,rclifyprovidesrclpy, and in ROS1 it provides
matching API modules and classes and functions.Covers all the parts ofrclpy.nodethat are supportable in ROS1, focused on:
creating nodes, publishing and subscribing topics, providing and invoking services,
working with rates and timers.Main behavioral differences ofrclifyin ROS1 vs ROS2:one single node as always in ROS1QoS profiles ignore all options exceptdepth(taken asqueue_size),
anddurabilityequalingTRANSIENT_LOCAL(taken aslatch=True)no contexts, no executors, no waitablesno callback groups, no guard conditions, no middleware eventsMethods pertaining to unsupported functionality do nothing and returnNone.
Unsupported arguments in methods are ignored.InstallationUsing pippip install rosrosThis will make therosroslibrary available to Python packages.Requires ROS Python packages
(ROS1: rospy, roslib, rosbag, rosservice, genpy;
ROS2: rclpy, rosidl_parser, rosidl_runtime_py, ament_index_python, builtin_interfaces).Using catkinIn a ROS1 workspace, under the source directory:git clone https://github.com/suurjaak/rosros.git
cd rosros
catkin build --thisThis will make therosroslibrary available in the ROS1 workspace.Using colconIn a ROS2 workspace, at the workspace root:git clone https://github.com/suurjaak/rosros.git src/rosros
colcon build --packages-select rosrosThis will make therosroslibrary available in the ROS2 workspace.Running testsROS1:catkin test rosrosROS2:colcon test --packages-select rosrosTo run ROS2 tests with full log output:colcon test --packages-select rosros --event-handlers console_direct+ \
            --pytest-args "--capture=tee-sys"Using pytest, either ROS1 or ROS2 (roscore needs to be running if ROS1):cd rosros
pytestDependenciespyyaml (https://pypi.org/project/PyYAML)ROS1:genpyrosbagroslibrospyrosserviceROS2:ament_index_pythonbuiltin_interfacesrclpyrosidl_parserrosidl_runtime_pyROS2 test dependencies:pytest (https://pypi.org/project/pytest)pytest-forked (https://pypi.org/project/pytest-forked)For generating API documentation:doxypypy (https://pypi.org/project/doxypypy;
needs latest master:pip install git+https://github.com/Feneric/doxypypy)AttributionThis package includes partially copied and modified code fromrclpycore library,
released under the Apache-2.0 license.
Seedoc/3rd-party licenses.txtfor full details.LicenseCopyright (c) 2022 by Erki Suurjaak.
Released as free open source software under the BSD License,
see LICENSE.md for full details.