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,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)