Skip to main content

ROS2 Integration

Bot Pulse works alongside ROS2. This tutorial shows how to instrument a ROS2 node with traces, logs, and metrics using the Bot Pulse Python API.

Setup

Install Bot Pulse alongside your ROS2 workspace:

pip install botpulse

No ROS2-specific wrapper is needed — Bot Pulse is a pure Python library that works in any process.

Example: Instrumented navigation node

Here's a ROS2 node that uses Bot Pulse for full observability:

navigation_node.py
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped
import botpulse
import time


class NavigationNode(Node):
def __init__(self):
super().__init__("navigation_node")
self.pulse = botpulse.init(
service_name="navigation",
robot_id="robot-01",
)
self.waypoint_sub = self.create_subscription(
PoseStamped, "/goal_pose", self.on_waypoint, 10
)
self.get_logger().info("Navigation node started")

def on_waypoint(self, msg: PoseStamped):
x = msg.pose.position.x
y = msg.pose.position.y

with self.pulse.trace("navigate_to_waypoint") as span:
span.set_attribute("waypoint.x", x)
span.set_attribute("waypoint.y", y)

self.pulse.log(f"Received waypoint ({x}, {y})")

with self.pulse.child_span("plan_path") as plan_span:
path = self.plan_path(x, y)
plan_span.set_attribute("path_length_m", path.length)

with self.pulse.child_span("follow_path") as follow_span:
self.execute_path(path)
follow_span.set_attribute("completed", True)

self.pulse.metric("waypoint_latency_ms", span.duration_ms)
self.pulse.log("Waypoint reached")

def plan_path(self, x, y):
# Your path planning logic
time.sleep(0.02)
return Path(length=((x ** 2 + y ** 2) ** 0.5))

def execute_path(self, path):
# Your trajectory execution logic
time.sleep(0.1)


def main():
rclpy.init()
node = NavigationNode()
rclpy.spin(node)
rclpy.shutdown()


if __name__ == "__main__":
main()

Example: Battery monitor node

battery_node.py
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import BatteryState
import botpulse


class BatteryNode(Node):
def __init__(self):
super().__init__("battery_node")
self.pulse = botpulse.init(
service_name="battery-monitor",
robot_id="robot-01",
)
self.battery_pub = self.create_publisher(BatteryState, "/battery", 10)
self.timer = self.create_timer(1.0, self.publish_battery)
self.cycle_count = 0

def publish_battery(self):
with self.pulse.trace("read_battery") as span:
voltage = self.read_voltage()
current = self.read_current()
soc = self.compute_soc(voltage)

self.pulse.gauge("battery_voltage", value=voltage)
self.pulse.gauge("battery_soc_percent", value=soc)
self.pulse.counter("battery_read_cycles", value=1)

span.set_attribute("voltage", voltage)
span.set_attribute("soc_percent", soc)

if soc < 20.0:
self.pulse.log("Battery critically low", level="warning")

msg = BatteryState()
msg.voltage = voltage
msg.current = current
self.battery_pub.publish(msg)

self.cycle_count += 1

def read_voltage(self):
return 22.4

def read_current(self):
return 3.2

def compute_soc(self, voltage):
return max(0.0, min(100.0, (voltage - 18.0) / (25.2 - 18.0) * 100))


def main():
rclpy.init()
node = BatteryNode()
rclpy.spin(node)
rclpy.shutdown()


if __name__ == "__main__":
main()

Using one BotPulse instance per process

Create the botpulse.init() instance once (in __init__) and reuse it. This ensures:

  • Trace context is shared across all operations
  • Batching and flushing are handled efficiently
  • No duplicate connections to the collector
# Good — single instance, reused
class MyNode(Node):
def __init__(self):
self.pulse = botpulse.init(service_name="my_node", robot_id="robot-01")

# Bad — creates a new connection every time
def callback(self, msg):
pulse = botpulse.init(service_name="my_node") # don't do this

Using with launch files

Bot Pulse needs no special launch configuration. Just start your node normally:

launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
return LaunchDescription([
Node(package="my_pkg", executable="navigation_node"),
Node(package="my_pkg", executable="battery_node"),
])

Environment variables

You can also configure Bot Pulse via environment variables, which is useful in containerized or multi-robot deployments:

export BOTPULSE_SERVICE_NAME=navigation-node
export BOTPULSE_ROBOT_ID=robot-01
export BOTPULSE_ENDPOINT=http://collector:4317
# Picks up configuration from environment variables
pulse = botpulse.init()

Next steps