foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.3__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/bridge_server.py +622 -0
- ate/cli.py +2625 -242
- ate/compatibility.py +580 -0
- ate/generators/__init__.py +19 -0
- ate/generators/docker_generator.py +461 -0
- ate/generators/hardware_config.py +469 -0
- ate/generators/ros2_generator.py +617 -0
- ate/generators/skill_generator.py +783 -0
- ate/marketplace.py +524 -0
- ate/mcp_server.py +1341 -107
- ate/primitives.py +1016 -0
- ate/robot_setup.py +2222 -0
- ate/skill_schema.py +537 -0
- ate/telemetry/__init__.py +33 -0
- ate/telemetry/cli.py +455 -0
- ate/telemetry/collector.py +444 -0
- ate/telemetry/context.py +318 -0
- ate/telemetry/fleet_agent.py +419 -0
- ate/telemetry/formats/__init__.py +18 -0
- ate/telemetry/formats/hdf5_serializer.py +503 -0
- ate/telemetry/formats/mcap_serializer.py +457 -0
- ate/telemetry/types.py +334 -0
- foodforthought_cli-0.2.3.dist-info/METADATA +300 -0
- foodforthought_cli-0.2.3.dist-info/RECORD +44 -0
- foodforthought_cli-0.2.3.dist-info/top_level.txt +6 -0
- mechdog_labeled/__init__.py +3 -0
- mechdog_labeled/primitives.py +113 -0
- mechdog_labeled/servo_map.py +209 -0
- mechdog_output/__init__.py +3 -0
- mechdog_output/primitives.py +59 -0
- mechdog_output/servo_map.py +203 -0
- test_autodetect/__init__.py +3 -0
- test_autodetect/primitives.py +113 -0
- test_autodetect/servo_map.py +209 -0
- test_full_auto/__init__.py +3 -0
- test_full_auto/primitives.py +113 -0
- test_full_auto/servo_map.py +209 -0
- test_smart_detect/__init__.py +3 -0
- test_smart_detect/primitives.py +113 -0
- test_smart_detect/servo_map.py +209 -0
- foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
- foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
- foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.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()
|