TurtleBotJEPA-PPO / map_goals.py
penji2803's picture
TurtleBotJEPA-PPO: ready-to-run JEPA + PPO policy for a real TurtleBot3 (LDS-02), guide incl. mapping and map goals
4421dc3 verified
Raw History Blame Contribute Delete
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()