Download map_goals.py from penji2803/TurtleBotJEPA-PPO: direct link, hf CLI and curl.
- Browser
- Download file 7.6 kB
-
https://huggingface.co/penji2803/TurtleBotJEPA-PPO/resolve/main/map_goals.py
- Command line
-
hf download hf://penji2803/TurtleBotJEPA-PPO/map_goals.py
-
curl -L -o map_goals.py https://huggingface.co/penji2803/TurtleBotJEPA-PPO/resolve/main/map_goals.py
7.6 kB
| #!/usr/bin/env python3 | |
| """Click places on your map in RViz and the JEPA + PPO policy drives the robot there, one goal after another. | |
| Needs (see README "Goals on the map"): the robot bringup, map localization (AMCL) with your saved map, and RViz with | |
| rviz/map_goals.rviz. Then, on the laptop: | |
| python3 map_goals.py # waits for goals; Ctrl+C to quit | |
| In RViz: "2D Pose Estimate" = tell AMCL where the robot is (once, at the start) | |
| "Publish Point" (single click) or "2D Goal Pose" = send the robot to that spot | |
| A new click while driving replaces the current goal. The robot only moves if AMCL is confident about its position | |
| (position spread < 0.25 m, heading spread < 15 deg). Every run uses the same safety stops as run_policy.py. | |
| The goal is kept in map coordinates and re-converted to odometry coordinates every step, so AMCL corrections for | |
| wheel-odometry drift are applied while driving. | |
| """ | |
| import argparse | |
| import math | |
| import os | |
| import signal | |
| import sys | |
| import time | |
| import numpy as np | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, HERE) | |
| from run_policy import PolicyRunner, build_parser # noqa: E402 | |
| try: | |
| import rclpy | |
| from geometry_msgs.msg import PointStamped, PoseStamped, PoseWithCovarianceStamped | |
| from rclpy.executors import SingleThreadedExecutor | |
| from rclpy.node import Node | |
| from rclpy.time import Time | |
| from tf2_ros import Buffer, TransformListener | |
| except ImportError: | |
| sys.exit('ROS 2 is not set up in this terminal. Run first: cd to this folder and run: source setup_terminal.sh') | |
| def yaw_of(q): | |
| return math.atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z)) | |
| class GoalManager(Node): | |
| def __init__(self, a): | |
| super().__init__('map_goal_manager') | |
| self.a = a | |
| self.tf = Buffer() | |
| self.tfl = TransformListener(self.tf, self) | |
| self.create_subscription(PoseStamped, a.goal_topic, self.on_goal_pose, 10) | |
| self.create_subscription(PointStamped, a.point_topic, self.on_point, 10) | |
| self.create_subscription(PoseWithCovarianceStamped, 'amcl_pose', self.on_amcl, 10) | |
| self.pending, self.goal_map, self.loc = None, None, None | |
| self.last_goal = (None, -1e9) | |
| def _new(self, frame, x, y): | |
| (g, t) = self.last_goal # the same click delivered twice -> ignore the repeat | |
| if g is not None and g[0] == frame and math.hypot(g[1] - x, g[2] - y) < 0.05 and time.time() - t < 2.0: | |
| return | |
| self.last_goal = ((frame, x, y), time.time()) | |
| self.pending = (frame, x, y) | |
| def on_goal_pose(self, m): | |
| self._new(m.header.frame_id or 'map', m.pose.position.x, m.pose.position.y) | |
| def on_point(self, m): | |
| self._new(m.header.frame_id or 'map', m.point.x, m.point.y) | |
| def on_amcl(self, m): | |
| c = m.pose.covariance | |
| self.loc = (time.time(), math.sqrt(max(c[0], c[7])), math.sqrt(c[35])) | |
| def localized(self): | |
| """-> (ok, reason).""" | |
| if self.a.no_loc_check: | |
| return True, 'check disabled' | |
| if self.loc is None: | |
| return False, 'no /amcl_pose yet: is localization running? Set the robot position with "2D Pose Estimate".' | |
| _, sxy, syaw = self.loc | |
| if sxy > 0.25 or syaw > math.radians(15): | |
| return False, (f'AMCL is unsure where the robot is (position spread {sxy:.2f} m, heading spread ' | |
| f'{math.degrees(syaw):.0f} deg). Use "2D Pose Estimate", or drive a little with teleop, then click again.') | |
| return True, f'position spread {sxy:.2f} m, heading spread {math.degrees(syaw):.0f} deg' | |
| def to_odom(self, frame, x, y): | |
| """Point in `frame` -> odom frame using the latest TF (map -> odom comes from AMCL).""" | |
| if frame == self.a.odom_frame: | |
| return x, y | |
| try: | |
| t = self.tf.lookup_transform(self.a.odom_frame, frame, Time()) | |
| except Exception: | |
| return None | |
| tr, yaw = t.transform.translation, yaw_of(t.transform.rotation) | |
| return (tr.x + math.cos(yaw) * x - math.sin(yaw) * y, tr.y + math.sin(yaw) * x + math.cos(yaw) * y) | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument('--speed-scale', type=float, default=0.5) | |
| ap.add_argument('--goal-topic', default='goal_pose') | |
| ap.add_argument('--point-topic', default='clicked_point') | |
| ap.add_argument('--odom-frame', default='odom') | |
| ap.add_argument('--no-loc-check', action='store_true', help=argparse.SUPPRESS) # testing only | |
| ap.add_argument('--extra', default='', help=argparse.SUPPRESS) # extra run_policy options | |
| a = ap.parse_args() | |
| from v6_policy import DeployedPolicy | |
| policy = DeployedPolicy.load() | |
| rclpy.init() | |
| mgr = GoalManager(a) | |
| ex = SingleThreadedExecutor() | |
| ex.add_node(mgr) | |
| stop = {'flag': False} | |
| signal.signal(signal.SIGINT, lambda *_: stop.update(flag=True)) | |
| signal.signal(signal.SIGTERM, lambda *_: stop.update(flag=True)) | |
| runner, n = None, 0 | |
| print('Ready. In RViz: "2D Pose Estimate" to localize, then "Publish Point" or "2D Goal Pose" to send the robot. Ctrl+C to quit.', | |
| flush=True) | |
| try: | |
| while rclpy.ok() and not stop['flag']: | |
| ex.spin_once(timeout_sec=0.02) | |
| if mgr.pending is not None: | |
| frame, gx, gy = mgr.pending | |
| mgr.pending = None | |
| ok, why = mgr.localized() | |
| g = mgr.to_odom(frame, gx, gy) | |
| if not ok or g is None: | |
| print(f'\n[goal ({gx:.2f}, {gy:.2f}) in {frame}] NOT STARTED: ' | |
| + (why if not ok else f'no transform {frame} -> {a.odom_frame} yet (is localization running?)'), flush=True) | |
| continue | |
| if runner is not None and runner.state != 'done': | |
| runner.finish('NEW GOAL', 'replaced by a new click') | |
| ex.remove_node(runner) | |
| runner.destroy_node() | |
| n += 1 | |
| print(f'\n[goal {n}] map ({gx:.2f}, {gy:.2f}) -> odom ({g[0]:.2f}, {g[1]:.2f}); localization OK ({why})', flush=True) | |
| args = build_parser().parse_args(['--goal-odom', str(g[0]), str(g[1]), '--speed-scale', str(a.speed_scale), | |
| '--countdown', '1.0'] + a.extra.split()) | |
| args.max_time = 60.0 / a.speed_scale | |
| target = (frame, gx, gy) | |
| runner = PolicyRunner(args, policy, name=f'map_goal_runner_{n}', log_prefix=f'mapgoal_{n}', | |
| goal_provider=lambda t=target: mgr.to_odom(*t)) | |
| ex.add_node(runner) | |
| if runner is not None and runner.state == 'done' and runner.result != 'NEW GOAL': | |
| for _ in range(5): | |
| runner.send(0.0, 0.0) | |
| print(f'[goal {n}] {runner.result}: {runner.detail} | driven {runner.path:.2f} m, closest obstacle ' | |
| f'{runner.min_seen:.2f} m. Waiting for the next click.', flush=True) | |
| ex.remove_node(runner) | |
| runner.destroy_node() | |
| runner = None | |
| finally: | |
| if runner is not None: | |
| runner.finish('STOPPED BY YOU', 'Ctrl+C pressed') | |
| for _ in range(10): | |
| runner.send(0.0, 0.0) | |
| time.sleep(0.03) | |
| mgr.destroy_node() | |
| rclpy.shutdown() | |
| if __name__ == '__main__': | |
| main() | |