2626from dimos .core import Module , In , Out , rpc
2727from dimos .msgs .geometry_msgs import PoseStamped
2828from dimos .navigation .local_planner .local_planner import BaseLocalPlanner
29+ from dimos .protocol .tf import TF
2930from dimos .utils .logging_config import setup_logger
3031from dimos_lcm .std_msgs import Bool
32+ from dimos .utils .transform_utils import apply_transform
3133
3234logger = setup_logger ("dimos.navigation.bt_navigator" )
3335
@@ -92,6 +94,9 @@ def __init__(self, publishing_frequency: float = 1.0, **kwargs):
9294 # Local planner reference (to be connected externally)
9395 self .local_planner : Optional [BaseLocalPlanner ] = None
9496
97+ # TF listener
98+ self .tf = TF ()
99+
95100 logger .info ("Navigator initialized" )
96101
97102 @rpc
@@ -146,14 +151,19 @@ def set_goal(self, goal: PoseStamped) -> bool:
146151 Returns:
147152 True if goal was accepted, False otherwise
148153 """
154+ transformed_goal = self ._transform_goal_to_odom_frame (goal )
155+ if not transformed_goal :
156+ logger .error ("Failed to transform goal to odometry frame" )
157+ return False
158+
149159 with self .goal_lock :
150- self .current_goal = goal
160+ self .current_goal = transformed_goal
151161
152162 with self .state_lock :
153163 self .state = NavigatorState .FOLLOWING_PATH
154164
155165 logger .info (
156- f"New goal set: ({ goal .position .x :.2f} , { goal .position .y :.2f} , { goal .position .z :.2f} )"
166+ f"New goal set: ({ transformed_goal .position .x :.2f} , { transformed_goal .position .y :.2f} , { transformed_goal .position .z :.2f} )"
157167 )
158168 return True
159169
@@ -163,6 +173,11 @@ def connect_local_planner(self, local_planner):
163173 self .local_planner = local_planner
164174 logger .info ("Local planner connected" )
165175
176+ @rpc
177+ def get_state (self ) -> NavigatorState :
178+ """Get the current state of the navigator."""
179+ return self .state
180+
166181 def _on_odom (self , msg : PoseStamped ):
167182 """Handle incoming odometry messages."""
168183 self .latest_odom = msg
@@ -171,70 +186,94 @@ def _on_goal_request(self, msg: PoseStamped):
171186 """Handle incoming goal requests."""
172187 self .set_goal (msg )
173188
189+ def _transform_goal_to_odom_frame (self , goal : PoseStamped ) -> Optional [PoseStamped ]:
190+ """Transform goal pose to the odometry frame."""
191+ if not self .latest_odom :
192+ logger .warning ("No odometry available yet, cannot transform goal" )
193+ return None
194+
195+ if not goal .frame_id :
196+ return goal
197+
198+ odom_frame = self .latest_odom .frame_id
199+ if goal .frame_id == odom_frame :
200+ return goal
201+
202+ try :
203+ transform = self .tf .get (
204+ parent_frame = odom_frame ,
205+ child_frame = goal .frame_id ,
206+ time_point = goal .ts ,
207+ time_tolerance = 1.0 ,
208+ )
209+
210+ if not transform :
211+ logger .error (f"Could not find transform from '{ goal .frame_id } ' to '{ odom_frame } '" )
212+ return None
213+
214+ pose = apply_transform (goal , transform )
215+ transformed_goal = PoseStamped (
216+ position = pose .position ,
217+ orientation = pose .orientation ,
218+ frame_id = odom_frame ,
219+ ts = goal .ts ,
220+ )
221+ return transformed_goal
222+
223+ except Exception as e :
224+ logger .error (f"Failed to transform goal: { e } " )
225+ return None
226+
174227 def _control_loop (self ):
175228 """Main control loop running in separate thread."""
176229 while not self .stop_event .is_set ():
177230 with self .state_lock :
178231 current_state = self .state
179232
180233 if current_state == NavigatorState .FOLLOWING_PATH :
181- # Publish goal to global planner
182234 with self .goal_lock :
183235 goal = self .current_goal
184236
185237 if goal is not None :
186238 self .goal .publish (goal )
187239
188- # Check if goal reached
189240 if self .local_planner and self .is_goal_reached ():
190241 logger .info ("Goal reached!" )
191242 reached_msg = Bool ()
192243 reached_msg .data = True
193244 self .goal_reached .publish (reached_msg )
194- # Reset local planner to clear the path
195245 self .local_planner .reset ()
196246 with self .goal_lock :
197247 self .current_goal = None
198248 with self .state_lock :
199249 self .state = NavigatorState .IDLE
200250
201251 elif current_state == NavigatorState .RECOVERY :
202- # Recovery mode placeholder - transitions back to IDLE
203252 with self .state_lock :
204253 self .state = NavigatorState .IDLE
205254
206- # Sleep for control period
207255 time .sleep (self .publishing_period )
208256
209257 @rpc
210258 def is_goal_reached (self ) -> bool :
211- """
212- Check if the current goal has been reached.
213-
214- Returns:
215- True if goal reached, False otherwise
216- """
259+ """Check if the current goal has been reached."""
217260 if self .local_planner is None :
218261 return False
219262
220263 try :
221- # Call local planner's is_goal_reached RPC
222264 return self .local_planner .is_goal_reached ()
223265 except Exception as e :
224266 logger .error (f"Failed to check goal status: { e } " )
225267 return False
226268
227269 def stop (self ):
228270 """Stop navigation and return to IDLE state."""
229- # Cancel any active goal
230271 with self .goal_lock :
231272 self .current_goal = None
232273
233- # Set state to IDLE
234274 with self .state_lock :
235275 self .state = NavigatorState .IDLE
236276
237- # Stop the local planner
238277 if self .local_planner :
239278 self .local_planner .reset ()
240279
0 commit comments