foodforthought-cli 0.2.7__py3-none-any.whl → 0.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,389 @@
1
+ """
2
+ Navigation interface for autonomous robot movement.
3
+
4
+ This is a HIGHER-LEVEL interface that abstracts:
5
+ - Path planning
6
+ - Obstacle avoidance
7
+ - Localization
8
+ - Goal-directed movement
9
+
10
+ Design principle: The interface handles "navigate to X" commands
11
+ without exposing the underlying locomotion complexity.
12
+ """
13
+
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass, field
16
+ from typing import List, Optional, Callable, Tuple
17
+ from enum import Enum, auto
18
+
19
+ from .types import Vector3, Quaternion, Pose, ActionResult
20
+
21
+
22
+ class NavigationState(Enum):
23
+ """Current state of the navigation system."""
24
+ IDLE = auto() # Not navigating
25
+ NAVIGATING = auto() # Moving toward goal
26
+ ROTATING = auto() # Rotating in place
27
+ AVOIDING = auto() # Avoiding obstacle
28
+ PAUSED = auto() # Navigation paused
29
+ COMPLETED = auto() # Reached goal
30
+ FAILED = auto() # Failed to reach goal
31
+
32
+
33
+ @dataclass
34
+ class NavigationGoal:
35
+ """A navigation goal."""
36
+ position: Vector3 # Target position
37
+ orientation: Optional[Quaternion] = None # Target orientation (optional)
38
+ tolerance_position: float = 0.1 # Position tolerance in meters
39
+ tolerance_rotation: float = 0.1 # Rotation tolerance in radians
40
+ timeout: float = 60.0 # Timeout in seconds
41
+ frame_id: str = "world" # Coordinate frame
42
+
43
+
44
+ @dataclass
45
+ class NavigationStatus:
46
+ """Current status of navigation."""
47
+ state: NavigationState
48
+ goal: Optional[NavigationGoal] = None
49
+ distance_to_goal: float = 0.0
50
+ estimated_time_remaining: float = 0.0
51
+ current_pose: Optional[Pose] = None
52
+ path_length: float = 0.0
53
+ obstacles_detected: int = 0
54
+ error_message: str = ""
55
+
56
+
57
+ @dataclass
58
+ class Waypoint:
59
+ """A waypoint in a path."""
60
+ position: Vector3
61
+ orientation: Optional[Quaternion] = None
62
+ speed: float = 0.5 # Desired speed at this waypoint
63
+ name: str = ""
64
+
65
+
66
+ class NavigationInterface(ABC):
67
+ """
68
+ Interface for autonomous navigation.
69
+
70
+ Abstracts the complexity of:
71
+ - SLAM (mapping and localization)
72
+ - Path planning (global and local)
73
+ - Obstacle avoidance
74
+ - Recovery behaviors
75
+
76
+ Use cases:
77
+ - Navigate to a specific location
78
+ - Follow a predefined path
79
+ - Patrol an area
80
+ - Return to home/charging station
81
+ """
82
+
83
+ @abstractmethod
84
+ def navigate_to(self, goal: NavigationGoal) -> ActionResult:
85
+ """
86
+ Start navigating to a goal.
87
+
88
+ This is NON-BLOCKING - returns immediately.
89
+ Use get_status() or wait_for_goal() to monitor progress.
90
+
91
+ Args:
92
+ goal: Navigation goal
93
+
94
+ Returns:
95
+ ActionResult indicating if navigation started
96
+ """
97
+ pass
98
+
99
+ @abstractmethod
100
+ def get_status(self) -> NavigationStatus:
101
+ """
102
+ Get current navigation status.
103
+
104
+ Returns:
105
+ NavigationStatus with current state
106
+ """
107
+ pass
108
+
109
+ @abstractmethod
110
+ def cancel(self) -> ActionResult:
111
+ """
112
+ Cancel current navigation.
113
+
114
+ Returns:
115
+ ActionResult indicating if cancelled
116
+ """
117
+ pass
118
+
119
+ @abstractmethod
120
+ def get_pose(self) -> Pose:
121
+ """
122
+ Get current robot pose.
123
+
124
+ Returns:
125
+ Pose in the world frame
126
+ """
127
+ pass
128
+
129
+ # =========================================================================
130
+ # Convenience methods
131
+ # =========================================================================
132
+
133
+ def navigate_to_point(
134
+ self,
135
+ x: float,
136
+ y: float,
137
+ z: float = 0.0,
138
+ timeout: float = 60.0
139
+ ) -> ActionResult:
140
+ """
141
+ Navigate to a point (convenience wrapper).
142
+
143
+ Args:
144
+ x, y, z: Target coordinates
145
+ timeout: Maximum time to reach goal
146
+
147
+ Returns:
148
+ ActionResult
149
+ """
150
+ goal = NavigationGoal(
151
+ position=Vector3(x, y, z),
152
+ timeout=timeout
153
+ )
154
+ return self.navigate_to(goal)
155
+
156
+ def navigate_to_pose(
157
+ self,
158
+ x: float,
159
+ y: float,
160
+ yaw: float,
161
+ timeout: float = 60.0
162
+ ) -> ActionResult:
163
+ """
164
+ Navigate to a specific pose (position + heading).
165
+
166
+ Args:
167
+ x, y: Target position
168
+ yaw: Target heading in radians
169
+ timeout: Maximum time
170
+
171
+ Returns:
172
+ ActionResult
173
+ """
174
+ import math
175
+ goal = NavigationGoal(
176
+ position=Vector3(x, y, 0),
177
+ orientation=Quaternion.from_euler(0, 0, yaw),
178
+ timeout=timeout
179
+ )
180
+ return self.navigate_to(goal)
181
+
182
+ def wait_for_goal(self, timeout: float = 60.0) -> NavigationStatus:
183
+ """
184
+ Block until navigation completes or times out.
185
+
186
+ Args:
187
+ timeout: Maximum wait time
188
+
189
+ Returns:
190
+ Final NavigationStatus
191
+ """
192
+ import time
193
+ start = time.time()
194
+ while time.time() - start < timeout:
195
+ status = self.get_status()
196
+ if status.state in [NavigationState.COMPLETED, NavigationState.FAILED, NavigationState.IDLE]:
197
+ return status
198
+ time.sleep(0.1)
199
+ return self.get_status()
200
+
201
+ def rotate_to(self, yaw: float) -> ActionResult:
202
+ """
203
+ Rotate in place to face a direction.
204
+
205
+ Args:
206
+ yaw: Target heading in radians
207
+
208
+ Returns:
209
+ ActionResult
210
+ """
211
+ current = self.get_pose()
212
+ return self.navigate_to_pose(
213
+ current.position.x,
214
+ current.position.y,
215
+ yaw,
216
+ timeout=10.0
217
+ )
218
+
219
+ def move_forward(self, distance: float) -> ActionResult:
220
+ """
221
+ Move forward by a distance.
222
+
223
+ Args:
224
+ distance: Distance in meters
225
+
226
+ Returns:
227
+ ActionResult
228
+ """
229
+ import math
230
+ current = self.get_pose()
231
+ _, _, yaw = current.orientation.to_euler()
232
+ target_x = current.position.x + distance * math.cos(yaw)
233
+ target_y = current.position.y + distance * math.sin(yaw)
234
+ return self.navigate_to_point(target_x, target_y)
235
+
236
+ def move_backward(self, distance: float) -> ActionResult:
237
+ """
238
+ Move backward by a distance.
239
+
240
+ Args:
241
+ distance: Distance in meters
242
+
243
+ Returns:
244
+ ActionResult
245
+ """
246
+ return self.move_forward(-distance)
247
+
248
+ # =========================================================================
249
+ # Path following
250
+ # =========================================================================
251
+
252
+ def follow_path(self, waypoints: List[Waypoint]) -> ActionResult:
253
+ """
254
+ Follow a sequence of waypoints.
255
+
256
+ Args:
257
+ waypoints: List of waypoints to follow
258
+
259
+ Returns:
260
+ ActionResult
261
+ """
262
+ for i, wp in enumerate(waypoints):
263
+ goal = NavigationGoal(
264
+ position=wp.position,
265
+ orientation=wp.orientation
266
+ )
267
+ result = self.navigate_to(goal)
268
+ if not result.success:
269
+ return ActionResult.error(f"Failed at waypoint {i}: {result.message}")
270
+ status = self.wait_for_goal()
271
+ if status.state != NavigationState.COMPLETED:
272
+ return ActionResult.error(f"Failed at waypoint {i}: {status.error_message}")
273
+ return ActionResult.success("Path completed")
274
+
275
+ def patrol(
276
+ self,
277
+ waypoints: List[Waypoint],
278
+ loops: int = 1,
279
+ callback: Optional[Callable[[int, Waypoint], None]] = None
280
+ ) -> ActionResult:
281
+ """
282
+ Patrol a set of waypoints.
283
+
284
+ Args:
285
+ waypoints: List of patrol waypoints
286
+ loops: Number of patrol loops (0 = infinite)
287
+ callback: Called at each waypoint (loop_num, waypoint)
288
+
289
+ Returns:
290
+ ActionResult
291
+ """
292
+ loop_count = 0
293
+ while loops == 0 or loop_count < loops:
294
+ for wp in waypoints:
295
+ if callback:
296
+ callback(loop_count, wp)
297
+ result = self.navigate_to(NavigationGoal(position=wp.position))
298
+ if not result.success:
299
+ return result
300
+ status = self.wait_for_goal()
301
+ if status.state != NavigationState.COMPLETED:
302
+ return ActionResult.error(status.error_message)
303
+ loop_count += 1
304
+ return ActionResult.success(f"Completed {loop_count} patrol loops")
305
+
306
+ # =========================================================================
307
+ # Obstacle handling
308
+ # =========================================================================
309
+
310
+ def is_path_clear(self) -> bool:
311
+ """
312
+ Check if the current path is clear of obstacles.
313
+
314
+ Returns:
315
+ True if path is clear
316
+ """
317
+ status = self.get_status()
318
+ return status.obstacles_detected == 0
319
+
320
+ def get_obstacle_distance(self) -> Optional[float]:
321
+ """
322
+ Get distance to nearest obstacle.
323
+
324
+ Returns:
325
+ Distance in meters, or None if no obstacles
326
+ """
327
+ return None # Default implementation
328
+
329
+ # =========================================================================
330
+ # Home/docking
331
+ # =========================================================================
332
+
333
+ def go_home(self) -> ActionResult:
334
+ """
335
+ Navigate to the home position.
336
+
337
+ Returns:
338
+ ActionResult
339
+ """
340
+ return ActionResult.error("Home position not configured")
341
+
342
+ def set_home(self) -> ActionResult:
343
+ """
344
+ Set current position as home.
345
+
346
+ Returns:
347
+ ActionResult
348
+ """
349
+ return ActionResult.error("Home position not supported")
350
+
351
+
352
+ class SimpleNavigationInterface(NavigationInterface):
353
+ """
354
+ Simple navigation using dead reckoning.
355
+
356
+ For robots without full SLAM - uses odometry/IMU only.
357
+ Good for short distances in clear environments.
358
+ """
359
+
360
+ def __init__(self, locomotion_interface):
361
+ """
362
+ Args:
363
+ locomotion_interface: QuadrupedLocomotion or similar
364
+ """
365
+ self._locomotion = locomotion_interface
366
+ self._state = NavigationState.IDLE
367
+ self._current_goal = None
368
+ self._pose = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1))
369
+
370
+ def navigate_to(self, goal: NavigationGoal) -> ActionResult:
371
+ """Simple point-to-point navigation."""
372
+ self._current_goal = goal
373
+ self._state = NavigationState.NAVIGATING
374
+ return ActionResult.success("Navigation started")
375
+
376
+ def get_status(self) -> NavigationStatus:
377
+ return NavigationStatus(
378
+ state=self._state,
379
+ goal=self._current_goal,
380
+ current_pose=self._pose
381
+ )
382
+
383
+ def cancel(self) -> ActionResult:
384
+ self._state = NavigationState.IDLE
385
+ self._current_goal = None
386
+ return ActionResult.success("Cancelled")
387
+
388
+ def get_pose(self) -> Pose:
389
+ return self._pose