Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ecaa20b
changed distance function for look ahead threshold
Beastvr00k Nov 12, 2025
24da811
Finished approaching target
Beastvr00k Dec 4, 2025
f64fa3a
Deflection
Beastvr00k Jan 14, 2026
79ef8b0
Merge remote-tracking branch 'origin/main' into VS/approach-target
Beastvr00k Jan 14, 2026
2ac2655
Fixed distance to target error
Beastvr00k Jan 18, 2026
58fb08e
merged approach-spin, main
Beastvr00k Feb 4, 2026
65c0f54
fixed not important changes
Beastvr00k Feb 4, 2026
47a8ec8
Added angle to rover threshold for stopping
Beastvr00k Feb 22, 2026
2442142
Merge remote-tracking branch 'origin/main' into VS/approach-spin
Beastvr00k Mar 12, 2026
97abb38
Added end point and working towards adding trajectory points when rea…
Beastvr00k Mar 12, 2026
efc2ef2
Added points to trajectory
Beastvr00k Mar 24, 2026
aa241eb
Merge remote-tracking branch 'origin/main' into VS/approach-spin
Beastvr00k Mar 24, 2026
3641f78
Merge remote-tracking branch 'origin' into VS/approach-spin
Beastvr00k Mar 31, 2026
6796b09
Adding trajectory points and scrapped using trajectory, instead using…
Beastvr00k Mar 31, 2026
cb194fb
Backup appears to be working
Beastvr00k Apr 3, 2026
3ba761e
Merge remote-tracking branch 'origin' into VS/approach-spin
Beastvr00k Apr 9, 2026
881e847
Removed logging info and fixed error
Beastvr00k Apr 9, 2026
6f9f9e9
Style fixes
Beastvr00k Apr 9, 2026
a5f8d42
Type hinted none to fix errors
Beastvr00k Apr 9, 2026
3005edd
Merge branch 'main' into VS/approach-spin
Beastvr00k Apr 9, 2026
3cbbff2
Merge remote-tracking branch 'refs/remotes/origin/VS/approach-spin' i…
Beastvr00k Apr 9, 2026
294516d
Changed none bool to enum
Beastvr00k Apr 10, 2026
419a277
Style fixes part 2
Beastvr00k Apr 10, 2026
79139ec
Switch to main mac
Beastvr00k Apr 16, 2026
b9a03e8
Parameterized update time and increased it
Beastvr00k Apr 16, 2026
3f6599a
Fixed double error
Beastvr00k Apr 16, 2026
7def3f2
Fixed exponent error
Beastvr00k Apr 16, 2026
084766c
Removed angle check
Beastvr00k Apr 17, 2026
e6bf38d
removed logging
Beastvr00k Apr 17, 2026
c69a9f0
Ready for merge 2
Beastvr00k Apr 17, 2026
85dad01
Merge branch 'main' into VS/approach-spin
Beastvr00k Apr 17, 2026
897f345
Changed object type to use is_object bool
Beastvr00k Apr 21, 2026
3169581
Merge remote-tracking branch 'refs/remotes/origin/VS/approach-spin' i…
Beastvr00k Apr 21, 2026
bd73d79
Fixed changes, including removing WITHIN_DIST, splitting in_distance_…
Beastvr00k Apr 21, 2026
a4662ab
Got rid of try catch for testing
Beastvr00k Apr 21, 2026
87e2826
Fixed error and tested
Beastvr00k Apr 24, 2026
1c2774a
Undoing state machine changes
Beastvr00k Apr 24, 2026
c434928
Added comments, added rock pick
Beastvr00k Apr 24, 2026
0defb5c
Merge branch 'main' into VS/approach-spin
Beastvr00k Apr 24, 2026
3cdc14c
Style checks
Beastvr00k Apr 24, 2026
7e66025
Merge remote-tracking branch 'refs/remotes/origin/VS/approach-spin' i…
Beastvr00k Apr 24, 2026
f84bbb9
fixed small changes
Beastvr00k Apr 24, 2026
e5525e7
Fixed another small error in context
Beastvr00k Apr 24, 2026
3229fe6
Merge branch 'main' into VS/approach-spin
Beastvr00k Apr 25, 2026
f396b15
Merge branch 'main' into VS/approach-spin
Beastvr00k May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/navigation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ navigation:
safe_approach_distance: 5.0
angle_thresh: 0.0872665 #pi/36 radians / 5 degrees
distance_threshold: 1.0
distance_look_threshold: 5.0
check_update_time: 0.06 #seconds
update_delay: 3.0

