foodforthought-cli 0.2.0__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 +2424 -148
- 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.0.dist-info/METADATA +0 -151
- foodforthought_cli-0.2.0.dist-info/RECORD +0 -9
- foodforthought_cli-0.2.0.dist-info/top_level.txt +0 -1
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.3.dist-info}/entry_points.txt +0 -0
ate/compatibility.py
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compatibility Checker - Verify skill compatibility with robots.
|
|
3
|
+
|
|
4
|
+
This module checks whether a skill can run on a given robot by:
|
|
5
|
+
- Comparing hardware requirements vs robot capabilities
|
|
6
|
+
- Identifying potential issues and their severity
|
|
7
|
+
- Suggesting adaptations to make incompatible skills work
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from .skill_schema import SkillSpecification, HardwareRequirement
|
|
16
|
+
from .generators.hardware_config import URDFInfo, parse_urdf, ATEConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IssueSeverity(str, Enum):
|
|
20
|
+
"""Severity levels for compatibility issues."""
|
|
21
|
+
INFO = "info" # Informational, no action needed
|
|
22
|
+
WARNING = "warning" # May work but with limitations
|
|
23
|
+
ERROR = "error" # Will not work without changes
|
|
24
|
+
CRITICAL = "critical" # Fundamentally incompatible
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AdaptationType(str, Enum):
|
|
28
|
+
"""Types of adaptations to make skills compatible."""
|
|
29
|
+
IK_CONSTRAINT = "ik_constraint" # Lock or limit certain joints
|
|
30
|
+
SPEED_LIMIT = "speed_limit" # Reduce maximum speed
|
|
31
|
+
WORKSPACE_LIMIT = "workspace_limit" # Limit operational workspace
|
|
32
|
+
FORCE_LIMIT = "force_limit" # Reduce force limits
|
|
33
|
+
TRAJECTORY_REMAP = "trajectory_remap" # Remap joint trajectories
|
|
34
|
+
GRIPPER_ADAPT = "gripper_adapt" # Adapt gripper behavior
|
|
35
|
+
SENSOR_SUBSTITUTE = "sensor_substitute" # Use alternative sensor
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CompatibilityIssue:
|
|
40
|
+
"""A single compatibility issue between skill and robot."""
|
|
41
|
+
severity: IssueSeverity
|
|
42
|
+
category: str # e.g., "hardware", "kinematics", "safety"
|
|
43
|
+
message: str
|
|
44
|
+
details: Optional[str] = None
|
|
45
|
+
mitigation: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"severity": self.severity.value,
|
|
50
|
+
"category": self.category,
|
|
51
|
+
"message": self.message,
|
|
52
|
+
"details": self.details,
|
|
53
|
+
"mitigation": self.mitigation,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Adaptation:
|
|
59
|
+
"""An adaptation to make a skill compatible with a robot."""
|
|
60
|
+
type: AdaptationType
|
|
61
|
+
description: str
|
|
62
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
automated: bool = False # Can this be automatically applied?
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"type": self.type.value,
|
|
68
|
+
"description": self.description,
|
|
69
|
+
"config": self.config,
|
|
70
|
+
"automated": self.automated,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class CompatibilityReport:
|
|
76
|
+
"""Complete compatibility report between a skill and robot."""
|
|
77
|
+
skill_name: str
|
|
78
|
+
robot_name: str
|
|
79
|
+
compatible: bool
|
|
80
|
+
score: float # 0.0 to 1.0 compatibility score
|
|
81
|
+
issues: List[CompatibilityIssue] = field(default_factory=list)
|
|
82
|
+
adaptations: List[Adaptation] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
85
|
+
return {
|
|
86
|
+
"skill_name": self.skill_name,
|
|
87
|
+
"robot_name": self.robot_name,
|
|
88
|
+
"compatible": self.compatible,
|
|
89
|
+
"score": self.score,
|
|
90
|
+
"issues": [i.to_dict() for i in self.issues],
|
|
91
|
+
"adaptations": [a.to_dict() for a in self.adaptations],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
status = "COMPATIBLE" if self.compatible else "INCOMPATIBLE"
|
|
96
|
+
lines = [
|
|
97
|
+
f"Compatibility Report: {self.skill_name} -> {self.robot_name}",
|
|
98
|
+
f"Status: {status} (Score: {self.score:.1%})",
|
|
99
|
+
"",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if self.issues:
|
|
103
|
+
lines.append("Issues:")
|
|
104
|
+
for issue in self.issues:
|
|
105
|
+
severity_icon = {
|
|
106
|
+
IssueSeverity.INFO: "ℹ️",
|
|
107
|
+
IssueSeverity.WARNING: "⚠️",
|
|
108
|
+
IssueSeverity.ERROR: "❌",
|
|
109
|
+
IssueSeverity.CRITICAL: "🚫",
|
|
110
|
+
}.get(issue.severity, "•")
|
|
111
|
+
lines.append(f" {severity_icon} [{issue.category}] {issue.message}")
|
|
112
|
+
if issue.mitigation:
|
|
113
|
+
lines.append(f" Mitigation: {issue.mitigation}")
|
|
114
|
+
|
|
115
|
+
if self.adaptations:
|
|
116
|
+
lines.append("")
|
|
117
|
+
lines.append("Suggested Adaptations:")
|
|
118
|
+
for adapt in self.adaptations:
|
|
119
|
+
auto = " (automatic)" if adapt.automated else ""
|
|
120
|
+
lines.append(f" • {adapt.description}{auto}")
|
|
121
|
+
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class RobotProfile:
|
|
127
|
+
"""Profile describing a robot's capabilities."""
|
|
128
|
+
name: str
|
|
129
|
+
urdf_info: Optional[URDFInfo] = None
|
|
130
|
+
ate_config: Optional[ATEConfig] = None
|
|
131
|
+
|
|
132
|
+
# Hardware capabilities
|
|
133
|
+
arm_dof: int = 0
|
|
134
|
+
has_gripper: bool = False
|
|
135
|
+
gripper_type: Optional[str] = None
|
|
136
|
+
has_force_sensor: bool = False
|
|
137
|
+
has_camera: bool = False
|
|
138
|
+
has_mobile_base: bool = False
|
|
139
|
+
|
|
140
|
+
# Kinematic properties
|
|
141
|
+
max_reach: Optional[float] = None # meters
|
|
142
|
+
payload: Optional[float] = None # kg
|
|
143
|
+
workspace_bounds: Optional[Dict[str, Tuple[float, float]]] = None
|
|
144
|
+
|
|
145
|
+
# Performance limits
|
|
146
|
+
max_joint_velocity: Optional[float] = None # rad/s
|
|
147
|
+
max_cartesian_velocity: Optional[float] = None # m/s
|
|
148
|
+
max_force: Optional[float] = None # N
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_urdf(cls, urdf_path: str, name: Optional[str] = None) -> "RobotProfile":
|
|
152
|
+
"""Create a robot profile from URDF file."""
|
|
153
|
+
urdf_info = parse_urdf(urdf_path)
|
|
154
|
+
|
|
155
|
+
profile = cls(
|
|
156
|
+
name=name or urdf_info.name,
|
|
157
|
+
urdf_info=urdf_info,
|
|
158
|
+
arm_dof=urdf_info.dof,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Detect gripper from joint names
|
|
162
|
+
gripper_joints = [
|
|
163
|
+
j for j in urdf_info.movable_joints
|
|
164
|
+
if any(kw in j.name.lower() for kw in ["gripper", "finger", "hand"])
|
|
165
|
+
]
|
|
166
|
+
if gripper_joints:
|
|
167
|
+
profile.has_gripper = True
|
|
168
|
+
profile.gripper_type = "parallel" # Default assumption
|
|
169
|
+
|
|
170
|
+
# Extract velocity limits
|
|
171
|
+
velocities = [
|
|
172
|
+
j.velocity_limit for j in urdf_info.movable_joints
|
|
173
|
+
if j.velocity_limit is not None
|
|
174
|
+
]
|
|
175
|
+
if velocities:
|
|
176
|
+
profile.max_joint_velocity = min(velocities)
|
|
177
|
+
|
|
178
|
+
return profile
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_ate_dir(cls, ate_dir: str, name: Optional[str] = None) -> "RobotProfile":
|
|
182
|
+
"""Create a robot profile from ATE configuration directory."""
|
|
183
|
+
ate_config = ATEConfig.from_directory(ate_dir)
|
|
184
|
+
|
|
185
|
+
profile = cls(
|
|
186
|
+
name=name or Path(ate_dir).name,
|
|
187
|
+
ate_config=ate_config,
|
|
188
|
+
arm_dof=len(ate_config.servo_map),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Detect gripper (typically last servo or specific IDs)
|
|
192
|
+
if ate_config.servo_map:
|
|
193
|
+
max_id = max(ate_config.servo_map.keys())
|
|
194
|
+
# Heuristic: if there's a servo with different characteristics, it might be gripper
|
|
195
|
+
profile.has_gripper = True
|
|
196
|
+
|
|
197
|
+
return profile
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CompatibilityChecker:
|
|
201
|
+
"""
|
|
202
|
+
Check compatibility between a skill and a robot.
|
|
203
|
+
|
|
204
|
+
Performs comprehensive checks including:
|
|
205
|
+
- Hardware requirements
|
|
206
|
+
- Kinematic constraints
|
|
207
|
+
- Safety limits
|
|
208
|
+
- Sensor requirements
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, spec: SkillSpecification, robot: RobotProfile):
|
|
212
|
+
"""
|
|
213
|
+
Initialize the checker.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
spec: Skill specification to check
|
|
217
|
+
robot: Robot profile to check against
|
|
218
|
+
"""
|
|
219
|
+
self.spec = spec
|
|
220
|
+
self.robot = robot
|
|
221
|
+
self.issues: List[CompatibilityIssue] = []
|
|
222
|
+
self.adaptations: List[Adaptation] = []
|
|
223
|
+
|
|
224
|
+
def check(self) -> CompatibilityReport:
|
|
225
|
+
"""
|
|
226
|
+
Perform all compatibility checks.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
CompatibilityReport with results
|
|
230
|
+
"""
|
|
231
|
+
self.issues = []
|
|
232
|
+
self.adaptations = []
|
|
233
|
+
|
|
234
|
+
# Run all checks
|
|
235
|
+
self._check_hardware_requirements()
|
|
236
|
+
self._check_kinematic_requirements()
|
|
237
|
+
self._check_safety_requirements()
|
|
238
|
+
self._check_primitive_support()
|
|
239
|
+
|
|
240
|
+
# Calculate compatibility
|
|
241
|
+
compatible, score = self._calculate_compatibility()
|
|
242
|
+
|
|
243
|
+
return CompatibilityReport(
|
|
244
|
+
skill_name=self.spec.name,
|
|
245
|
+
robot_name=self.robot.name,
|
|
246
|
+
compatible=compatible,
|
|
247
|
+
score=score,
|
|
248
|
+
issues=self.issues,
|
|
249
|
+
adaptations=self.adaptations,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def _check_hardware_requirements(self) -> None:
|
|
253
|
+
"""Check hardware requirements against robot capabilities."""
|
|
254
|
+
for req in self.spec.hardware_requirements:
|
|
255
|
+
if req.component_type == "arm":
|
|
256
|
+
self._check_arm_requirement(req)
|
|
257
|
+
elif req.component_type == "gripper":
|
|
258
|
+
self._check_gripper_requirement(req)
|
|
259
|
+
elif req.component_type == "force_sensor":
|
|
260
|
+
self._check_force_sensor_requirement(req)
|
|
261
|
+
elif req.component_type == "camera":
|
|
262
|
+
self._check_camera_requirement(req)
|
|
263
|
+
elif req.component_type == "mobile_base":
|
|
264
|
+
self._check_mobile_base_requirement(req)
|
|
265
|
+
|
|
266
|
+
def _check_arm_requirement(self, req: HardwareRequirement) -> None:
|
|
267
|
+
"""Check arm hardware requirements."""
|
|
268
|
+
# Check DOF
|
|
269
|
+
required_dof = req.constraints.get("dof")
|
|
270
|
+
if required_dof:
|
|
271
|
+
# Parse requirement like ">=6" or "6"
|
|
272
|
+
if isinstance(required_dof, str):
|
|
273
|
+
if required_dof.startswith(">="):
|
|
274
|
+
min_dof = int(required_dof[2:])
|
|
275
|
+
if self.robot.arm_dof < min_dof:
|
|
276
|
+
self.issues.append(CompatibilityIssue(
|
|
277
|
+
severity=IssueSeverity.ERROR,
|
|
278
|
+
category="hardware",
|
|
279
|
+
message=f"Robot has {self.robot.arm_dof} DOF, skill requires >= {min_dof}",
|
|
280
|
+
mitigation="Consider using a different skill or robot"
|
|
281
|
+
))
|
|
282
|
+
elif self.robot.arm_dof > min_dof:
|
|
283
|
+
# More DOF than needed - just informational
|
|
284
|
+
self.issues.append(CompatibilityIssue(
|
|
285
|
+
severity=IssueSeverity.INFO,
|
|
286
|
+
category="hardware",
|
|
287
|
+
message=f"Robot has {self.robot.arm_dof} DOF, skill designed for {min_dof}+",
|
|
288
|
+
details="Extra DOF may provide redundancy"
|
|
289
|
+
))
|
|
290
|
+
elif isinstance(required_dof, int):
|
|
291
|
+
if self.robot.arm_dof < required_dof:
|
|
292
|
+
diff = required_dof - self.robot.arm_dof
|
|
293
|
+
severity = IssueSeverity.ERROR if diff > 1 else IssueSeverity.WARNING
|
|
294
|
+
self.issues.append(CompatibilityIssue(
|
|
295
|
+
severity=severity,
|
|
296
|
+
category="hardware",
|
|
297
|
+
message=f"Robot has {self.robot.arm_dof} DOF, skill designed for {required_dof}",
|
|
298
|
+
mitigation="Reduced workspace, may not reach all poses" if diff == 1 else None
|
|
299
|
+
))
|
|
300
|
+
if diff == 1:
|
|
301
|
+
self.adaptations.append(Adaptation(
|
|
302
|
+
type=AdaptationType.IK_CONSTRAINT,
|
|
303
|
+
description="Lock one joint to compensate for missing DOF",
|
|
304
|
+
config={"fixed_joints": [], "note": "Auto-detect best joint to lock"},
|
|
305
|
+
automated=True
|
|
306
|
+
))
|
|
307
|
+
|
|
308
|
+
# Check payload
|
|
309
|
+
required_payload = req.constraints.get("payload")
|
|
310
|
+
if required_payload and self.robot.payload:
|
|
311
|
+
# Parse ">=1kg" format
|
|
312
|
+
if isinstance(required_payload, str):
|
|
313
|
+
required_payload = required_payload.replace("kg", "").replace(">=", "")
|
|
314
|
+
try:
|
|
315
|
+
required_payload = float(required_payload)
|
|
316
|
+
except ValueError:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
if isinstance(required_payload, (int, float)):
|
|
320
|
+
if self.robot.payload < required_payload:
|
|
321
|
+
self.issues.append(CompatibilityIssue(
|
|
322
|
+
severity=IssueSeverity.WARNING,
|
|
323
|
+
category="hardware",
|
|
324
|
+
message=f"Robot payload ({self.robot.payload}kg) may be insufficient ({required_payload}kg required)",
|
|
325
|
+
mitigation="Reduce object weight or use slower movements"
|
|
326
|
+
))
|
|
327
|
+
self.adaptations.append(Adaptation(
|
|
328
|
+
type=AdaptationType.SPEED_LIMIT,
|
|
329
|
+
description="Reduce speed to compensate for payload limit",
|
|
330
|
+
config={"max_speed_factor": self.robot.payload / required_payload},
|
|
331
|
+
automated=True
|
|
332
|
+
))
|
|
333
|
+
|
|
334
|
+
def _check_gripper_requirement(self, req: HardwareRequirement) -> None:
|
|
335
|
+
"""Check gripper hardware requirements."""
|
|
336
|
+
if not self.robot.has_gripper:
|
|
337
|
+
self.issues.append(CompatibilityIssue(
|
|
338
|
+
severity=IssueSeverity.ERROR,
|
|
339
|
+
category="hardware",
|
|
340
|
+
message="Skill requires gripper but robot does not have one",
|
|
341
|
+
mitigation="Add a gripper to the robot"
|
|
342
|
+
))
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# Check gripper type
|
|
346
|
+
required_type = req.constraints.get("type")
|
|
347
|
+
if required_type and self.robot.gripper_type:
|
|
348
|
+
if required_type != self.robot.gripper_type:
|
|
349
|
+
self.issues.append(CompatibilityIssue(
|
|
350
|
+
severity=IssueSeverity.WARNING,
|
|
351
|
+
category="hardware",
|
|
352
|
+
message=f"Skill expects {required_type} gripper, robot has {self.robot.gripper_type}",
|
|
353
|
+
mitigation="Gripper behavior may need adaptation"
|
|
354
|
+
))
|
|
355
|
+
self.adaptations.append(Adaptation(
|
|
356
|
+
type=AdaptationType.GRIPPER_ADAPT,
|
|
357
|
+
description=f"Adapt gripper commands from {required_type} to {self.robot.gripper_type}",
|
|
358
|
+
config={"source_type": required_type, "target_type": self.robot.gripper_type},
|
|
359
|
+
automated=True
|
|
360
|
+
))
|
|
361
|
+
|
|
362
|
+
def _check_force_sensor_requirement(self, req: HardwareRequirement) -> None:
|
|
363
|
+
"""Check force sensor requirements."""
|
|
364
|
+
if not self.robot.has_force_sensor:
|
|
365
|
+
if req.optional:
|
|
366
|
+
self.issues.append(CompatibilityIssue(
|
|
367
|
+
severity=IssueSeverity.WARNING,
|
|
368
|
+
category="hardware",
|
|
369
|
+
message="Skill can use force sensor but robot does not have one",
|
|
370
|
+
details="Force-based features will be disabled"
|
|
371
|
+
))
|
|
372
|
+
else:
|
|
373
|
+
self.issues.append(CompatibilityIssue(
|
|
374
|
+
severity=IssueSeverity.ERROR,
|
|
375
|
+
category="hardware",
|
|
376
|
+
message="Skill requires force sensor but robot does not have one",
|
|
377
|
+
mitigation="Consider using position-based control instead"
|
|
378
|
+
))
|
|
379
|
+
self.adaptations.append(Adaptation(
|
|
380
|
+
type=AdaptationType.SENSOR_SUBSTITUTE,
|
|
381
|
+
description="Use current-based force estimation instead of F/T sensor",
|
|
382
|
+
config={"method": "current_estimation"},
|
|
383
|
+
automated=False
|
|
384
|
+
))
|
|
385
|
+
|
|
386
|
+
def _check_camera_requirement(self, req: HardwareRequirement) -> None:
|
|
387
|
+
"""Check camera requirements."""
|
|
388
|
+
if not self.robot.has_camera:
|
|
389
|
+
self.issues.append(CompatibilityIssue(
|
|
390
|
+
severity=IssueSeverity.ERROR if not req.optional else IssueSeverity.WARNING,
|
|
391
|
+
category="hardware",
|
|
392
|
+
message="Skill requires camera but robot does not have one",
|
|
393
|
+
mitigation="Add a camera to the robot or provide pre-computed poses"
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
def _check_mobile_base_requirement(self, req: HardwareRequirement) -> None:
|
|
397
|
+
"""Check mobile base requirements."""
|
|
398
|
+
if not self.robot.has_mobile_base:
|
|
399
|
+
self.issues.append(CompatibilityIssue(
|
|
400
|
+
severity=IssueSeverity.ERROR,
|
|
401
|
+
category="hardware",
|
|
402
|
+
message="Skill requires mobile base but robot is stationary",
|
|
403
|
+
mitigation="Skill cannot be executed on stationary robot"
|
|
404
|
+
))
|
|
405
|
+
|
|
406
|
+
def _check_kinematic_requirements(self) -> None:
|
|
407
|
+
"""Check kinematic constraints and workspace."""
|
|
408
|
+
# Check workspace bounds
|
|
409
|
+
if self.spec.workspace_bounds and self.robot.workspace_bounds:
|
|
410
|
+
for axis in ["x", "y", "z"]:
|
|
411
|
+
if axis in self.spec.workspace_bounds and axis in self.robot.workspace_bounds:
|
|
412
|
+
skill_min, skill_max = self.spec.workspace_bounds[axis]
|
|
413
|
+
robot_min, robot_max = self.robot.workspace_bounds[axis]
|
|
414
|
+
|
|
415
|
+
if skill_min < robot_min or skill_max > robot_max:
|
|
416
|
+
self.issues.append(CompatibilityIssue(
|
|
417
|
+
severity=IssueSeverity.WARNING,
|
|
418
|
+
category="kinematics",
|
|
419
|
+
message=f"Skill workspace ({axis}: {skill_min} to {skill_max}) "
|
|
420
|
+
f"exceeds robot workspace ({robot_min} to {robot_max})",
|
|
421
|
+
mitigation=f"Limit {axis} axis to robot range"
|
|
422
|
+
))
|
|
423
|
+
self.adaptations.append(Adaptation(
|
|
424
|
+
type=AdaptationType.WORKSPACE_LIMIT,
|
|
425
|
+
description=f"Limit {axis} workspace to robot capability",
|
|
426
|
+
config={
|
|
427
|
+
"axis": axis,
|
|
428
|
+
"min": max(skill_min, robot_min),
|
|
429
|
+
"max": min(skill_max, robot_max)
|
|
430
|
+
},
|
|
431
|
+
automated=True
|
|
432
|
+
))
|
|
433
|
+
|
|
434
|
+
def _check_safety_requirements(self) -> None:
|
|
435
|
+
"""Check safety-related constraints."""
|
|
436
|
+
# Check velocity limits
|
|
437
|
+
if self.spec.max_velocity and self.robot.max_cartesian_velocity:
|
|
438
|
+
if self.spec.max_velocity > self.robot.max_cartesian_velocity:
|
|
439
|
+
self.issues.append(CompatibilityIssue(
|
|
440
|
+
severity=IssueSeverity.WARNING,
|
|
441
|
+
category="safety",
|
|
442
|
+
message=f"Skill max velocity ({self.spec.max_velocity}m/s) "
|
|
443
|
+
f"exceeds robot limit ({self.robot.max_cartesian_velocity}m/s)",
|
|
444
|
+
mitigation="Skill will run at reduced speed"
|
|
445
|
+
))
|
|
446
|
+
self.adaptations.append(Adaptation(
|
|
447
|
+
type=AdaptationType.SPEED_LIMIT,
|
|
448
|
+
description="Cap velocity to robot maximum",
|
|
449
|
+
config={"max_velocity": self.robot.max_cartesian_velocity},
|
|
450
|
+
automated=True
|
|
451
|
+
))
|
|
452
|
+
|
|
453
|
+
# Check force limits
|
|
454
|
+
if self.spec.max_force and self.robot.max_force:
|
|
455
|
+
if self.spec.max_force > self.robot.max_force:
|
|
456
|
+
self.issues.append(CompatibilityIssue(
|
|
457
|
+
severity=IssueSeverity.WARNING,
|
|
458
|
+
category="safety",
|
|
459
|
+
message=f"Skill max force ({self.spec.max_force}N) "
|
|
460
|
+
f"exceeds robot limit ({self.robot.max_force}N)",
|
|
461
|
+
mitigation="Force-limited operations may fail"
|
|
462
|
+
))
|
|
463
|
+
self.adaptations.append(Adaptation(
|
|
464
|
+
type=AdaptationType.FORCE_LIMIT,
|
|
465
|
+
description="Cap force commands to robot maximum",
|
|
466
|
+
config={"max_force": self.robot.max_force},
|
|
467
|
+
automated=True
|
|
468
|
+
))
|
|
469
|
+
|
|
470
|
+
def _check_primitive_support(self) -> None:
|
|
471
|
+
"""Check if robot can execute required primitives."""
|
|
472
|
+
from .primitives import PRIMITIVE_REGISTRY, get_required_hardware
|
|
473
|
+
|
|
474
|
+
# Get hardware required by all primitives
|
|
475
|
+
required_hardware = get_required_hardware(self.spec.primitives)
|
|
476
|
+
|
|
477
|
+
# Check each required hardware type
|
|
478
|
+
hardware_map = {
|
|
479
|
+
"arm": self.robot.arm_dof > 0,
|
|
480
|
+
"gripper": self.robot.has_gripper,
|
|
481
|
+
"force_sensor": self.robot.has_force_sensor,
|
|
482
|
+
"camera": self.robot.has_camera,
|
|
483
|
+
"mobile_base": self.robot.has_mobile_base,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
for hw in required_hardware:
|
|
487
|
+
if hw in hardware_map and not hardware_map.get(hw, False):
|
|
488
|
+
# Find which primitives need this hardware
|
|
489
|
+
prims_needing_hw = [
|
|
490
|
+
p for p in self.spec.primitives
|
|
491
|
+
if p in PRIMITIVE_REGISTRY and hw in PRIMITIVE_REGISTRY[p].hardware
|
|
492
|
+
]
|
|
493
|
+
self.issues.append(CompatibilityIssue(
|
|
494
|
+
severity=IssueSeverity.ERROR,
|
|
495
|
+
category="primitives",
|
|
496
|
+
message=f"Primitives {prims_needing_hw} require {hw} which robot lacks",
|
|
497
|
+
details=f"Missing hardware: {hw}"
|
|
498
|
+
))
|
|
499
|
+
|
|
500
|
+
def _calculate_compatibility(self) -> Tuple[bool, float]:
|
|
501
|
+
"""
|
|
502
|
+
Calculate overall compatibility score.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Tuple of (is_compatible, score)
|
|
506
|
+
"""
|
|
507
|
+
# Weight by severity
|
|
508
|
+
severity_weights = {
|
|
509
|
+
IssueSeverity.INFO: 0,
|
|
510
|
+
IssueSeverity.WARNING: 0.1,
|
|
511
|
+
IssueSeverity.ERROR: 0.4,
|
|
512
|
+
IssueSeverity.CRITICAL: 1.0,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
total_penalty = sum(
|
|
516
|
+
severity_weights.get(issue.severity, 0)
|
|
517
|
+
for issue in self.issues
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Cap penalty at 1.0
|
|
521
|
+
score = max(0.0, 1.0 - min(1.0, total_penalty))
|
|
522
|
+
|
|
523
|
+
# Not compatible if any critical or error issues without mitigation
|
|
524
|
+
has_blocking = any(
|
|
525
|
+
issue.severity in (IssueSeverity.CRITICAL, IssueSeverity.ERROR)
|
|
526
|
+
and not issue.mitigation
|
|
527
|
+
for issue in self.issues
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
compatible = not has_blocking and score >= 0.5
|
|
531
|
+
|
|
532
|
+
return compatible, score
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def check_compatibility(
|
|
536
|
+
spec: SkillSpecification,
|
|
537
|
+
robot: RobotProfile,
|
|
538
|
+
) -> CompatibilityReport:
|
|
539
|
+
"""
|
|
540
|
+
Convenience function to check skill-robot compatibility.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
spec: Skill specification
|
|
544
|
+
robot: Robot profile
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
CompatibilityReport with results
|
|
548
|
+
"""
|
|
549
|
+
checker = CompatibilityChecker(spec, robot)
|
|
550
|
+
return checker.check()
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def check_compatibility_from_paths(
|
|
554
|
+
skill_yaml: str,
|
|
555
|
+
robot_urdf: Optional[str] = None,
|
|
556
|
+
robot_ate_dir: Optional[str] = None,
|
|
557
|
+
robot_name: Optional[str] = None,
|
|
558
|
+
) -> CompatibilityReport:
|
|
559
|
+
"""
|
|
560
|
+
Check compatibility from file paths.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
skill_yaml: Path to skill.yaml
|
|
564
|
+
robot_urdf: Path to robot URDF (optional)
|
|
565
|
+
robot_ate_dir: Path to ATE config directory (optional)
|
|
566
|
+
robot_name: Name for the robot profile
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
CompatibilityReport with results
|
|
570
|
+
"""
|
|
571
|
+
spec = SkillSpecification.from_yaml(skill_yaml)
|
|
572
|
+
|
|
573
|
+
if robot_urdf:
|
|
574
|
+
robot = RobotProfile.from_urdf(robot_urdf, robot_name)
|
|
575
|
+
elif robot_ate_dir:
|
|
576
|
+
robot = RobotProfile.from_ate_dir(robot_ate_dir, robot_name)
|
|
577
|
+
else:
|
|
578
|
+
robot = RobotProfile(name=robot_name or "unknown")
|
|
579
|
+
|
|
580
|
+
return check_compatibility(spec, robot)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Compiler Generators.
|
|
3
|
+
|
|
4
|
+
This module provides code generators for transforming skill specifications
|
|
5
|
+
into deployable packages for various target platforms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .skill_generator import SkillCodeGenerator
|
|
9
|
+
from .ros2_generator import ROS2PackageGenerator
|
|
10
|
+
from .docker_generator import DockerGenerator
|
|
11
|
+
from .hardware_config import HardwareConfigGenerator, generate_hardware_config
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SkillCodeGenerator",
|
|
15
|
+
"ROS2PackageGenerator",
|
|
16
|
+
"DockerGenerator",
|
|
17
|
+
"HardwareConfigGenerator",
|
|
18
|
+
"generate_hardware_config",
|
|
19
|
+
]
|