foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.4__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 (46) hide show
  1. ate/__init__.py +1 -1
  2. ate/bridge_server.py +622 -0
  3. ate/cli.py +2625 -242
  4. ate/compatibility.py +580 -0
  5. ate/generators/__init__.py +19 -0
  6. ate/generators/docker_generator.py +461 -0
  7. ate/generators/hardware_config.py +469 -0
  8. ate/generators/ros2_generator.py +617 -0
  9. ate/generators/skill_generator.py +783 -0
  10. ate/marketplace.py +524 -0
  11. ate/mcp_server.py +1341 -107
  12. ate/primitives.py +1016 -0
  13. ate/robot_setup.py +2222 -0
  14. ate/skill_schema.py +537 -0
  15. ate/telemetry/__init__.py +33 -0
  16. ate/telemetry/cli.py +455 -0
  17. ate/telemetry/collector.py +444 -0
  18. ate/telemetry/context.py +318 -0
  19. ate/telemetry/fleet_agent.py +419 -0
  20. ate/telemetry/formats/__init__.py +18 -0
  21. ate/telemetry/formats/hdf5_serializer.py +503 -0
  22. ate/telemetry/formats/mcap_serializer.py +457 -0
  23. ate/telemetry/types.py +334 -0
  24. foodforthought_cli-0.2.4.dist-info/METADATA +300 -0
  25. foodforthought_cli-0.2.4.dist-info/RECORD +44 -0
  26. foodforthought_cli-0.2.4.dist-info/top_level.txt +6 -0
  27. mechdog_labeled/__init__.py +3 -0
  28. mechdog_labeled/primitives.py +113 -0
  29. mechdog_labeled/servo_map.py +209 -0
  30. mechdog_output/__init__.py +3 -0
  31. mechdog_output/primitives.py +59 -0
  32. mechdog_output/servo_map.py +203 -0
  33. test_autodetect/__init__.py +3 -0
  34. test_autodetect/primitives.py +113 -0
  35. test_autodetect/servo_map.py +209 -0
  36. test_full_auto/__init__.py +3 -0
  37. test_full_auto/primitives.py +113 -0
  38. test_full_auto/servo_map.py +209 -0
  39. test_smart_detect/__init__.py +3 -0
  40. test_smart_detect/primitives.py +113 -0
  41. test_smart_detect/servo_map.py +209 -0
  42. foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
  43. foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
  44. foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
  45. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/WHEEL +0 -0
  46. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,469 @@