single_tag:
Expand Down
60 changes: 43 additions & 17 deletions navigation/approach_target.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import numpy as np
from typing import Any
from navigation.trajectory import Trajectory
from navigation.astar import AStar, NoPath, OutOfBounds
from . import costmap_search, stuck_recovery, waypoint, backup, state
from .context import Context
from state_machine.state import State
from geometry_msgs.msg import Twist
from nav_msgs.msg import Path
from visualization_msgs.msg import Marker
from rclpy.publisher import Publisher
from rclpy.time import Time
from rclpy.timer import Timer
from rclpy.duration import Duration
Expand All @@ -19,6 +16,8 @@ class ApproachTargetState(State):
UPDATE_DELAY: float
USE_COSTMAP: bool
DISTANCE_THRESHOLD: float
LOOK_DISTANCE_THRESHOLD: float
CHECK_UPDATE_TIME: float
time_begin: Time
astar_traj: Trajectory
target_traj: Trajectory
Expand All @@ -27,6 +26,7 @@ class ApproachTargetState(State):
target_position: np.ndarray | None
marker_timer: Timer
update_timer: Timer
looking_for_object: bool

def on_enter(self, context: Context) -> None:
from .long_range import LongRangeState
Expand All @@ -46,12 +46,15 @@ def on_enter(self, context: Context) -> None:

self.USE_COSTMAP = context.node.get_parameter("costmap.use_costmap").value or current_waypoint.enable_costmap
self.DISTANCE_THRESHOLD = context.node.get_parameter("search.distance_threshold").value
self.LOOK_DISTANCE_THRESHOLD = context.node.get_parameter("search.distance_look_threshold").value
self.CHECK_UPDATE_TIME = context.node.get_parameter("search.check_update_time").value
self.astar_traj = Trajectory(np.array([]))
self.target_traj = Trajectory(np.array([]))
self.astar = AStar(context=context)
self.target_position = None
self.time_last_updated = context.node.get_clock().now()
self.time_begin = context.node.get_clock().now()
self.looking_for_object = context.course.look_for_object()

self.marker_timer = context.node.create_timer(
context.node.get_parameter("pub_path_rate").value, lambda: self.display_markers(context=context)
Expand Down Expand Up @@ -156,7 +159,6 @@ def on_loop_costmap_enabled(self, context: Context) -> State:
context.node.get_logger().warn("Rover has no pose, waiting...")
context.rover.send_drive_command(Twist())
return self

# If the target trajectory is empty, develop a new path to it
if len(self.target_traj.coordinates) == 0:
context.node.get_logger().info("Generating approach segmented path")
Expand All @@ -171,9 +173,7 @@ def on_loop_costmap_enabled(self, context: Context) -> State:
context=context, point=self.target_traj.get_current_point()
):
context.node.get_logger().info(f"Skipped high cost point")
self.target_traj.increment_point()

if self.target_traj.done():
if self.target_traj.increment_point():
break

if not self.target_traj.done():
Expand Down Expand Up @@ -236,7 +236,6 @@ def on_loop_costmap_enabled(self, context: Context) -> State:
self.astar_traj.clear()
context.node.get_logger().info("Arrived at segment point")
self.target_traj.increment_point()

# If we finished the target trajectory
if self.target_traj.done():
self.target_traj.clear()
Expand All @@ -246,16 +245,13 @@ def on_loop_costmap_enabled(self, context: Context) -> State:
self.target_position = self.get_target_position(context)
return self

# If we are within the distance threshold of the target we have finished
if self.self_in_distance_threshold(context):
return self.next_state(context=context, is_finished=True)

# Otherwise we need to dilate to get closer
else:
context.node.get_logger().info("Too far from target, dilating costmap")
if not context.shrink_dilation():
# Fully dilated and still failed, go to next state
return self.next_state(context=context, is_finished=True)
context.node.get_logger().info("Exited without distance threshold")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what we want though? If we "reach" the target but don't see it, and it necessary to the mission to be seeing it, then we should not move onto the next state.

@Beastvr00k Beastvr00k Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an error check. I can't see any scenario where it gets to this line, but if it does we are going into the done state. I could also get rid of this or change this, so it enters the costmap search state.

return self.next_state(context, is_finished=True)
return self

else:
Expand All @@ -280,7 +276,6 @@ def on_loop_costmap_disabled(self, context: Context) -> State:
context.node.get_parameter("single_tag.stop_threshold").value,
context.node.get_parameter("waypoint.drive_forward_threshold").value,
)

if arrived:
if isinstance(self, LongRangeState):
self.target_position = self.get_target_position(context)
Expand Down Expand Up @@ -352,6 +347,26 @@ def on_loop(self, context: Context) -> State:
# close so we should just return to spiral searching
return costmap_search.CostmapSearchState()

if self.self_in_distance_threshold(context, self.looking_for_object):
# If we are looking for post or in no_search state or looking at an object, we can enter the done state
if not self.looking_for_object or self.looking_at_object(context):
context.node.get_logger().info("Exited through distance threshold")
return self.next_state(context, True)
# else we are not looking at the object, so we want to spin the rover
else:
context.node.get_logger().info("Within distance but not looking at object")
# Use the spin rover function in drive.py
cmd_vel, arrived = context.drive.spin_rover(
context.rover.get_pose_in_map(), self.target_position, self.DISTANCE_THRESHOLD
)
# If we have finished spinning and haven't moved onto a different state, we want to enter the costmap search state
if arrived:
return costmap_search.CostmapSearchState()
# else we want to have the spin drive command applied to the rover
else:
context.rover.send_drive_command(cmd_vel)
return self

