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.
- ate/behaviors/__init__.py +88 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +610 -54
- ate/drivers/__init__.py +27 -0
- ate/drivers/mechdog.py +606 -0
- ate/interfaces/__init__.py +171 -0
- ate/interfaces/base.py +271 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/types.py +371 -0
- ate/mcp_server.py +387 -0
- ate/recording/__init__.py +44 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +405 -0
- ate/recording/upload.py +304 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +79 -0
- ate/robot/calibration.py +583 -0
- ate/robot/commands.py +3603 -0
- ate/robot/discovery.py +339 -0
- ate/robot/introspection.py +330 -0
- ate/robot/manager.py +270 -0
- ate/robot/profiles.py +275 -0
- ate/robot/registry.py +319 -0
- ate/robot/skill_upload.py +393 -0
- ate/robot/visual_labeler.py +1039 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/METADATA +9 -1
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/RECORD +36 -7
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -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
|