foodforthought-cli 0.2.4__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.
Files changed (37) hide show
  1. ate/__init__.py +1 -1
  2. ate/behaviors/__init__.py +88 -0
  3. ate/behaviors/common.py +686 -0
  4. ate/behaviors/tree.py +454 -0
  5. ate/cli.py +610 -54
  6. ate/drivers/__init__.py +27 -0
  7. ate/drivers/mechdog.py +606 -0
  8. ate/interfaces/__init__.py +171 -0
  9. ate/interfaces/base.py +271 -0
  10. ate/interfaces/body.py +267 -0
  11. ate/interfaces/detection.py +282 -0
  12. ate/interfaces/locomotion.py +422 -0
  13. ate/interfaces/manipulation.py +408 -0
  14. ate/interfaces/navigation.py +389 -0
  15. ate/interfaces/perception.py +362 -0
  16. ate/interfaces/types.py +371 -0
  17. ate/mcp_server.py +387 -0
  18. ate/recording/__init__.py +44 -0
  19. ate/recording/demonstration.py +378 -0
  20. ate/recording/session.py +405 -0
  21. ate/recording/upload.py +304 -0
  22. ate/recording/wrapper.py +95 -0
  23. ate/robot/__init__.py +79 -0
  24. ate/robot/calibration.py +583 -0
  25. ate/robot/commands.py +3603 -0
  26. ate/robot/discovery.py +339 -0
  27. ate/robot/introspection.py +330 -0
  28. ate/robot/manager.py +270 -0
  29. ate/robot/profiles.py +275 -0
  30. ate/robot/registry.py +319 -0
  31. ate/robot/skill_upload.py +393 -0
  32. ate/robot/visual_labeler.py +1039 -0
  33. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/METADATA +9 -1
  34. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/RECORD +37 -8
  35. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/WHEEL +0 -0
  36. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/entry_points.txt +0 -0
  37. {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,171 @@
1
+ """
2
+ FoodforThought Robot Interfaces
3
+
4
+ Abstract interfaces that define capabilities for any robot.
5
+ Implementations are hardware-specific, but skills are written against these interfaces.
6
+
7
+ Architecture:
8
+ RobotInterface (base)
9
+ ├── Capabilities (mixins)
10
+ │ ├── Locomotion
11
+ │ │ ├── QuadrupedLocomotion
12
+ │ │ ├── BipedLocomotion
13
+ │ │ ├── WheeledLocomotion
14
+ │ │ └── AerialLocomotion
15
+ │ ├── Manipulation
16
+ │ │ ├── ArmInterface
17
+ │ │ └── GripperInterface
18
+ │ ├── Perception
19
+ │ │ ├── CameraInterface
20
+ │ │ ├── DepthCameraInterface
21
+ │ │ ├── LidarInterface
22
+ │ │ └── IMUInterface
23
+ │ └── Body
24
+ │ └── BodyPoseInterface
25
+ └── Safety
26
+ └── SafetyInterface
27
+
28
+ Example:
29
+ # MechDog implements:
30
+ class MechDogDriver(QuadrupedLocomotion, BodyPoseInterface, SafetyInterface):
31
+ ...
32
+
33
+ # Spot with arm implements:
34
+ class SpotDriver(QuadrupedLocomotion, BodyPoseInterface, ArmInterface,
35
+ GripperInterface, CameraInterface, LidarInterface, SafetyInterface):
36
+ ...
37
+
38
+ # Skills are written against interfaces:
39
+ def pick_and_place(robot: QuadrupedLocomotion & GripperInterface, target: Vector3):
40
+ robot.walk_to(target)
41
+ robot.lower_body(height=0.1)
42
+ robot.grasp()
43
+ ...
44
+ """
45
+
46
+ from .types import (
47
+ Vector3,
48
+ Quaternion,
49
+ Pose,
50
+ Twist,
51
+ JointState,
52
+ JointLimits,
53
+ Image,
54
+ DepthImage,
55
+ PointCloud,
56
+ IMUReading,
57
+ ForceTorqueReading,
58
+ BatteryState,
59
+ RobotStatus,
60
+ GaitType,
61
+ GripperState,
62
+ GripperStatus,
63
+ ActionResult,
64
+ )
65
+
66
+ from .base import (
67
+ RobotInterface,
68
+ SafetyInterface,
69
+ Capability,
70
+ RobotInfo,
71
+ )
72
+
73
+ from .locomotion import (
74
+ LocomotionInterface,
75
+ QuadrupedLocomotion,
76
+ BipedLocomotion,
77
+ WheeledLocomotion,
78
+ AerialLocomotion,
79
+ )
80
+
81
+ from .manipulation import (
82
+ ArmInterface,
83
+ GripperInterface,
84
+ DualArmInterface,
85
+ )
86
+
87
+ from .perception import (
88
+ CameraInterface,
89
+ DepthCameraInterface,
90
+ LidarInterface,
91
+ IMUInterface,
92
+ ForceTorqueInterface,
93
+ )
94
+
95
+ from .body import (
96
+ BodyPoseInterface,
97
+ )
98
+
99
+ from .detection import (
100
+ BoundingBox,
101
+ Detection,
102
+ DetectionResult,
103
+ ObjectDetectionInterface,
104
+ TrashDetectionInterface,
105
+ )
106
+
107
+ from .navigation import (
108
+ NavigationState,
109
+ NavigationGoal,
110
+ NavigationStatus,
111
+ Waypoint,
112
+ NavigationInterface,
113
+ SimpleNavigationInterface,
114
+ )
115
+
116
+ __all__ = [
117
+ # Types
118
+ "Vector3",
119
+ "Quaternion",
120
+ "Pose",
121
+ "Twist",
122
+ "JointState",
123
+ "JointLimits",
124
+ "Image",
125
+ "DepthImage",
126
+ "PointCloud",
127
+ "IMUReading",
128
+ "ForceTorqueReading",
129
+ "BatteryState",
130
+ "RobotStatus",
131
+ "GaitType",
132
+ "GripperState",
133
+ "GripperStatus",
134
+ "ActionResult",
135
+ # Base
136
+ "RobotInterface",
137
+ "SafetyInterface",
138
+ "Capability",
139
+ "RobotInfo",
140
+ # Locomotion
141
+ "LocomotionInterface",
142
+ "QuadrupedLocomotion",
143
+ "BipedLocomotion",
144
+ "WheeledLocomotion",
145
+ "AerialLocomotion",
146
+ # Manipulation
147
+ "ArmInterface",
148
+ "GripperInterface",
149
+ "DualArmInterface",
150
+ # Perception
151
+ "CameraInterface",
152
+ "DepthCameraInterface",
153
+ "LidarInterface",
154
+ "IMUInterface",
155
+ "ForceTorqueInterface",
156
+ # Body
157
+ "BodyPoseInterface",
158
+ # Detection (higher-level)
159
+ "BoundingBox",
160
+ "Detection",
161
+ "DetectionResult",
162
+ "ObjectDetectionInterface",
163
+ "TrashDetectionInterface",
164
+ # Navigation (higher-level)
165
+ "NavigationState",
166
+ "NavigationGoal",
167
+ "NavigationStatus",
168
+ "Waypoint",
169
+ "NavigationInterface",
170
+ "SimpleNavigationInterface",
171
+ ]
ate/interfaces/base.py ADDED
@@ -0,0 +1,271 @@
1
+ """
2
+ Base interfaces that all robots must implement.
3
+
4
+ Every robot, regardless of type, has:
5
+ 1. Identity - what it is, what it can do
6
+ 2. Safety - emergency stop, status monitoring
7
+ 3. Lifecycle - connect, disconnect, ready state
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass, field
12
+ from typing import List, Optional, Set, Type, TYPE_CHECKING
13
+ from enum import Enum, auto
14
+
15
+ from .types import RobotStatus, BatteryState, ActionResult
16
+
17
+ if TYPE_CHECKING:
18
+ from .locomotion import LocomotionInterface
19
+ from .manipulation import ArmInterface, GripperInterface
20
+ from .perception import CameraInterface, LidarInterface, IMUInterface
21
+
22
+
23
+ class Capability(Enum):
24
+ """
25
+ Capabilities a robot can have.
26
+ Used for runtime capability checking and skill compatibility.
27
+ """
28
+ # Locomotion
29
+ QUADRUPED = auto()
30
+ BIPED = auto()
31
+ WHEELED = auto()
32
+ AERIAL = auto()
33
+ AQUATIC = auto()
34
+
35
+ # Manipulation
36
+ ARM = auto()
37
+ DUAL_ARM = auto()
38
+ GRIPPER = auto()
39
+ SUCTION = auto()
40
+
41
+ # Perception
42
+ CAMERA = auto()
43
+ DEPTH_CAMERA = auto()
44
+ LIDAR = auto()
45
+ IMU = auto()
46
+ FORCE_TORQUE = auto()
47
+ TACTILE = auto()
48
+
49
+ # Body control
50
+ BODY_POSE = auto()
51
+
52
+ # Communication
53
+ AUDIO_INPUT = auto()
54
+ AUDIO_OUTPUT = auto()
55
+
56
+
57
+ @dataclass
58
+ class RobotInfo:
59
+ """
60
+ Static information about a robot.
61
+ Used for compatibility checking and UI display.
62
+ """
63
+ # Identity
64
+ name: str # Human-readable name
65
+ model: str # Model identifier (e.g., "hiwonder_mechdog")
66
+ manufacturer: str # Manufacturer name
67
+ archetype: str # Primary type: "quadruped", "humanoid", "arm", etc.
68
+
69
+ # Capabilities
70
+ capabilities: Set[Capability] = field(default_factory=set)
71
+
72
+ # Physical properties
73
+ mass: Optional[float] = None # kg
74
+ payload_capacity: Optional[float] = None # kg
75
+ reach: Optional[float] = None # meters (for arms)
76
+ dimensions: Optional[tuple] = None # (length, width, height) in meters
77
+
78
+ # Workspace limits (in base frame)
79
+ workspace_min: Optional[tuple] = None # (x, y, z) min bounds
80
+ workspace_max: Optional[tuple] = None # (x, y, z) max bounds
81
+
82
+ # Performance
83
+ max_speed: Optional[float] = None # m/s
84
+ battery_capacity: Optional[float] = None # Wh
85
+
86
+ # Metadata
87
+ firmware_version: Optional[str] = None
88
+ serial_number: Optional[str] = None
89
+ description: str = ""
90
+
91
+ def has_capability(self, cap: Capability) -> bool:
92
+ return cap in self.capabilities
93
+
94
+ def has_all_capabilities(self, caps: Set[Capability]) -> bool:
95
+ return caps.issubset(self.capabilities)
96
+
97
+
98
+ class RobotInterface(ABC):
99
+ """
100
+ Base interface that ALL robots must implement.
101
+
102
+ This provides:
103
+ - Identity and capability information
104
+ - Connection lifecycle
105
+ - Status monitoring
106
+
107
+ Specific capabilities (locomotion, manipulation, etc.) are added via mixins.
108
+ """
109
+
110
+ @abstractmethod
111
+ def get_info(self) -> RobotInfo:
112
+ """
113
+ Get static information about this robot.
114
+
115
+ Returns:
116
+ RobotInfo with name, model, capabilities, etc.
117
+ """
118
+ pass
119
+
120
+ @abstractmethod
121
+ def connect(self) -> ActionResult:
122
+ """
123
+ Establish connection to the robot hardware.
124
+
125
+ Returns:
126
+ ActionResult indicating success/failure
127
+ """
128
+ pass
129
+
130
+ @abstractmethod
131
+ def disconnect(self) -> ActionResult:
132
+ """
133
+ Safely disconnect from the robot hardware.
134
+
135
+ Returns:
136
+ ActionResult indicating success/failure
137
+ """
138
+ pass
139
+
140
+ @abstractmethod
141
+ def is_connected(self) -> bool:
142
+ """
143
+ Check if currently connected to the robot.
144
+
145
+ Returns:
146
+ True if connected and communication is working
147
+ """
148
+ pass
149
+
150
+ @abstractmethod
151
+ def get_status(self) -> RobotStatus:
152
+ """
153
+ Get current robot status.
154
+
155
+ Returns:
156
+ RobotStatus with mode, errors, battery, etc.
157
+ """
158
+ pass
159
+
160
+ def has_capability(self, cap: Capability) -> bool:
161
+ """Check if robot has a specific capability."""
162
+ return self.get_info().has_capability(cap)
163
+
164
+ def get_capabilities(self) -> Set[Capability]:
165
+ """Get all capabilities of this robot."""
166
+ return self.get_info().capabilities
167
+
168
+
169
+ class SafetyInterface(ABC):
170
+ """
171
+ Safety interface that all robots should implement.
172
+
173
+ Provides emergency stop and safety monitoring.
174
+ """
175
+
176
+ @abstractmethod
177
+ def emergency_stop(self) -> ActionResult:
178
+ """
179
+ Immediately stop all motion and enter safe state.
180
+
181
+ This should:
182
+ - Stop all actuators immediately
183
+ - Disable motor power if possible
184
+ - Set robot to ESTOPPED mode
185
+
186
+ Returns:
187
+ ActionResult (should rarely fail)
188
+ """
189
+ pass
190
+
191
+ @abstractmethod
192
+ def reset_emergency_stop(self) -> ActionResult:
193
+ """
194
+ Clear emergency stop state and return to ready.
195
+
196
+ Returns:
197
+ ActionResult indicating if reset was successful
198
+ """
199
+ pass
200
+
201
+ @abstractmethod
202
+ def is_estopped(self) -> bool:
203
+ """
204
+ Check if emergency stop is active.
205
+
206
+ Returns:
207
+ True if robot is in emergency stop state
208
+ """
209
+ pass
210
+
211
+ @abstractmethod
212
+ def get_battery_state(self) -> Optional[BatteryState]:
213
+ """
214
+ Get current battery status.
215
+
216
+ Returns:
217
+ BatteryState or None if no battery
218
+ """
219
+ pass
220
+
221
+ def check_safety(self) -> List[str]:
222
+ """
223
+ Run safety checks and return list of issues.
224
+
225
+ Returns:
226
+ List of safety warning/error messages (empty if all OK)
227
+ """
228
+ issues = []
229
+
230
+ battery = self.get_battery_state()
231
+ if battery:
232
+ if battery.percentage < 0.1:
233
+ issues.append(f"Critical battery: {battery.percentage*100:.0f}%")
234
+ elif battery.percentage < 0.2:
235
+ issues.append(f"Low battery: {battery.percentage*100:.0f}%")
236
+
237
+ if self.is_estopped():
238
+ issues.append("Emergency stop is active")
239
+
240
+ return issues
241
+
242
+
243
+ # =============================================================================
244
+ # Utility functions for capability checking
245
+ # =============================================================================
246
+
247
+ def requires_capabilities(*caps: Capability):
248
+ """
249
+ Decorator to mark that a skill requires certain capabilities.
250
+
251
+ Usage:
252
+ @requires_capabilities(Capability.QUADRUPED, Capability.GRIPPER)
253
+ def pick_up_object(robot, target):
254
+ ...
255
+ """
256
+ def decorator(func):
257
+ func._required_capabilities = set(caps)
258
+ return func
259
+ return decorator
260
+
261
+
262
+ def check_robot_compatibility(robot: RobotInterface, required: Set[Capability]) -> List[str]:
263
+ """
264
+ Check if a robot is compatible with required capabilities.
265
+
266
+ Returns:
267
+ List of missing capabilities (empty if compatible)
268
+ """
269
+ robot_caps = robot.get_capabilities()
270
+ missing = required - robot_caps
271
+ return [cap.name for cap in missing]
ate/interfaces/body.py ADDED
@@ -0,0 +1,267 @@
1
+ """
2
+ Body pose interfaces for robots with adjustable body position/orientation.
3
+
4
+ Primarily for legged robots that can:
5
+ - Adjust body height
6
+ - Tilt body (roll, pitch, yaw)
7
+ - Shift body position relative to feet
8
+
9
+ These are independent of locomotion - you can adjust pose while standing still.
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+ from typing import Optional, Tuple
14
+
15
+ from .types import (
16
+ Vector3,
17
+ Quaternion,
18
+ Pose,
19
+ ActionResult,
20
+ )
21
+
22
+
23
+ class BodyPoseInterface(ABC):
24
+ """
25
+ Interface for controlling robot body pose.
26
+
27
+ Implemented by quadrupeds, bipeds, and other robots that can adjust
28
+ their body position/orientation independent of locomotion.
29
+
30
+ Coordinate frame:
31
+ - Body origin is typically at the center of the robot
32
+ - Z+ is up, X+ is forward, Y+ is left
33
+ """
34
+
35
+ # =========================================================================
36
+ # Body height control
37
+ # =========================================================================
38
+
39
+ @abstractmethod
40
+ def set_body_height(self, height: float) -> ActionResult:
41
+ """
42
+ Set body height above ground.
43
+
44
+ Args:
45
+ height: Height in meters (from nominal ground level)
46
+
47
+ Returns:
48
+ ActionResult
49
+ """
50
+ pass
51
+
52
+ @abstractmethod
53
+ def get_body_height(self) -> float:
54
+ """
55
+ Get current body height.
56
+
57
+ Returns:
58
+ Height in meters
59
+ """
60
+ pass
61
+
62
+ def get_height_limits(self) -> Tuple[float, float]:
63
+ """
64
+ Get allowed body height range.
65
+
66
+ Returns:
67
+ (min_height, max_height) in meters
68
+ """
69
+ return (0.05, 0.30) # Default for small quadrupeds
70
+
71
+ # =========================================================================
72
+ # Body orientation control
73
+ # =========================================================================
74
+
75
+ @abstractmethod
76
+ def set_body_orientation(
77
+ self,
78
+ roll: float = 0.0,
79
+ pitch: float = 0.0,
80
+ yaw: float = 0.0
81
+ ) -> ActionResult:
82
+ """
83
+ Set body orientation (tilt).
84
+
85
+ Args:
86
+ roll: Roll angle in radians (positive = right side down)
87
+ pitch: Pitch angle in radians (positive = nose down)
88
+ yaw: Yaw angle in radians (positive = turn left)
89
+
90
+ Returns:
91
+ ActionResult
92
+ """
93
+ pass
94
+
95
+ @abstractmethod
96
+ def get_body_orientation(self) -> Tuple[float, float, float]:
97
+ """
98
+ Get current body orientation.
99
+
100
+ Returns:
101
+ (roll, pitch, yaw) in radians
102
+ """
103
+ pass
104
+
105
+ def get_orientation_limits(self) -> Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]]:
106
+ """
107
+ Get allowed orientation ranges.
108
+
109
+ Returns:
110
+ ((roll_min, roll_max), (pitch_min, pitch_max), (yaw_min, yaw_max))
111
+ All in radians
112
+ """
113
+ import math
114
+ return (
115
+ (-math.pi/6, math.pi/6), # ±30° roll
116
+ (-math.pi/6, math.pi/6), # ±30° pitch
117
+ (-math.pi/4, math.pi/4), # ±45° yaw
118
+ )
119
+
120
+ # =========================================================================
121
+ # Body position control
122
+ # =========================================================================
123
+
124
+ def set_body_position(self, offset: Vector3) -> ActionResult:
125
+ """
126
+ Shift body position relative to feet.
127
+
128
+ This moves the body while keeping feet planted.
129
+
130
+ Args:
131
+ offset: Position offset from center (x=forward, y=left, z=up)
132
+
133
+ Returns:
134
+ ActionResult
135
+ """
136
+ return ActionResult.error("Body position shift not supported")
137
+
138
+ def get_body_position(self) -> Vector3:
139
+ """
140
+ Get current body position offset.
141
+
142
+ Returns:
143
+ Vector3 offset from center
144
+ """
145
+ return Vector3.zero()
146
+
147
+ # =========================================================================
148
+ # Combined pose control
149
+ # =========================================================================
150
+
151
+ @abstractmethod
152
+ def set_body_pose(
153
+ self,
154
+ height: Optional[float] = None,
155
+ roll: Optional[float] = None,
156
+ pitch: Optional[float] = None,
157
+ yaw: Optional[float] = None,
158
+ x_offset: Optional[float] = None,
159
+ y_offset: Optional[float] = None
160
+ ) -> ActionResult:
161
+ """
162
+ Set multiple body pose parameters at once.
163
+
164
+ Args:
165
+ height: Body height (None = keep current)
166
+ roll, pitch, yaw: Orientation (None = keep current)
167
+ x_offset, y_offset: Body position offset (None = keep current)
168
+
169
+ Returns:
170
+ ActionResult
171
+ """
172
+ pass
173
+
174
+ def get_body_pose(self) -> dict:
175
+ """
176
+ Get complete body pose.
177
+
178
+ Returns:
179
+ Dict with height, roll, pitch, yaw, x_offset, y_offset
180
+ """
181
+ roll, pitch, yaw = self.get_body_orientation()
182
+ pos = self.get_body_position()
183
+ return {
184
+ "height": self.get_body_height(),
185
+ "roll": roll,
186
+ "pitch": pitch,
187
+ "yaw": yaw,
188
+ "x_offset": pos.x,
189
+ "y_offset": pos.y,
190
+ }
191
+
192
+ # =========================================================================
193
+ # Preset poses
194
+ # =========================================================================
195
+
196
+ def reset_pose(self) -> ActionResult:
197
+ """
198
+ Reset to default/neutral body pose.
199
+
200
+ Returns:
201
+ ActionResult
202
+ """
203
+ height_min, height_max = self.get_height_limits()
204
+ default_height = (height_min + height_max) / 2
205
+ return self.set_body_pose(
206
+ height=default_height,
207
+ roll=0.0,
208
+ pitch=0.0,
209
+ yaw=0.0,
210
+ x_offset=0.0,
211
+ y_offset=0.0
212
+ )
213
+
214
+ def lower_body(self, height: Optional[float] = None) -> ActionResult:
215
+ """
216
+ Lower body (for grasping objects on ground).
217
+
218
+ Args:
219
+ height: Target height (None = minimum safe height)
220
+
221
+ Returns:
222
+ ActionResult
223
+ """
224
+ if height is None:
225
+ height = self.get_height_limits()[0] + 0.02 # 2cm above min
226
+ return self.set_body_height(height)
227
+
228
+ def raise_body(self, height: Optional[float] = None) -> ActionResult:
229
+ """
230
+ Raise body to maximum comfortable height.
231
+
232
+ Args:
233
+ height: Target height (None = default standing height)
234
+
235
+ Returns:
236
+ ActionResult
237
+ """
238
+ if height is None:
239
+ height_min, height_max = self.get_height_limits()
240
+ height = (height_min + height_max) / 2
241
+ return self.set_body_height(height)
242
+
243
+ def look_at(self, direction: Vector3) -> ActionResult:
244
+ """
245
+ Tilt body to "look" in a direction.
246
+
247
+ Adjusts pitch and yaw to point forward axis toward direction.
248
+
249
+ Args:
250
+ direction: Direction to look (in robot base frame)
251
+
252
+ Returns:
253
+ ActionResult
254
+ """
255
+ import math
256
+
257
+ # Calculate required yaw and pitch
258
+ yaw = math.atan2(direction.y, direction.x)
259
+ horizontal_dist = math.sqrt(direction.x**2 + direction.y**2)
260
+ pitch = -math.atan2(direction.z, horizontal_dist)
261
+
262
+ # Clamp to limits
263
+ roll_lim, pitch_lim, yaw_lim = self.get_orientation_limits()
264
+ pitch = max(pitch_lim[0], min(pitch_lim[1], pitch))
265
+ yaw = max(yaw_lim[0], min(yaw_lim[1], yaw))
266
+
267
+ return self.set_body_orientation(roll=0.0, pitch=pitch, yaw=yaw)