route_distance_calibrator.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env python3
  2. """Measure odometry path length and yaw without commanding the robot."""
  3. import math
  4. import threading
  5. import rospy
  6. from nav_msgs.msg import Odometry
  7. from std_msgs.msg import Float32, String
  8. from std_srvs.srv import Trigger, TriggerResponse
  9. from traffic_line_task.srv import MarkDistance, MarkDistanceResponse
  10. class RouteDistanceCalibrator:
  11. LABELS = (
  12. "first_left_turn_distance_m",
  13. "first_right_turn_distance_m",
  14. "first_straight_entry_distance_m",
  15. "direct_finish_distance_m",
  16. "straight_to_second_distance_m",
  17. "second_right_turn_distance_m",
  18. "after_second_finish_distance_m",
  19. )
  20. def __init__(self):
  21. rospy.init_node("route_distance_calibrator")
  22. self.lock = threading.RLock()
  23. self.odom_topic = rospy.get_param("~odom_topic", "/odom")
  24. self.odom_timeout = max(0.1, float(rospy.get_param("~odom_timeout", 0.5)))
  25. self.odom_jump_limit_m = max(
  26. 0.01, float(rospy.get_param("~odom_jump_limit_m", 0.25))
  27. )
  28. self.publish_rate = max(1.0, float(rospy.get_param("~publish_rate", 10.0)))
  29. self.position = None
  30. self.yaw = None
  31. self.last_odom_time = None
  32. self.total_distance = 0.0
  33. self.total_yaw = 0.0
  34. self.zero_distance = 0.0
  35. self.zero_yaw = 0.0
  36. self.odom_fault = ""
  37. self.distance_pub = rospy.Publisher("~distance", Float32, queue_size=1)
  38. self.yaw_pub = rospy.Publisher("~yaw_change", Float32, queue_size=1)
  39. self.last_mark_pub = rospy.Publisher(
  40. "~last_mark", String, queue_size=1, latch=True
  41. )
  42. self.odom_sub = rospy.Subscriber(
  43. self.odom_topic, Odometry, self.odom_callback, queue_size=20
  44. )
  45. self.reset_service = rospy.Service("~reset", Trigger, self.reset_callback)
  46. self.mark_service = rospy.Service("~mark", MarkDistance, self.mark_callback)
  47. self.timer = rospy.Timer(
  48. rospy.Duration(1.0 / self.publish_rate), self.publish_callback
  49. )
  50. rospy.loginfo(
  51. "Route-distance calibrator is measurement only; odom=%s. "
  52. "It never publishes /cmd_vel.",
  53. self.odom_topic,
  54. )
  55. @staticmethod
  56. def wrap_angle(angle):
  57. return math.atan2(math.sin(angle), math.cos(angle))
  58. @staticmethod
  59. def yaw_from_odometry(message):
  60. q = message.pose.pose.orientation
  61. siny_cosp = 2.0 * (q.w * q.z + q.x * q.y)
  62. cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
  63. return math.atan2(siny_cosp, cosy_cosp)
  64. def odom_callback(self, message):
  65. with self.lock:
  66. point = message.pose.pose.position
  67. current_position = (float(point.x), float(point.y))
  68. current_yaw = self.yaw_from_odometry(message)
  69. if self.position is not None:
  70. step = math.hypot(
  71. current_position[0] - self.position[0],
  72. current_position[1] - self.position[1],
  73. )
  74. if step > self.odom_jump_limit_m:
  75. self.odom_fault = "ODOM_JUMP: %.3f m" % step
  76. else:
  77. self.total_distance += step
  78. if self.yaw is not None:
  79. self.total_yaw += self.wrap_angle(current_yaw - self.yaw)
  80. self.position = current_position
  81. self.yaw = current_yaw
  82. self.last_odom_time = rospy.get_time()
  83. def measurement(self):
  84. return (
  85. max(0.0, self.total_distance - self.zero_distance),
  86. self.total_yaw - self.zero_yaw,
  87. )
  88. def odom_error(self):
  89. if self.odom_fault:
  90. return self.odom_fault
  91. if self.last_odom_time is None:
  92. return "ODOM_UNAVAILABLE"
  93. age = rospy.get_time() - self.last_odom_time
  94. if age > self.odom_timeout:
  95. return "ODOM_TIMEOUT: %.3f s" % age
  96. return ""
  97. def reset_callback(self, _request):
  98. with self.lock:
  99. error = self.odom_error()
  100. if error and error != self.odom_fault:
  101. return TriggerResponse(success=False, message=error)
  102. self.odom_fault = ""
  103. self.zero_distance = self.total_distance
  104. self.zero_yaw = self.total_yaw
  105. self.last_mark_pub.publish(String(data=""))
  106. rospy.loginfo("Route-distance calibration measurement reset to zero.")
  107. return TriggerResponse(success=True, message="measurement reset")
  108. def mark_callback(self, request):
  109. with self.lock:
  110. label = str(request.label).strip()
  111. if label not in self.LABELS:
  112. return MarkDistanceResponse(
  113. success=False,
  114. distance_m=0.0,
  115. yaw_rad=0.0,
  116. message="unknown label; choose one of: %s" % ", ".join(self.LABELS),
  117. )
  118. error = self.odom_error()
  119. if error:
  120. return MarkDistanceResponse(
  121. success=False,
  122. distance_m=0.0,
  123. yaw_rad=0.0,
  124. message=error,
  125. )
  126. distance, yaw = self.measurement()
  127. result = "%s: %.4f # yaw_change_rad=%.4f" % (label, distance, yaw)
  128. self.last_mark_pub.publish(String(data=result))
  129. rospy.loginfo("CALIBRATION MARK: %s", result)
  130. return MarkDistanceResponse(
  131. success=True,
  132. distance_m=distance,
  133. yaw_rad=yaw,
  134. message=result,
  135. )
  136. def publish_callback(self, _event):
  137. with self.lock:
  138. distance, yaw = self.measurement()
  139. self.distance_pub.publish(Float32(data=distance))
  140. self.yaw_pub.publish(Float32(data=yaw))
  141. if __name__ == "__main__":
  142. try:
  143. RouteDistanceCalibrator()
  144. rospy.spin()
  145. except rospy.ROSInterruptException:
  146. pass