| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- #!/usr/bin/env python3
- """Measure odometry path length and yaw without commanding the robot."""
- import math
- import threading
- import rospy
- from nav_msgs.msg import Odometry
- from std_msgs.msg import Float32, String
- from std_srvs.srv import Trigger, TriggerResponse
- from traffic_line_task.srv import MarkDistance, MarkDistanceResponse
- class RouteDistanceCalibrator:
- LABELS = (
- "first_left_turn_distance_m",
- "first_right_turn_distance_m",
- "first_straight_entry_distance_m",
- "direct_finish_distance_m",
- "straight_to_second_distance_m",
- "second_right_turn_distance_m",
- "after_second_finish_distance_m",
- )
- def __init__(self):
- rospy.init_node("route_distance_calibrator")
- self.lock = threading.RLock()
- self.odom_topic = rospy.get_param("~odom_topic", "/odom")
- self.odom_timeout = max(0.1, float(rospy.get_param("~odom_timeout", 0.5)))
- self.odom_jump_limit_m = max(
- 0.01, float(rospy.get_param("~odom_jump_limit_m", 0.25))
- )
- self.publish_rate = max(1.0, float(rospy.get_param("~publish_rate", 10.0)))
- self.position = None
- self.yaw = None
- self.last_odom_time = None
- self.total_distance = 0.0
- self.total_yaw = 0.0
- self.zero_distance = 0.0
- self.zero_yaw = 0.0
- self.odom_fault = ""
- self.distance_pub = rospy.Publisher("~distance", Float32, queue_size=1)
- self.yaw_pub = rospy.Publisher("~yaw_change", Float32, queue_size=1)
- self.last_mark_pub = rospy.Publisher(
- "~last_mark", String, queue_size=1, latch=True
- )
- self.odom_sub = rospy.Subscriber(
- self.odom_topic, Odometry, self.odom_callback, queue_size=20
- )
- self.reset_service = rospy.Service("~reset", Trigger, self.reset_callback)
- self.mark_service = rospy.Service("~mark", MarkDistance, self.mark_callback)
- self.timer = rospy.Timer(
- rospy.Duration(1.0 / self.publish_rate), self.publish_callback
- )
- rospy.loginfo(
- "Route-distance calibrator is measurement only; odom=%s. "
- "It never publishes /cmd_vel.",
- self.odom_topic,
- )
- @staticmethod
- def wrap_angle(angle):
- return math.atan2(math.sin(angle), math.cos(angle))
- @staticmethod
- def yaw_from_odometry(message):
- q = message.pose.pose.orientation
- siny_cosp = 2.0 * (q.w * q.z + q.x * q.y)
- cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
- return math.atan2(siny_cosp, cosy_cosp)
- def odom_callback(self, message):
- with self.lock:
- point = message.pose.pose.position
- current_position = (float(point.x), float(point.y))
- current_yaw = self.yaw_from_odometry(message)
- if self.position is not None:
- step = math.hypot(
- current_position[0] - self.position[0],
- current_position[1] - self.position[1],
- )
- if step > self.odom_jump_limit_m:
- self.odom_fault = "ODOM_JUMP: %.3f m" % step
- else:
- self.total_distance += step
- if self.yaw is not None:
- self.total_yaw += self.wrap_angle(current_yaw - self.yaw)
- self.position = current_position
- self.yaw = current_yaw
- self.last_odom_time = rospy.get_time()
- def measurement(self):
- return (
- max(0.0, self.total_distance - self.zero_distance),
- self.total_yaw - self.zero_yaw,
- )
- def odom_error(self):
- if self.odom_fault:
- return self.odom_fault
- if self.last_odom_time is None:
- return "ODOM_UNAVAILABLE"
- age = rospy.get_time() - self.last_odom_time
- if age > self.odom_timeout:
- return "ODOM_TIMEOUT: %.3f s" % age
- return ""
- def reset_callback(self, _request):
- with self.lock:
- error = self.odom_error()
- if error and error != self.odom_fault:
- return TriggerResponse(success=False, message=error)
- self.odom_fault = ""
- self.zero_distance = self.total_distance
- self.zero_yaw = self.total_yaw
- self.last_mark_pub.publish(String(data=""))
- rospy.loginfo("Route-distance calibration measurement reset to zero.")
- return TriggerResponse(success=True, message="measurement reset")
- def mark_callback(self, request):
- with self.lock:
- label = str(request.label).strip()
- if label not in self.LABELS:
- return MarkDistanceResponse(
- success=False,
- distance_m=0.0,
- yaw_rad=0.0,
- message="unknown label; choose one of: %s" % ", ".join(self.LABELS),
- )
- error = self.odom_error()
- if error:
- return MarkDistanceResponse(
- success=False,
- distance_m=0.0,
- yaw_rad=0.0,
- message=error,
- )
- distance, yaw = self.measurement()
- result = "%s: %.4f # yaw_change_rad=%.4f" % (label, distance, yaw)
- self.last_mark_pub.publish(String(data=result))
- rospy.loginfo("CALIBRATION MARK: %s", result)
- return MarkDistanceResponse(
- success=True,
- distance_m=distance,
- yaw_rad=yaw,
- message=result,
- )
- def publish_callback(self, _event):
- with self.lock:
- distance, yaw = self.measurement()
- self.distance_pub.publish(Float32(data=distance))
- self.yaw_pub.publish(Float32(data=yaw))
- if __name__ == "__main__":
- try:
- RouteDistanceCalibrator()
- rospy.spin()
- except rospy.ROSInterruptException:
- pass
|