1
+ """
2
+ Hardware Config Generator - Map skill requirements to robot hardware.
3
+
4
+ This generator creates hardware configuration that bridges:
5
+ - Abstract skill hardware requirements (arm, gripper, etc.)
6
+ - Concrete robot hardware (joint names, controllers, servo IDs)
7
+ """
8
+
9
+ import re
10
+ import xml.etree.ElementTree as ET
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+ import yaml
15
+
16
+ from ..skill_schema import SkillSpecification, HardwareRequirement
17
+
18
+
19
+ @dataclass
20
+ class JointInfo:
21
+ """Information about a robot joint from URDF."""
22
+ name: str
23
+ type: str # revolute, prismatic, continuous, fixed
24
+ parent_link: str
25
+ child_link: str
26
+ axis: Tuple[float, float, float] = (0, 0, 1)
27
+ lower_limit: Optional[float] = None
28
+ upper_limit: Optional[float] = None
29
+ velocity_limit: Optional[float] = None
30
+ effort_limit: Optional[float] = None
31
+
32
+
33
+ @dataclass
34
+ class LinkInfo:
35
+ """Information about a robot link from URDF."""
36
+ name: str
37
+ has_visual: bool = False
38
+ has_collision: bool = False
39
+ has_inertial: bool = False
40
+
41
+
42
+ @dataclass
43
+ class URDFInfo:
44
+ """Parsed information from a URDF file."""
45
+ name: str
46
+ joints: List[JointInfo] = field(default_factory=list)
47
+ links: List[LinkInfo] = field(default_factory=list)
48
+
49
+ @property
50
+ def movable_joints(self) -> List[JointInfo]:
51
+ """Get joints that can move (not fixed)."""
52
+ return [j for j in self.joints if j.type != "fixed"]
53
+
54
+ @property
55
+ def dof(self) -> int:
56
+ """Get degrees of freedom."""
57
+ return len(self.movable_joints)
58
+
59
+ def get_joint_chain(self, start_link: str, end_link: str) -> List[JointInfo]:
60
+ """Get the kinematic chain between two links."""
61
+ chain = []
62
+ current = end_link
63
+
64
+ # Build parent map
65
+ child_to_parent = {}
66
+ child_to_joint = {}
67
+ for joint in self.joints:
68
+ child_to_parent[joint.child_link] = joint.parent_link
69
+ child_to_joint[joint.child_link] = joint
70
+
71
+ # Traverse from end to start
72
+ while current != start_link and current in child_to_parent:
73
+ if current in child_to_joint:
74
+ chain.append(child_to_joint[current])
75
+ current = child_to_parent.get(current)
76
+
77
+ return list(reversed(chain))
78
+
79
+
80
+ def parse_urdf(urdf_path: str) -> URDFInfo:
81
+ """
82
+ Parse a URDF file and extract robot information.
83
+
84
+ Args:
85
+ urdf_path: Path to URDF file
86
+
87
+ Returns:
88
+ URDFInfo with parsed robot data
89
+ """
90
+ tree = ET.parse(urdf_path)
91
+ root = tree.getroot()
92
+
93
+ robot_name = root.get("name", "unknown")
94
+
95
+ # Parse joints
96
+ joints = []
97
+ for joint_elem in root.findall("joint"):
98
+ joint_name = joint_elem.get("name", "")
99
+ joint_type = joint_elem.get("type", "fixed")
100
+
101
+ parent_elem = joint_elem.find("parent")
102
+ child_elem = joint_elem.find("child")
103
+
104
+ parent_link = parent_elem.get("link", "") if parent_elem is not None else ""
105
+ child_link = child_elem.get("link", "") if child_elem is not None else ""
106
+
107
+ # Parse axis
108
+ axis_elem = joint_elem.find("axis")
109
+ if axis_elem is not None:
110
+ xyz = axis_elem.get("xyz", "0 0 1")
111
+ axis = tuple(float(x) for x in xyz.split())
112
+ else:
113
+ axis = (0, 0, 1)
114
+
115
+ # Parse limits
116
+ limit_elem = joint_elem.find("limit")
117
+ lower_limit = None
118
+ upper_limit = None
119
+ velocity_limit = None
120
+ effort_limit = None
121
+
122
+ if limit_elem is not None:
123
+ if "lower" in limit_elem.attrib:
124
+ lower_limit = float(limit_elem.get("lower"))
125
+ if "upper" in limit_elem.attrib:
126
+ upper_limit = float(limit_elem.get("upper"))
127
+ if "velocity" in limit_elem.attrib:
128
+ velocity_limit = float(limit_elem.get("velocity"))
129
+ if "effort" in limit_elem.attrib:
130
+ effort_limit = float(limit_elem.get("effort"))
131
+
132
+ joints.append(JointInfo(
133
+ name=joint_name,
134
+ type=joint_type,
135
+ parent_link=parent_link,
136
+ child_link=child_link,
137
+ axis=axis,
138
+ lower_limit=lower_limit,
139
+ upper_limit=upper_limit,
140
+ velocity_limit=velocity_limit,
141
+ effort_limit=effort_limit,
142
+ ))
143
+
144
+ # Parse links
145
+ links = []
146
+ for link_elem in root.findall("link"):
147
+ link_name = link_elem.get("name", "")
148
+ links.append(LinkInfo(
149
+ name=link_name,
150
+ has_visual=link_elem.find("visual") is not None,
151
+ has_collision=link_elem.find("collision") is not None,
152
+ has_inertial=link_elem.find("inertial") is not None,
153
+ ))
154
+
155
+ return URDFInfo(name=robot_name, joints=joints, links=links)
156
+
157
+
158
+ @dataclass
159
+ class ATEConfig:
160
+ """Configuration from ATE CLI (.ate/config.json or primitives)."""
161
+ servo_map: Dict[int, Dict[str, Any]] = field(default_factory=dict)
162
+ protocol: Optional[str] = None
163
+ port: Optional[str] = None
164
+ baud_rate: int = 115200
165
+
166
+ @classmethod
167
+ def from_directory(cls, dir_path: str) -> "ATEConfig":
168
+ """Load ATE configuration from a directory."""
169
+ dir_path = Path(dir_path)
170
+ config = cls()
171
+
172
+ # Try to load servo_map.py
173
+ servo_map_path = dir_path / "servo_map.py"
174
+ if servo_map_path.exists():
175
+ config.servo_map = cls._parse_servo_map(servo_map_path)
176
+
177
+ # Try to load protocol.json
178
+ protocol_path = dir_path / "protocol.json"
179
+ if protocol_path.exists():
180
+ import json
181
+ with open(protocol_path) as f:
182
+ proto_data = json.load(f)
183
+ config.protocol = proto_data.get("type")
184
+ config.port = proto_data.get("port")
185
+ config.baud_rate = proto_data.get("baud_rate", 115200)
186
+
187
+ return config
188
+
189
+ @staticmethod
190
+ def _parse_servo_map(servo_map_path: Path) -> Dict[int, Dict[str, Any]]:
191
+ """Parse servo_map.py to extract servo configuration."""
192
+ content = servo_map_path.read_text()
193
+ servo_map = {}
194
+
195
+ # Extract SERVO_LIMITS dict
196
+ limits_match = re.search(
197
+ r"SERVO_LIMITS\s*=\s*\{([^}]+)\}",
198
+ content,
199
+ re.MULTILINE | re.DOTALL
200
+ )
201
+
202
+ if limits_match:
203
+ # Parse limits (simplified parsing)
204
+ limits_content = limits_match.group(1)
205
+ # Match patterns like ServoID.SERVO_0: {"min": 0, "max": 1000, ...}
206
+ pattern = r"ServoID\.(\w+):\s*\{([^}]+)\}"
207
+ for match in re.finditer(pattern, limits_content):
208
+ servo_name = match.group(1)
209
+ # Extract servo ID from name (SERVO_0 -> 0)
210
+ id_match = re.search(r"(\d+)", servo_name)
211
+ if id_match:
212
+ servo_id = int(id_match.group(1))
213
+ # Parse the dict content
214
+ dict_content = match.group(2)
215
+ servo_data = {}
216
+ for kv_match in re.finditer(r'"(\w+)":\s*([\d.]+)', dict_content):
217
+ key = kv_match.group(1)
218
+ value = float(kv_match.group(2))
219
+ servo_data[key] = value
220
+ servo_map[servo_id] = servo_data
221
+
222
+ return servo_map
223
+
224
+
225
+ class HardwareConfigGenerator:
226
+ """
227
+ Generate hardware configuration for a skill.
228
+
229
+ Maps abstract hardware requirements to concrete robot hardware
230
+ based on URDF and ATE configuration.
231
+ """
232
+
233
+ def __init__(
234
+ self,
235
+ spec: SkillSpecification,
236
+ urdf_info: Optional[URDFInfo] = None,
237
+ ate_config: Optional[ATEConfig] = None,
238
+ ):
239
+ """
240
+ Initialize the generator.
241
+
242
+ Args:
243
+ spec: Skill specification with hardware requirements
244
+ urdf_info: Parsed URDF information (optional)
245
+ ate_config: ATE configuration (optional)
246
+ """
247
+ self.spec = spec
248
+ self.urdf_info = urdf_info
249
+ self.ate_config = ate_config
250
+
251
+ def generate(self) -> Dict[str, Any]:
252
+ """
253
+ Generate hardware configuration.
254
+
255
+ Returns:
256
+ Hardware configuration dictionary
257
+ """
258
+ config = {}
259
+
260
+ for req in self.spec.hardware_requirements:
261
+ if req.component_type == "arm":
262
+ config["arm"] = self._generate_arm_config(req)
263
+ elif req.component_type == "gripper":
264
+ config["gripper"] = self._generate_gripper_config(req)
265
+ elif req.component_type == "camera":
266
+ config["camera"] = self._generate_camera_config(req)
267
+ elif req.component_type == "force_sensor":
268
+ config["force_sensor"] = self._generate_force_sensor_config(req)
269
+ elif req.component_type == "mobile_base":
270
+ config["mobile_base"] = self._generate_mobile_base_config(req)
271
+ else:
272
+ config[req.component_type] = self._generate_generic_config(req)
273
+
274
+ return config
275
+
276
+ def _generate_arm_config(self, req: HardwareRequirement) -> Dict[str, Any]:
277
+ """Generate arm configuration."""
278
+ config = {
279
+ "driver": "mock",
280
+ "controller": "/arm_controller",
281
+ "joints": [],
282
+ "ik_solver": "kdl",
283
+ }
284
+
285
+ # If we have URDF info, use actual joint names
286
+ if self.urdf_info:
287
+ arm_joints = self._detect_arm_joints()
288
+ config["joints"] = [j.name for j in arm_joints]
289
+ config["joint_limits"] = {
290
+ j.name: {
291
+ "lower": j.lower_limit,
292
+ "upper": j.upper_limit,
293
+ "velocity": j.velocity_limit,
294
+ "effort": j.effort_limit,
295
+ }
296
+ for j in arm_joints
297
+ if j.lower_limit is not None
298
+ }
299
+
300
+ # If we have ATE config, add servo mapping
301
+ if self.ate_config and self.ate_config.servo_map:
302
+ config["servo_mapping"] = {}
303
+ # Map joints to servos (simplified - assumes 1:1 mapping)
304
+ for i, joint_name in enumerate(config.get("joints", [])):
305
+ if i in self.ate_config.servo_map:
306
+ config["servo_mapping"][joint_name] = {
307
+ "servo_id": i,
308
+ **self.ate_config.servo_map[i]
309
+ }
310
+
311
+ config["driver"] = "serial"
312
+ if self.ate_config.protocol:
313
+ config["protocol"] = self.ate_config.protocol
314
+ if self.ate_config.port:
315
+ config["port"] = self.ate_config.port
316
+ config["baud_rate"] = self.ate_config.baud_rate
317
+
318
+ return config
319
+
320
+ def _detect_arm_joints(self) -> List[JointInfo]:
321
+ """Detect arm joints from URDF (heuristic-based)."""
322
+ if not self.urdf_info:
323
+ return []
324
+
325
+ # Look for common arm joint patterns
326
+ arm_patterns = [
327
+ r"shoulder|elbow|wrist|arm|joint_\d",
328
+ r"j[1-7]|joint[1-7]",
329
+ r"axis_[1-6]",
330
+ ]
331
+
332
+ pattern = re.compile("|".join(arm_patterns), re.IGNORECASE)
333
+ arm_joints = [
334
+ j for j in self.urdf_info.movable_joints
335
+ if pattern.search(j.name)
336
+ ]
337
+
338
+ # If no matches, return all movable joints (excluding gripper-like names)
339
+ if not arm_joints:
340
+ gripper_pattern = re.compile(r"gripper|finger|hand|grasp", re.IGNORECASE)
341
+ arm_joints = [
342
+ j for j in self.urdf_info.movable_joints
343
+ if not gripper_pattern.search(j.name)
344
+ ]
345
+
346
+ return arm_joints
347
+
348
+ def _generate_gripper_config(self, req: HardwareRequirement) -> Dict[str, Any]:
349
+ """Generate gripper configuration."""
350
+ config = {
351
+ "driver": "mock",
352
+ "controller": "/gripper_controller",
353
+ "type": req.constraints.get("type", "parallel"),
354
+ "open_position": 0.08,
355
+ "close_position": 0.0,
356
+ "max_force": 100.0,
357
+ }
358
+
359
+ # If we have URDF info, detect gripper joints
360
+ if self.urdf_info:
361
+ gripper_joints = self._detect_gripper_joints()
362
+ if gripper_joints:
363
+ config["joints"] = [j.name for j in gripper_joints]
364
+
365
+ # If we have ATE config, add gripper servo
366
+ if self.ate_config and self.ate_config.servo_map:
367
+ # Gripper is typically the last servo
368
+ max_servo_id = max(self.ate_config.servo_map.keys(), default=-1)
369
+ if max_servo_id >= 0:
370
+ config["servo_id"] = max_servo_id
371
+ config["driver"] = "serial"
372
+ gripper_limits = self.ate_config.servo_map.get(max_servo_id, {})
373
+ config["open_position"] = gripper_limits.get("max", 1000)
374
+ config["close_position"] = gripper_limits.get("min", 0)
375
+
376
+ return config
377
+
378
+ def _detect_gripper_joints(self) -> List[JointInfo]:
379
+ """Detect gripper joints from URDF (heuristic-based)."""
380
+ if not self.urdf_info:
381
+ return []
382
+
383
+ gripper_pattern = re.compile(
384
+ r"gripper|finger|hand|grasp|claw",
385
+ re.IGNORECASE
386
+ )
387
+
388
+ return [
389
+ j for j in self.urdf_info.movable_joints
390
+ if gripper_pattern.search(j.name)
391
+ ]
392
+
393
+ def _generate_camera_config(self, req: HardwareRequirement) -> Dict[str, Any]:
394
+ """Generate camera configuration."""
395
+ return {
396
+ "driver": "mock",
397
+ "device": "/dev/video0",
398
+ "width": req.constraints.get("width", 640),
399
+ "height": req.constraints.get("height", 480),
400
+ "fps": req.constraints.get("fps", 30),
401
+ "topic": "/camera/image_raw",
402
+ }
403
+
404
+ def _generate_force_sensor_config(self, req: HardwareRequirement) -> Dict[str, Any]:
405
+ """Generate force/torque sensor configuration."""
406
+ return {
407
+ "driver": "mock",
408
+ "topic": "/ft_sensor/wrench",
409
+ "frame": "ft_sensor_link",
410
+ "rate": req.constraints.get("rate", 100),
411
+ }
412
+
413
+ def _generate_mobile_base_config(self, req: HardwareRequirement) -> Dict[str, Any]:
414
+ """Generate mobile base configuration."""
415
+ return {
416
+ "driver": "mock",
417
+ "type": req.constraints.get("type", "differential"),
418
+ "cmd_vel_topic": "/cmd_vel",
419
+ "odom_topic": "/odom",
420
+ "max_linear_velocity": req.constraints.get("max_velocity", 1.0),
421
+ "max_angular_velocity": req.constraints.get("max_angular_velocity", 2.0),
422
+ }
423
+
424
+ def _generate_generic_config(self, req: HardwareRequirement) -> Dict[str, Any]:
425
+ """Generate generic hardware configuration."""
426
+ return {
427
+ "driver": "mock",
428
+ "type": req.component_type,
429
+ **req.constraints,
430
+ }
431
+
432
+ def to_yaml(self) -> str:
433
+ """Generate YAML configuration string."""
434
+ config = self.generate()
435
+ return yaml.dump(config, default_flow_style=False, sort_keys=False)
436
+
437
+ def save(self, output_path: str) -> None:
438
+ """Save configuration to YAML file."""
439
+ output_path = Path(output_path)
440
+ output_path.parent.mkdir(parents=True, exist_ok=True)
441
+ output_path.write_text(self.to_yaml())
442
+
443
+
444
+ def generate_hardware_config(
445
+ spec: SkillSpecification,
446
+ urdf_path: Optional[str] = None,
447
+ ate_dir: Optional[str] = None,
448
+ ) -> Dict[str, Any]:
449
+ """
450
+ Convenience function to generate hardware configuration.
451
+
452
+ Args:
453
+ spec: Skill specification
454
+ urdf_path: Path to URDF file (optional)
455
+ ate_dir: Path to ATE configuration directory (optional)
456
+
457
+ Returns:
458
+ Hardware configuration dictionary
459
+ """
460
+ urdf_info = None
461
+ if urdf_path:
462
+ urdf_info = parse_urdf(urdf_path)
463
+
464
+ ate_config = None
465
+ if ate_dir:
466
+ ate_config = ATEConfig.from_directory(ate_dir)
467
+
468
+ generator = HardwareConfigGenerator(spec, urdf_info, ate_config)
469
+ return generator.generate()