if self.USE_COSTMAP:
return self.on_loop_costmap_enabled(context=context)
else:
Expand All @@ -376,18 +391,29 @@ def display_markers(self, context: Context):
points=np.array([self.target_position]), color=[1.0, 1.0, 0.0], ns=str(type(self))
)

def self_in_distance_threshold(self, context: Context):
def self_in_distance_threshold(self, context: Context, is_object: bool) -> bool:
rover_SE3 = context.rover.get_pose_in_map()
if rover_SE3 is None:
return False

target_pos = context.env.current_target_pos()
if target_pos is None:
return False

rover_translation = rover_SE3.translation()[0:2]
# Calculate the distance from the rover to the target
distance_to_target = d_calc(rover_translation, tuple(target_pos))
return distance_to_target < self.DISTANCE_THRESHOLD
# If the target isn't an object, use a smaller distance
if is_object:
return distance_to_target < self.LOOK_DISTANCE_THRESHOLD
else:
return distance_to_target < self.DISTANCE_THRESHOLD

def looking_at_object(self, context: Context) -> bool:
# Figure out how long its been since the TF tree published last
time_diff = context.env.current_time_diff()
if time_diff is None:
return False
return time_diff < Duration(nanoseconds=self.CHECK_UPDATE_TIME * 10**9)

def point_in_distance_threshold(self, context: Context, point):
if point is None:
Expand Down
33 changes: 32 additions & 1 deletion navigation/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ def get_target_position(self, frame: str) -> np.ndarray | None:

return target_pose.translation()

def get_time_diff(self, frame: str) -> None | Duration:
# Try to get message from TF tree with associated time
try:
_, t = SE3.from_tf_tree_with_time(self.ctx.tf_buffer, frame, self.ctx.world_frame)
except (
tf2_ros.LookupException,
tf2_ros.ConnectivityException,
tf2_ros.ExtrapolationException,
):
return None

now = self.ctx.node.get_clock().now()
# Calculate difference between current time and TF tree time
time = Time.from_msg(t)
return now - time

def current_target_pos(self) -> np.ndarray | None:
assert self.ctx.course is not None

Expand All @@ -121,6 +137,21 @@ def current_target_pos(self) -> np.ndarray | None:
case _:
return None

def current_time_diff(self):
assert self.ctx.course is not None
# Return the time difference with an associated target frame
match self.ctx.course.current_waypoint():
case Waypoint(type=WaypointType(val=WaypointType.POST), tag_id=tag_id):
return self.get_time_diff(f"tag{tag_id}")
case Waypoint(type=WaypointType(val=WaypointType.MALLET)):
return self.get_time_diff("hammer")
case Waypoint(type=WaypointType(val=WaypointType.WATER_BOTTLE)):
return self.get_time_diff("bottle")
case Waypoint(type=WaypointType(val=WaypointType.ROCK_PICK)):
return self.get_time_diff("pick")
case _:
return None


class ImageTargetsStore:
"""
Expand Down Expand Up @@ -254,7 +285,7 @@ def look_for_post(self) -> bool:
def look_for_object(self) -> bool:
"""
:return: Whether the currently active waypoint is an object (if it exists).
Either the mallet or the water bottle.
Either the mallet, water bottle, or the rock pick.
"""
current_waypoint = self.current_waypoint()
return current_waypoint is not None and current_waypoint.type.val in {
Expand Down
7 changes: 7 additions & 0 deletions navigation/drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ def get_drive_command(
rover_pose,
)

def spin_rover(self, rover_pose: SE3, target_pos: np.ndarray | None, distance_thresh: float) -> tuple[Twist, bool]:
if target_pos is None:
return Twist(), True
# Use the get_drive_command to spin the rover by setting the angle threshold to a tiny value
cmd_vel, arrived = self.get_drive_command(target_pos, rover_pose, distance_thresh, 1e-8)
return cmd_vel, arrived

def get_default_drive_command(
self: DriveController,
target_pos: np.ndarray,
Expand Down
2 changes: 2 additions & 0 deletions navigation/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def __init__(self, ctx: Context) -> None:
("search.safe_approach_distance", Parameter.Type.DOUBLE),
("search.angle_thresh", Parameter.Type.DOUBLE),
("search.distance_threshold", Parameter.Type.DOUBLE),
("search.distance_look_threshold", Parameter.Type.DOUBLE),
("search.check_update_time", Parameter.Type.DOUBLE),
# Image Targets
("image_targets.increment_weight", Parameter.Type.INTEGER),
("image_targets.decrement_weight", Parameter.Type.INTEGER),
Expand Down
Loading