traffic_line_state_machine.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. #!/usr/bin/env python3
  2. """Coordinate LED-sign recognition, open-loop junctions, and line following."""
  3. import math
  4. import threading
  5. import rospy
  6. from geometry_msgs.msg import PointStamped, Twist
  7. from nav_msgs.msg import Odometry
  8. from std_msgs.msg import Bool, Float32, String
  9. from std_srvs.srv import SetBool, Trigger, TriggerResponse
  10. class TrafficLineStateMachine:
  11. STATES = (
  12. "INIT",
  13. "WAIT_SIGN",
  14. "RESTORE_CAMERA",
  15. "FIRST_MANEUVER",
  16. "REACQUIRE_LINE",
  17. "FOLLOW_DIRECT_FINISH",
  18. "FOLLOW_TO_SECOND",
  19. "SECOND_JUNCTION_FORWARD",
  20. "SECOND_RIGHT_MANEUVER",
  21. "FOLLOW_AFTER_SECOND",
  22. "WAIT_LANE_RECOVERY",
  23. "CONFIRM_FINISH",
  24. "FINISHED",
  25. "FAULT",
  26. "ABORTED",
  27. )
  28. DIRECTIONS = ("LEFT", "RIGHT", "STRAIGHT")
  29. TERMINAL_STATES = ("FINISHED", "FAULT", "ABORTED")
  30. def __init__(self):
  31. rospy.init_node("traffic_line_task")
  32. self.lock = threading.RLock()
  33. self.autostart = self._as_bool(rospy.get_param("~autostart", True))
  34. self.control_rate = max(1.0, float(rospy.get_param("~control_rate", 20.0)))
  35. self.cmd_vel_topic = rospy.get_param("~cmd_vel_topic", "/cmd_vel")
  36. self.odom_topic = rospy.get_param("~odom_topic", "/odom")
  37. self.lane_valid_topic = rospy.get_param(
  38. "~lane_valid_topic", "/line_follow_debug/lane_valid"
  39. )
  40. self.lookahead_target_topic = rospy.get_param(
  41. "~lookahead_target_topic", "/line_follow_debug/lookahead_target"
  42. )
  43. self.sign_direction_topic = rospy.get_param(
  44. "~sign_direction_topic", "/traffic_sign/direction"
  45. )
  46. self.sign_confidence_topic = rospy.get_param(
  47. "~sign_confidence_topic", "/traffic_sign/confidence"
  48. )
  49. self.sign_enable_service_name = rospy.get_param(
  50. "~sign_enable_service", "/traffic_sign_recognition/set_enabled"
  51. )
  52. self.line_enable_service_name = rospy.get_param(
  53. "~line_enable_service", "/line_follow_control/set_enabled"
  54. )
  55. self.sign_confirmations = max(1, int(rospy.get_param("~sign_confirmations", 2)))
  56. self.sign_topic_timeout = max(
  57. 0.5, float(rospy.get_param("~sign_topic_timeout", 3.0))
  58. )
  59. self.minimum_sign_confidence = max(
  60. 0.0, float(rospy.get_param("~minimum_sign_confidence", 0.25))
  61. )
  62. self.camera_settle_time = max(
  63. 0.0, float(rospy.get_param("~camera_settle_time", 2.0))
  64. )
  65. self.odom_timeout = max(0.1, float(rospy.get_param("~odom_timeout", 0.5)))
  66. self.odom_jump_limit_m = max(
  67. 0.01, float(rospy.get_param("~odom_jump_limit_m", 0.25))
  68. )
  69. self.maximum_turn_yaw_rad = max(
  70. 0.1, float(rospy.get_param("~maximum_turn_yaw_rad", 2.20))
  71. )
  72. self.wrong_way_yaw_tolerance_rad = max(
  73. 0.01, float(rospy.get_param("~wrong_way_yaw_tolerance_rad", 0.10))
  74. )
  75. self.straight_yaw_limit_rad = max(
  76. 0.05, float(rospy.get_param("~straight_yaw_limit_rad", 0.35))
  77. )
  78. self.maneuver_timeout_scale = max(
  79. 1.0, float(rospy.get_param("~maneuver_timeout_scale", 2.0))
  80. )
  81. self.maneuver_timeout_min = max(
  82. 1.0, float(rospy.get_param("~maneuver_timeout_min", 3.0))
  83. )
  84. self.transition_stop_time = max(
  85. 0.0, float(rospy.get_param("~transition_stop_time", 0.30))
  86. )
  87. self.maneuver_linear_speed = min(
  88. 0.10,
  89. max(0.01, float(rospy.get_param("~maneuver_linear_speed", 0.10))),
  90. )
  91. self.first_turn_angular_speed = min(
  92. 0.20, max(0.01, float(rospy.get_param("~first_turn_angular_speed", 0.20)))
  93. )
  94. self.second_turn_angular_speed = min(
  95. 0.20, max(0.01, float(rospy.get_param("~second_turn_angular_speed", 0.20)))
  96. )
  97. self.second_junction_forward_speed = min(
  98. 0.10,
  99. max(
  100. 0.01,
  101. float(rospy.get_param("~second_junction_forward_speed", 0.10)),
  102. ),
  103. )
  104. self.second_junction_forward_duration = max(
  105. 0.0,
  106. float(rospy.get_param("~second_junction_forward_duration", 3.0)),
  107. )
  108. self.stop_after_second_maneuver = bool(
  109. rospy.get_param("~stop_after_second_maneuver", False)
  110. )
  111. self.use_finish_distance_limits = self._as_bool(
  112. rospy.get_param("~use_finish_distance_limits", True)
  113. )
  114. self.distances = {
  115. "first_left_turn": float(rospy.get_param("~first_left_turn_distance_m", 0.0)),
  116. "first_right_turn": float(rospy.get_param("~first_right_turn_distance_m", 0.0)),
  117. "first_straight_entry": float(
  118. rospy.get_param("~first_straight_entry_distance_m", 0.0)
  119. ),
  120. "direct_finish": float(rospy.get_param("~direct_finish_distance_m", 0.0)),
  121. "straight_to_second": float(
  122. rospy.get_param("~straight_to_second_distance_m", 0.0)
  123. ),
  124. "second_right_turn": float(
  125. rospy.get_param("~second_right_turn_distance_m", 0.0)
  126. ),
  127. "after_second_finish": float(
  128. rospy.get_param("~after_second_finish_distance_m", 0.0)
  129. ),
  130. }
  131. self.lane_reacquire_timeout = max(
  132. 0.1, float(rospy.get_param("~lane_reacquire_timeout", 2.0))
  133. )
  134. self.second_junction_arm_distance = max(
  135. 0.0,
  136. float(
  137. rospy.get_param(
  138. "~second_junction_arm_distance_m",
  139. 0.75 * self.distances["straight_to_second"],
  140. )
  141. ),
  142. )
  143. self.second_junction_max_distance = max(
  144. 0.0,
  145. float(
  146. rospy.get_param(
  147. "~second_junction_max_distance_m",
  148. 1.40 * self.distances["straight_to_second"],
  149. )
  150. ),
  151. )
  152. self.second_junction_lost_frames = max(
  153. 1, int(rospy.get_param("~second_junction_lost_frames", 3))
  154. )
  155. self.lane_stable_frames = max(
  156. 1, int(rospy.get_param("~lane_stable_frames", 5))
  157. )
  158. self.target_fresh_timeout = max(
  159. 0.05, float(rospy.get_param("~target_fresh_timeout", 0.5))
  160. )
  161. self.finish_lane_loss_confirm_time = max(
  162. 0.1, float(rospy.get_param("~finish_lane_loss_confirm_time", 1.0))
  163. )
  164. self.completion_announcement_topic = rospy.get_param(
  165. "~completion_announcement_topic", "/speech_command/announce"
  166. )
  167. self.completion_announcement_text = str(
  168. rospy.get_param("~completion_announcement_text", "任务完成")
  169. ).strip()
  170. self.state = "INIT" if self.autostart else "ABORTED"
  171. self.state_enter_time = rospy.get_time()
  172. self.first_direction = "NONE"
  173. self.fault_reason = ""
  174. self.sign_candidate = "NONE"
  175. self.sign_candidate_count = 0
  176. self.last_sign_time = None
  177. self.sign_confidence = 0.0
  178. self.lane_valid = False
  179. self.last_target_time = None
  180. self.lane_stable_count = 0
  181. self.resume_state = None
  182. self.resume_segment_start_distance = None
  183. self.next_follow_state = None
  184. self.second_junction_loss_count = 0
  185. self.completion_announcement_sent = False
  186. self.odom_position = None
  187. self.odom_yaw = None
  188. self.last_odom_time = None
  189. self.total_odom_distance = 0.0
  190. self.odom_fault_reason = ""
  191. self.segment_start_distance = 0.0
  192. self.segment_start_yaw = None
  193. self.cmd_pub = rospy.Publisher(self.cmd_vel_topic, Twist, queue_size=1)
  194. self.state_pub = rospy.Publisher("~state", String, queue_size=1, latch=True)
  195. self.direction_pub = rospy.Publisher(
  196. "~first_direction", String, queue_size=1, latch=True
  197. )
  198. self.distance_pub = rospy.Publisher("~segment_distance", Float32, queue_size=1)
  199. self.finished_pub = rospy.Publisher("~finished", Bool, queue_size=1, latch=True)
  200. self.fault_pub = rospy.Publisher("~fault", String, queue_size=1, latch=True)
  201. self.completion_announcement_pub = rospy.Publisher(
  202. self.completion_announcement_topic, String, queue_size=1
  203. )
  204. self.sign_sub = rospy.Subscriber(
  205. self.sign_direction_topic, String, self.sign_callback, queue_size=1
  206. )
  207. self.confidence_sub = rospy.Subscriber(
  208. self.sign_confidence_topic, Float32, self.confidence_callback, queue_size=1
  209. )
  210. self.lane_sub = rospy.Subscriber(
  211. self.lane_valid_topic, Bool, self.lane_callback, queue_size=1
  212. )
  213. self.target_sub = rospy.Subscriber(
  214. self.lookahead_target_topic,
  215. PointStamped,
  216. self.target_callback,
  217. queue_size=1,
  218. )
  219. self.odom_sub = rospy.Subscriber(
  220. self.odom_topic, Odometry, self.odom_callback, queue_size=10
  221. )
  222. self.reset_service = rospy.Service("~reset", Trigger, self.reset_callback)
  223. self.abort_service = rospy.Service("~abort", Trigger, self.abort_callback)
  224. self.sign_enable = rospy.ServiceProxy(self.sign_enable_service_name, SetBool)
  225. self.line_enable = rospy.ServiceProxy(self.line_enable_service_name, SetBool)
  226. self.timer = rospy.Timer(
  227. rospy.Duration(1.0 / self.control_rate), self.control_callback
  228. )
  229. rospy.on_shutdown(self.shutdown)
  230. self.state_pub.publish(String(data=self.state))
  231. self.direction_pub.publish(String(data="NONE"))
  232. self.finished_pub.publish(Bool(data=False))
  233. self.fault_pub.publish(String(data=""))
  234. rospy.loginfo("Traffic-line task ready: state=%s autostart=%s", self.state, self.autostart)
  235. @staticmethod
  236. def _as_bool(value):
  237. if isinstance(value, str):
  238. return value.strip().lower() in ("1", "true", "yes", "on")
  239. return bool(value)
  240. @staticmethod
  241. def wrap_angle(angle):
  242. return math.atan2(math.sin(angle), math.cos(angle))
  243. @staticmethod
  244. def follow_state_after_first(direction):
  245. if direction == "STRAIGHT":
  246. return "FOLLOW_TO_SECOND"
  247. if direction in ("LEFT", "RIGHT"):
  248. return "FOLLOW_DIRECT_FINISH"
  249. raise ValueError("unsupported first direction: %s" % direction)
  250. @staticmethod
  251. def yaw_from_odometry(message):
  252. q = message.pose.pose.orientation
  253. siny_cosp = 2.0 * (q.w * q.z + q.x * q.y)
  254. cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
  255. return math.atan2(siny_cosp, cosy_cosp)
  256. def sign_callback(self, message):
  257. with self.lock:
  258. direction = str(message.data).strip().upper()
  259. self.last_sign_time = rospy.get_time()
  260. if self.state != "WAIT_SIGN":
  261. return
  262. if direction not in self.DIRECTIONS:
  263. self.sign_candidate = "NONE"
  264. self.sign_candidate_count = 0
  265. return
  266. if self.sign_confidence < self.minimum_sign_confidence:
  267. return
  268. if direction == self.sign_candidate:
  269. self.sign_candidate_count += 1
  270. else:
  271. self.sign_candidate = direction
  272. self.sign_candidate_count = 1
  273. def confidence_callback(self, message):
  274. with self.lock:
  275. self.sign_confidence = max(0.0, float(message.data))
  276. def lane_callback(self, message):
  277. with self.lock:
  278. self.lane_valid = bool(message.data)
  279. def target_callback(self, _message):
  280. with self.lock:
  281. self.last_target_time = rospy.get_time()
  282. def odom_callback(self, message):
  283. with self.lock:
  284. position = message.pose.pose.position
  285. current = (float(position.x), float(position.y))
  286. if self.odom_position is not None:
  287. step = math.hypot(
  288. current[0] - self.odom_position[0],
  289. current[1] - self.odom_position[1],
  290. )
  291. if step > self.odom_jump_limit_m:
  292. self.odom_fault_reason = "ODOM_JUMP: %.3f m" % step
  293. else:
  294. self.total_odom_distance += step
  295. self.odom_position = current
  296. self.odom_yaw = self.yaw_from_odometry(message)
  297. self.last_odom_time = rospy.get_time()
  298. def reset_callback(self, _request):
  299. with self.lock:
  300. self._disable_line()
  301. self._disable_sign()
  302. self._publish_stop()
  303. self._clear_task()
  304. self._transition("INIT" if self.autostart else "ABORTED")
  305. return TriggerResponse(success=True, message="traffic-line task reset")
  306. def abort_callback(self, _request):
  307. with self.lock:
  308. self._enter_terminal("ABORTED", "operator abort")
  309. return TriggerResponse(success=True, message="traffic-line task aborted")
  310. def _clear_task(self):
  311. self.first_direction = "NONE"
  312. self.fault_reason = ""
  313. self.sign_candidate = "NONE"
  314. self.sign_candidate_count = 0
  315. self.last_sign_time = None
  316. self.sign_confidence = 0.0
  317. self.lane_stable_count = 0
  318. self.resume_state = None
  319. self.next_follow_state = None
  320. self.second_junction_loss_count = 0
  321. self.completion_announcement_sent = False
  322. self.odom_fault_reason = ""
  323. self.direction_pub.publish(String(data="NONE"))
  324. self.finished_pub.publish(Bool(data=False))
  325. self.fault_pub.publish(String(data=""))
  326. def _transition(self, state, reset_segment=False):
  327. if state not in self.STATES:
  328. raise ValueError("unknown traffic-line state: %s" % state)
  329. previous = self.state
  330. self.state = state
  331. self.state_enter_time = rospy.get_time()
  332. self.lane_stable_count = 0
  333. if state == "FOLLOW_TO_SECOND":
  334. self.second_junction_loss_count = 0
  335. if reset_segment:
  336. self.segment_start_distance = self.total_odom_distance
  337. self.segment_start_yaw = self.odom_yaw
  338. self.state_pub.publish(String(data=state))
  339. rospy.loginfo("Traffic-line state: %s -> %s", previous, state)
  340. def _publish_stop(self):
  341. try:
  342. self.cmd_pub.publish(Twist())
  343. except rospy.ROSException:
  344. # A timer callback can overlap ROS shutdown after the publisher has
  345. # already been unregistered. The base timeout still provides the
  346. # final stop in that teardown-only condition.
  347. if not rospy.is_shutdown():
  348. raise
  349. def _publish_maneuver(self, angular):
  350. command = Twist()
  351. command.linear.x = self.maneuver_linear_speed
  352. command.angular.z = angular
  353. self.cmd_pub.publish(command)
  354. def _publish_forward(self, speed):
  355. command = Twist()
  356. command.linear.x = speed
  357. self.cmd_pub.publish(command)
  358. def _service_ready(self, name):
  359. try:
  360. rospy.wait_for_service(name, timeout=0.01)
  361. return True
  362. except rospy.ROSException:
  363. return False
  364. def _set_enabled(self, proxy, name, enabled):
  365. try:
  366. response = proxy(enabled)
  367. except (rospy.ServiceException, rospy.ROSException) as error:
  368. rospy.logerr("Service %s failed: %s", name, error)
  369. return False
  370. if not response.success:
  371. rospy.logerr("Service %s rejected request: %s", name, response.message)
  372. return False
  373. return True
  374. def _disable_line(self):
  375. if self._service_ready(self.line_enable_service_name):
  376. return self._set_enabled(
  377. self.line_enable, self.line_enable_service_name, False
  378. )
  379. return False
  380. def _enable_line(self):
  381. return self._set_enabled(self.line_enable, self.line_enable_service_name, True)
  382. def _disable_sign(self):
  383. if self._service_ready(self.sign_enable_service_name):
  384. return self._set_enabled(
  385. self.sign_enable, self.sign_enable_service_name, False
  386. )
  387. return False
  388. def _enable_sign(self):
  389. return self._set_enabled(self.sign_enable, self.sign_enable_service_name, True)
  390. def _configuration_error(self):
  391. invalid = [name for name, value in self.distances.items() if value <= 0.0]
  392. if invalid:
  393. return "CONFIG_INVALID: set positive calibration values for %s" % ", ".join(invalid)
  394. if self.second_junction_arm_distance <= 0.0:
  395. return "CONFIG_INVALID: second_junction_arm_distance_m must be positive"
  396. if self.second_junction_max_distance <= self.second_junction_arm_distance:
  397. return (
  398. "CONFIG_INVALID: second_junction_max_distance_m must be greater "
  399. "than second_junction_arm_distance_m"
  400. )
  401. return ""
  402. def _odom_fresh(self, now):
  403. return self.last_odom_time is not None and now - self.last_odom_time <= self.odom_timeout
  404. def _target_fresh(self, now):
  405. return self.last_target_time is not None and now - self.last_target_time <= self.target_fresh_timeout
  406. def _lane_ready(self, now):
  407. return self.lane_valid and self._target_fresh(now)
  408. def _segment_distance(self):
  409. return max(0.0, self.total_odom_distance - self.segment_start_distance)
  410. def _segment_yaw(self):
  411. if self.segment_start_yaw is None or self.odom_yaw is None:
  412. return 0.0
  413. return self.wrap_angle(self.odom_yaw - self.segment_start_yaw)
  414. def _maneuver_timeout(self, target_distance):
  415. nominal = target_distance / self.maneuver_linear_speed
  416. return self.transition_stop_time + max(
  417. self.maneuver_timeout_min, nominal * self.maneuver_timeout_scale
  418. )
  419. def _enter_fault(self, reason):
  420. self._enter_terminal("FAULT", reason)
  421. def _enter_terminal(self, state, reason=""):
  422. self._disable_line()
  423. self._disable_sign()
  424. self._publish_stop()
  425. self.fault_reason = reason if state == "FAULT" else ""
  426. self.finished_pub.publish(Bool(data=(state == "FINISHED")))
  427. self.fault_pub.publish(String(data=self.fault_reason))
  428. self._transition(state)
  429. if state == "FINISHED" and not self.completion_announcement_sent:
  430. self.completion_announcement_pub.publish(
  431. String(data=self.completion_announcement_text)
  432. )
  433. self.completion_announcement_sent = True
  434. rospy.loginfo(
  435. "Task completion announcement requested: %s",
  436. self.completion_announcement_text,
  437. )
  438. if reason:
  439. rospy.logerr("Traffic-line %s: %s", state, reason)
  440. def _begin_first_maneuver(self):
  441. if not self._disable_line():
  442. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  443. return
  444. self._publish_stop()
  445. self._transition("FIRST_MANEUVER", reset_segment=True)
  446. def _begin_reacquire(self, next_state):
  447. if not self._disable_line():
  448. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  449. return
  450. self._publish_stop()
  451. self.next_follow_state = next_state
  452. self._transition("REACQUIRE_LINE")
  453. def _enable_follow_state(self, state, reset_segment):
  454. self._publish_stop()
  455. if not self._enable_line():
  456. self._enter_fault("LINE_ENABLE_SERVICE_FAILED")
  457. return
  458. self._transition(state, reset_segment=reset_segment)
  459. def _begin_lane_recovery(self):
  460. self.resume_state = self.state
  461. if not self._disable_line():
  462. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  463. return
  464. self._publish_stop()
  465. self._transition("WAIT_LANE_RECOVERY")
  466. def _run_reacquire(self, now, recovery):
  467. self._publish_stop()
  468. if self._lane_ready(now):
  469. self.lane_stable_count += 1
  470. else:
  471. self.lane_stable_count = 0
  472. if self.lane_stable_count >= self.lane_stable_frames:
  473. state = self.resume_state if recovery else self.next_follow_state
  474. self._enable_follow_state(state, reset_segment=not recovery)
  475. return
  476. if now - self.state_enter_time > self.lane_reacquire_timeout:
  477. self._enter_fault("LANE_REACQUIRE_TIMEOUT")
  478. def _first_maneuver_parameters(self):
  479. if self.first_direction == "LEFT":
  480. return self.distances["first_left_turn"], self.first_turn_angular_speed
  481. if self.first_direction == "RIGHT":
  482. return self.distances["first_right_turn"], -self.first_turn_angular_speed
  483. if self.first_direction == "STRAIGHT":
  484. return self.distances["first_straight_entry"], 0.0
  485. return 0.0, 0.0
  486. def _run_maneuver(self, now, target_distance, angular, completed_callback):
  487. distance = self._segment_distance()
  488. yaw = self._segment_yaw()
  489. elapsed = now - self.state_enter_time
  490. if elapsed < self.transition_stop_time:
  491. self._publish_stop()
  492. return
  493. if angular != 0.0:
  494. expected_sign = 1.0 if angular > 0.0 else -1.0
  495. if yaw * expected_sign < -self.wrong_way_yaw_tolerance_rad:
  496. self._enter_fault("MANEUVER_WRONG_YAW_DIRECTION: %.3f rad" % yaw)
  497. return
  498. if abs(yaw) > self.maximum_turn_yaw_rad:
  499. self._enter_fault("MANEUVER_YAW_LIMIT: %.3f rad" % yaw)
  500. return
  501. elif abs(yaw) > self.straight_yaw_limit_rad:
  502. self._enter_fault("STRAIGHT_YAW_LIMIT: %.3f rad" % yaw)
  503. return
  504. if distance >= target_distance:
  505. self._publish_stop()
  506. completed_callback()
  507. return
  508. if elapsed > self._maneuver_timeout(target_distance):
  509. self._enter_fault("MANEUVER_TIMEOUT: %.3f/%.3f m" % (distance, target_distance))
  510. return
  511. self._publish_maneuver(angular)
  512. def _run_follow(self, target_distance, completion):
  513. now = rospy.get_time()
  514. if not self._lane_ready(now):
  515. self._begin_lane_recovery()
  516. return
  517. if self._segment_distance() >= target_distance:
  518. if not self._disable_line():
  519. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  520. return
  521. self._publish_stop()
  522. completion()
  523. def _run_finish_follow(self, now, target_distance):
  524. if not self.use_finish_distance_limits:
  525. # Endpoint detection must come from the perception result itself.
  526. # A stale target alone may mean CPU/image delay and must not be
  527. # mistaken for the physical end of the painted lane.
  528. if not self.lane_valid:
  529. self.resume_state = self.state
  530. if not self._disable_line():
  531. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  532. return
  533. self._publish_stop()
  534. self._transition("CONFIRM_FINISH")
  535. return
  536. self._run_follow(
  537. target_distance,
  538. lambda: self._enter_terminal("FINISHED"),
  539. )
  540. def _run_finish_confirmation(self, now):
  541. """Stop immediately, then distinguish endpoint loss from a short dropout."""
  542. self._publish_stop()
  543. if self.lane_valid:
  544. if self._lane_ready(now):
  545. self.lane_stable_count += 1
  546. if self.lane_stable_count >= self.lane_stable_frames:
  547. resume_state = self.resume_state
  548. self._enable_follow_state(resume_state, reset_segment=False)
  549. else:
  550. self.lane_stable_count = 0
  551. return
  552. self.lane_stable_count = 0
  553. if now - self.state_enter_time >= self.finish_lane_loss_confirm_time:
  554. rospy.loginfo(
  555. "Endpoint confirmed after %.2f s without a valid lane.",
  556. self.finish_lane_loss_confirm_time,
  557. )
  558. self._enter_terminal("FINISHED")
  559. def _run_follow_to_second(self, now):
  560. """Trigger the second right turn from confirmed lane loss near the junction."""
  561. distance = self._segment_distance()
  562. if self._lane_ready(now):
  563. self.second_junction_loss_count = 0
  564. if distance > self.second_junction_max_distance:
  565. if not self._disable_line():
  566. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  567. return
  568. self._publish_stop()
  569. self._enter_fault(
  570. "SECOND_JUNCTION_NOT_DETECTED: %.3f m" % distance
  571. )
  572. return
  573. if distance < self.second_junction_arm_distance:
  574. # A loss well before the expected junction is ordinary perception
  575. # failure, not permission to turn into an arbitrary opening.
  576. self.second_junction_loss_count = 0
  577. self._begin_lane_recovery()
  578. return
  579. self.second_junction_loss_count += 1
  580. self._publish_stop()
  581. if self.second_junction_loss_count < self.second_junction_lost_frames:
  582. return
  583. if not self._disable_line():
  584. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  585. return
  586. rospy.loginfo(
  587. "Second junction confirmed by %d lost-lane frames at %.3f m.",
  588. self.second_junction_loss_count,
  589. distance,
  590. )
  591. self._transition("SECOND_JUNCTION_FORWARD")
  592. def _run_second_junction_forward(self, now):
  593. elapsed = max(0.0, now - self.state_enter_time)
  594. if elapsed >= self.second_junction_forward_duration:
  595. self._publish_stop()
  596. rospy.loginfo(
  597. "Second-junction forward approach complete: %.2f s at %.2f m/s.",
  598. self.second_junction_forward_duration,
  599. self.second_junction_forward_speed,
  600. )
  601. self._transition("SECOND_RIGHT_MANEUVER", reset_segment=True)
  602. return
  603. self._publish_forward(self.second_junction_forward_speed)
  604. def _complete_second_maneuver(self):
  605. if self.stop_after_second_maneuver:
  606. rospy.loginfo(
  607. "Second-junction test stop reached; line following remains disabled."
  608. )
  609. self._enter_terminal("FINISHED")
  610. return
  611. self._begin_reacquire("FOLLOW_AFTER_SECOND")
  612. def control_callback(self, _event):
  613. with self.lock:
  614. now = rospy.get_time()
  615. self.distance_pub.publish(Float32(data=self._segment_distance()))
  616. if self.state in self.TERMINAL_STATES:
  617. self._publish_stop()
  618. return
  619. # Validate the complete route before requiring odometry or enabling
  620. # either perception/control node. This guarantees an uncalibrated
  621. # task fails as CONFIG_INVALID without ever permitting motion.
  622. if self.state == "INIT":
  623. config_error = self._configuration_error()
  624. if config_error:
  625. self._enter_fault(config_error)
  626. return
  627. if self.odom_fault_reason:
  628. self._enter_fault(self.odom_fault_reason)
  629. return
  630. if not self._odom_fresh(now):
  631. if now - self.state_enter_time > self.odom_timeout:
  632. self._enter_fault("ODOM_TIMEOUT")
  633. else:
  634. self._publish_stop()
  635. return
  636. if self.state == "INIT":
  637. self._publish_stop()
  638. if not self._service_ready(self.sign_enable_service_name):
  639. if now - self.state_enter_time > self.sign_topic_timeout:
  640. self._enter_fault("SIGN_ENABLE_SERVICE_UNAVAILABLE")
  641. return
  642. if not self._service_ready(self.line_enable_service_name):
  643. if now - self.state_enter_time > self.sign_topic_timeout:
  644. self._enter_fault("LINE_ENABLE_SERVICE_UNAVAILABLE")
  645. return
  646. if not self._disable_line():
  647. self._enter_fault("LINE_DISABLE_SERVICE_FAILED")
  648. return
  649. if not self._enable_sign():
  650. self._enter_fault("SIGN_ENABLE_SERVICE_FAILED")
  651. return
  652. self.last_sign_time = now
  653. self._transition("WAIT_SIGN")
  654. return
  655. if self.state == "WAIT_SIGN":
  656. self._publish_stop()
  657. if self.last_sign_time is None or now - self.last_sign_time > self.sign_topic_timeout:
  658. self._enter_fault("SIGN_TOPIC_TIMEOUT")
  659. return
  660. if self.sign_candidate_count >= self.sign_confirmations:
  661. self.first_direction = self.sign_candidate
  662. self.direction_pub.publish(String(data=self.first_direction))
  663. if not self._disable_sign():
  664. self._enter_fault("SIGN_DISABLE_SERVICE_FAILED")
  665. return
  666. self._transition("RESTORE_CAMERA")
  667. return
  668. if self.state == "RESTORE_CAMERA":
  669. self._publish_stop()
  670. if now - self.state_enter_time >= self.camera_settle_time:
  671. self._begin_first_maneuver()
  672. return
  673. if self.state == "FIRST_MANEUVER":
  674. target, angular = self._first_maneuver_parameters()
  675. next_state = self.follow_state_after_first(self.first_direction)
  676. self._run_maneuver(
  677. now,
  678. target,
  679. angular,
  680. lambda: self._begin_reacquire(next_state),
  681. )
  682. return
  683. if self.state == "REACQUIRE_LINE":
  684. self._run_reacquire(now, recovery=False)
  685. return
  686. if self.state == "WAIT_LANE_RECOVERY":
  687. self._run_reacquire(now, recovery=True)
  688. return
  689. if self.state == "CONFIRM_FINISH":
  690. self._run_finish_confirmation(now)
  691. return
  692. if self.state == "FOLLOW_DIRECT_FINISH":
  693. self._run_finish_follow(now, self.distances["direct_finish"])
  694. return
  695. if self.state == "FOLLOW_TO_SECOND":
  696. self._run_follow_to_second(now)
  697. return
  698. if self.state == "SECOND_JUNCTION_FORWARD":
  699. self._run_second_junction_forward(now)
  700. return
  701. if self.state == "SECOND_RIGHT_MANEUVER":
  702. self._run_maneuver(
  703. now,
  704. self.distances["second_right_turn"],
  705. -self.second_turn_angular_speed,
  706. self._complete_second_maneuver,
  707. )
  708. return
  709. if self.state == "FOLLOW_AFTER_SECOND":
  710. self._run_finish_follow(now, self.distances["after_second_finish"])
  711. return
  712. self._enter_fault("UNHANDLED_STATE: %s" % self.state)
  713. def shutdown(self):
  714. with self.lock:
  715. self._disable_line()
  716. self._disable_sign()
  717. self._publish_stop()
  718. if __name__ == "__main__":
  719. try:
  720. TrafficLineStateMachine()
  721. rospy.spin()
  722. except rospy.ROSInterruptException:
  723. pass