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.
- ate/__init__.py +1 -1
- 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.4.dist-info → foodforthought_cli-0.2.8.dist-info}/METADATA +9 -1
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/RECORD +37 -8
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.4.dist-info → foodforthought_cli-0.2.8.dist-info}/top_level.txt +0 -0
ate/interfaces/types.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common data types for robot interfaces.
|
|
3
|
+
|
|
4
|
+
All units are SI:
|
|
5
|
+
- Distance: meters (m)
|
|
6
|
+
- Angle: radians (rad)
|
|
7
|
+
- Time: seconds (s)
|
|
8
|
+
- Force: Newtons (N)
|
|
9
|
+
- Torque: Newton-meters (Nm)
|
|
10
|
+
- Velocity: m/s or rad/s
|
|
11
|
+
- Acceleration: m/s² or rad/s²
|
|
12
|
+
|
|
13
|
+
These types are designed to be:
|
|
14
|
+
1. Serializable (for telemetry recording)
|
|
15
|
+
2. Immutable (for safety)
|
|
16
|
+
3. Hardware-agnostic (no servo IDs, raw values, etc.)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum, auto
|
|
21
|
+
from typing import List, Optional, Tuple
|
|
22
|
+
import math
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Spatial Types
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Vector3:
|
|
31
|
+
"""3D vector in meters (for position) or m/s (for velocity)."""
|
|
32
|
+
x: float = 0.0
|
|
33
|
+
y: float = 0.0
|
|
34
|
+
z: float = 0.0
|
|
35
|
+
|
|
36
|
+
def __add__(self, other: "Vector3") -> "Vector3":
|
|
37
|
+
return Vector3(self.x + other.x, self.y + other.y, self.z + other.z)
|
|
38
|
+
|
|
39
|
+
def __sub__(self, other: "Vector3") -> "Vector3":
|
|
40
|
+
return Vector3(self.x - other.x, self.y - other.y, self.z - other.z)
|
|
41
|
+
|
|
42
|
+
def __mul__(self, scalar: float) -> "Vector3":
|
|
43
|
+
return Vector3(self.x * scalar, self.y * scalar, self.z * scalar)
|
|
44
|
+
|
|
45
|
+
def magnitude(self) -> float:
|
|
46
|
+
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
|
|
47
|
+
|
|
48
|
+
def normalized(self) -> "Vector3":
|
|
49
|
+
mag = self.magnitude()
|
|
50
|
+
if mag == 0:
|
|
51
|
+
return Vector3(0, 0, 0)
|
|
52
|
+
return Vector3(self.x / mag, self.y / mag, self.z / mag)
|
|
53
|
+
|
|
54
|
+
def to_tuple(self) -> Tuple[float, float, float]:
|
|
55
|
+
return (self.x, self.y, self.z)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_tuple(cls, t: Tuple[float, float, float]) -> "Vector3":
|
|
59
|
+
return cls(t[0], t[1], t[2])
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def zero(cls) -> "Vector3":
|
|
63
|
+
return cls(0.0, 0.0, 0.0)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def forward(cls) -> "Vector3":
|
|
67
|
+
"""Unit vector in forward direction (+X in robot frame)."""
|
|
68
|
+
return cls(1.0, 0.0, 0.0)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def up(cls) -> "Vector3":
|
|
72
|
+
"""Unit vector in up direction (+Z in robot frame)."""
|
|
73
|
+
return cls(0.0, 0.0, 1.0)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class Quaternion:
|
|
78
|
+
"""Rotation as quaternion (w, x, y, z). Normalized."""
|
|
79
|
+
w: float = 1.0
|
|
80
|
+
x: float = 0.0
|
|
81
|
+
y: float = 0.0
|
|
82
|
+
z: float = 0.0
|
|
83
|
+
|
|
84
|
+
def to_euler(self) -> Tuple[float, float, float]:
|
|
85
|
+
"""Convert to Euler angles (roll, pitch, yaw) in radians."""
|
|
86
|
+
# Roll (x-axis rotation)
|
|
87
|
+
sinr_cosp = 2 * (self.w * self.x + self.y * self.z)
|
|
88
|
+
cosr_cosp = 1 - 2 * (self.x * self.x + self.y * self.y)
|
|
89
|
+
roll = math.atan2(sinr_cosp, cosr_cosp)
|
|
90
|
+
|
|
91
|
+
# Pitch (y-axis rotation)
|
|
92
|
+
sinp = 2 * (self.w * self.y - self.z * self.x)
|
|
93
|
+
if abs(sinp) >= 1:
|
|
94
|
+
pitch = math.copysign(math.pi / 2, sinp)
|
|
95
|
+
else:
|
|
96
|
+
pitch = math.asin(sinp)
|
|
97
|
+
|
|
98
|
+
# Yaw (z-axis rotation)
|
|
99
|
+
siny_cosp = 2 * (self.w * self.z + self.x * self.y)
|
|
100
|
+
cosy_cosp = 1 - 2 * (self.y * self.y + self.z * self.z)
|
|
101
|
+
yaw = math.atan2(siny_cosp, cosy_cosp)
|
|
102
|
+
|
|
103
|
+
return (roll, pitch, yaw)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_euler(cls, roll: float, pitch: float, yaw: float) -> "Quaternion":
|
|
107
|
+
"""Create from Euler angles (roll, pitch, yaw) in radians."""
|
|
108
|
+
cr, cp, cy = math.cos(roll/2), math.cos(pitch/2), math.cos(yaw/2)
|
|
109
|
+
sr, sp, sy = math.sin(roll/2), math.sin(pitch/2), math.sin(yaw/2)
|
|
110
|
+
|
|
111
|
+
return cls(
|
|
112
|
+
w=cr * cp * cy + sr * sp * sy,
|
|
113
|
+
x=sr * cp * cy - cr * sp * sy,
|
|
114
|
+
y=cr * sp * cy + sr * cp * sy,
|
|
115
|
+
z=cr * cp * sy - sr * sp * cy,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def identity(cls) -> "Quaternion":
|
|
120
|
+
return cls(1.0, 0.0, 0.0, 0.0)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class Pose:
|
|
125
|
+
"""6DOF pose: position + orientation."""
|
|
126
|
+
position: Vector3 = field(default_factory=Vector3.zero)
|
|
127
|
+
orientation: Quaternion = field(default_factory=Quaternion.identity)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def identity(cls) -> "Pose":
|
|
131
|
+
return cls(Vector3.zero(), Quaternion.identity())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class Twist:
|
|
136
|
+
"""Velocity in 6DOF: linear + angular velocity."""
|
|
137
|
+
linear: Vector3 = field(default_factory=Vector3.zero) # m/s
|
|
138
|
+
angular: Vector3 = field(default_factory=Vector3.zero) # rad/s
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def zero(cls) -> "Twist":
|
|
142
|
+
return cls(Vector3.zero(), Vector3.zero())
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# =============================================================================
|
|
146
|
+
# Joint/Motor Types
|
|
147
|
+
# =============================================================================
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class JointState:
|
|
151
|
+
"""State of all joints. Lists are ordered by joint index."""
|
|
152
|
+
positions: Tuple[float, ...] = () # radians
|
|
153
|
+
velocities: Tuple[float, ...] = () # rad/s
|
|
154
|
+
efforts: Tuple[float, ...] = () # Nm (torque)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def num_joints(self) -> int:
|
|
158
|
+
return len(self.positions)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True)
|
|
162
|
+
class JointLimits:
|
|
163
|
+
"""Limits for a single joint."""
|
|
164
|
+
position_min: float # radians
|
|
165
|
+
position_max: float # radians
|
|
166
|
+
velocity_max: float # rad/s
|
|
167
|
+
effort_max: float # Nm
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# Perception Types
|
|
172
|
+
# =============================================================================
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class Image:
|
|
176
|
+
"""RGB image from camera."""
|
|
177
|
+
data: bytes
|
|
178
|
+
width: int
|
|
179
|
+
height: int
|
|
180
|
+
encoding: str = "rgb8" # rgb8, bgr8, mono8, jpeg, png
|
|
181
|
+
timestamp: float = 0.0 # seconds since epoch
|
|
182
|
+
frame_id: str = "camera" # coordinate frame
|
|
183
|
+
|
|
184
|
+
def to_numpy(self):
|
|
185
|
+
"""Convert to numpy array. Requires numpy."""
|
|
186
|
+
import numpy as np
|
|
187
|
+
if self.encoding in ("rgb8", "bgr8"):
|
|
188
|
+
return np.frombuffer(self.data, dtype=np.uint8).reshape(
|
|
189
|
+
(self.height, self.width, 3)
|
|
190
|
+
)
|
|
191
|
+
elif self.encoding == "mono8":
|
|
192
|
+
return np.frombuffer(self.data, dtype=np.uint8).reshape(
|
|
193
|
+
(self.height, self.width)
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(f"Cannot convert encoding {self.encoding} to numpy")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass
|
|
200
|
+
class DepthImage:
|
|
201
|
+
"""Depth image from depth camera."""
|
|
202
|
+
data: bytes
|
|
203
|
+
width: int
|
|
204
|
+
height: int
|
|
205
|
+
encoding: str = "32FC1" # 32FC1 (float meters), 16UC1 (uint16 mm)
|
|
206
|
+
timestamp: float = 0.0
|
|
207
|
+
frame_id: str = "depth_camera"
|
|
208
|
+
min_range: float = 0.1 # meters
|
|
209
|
+
max_range: float = 10.0 # meters
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class PointCloud:
|
|
214
|
+
"""3D point cloud from lidar or depth camera."""
|
|
215
|
+
points: List[Vector3] # List of 3D points in sensor frame
|
|
216
|
+
intensities: Optional[List[float]] = None
|
|
217
|
+
colors: Optional[List[Tuple[int, int, int]]] = None # RGB
|
|
218
|
+
timestamp: float = 0.0
|
|
219
|
+
frame_id: str = "lidar"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(frozen=True)
|
|
223
|
+
class IMUReading:
|
|
224
|
+
"""Reading from Inertial Measurement Unit."""
|
|
225
|
+
orientation: Quaternion
|
|
226
|
+
angular_velocity: Vector3 # rad/s
|
|
227
|
+
linear_acceleration: Vector3 # m/s²
|
|
228
|
+
timestamp: float = 0.0
|
|
229
|
+
|
|
230
|
+
# Covariance matrices (optional, for Kalman filtering)
|
|
231
|
+
orientation_covariance: Optional[Tuple[float, ...]] = None
|
|
232
|
+
angular_velocity_covariance: Optional[Tuple[float, ...]] = None
|
|
233
|
+
linear_acceleration_covariance: Optional[Tuple[float, ...]] = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True)
|
|
237
|
+
class ForceTorqueReading:
|
|
238
|
+
"""Reading from force-torque sensor."""
|
|
239
|
+
force: Vector3 # Newtons
|
|
240
|
+
torque: Vector3 # Nm
|
|
241
|
+
timestamp: float = 0.0
|
|
242
|
+
frame_id: str = "ft_sensor"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# =============================================================================
|
|
246
|
+
# Robot State Types
|
|
247
|
+
# =============================================================================
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True)
|
|
250
|
+
class BatteryState:
|
|
251
|
+
"""Battery status."""
|
|
252
|
+
percentage: float # 0.0 to 1.0
|
|
253
|
+
voltage: float # Volts
|
|
254
|
+
current: float = 0.0 # Amps (positive = discharging)
|
|
255
|
+
is_charging: bool = False
|
|
256
|
+
time_remaining: Optional[float] = None # seconds
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class RobotMode(Enum):
|
|
260
|
+
"""Operating mode of the robot."""
|
|
261
|
+
IDLE = auto()
|
|
262
|
+
READY = auto()
|
|
263
|
+
MOVING = auto()
|
|
264
|
+
ESTOPPED = auto()
|
|
265
|
+
FAULT = auto()
|
|
266
|
+
CHARGING = auto()
|
|
267
|
+
CALIBRATING = auto()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class RobotStatus:
|
|
272
|
+
"""Overall robot status."""
|
|
273
|
+
mode: RobotMode = RobotMode.IDLE
|
|
274
|
+
is_ready: bool = False
|
|
275
|
+
is_moving: bool = False
|
|
276
|
+
errors: List[str] = field(default_factory=list)
|
|
277
|
+
warnings: List[str] = field(default_factory=list)
|
|
278
|
+
battery: Optional[BatteryState] = None
|
|
279
|
+
uptime: float = 0.0 # seconds since boot
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# =============================================================================
|
|
283
|
+
# Locomotion Types
|
|
284
|
+
# =============================================================================
|
|
285
|
+
|
|
286
|
+
class GaitType(Enum):
|
|
287
|
+
"""Gait patterns for legged robots."""
|
|
288
|
+
# Quadruped gaits
|
|
289
|
+
STAND = auto()
|
|
290
|
+
WALK = auto()
|
|
291
|
+
TROT = auto()
|
|
292
|
+
PACE = auto()
|
|
293
|
+
BOUND = auto()
|
|
294
|
+
GALLOP = auto()
|
|
295
|
+
CRAWL = auto()
|
|
296
|
+
|
|
297
|
+
# Biped gaits
|
|
298
|
+
BIPED_WALK = auto()
|
|
299
|
+
BIPED_RUN = auto()
|
|
300
|
+
|
|
301
|
+
# Special
|
|
302
|
+
CUSTOM = auto()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@dataclass(frozen=True)
|
|
306
|
+
class GaitParameters:
|
|
307
|
+
"""Parameters for gait control."""
|
|
308
|
+
gait_type: GaitType = GaitType.WALK
|
|
309
|
+
stride_length: float = 0.1 # meters
|
|
310
|
+
step_height: float = 0.05 # meters
|
|
311
|
+
cycle_time: float = 0.5 # seconds per gait cycle
|
|
312
|
+
duty_factor: float = 0.6 # fraction of cycle foot is on ground
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# =============================================================================
|
|
316
|
+
# Manipulation Types
|
|
317
|
+
# =============================================================================
|
|
318
|
+
|
|
319
|
+
class GripperState(Enum):
|
|
320
|
+
"""State of a gripper."""
|
|
321
|
+
OPEN = auto()
|
|
322
|
+
CLOSED = auto()
|
|
323
|
+
HOLDING = auto() # Closed and holding an object
|
|
324
|
+
MOVING = auto()
|
|
325
|
+
FAULT = auto()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass(frozen=True)
|
|
329
|
+
class GripperStatus:
|
|
330
|
+
"""Detailed gripper status."""
|
|
331
|
+
state: GripperState
|
|
332
|
+
position: float = 1.0 # 0.0 = closed, 1.0 = fully open
|
|
333
|
+
force: float = 0.0 # Newtons (gripping force)
|
|
334
|
+
object_detected: bool = False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# =============================================================================
|
|
338
|
+
# Coordinate Frames
|
|
339
|
+
# =============================================================================
|
|
340
|
+
|
|
341
|
+
class Frame(Enum):
|
|
342
|
+
"""Standard coordinate frames."""
|
|
343
|
+
WORLD = "world" # Global/map frame
|
|
344
|
+
ODOM = "odom" # Odometry frame
|
|
345
|
+
BASE = "base_link" # Robot base
|
|
346
|
+
BODY = "body" # Robot body center
|
|
347
|
+
CAMERA = "camera" # Camera optical frame
|
|
348
|
+
GRIPPER = "gripper" # End effector frame
|
|
349
|
+
IMU = "imu" # IMU sensor frame
|
|
350
|
+
LIDAR = "lidar" # Lidar sensor frame
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# =============================================================================
|
|
354
|
+
# Action Types (for skill definitions)
|
|
355
|
+
# =============================================================================
|
|
356
|
+
|
|
357
|
+
@dataclass
|
|
358
|
+
class ActionResult:
|
|
359
|
+
"""Result of an action/command."""
|
|
360
|
+
success: bool
|
|
361
|
+
message: str = ""
|
|
362
|
+
error_code: Optional[int] = None
|
|
363
|
+
duration: float = 0.0 # How long the action took
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def ok(cls, message: str = "Success", duration: float = 0.0) -> "ActionResult":
|
|
367
|
+
return cls(success=True, message=message, duration=duration)
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def error(cls, message: str, code: int = -1) -> "ActionResult":
|
|
371
|
+
return cls(success=False, message=message, error_code=code)
|