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
ate/skill_schema.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Schema - Data classes for skill specifications.
|
|
3
|
+
|
|
4
|
+
This module defines the complete schema for robot skill specifications,
|
|
5
|
+
which are the input to the Skill Compiler.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
12
|
+
import json
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ParameterType(str, Enum):
|
|
17
|
+
"""Supported parameter types for skill definitions."""
|
|
18
|
+
POSE = "Pose"
|
|
19
|
+
FLOAT = "float"
|
|
20
|
+
INT = "int"
|
|
21
|
+
BOOL = "bool"
|
|
22
|
+
STRING = "string"
|
|
23
|
+
ARRAY = "array"
|
|
24
|
+
JOINT_STATE = "JointState"
|
|
25
|
+
TRAJECTORY = "Trajectory"
|
|
26
|
+
POINT = "Point"
|
|
27
|
+
QUATERNION = "Quaternion"
|
|
28
|
+
TRANSFORM = "Transform"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HardwareType(str, Enum):
|
|
32
|
+
"""Types of hardware components."""
|
|
33
|
+
ARM = "arm"
|
|
34
|
+
GRIPPER = "gripper"
|
|
35
|
+
MOBILE_BASE = "mobile_base"
|
|
36
|
+
SENSOR = "sensor"
|
|
37
|
+
CAMERA = "camera"
|
|
38
|
+
FORCE_SENSOR = "force_sensor"
|
|
39
|
+
LIDAR = "lidar"
|
|
40
|
+
IMU = "imu"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ComparisonOperator(str, Enum):
|
|
44
|
+
"""Operators for hardware constraints."""
|
|
45
|
+
EQ = "=="
|
|
46
|
+
NE = "!="
|
|
47
|
+
GT = ">"
|
|
48
|
+
GE = ">="
|
|
49
|
+
LT = "<"
|
|
50
|
+
LE = "<="
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SkillParameter:
|
|
55
|
+
"""
|
|
56
|
+
Definition of a skill parameter.
|
|
57
|
+
|
|
58
|
+
Parameters define the inputs a skill accepts, their types,
|
|
59
|
+
defaults, and constraints.
|
|
60
|
+
"""
|
|
61
|
+
name: str
|
|
62
|
+
type: str # ParameterType value as string
|
|
63
|
+
description: str
|
|
64
|
+
default: Optional[Any] = None
|
|
65
|
+
range: Optional[Tuple[Any, Any]] = None
|
|
66
|
+
required: bool = True
|
|
67
|
+
units: Optional[str] = None # e.g., "meters", "radians", "newtons"
|
|
68
|
+
|
|
69
|
+
def validate_value(self, value: Any) -> List[str]:
|
|
70
|
+
"""Validate a value against this parameter's constraints."""
|
|
71
|
+
errors = []
|
|
72
|
+
|
|
73
|
+
if value is None:
|
|
74
|
+
if self.required and self.default is None:
|
|
75
|
+
errors.append(f"Parameter '{self.name}' is required but not provided")
|
|
76
|
+
return errors
|
|
77
|
+
|
|
78
|
+
# Type validation
|
|
79
|
+
type_validators = {
|
|
80
|
+
"float": lambda v: isinstance(v, (int, float)),
|
|
81
|
+
"int": lambda v: isinstance(v, int),
|
|
82
|
+
"bool": lambda v: isinstance(v, bool),
|
|
83
|
+
"string": lambda v: isinstance(v, str),
|
|
84
|
+
"array": lambda v: isinstance(v, (list, tuple)),
|
|
85
|
+
"Pose": lambda v: isinstance(v, dict) and all(
|
|
86
|
+
k in v for k in ["position", "orientation"]
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
validator = type_validators.get(self.type)
|
|
91
|
+
if validator and not validator(value):
|
|
92
|
+
errors.append(
|
|
93
|
+
f"Parameter '{self.name}' expected type {self.type}, "
|
|
94
|
+
f"got {type(value).__name__}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Range validation
|
|
98
|
+
if self.range and isinstance(value, (int, float)):
|
|
99
|
+
min_val, max_val = self.range
|
|
100
|
+
if value < min_val or value > max_val:
|
|
101
|
+
errors.append(
|
|
102
|
+
f"Parameter '{self.name}' value {value} out of range "
|
|
103
|
+
f"[{min_val}, {max_val}]"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return errors
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
109
|
+
"""Convert to dictionary representation."""
|
|
110
|
+
d = {
|
|
111
|
+
"type": self.type,
|
|
112
|
+
"description": self.description,
|
|
113
|
+
}
|
|
114
|
+
if self.default is not None:
|
|
115
|
+
d["default"] = self.default
|
|
116
|
+
if self.range is not None:
|
|
117
|
+
d["range"] = list(self.range)
|
|
118
|
+
if not self.required:
|
|
119
|
+
d["required"] = False
|
|
120
|
+
if self.units:
|
|
121
|
+
d["units"] = self.units
|
|
122
|
+
return d
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_dict(cls, name: str, data: Dict[str, Any]) -> "SkillParameter":
|
|
126
|
+
"""Create from dictionary representation."""
|
|
127
|
+
range_val = data.get("range")
|
|
128
|
+
if range_val:
|
|
129
|
+
range_val = tuple(range_val)
|
|
130
|
+
|
|
131
|
+
return cls(
|
|
132
|
+
name=name,
|
|
133
|
+
type=data["type"],
|
|
134
|
+
description=data.get("description", ""),
|
|
135
|
+
default=data.get("default"),
|
|
136
|
+
range=range_val,
|
|
137
|
+
required=data.get("required", True),
|
|
138
|
+
units=data.get("units"),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class HardwareConstraint:
|
|
144
|
+
"""A single constraint on hardware (e.g., dof >= 6)."""
|
|
145
|
+
property: str
|
|
146
|
+
operator: str # ComparisonOperator value
|
|
147
|
+
value: Any
|
|
148
|
+
|
|
149
|
+
def evaluate(self, actual_value: Any) -> bool:
|
|
150
|
+
"""Evaluate this constraint against an actual value."""
|
|
151
|
+
ops = {
|
|
152
|
+
"==": lambda a, b: a == b,
|
|
153
|
+
"!=": lambda a, b: a != b,
|
|
154
|
+
">": lambda a, b: a > b,
|
|
155
|
+
">=": lambda a, b: a >= b,
|
|
156
|
+
"<": lambda a, b: a < b,
|
|
157
|
+
"<=": lambda a, b: a <= b,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Handle string comparison operators like ">=6"
|
|
161
|
+
op = self.operator
|
|
162
|
+
if op not in ops:
|
|
163
|
+
# Try to parse from value string like ">=6"
|
|
164
|
+
if isinstance(self.value, str):
|
|
165
|
+
for test_op in [">=", "<=", "!=", "==", ">", "<"]:
|
|
166
|
+
if self.value.startswith(test_op):
|
|
167
|
+
op = test_op
|
|
168
|
+
self.value = self._parse_value(self.value[len(test_op):])
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
op_func = ops.get(op, ops["=="])
|
|
172
|
+
try:
|
|
173
|
+
return op_func(actual_value, self.value)
|
|
174
|
+
except TypeError:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def _parse_value(self, val_str: str) -> Any:
|
|
178
|
+
"""Parse a value string to appropriate type."""
|
|
179
|
+
val_str = val_str.strip()
|
|
180
|
+
# Try int
|
|
181
|
+
try:
|
|
182
|
+
return int(val_str)
|
|
183
|
+
except ValueError:
|
|
184
|
+
pass
|
|
185
|
+
# Try float
|
|
186
|
+
try:
|
|
187
|
+
return float(val_str)
|
|
188
|
+
except ValueError:
|
|
189
|
+
pass
|
|
190
|
+
# Return as string
|
|
191
|
+
return val_str
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class HardwareRequirement:
|
|
196
|
+
"""
|
|
197
|
+
Hardware requirement for a skill.
|
|
198
|
+
|
|
199
|
+
Defines what hardware components a skill needs and their constraints.
|
|
200
|
+
"""
|
|
201
|
+
component_type: str # HardwareType value as string
|
|
202
|
+
constraints: Dict[str, Any] = field(default_factory=dict)
|
|
203
|
+
optional: bool = False
|
|
204
|
+
|
|
205
|
+
def get_constraints(self) -> List[HardwareConstraint]:
|
|
206
|
+
"""Parse constraints into HardwareConstraint objects."""
|
|
207
|
+
result = []
|
|
208
|
+
for prop, value in self.constraints.items():
|
|
209
|
+
if isinstance(value, str) and any(
|
|
210
|
+
value.startswith(op) for op in [">=", "<=", "!=", "==", ">", "<"]
|
|
211
|
+
):
|
|
212
|
+
# Parse operator from value
|
|
213
|
+
for op in [">=", "<=", "!=", "==", ">", "<"]:
|
|
214
|
+
if value.startswith(op):
|
|
215
|
+
result.append(HardwareConstraint(
|
|
216
|
+
property=prop,
|
|
217
|
+
operator=op,
|
|
218
|
+
value=value[len(op):].strip()
|
|
219
|
+
))
|
|
220
|
+
break
|
|
221
|
+
else:
|
|
222
|
+
# Equality constraint
|
|
223
|
+
result.append(HardwareConstraint(
|
|
224
|
+
property=prop,
|
|
225
|
+
operator="==",
|
|
226
|
+
value=value
|
|
227
|
+
))
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
231
|
+
"""Convert to dictionary representation."""
|
|
232
|
+
return {self.component_type: self.constraints}
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def from_dict(cls, data: Dict[str, Any]) -> "HardwareRequirement":
|
|
236
|
+
"""Create from dictionary representation."""
|
|
237
|
+
# Handle format: {"arm": {"dof": ">=6"}}
|
|
238
|
+
if len(data) == 1:
|
|
239
|
+
component_type = list(data.keys())[0]
|
|
240
|
+
constraints = data[component_type]
|
|
241
|
+
if isinstance(constraints, dict):
|
|
242
|
+
return cls(
|
|
243
|
+
component_type=component_type,
|
|
244
|
+
constraints=constraints
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Handle format: {"component_type": "arm", "constraints": {...}}
|
|
248
|
+
return cls(
|
|
249
|
+
component_type=data.get("component_type", "unknown"),
|
|
250
|
+
constraints=data.get("constraints", {}),
|
|
251
|
+
optional=data.get("optional", False)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass
|
|
256
|
+
class SuccessCriterion:
|
|
257
|
+
"""
|
|
258
|
+
Success criterion for skill completion.
|
|
259
|
+
|
|
260
|
+
Defines how to determine if a skill has succeeded.
|
|
261
|
+
"""
|
|
262
|
+
name: str
|
|
263
|
+
condition: str # Condition expression
|
|
264
|
+
tolerance: Optional[float] = None
|
|
265
|
+
description: Optional[str] = None
|
|
266
|
+
|
|
267
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
268
|
+
"""Convert to dictionary representation."""
|
|
269
|
+
d = {"condition": self.condition}
|
|
270
|
+
if self.tolerance is not None:
|
|
271
|
+
d["tolerance"] = self.tolerance
|
|
272
|
+
if self.description:
|
|
273
|
+
d["description"] = self.description
|
|
274
|
+
return d
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_dict(cls, name: str, data: Dict[str, Any]) -> "SuccessCriterion":
|
|
278
|
+
"""Create from dictionary representation."""
|
|
279
|
+
if isinstance(data, str):
|
|
280
|
+
# Simple format: "object_at_place_pose"
|
|
281
|
+
return cls(name=name, condition=data)
|
|
282
|
+
|
|
283
|
+
return cls(
|
|
284
|
+
name=name,
|
|
285
|
+
condition=data.get("condition", ""),
|
|
286
|
+
tolerance=data.get("tolerance"),
|
|
287
|
+
description=data.get("description")
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@dataclass
|
|
292
|
+
class PrimitiveCall:
|
|
293
|
+
"""
|
|
294
|
+
A call to a primitive within a skill's execution flow.
|
|
295
|
+
"""
|
|
296
|
+
primitive: str
|
|
297
|
+
parameters: Dict[str, Any] = field(default_factory=dict)
|
|
298
|
+
condition: Optional[str] = None # Optional condition to execute
|
|
299
|
+
on_failure: Optional[str] = None # Action on failure: "abort", "retry", "continue"
|
|
300
|
+
retries: int = 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass
|
|
304
|
+
class SkillSpecification:
|
|
305
|
+
"""
|
|
306
|
+
Complete specification for a robot skill.
|
|
307
|
+
|
|
308
|
+
This is the main data structure that the Skill Compiler uses
|
|
309
|
+
to generate deployable skill packages.
|
|
310
|
+
"""
|
|
311
|
+
name: str
|
|
312
|
+
version: str
|
|
313
|
+
description: str
|
|
314
|
+
parameters: List[SkillParameter]
|
|
315
|
+
primitives: List[str] # List of primitive names used
|
|
316
|
+
hardware_requirements: List[HardwareRequirement]
|
|
317
|
+
success_criteria: List[SuccessCriterion]
|
|
318
|
+
|
|
319
|
+
# Optional fields
|
|
320
|
+
author: Optional[str] = None
|
|
321
|
+
license: Optional[str] = None
|
|
322
|
+
category: Optional[str] = None
|
|
323
|
+
tags: List[str] = field(default_factory=list)
|
|
324
|
+
dependencies: List[str] = field(default_factory=list) # Other skills
|
|
325
|
+
execution_flow: List[PrimitiveCall] = field(default_factory=list)
|
|
326
|
+
|
|
327
|
+
# Safety constraints
|
|
328
|
+
max_velocity: Optional[float] = None # m/s
|
|
329
|
+
max_force: Optional[float] = None # N
|
|
330
|
+
workspace_bounds: Optional[Dict[str, Tuple[float, float]]] = None
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def from_yaml(cls, path: Union[str, Path]) -> "SkillSpecification":
|
|
334
|
+
"""Load skill specification from YAML file."""
|
|
335
|
+
path = Path(path)
|
|
336
|
+
if not path.exists():
|
|
337
|
+
raise FileNotFoundError(f"Skill specification not found: {path}")
|
|
338
|
+
|
|
339
|
+
with open(path, "r") as f:
|
|
340
|
+
data = yaml.safe_load(f)
|
|
341
|
+
|
|
342
|
+
return cls.from_dict(data)
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SkillSpecification":
|
|
346
|
+
"""Create skill specification from dictionary."""
|
|
347
|
+
# Parse parameters
|
|
348
|
+
parameters = []
|
|
349
|
+
params_data = data.get("parameters", {})
|
|
350
|
+
for name, param_data in params_data.items():
|
|
351
|
+
parameters.append(SkillParameter.from_dict(name, param_data))
|
|
352
|
+
|
|
353
|
+
# Parse hardware requirements
|
|
354
|
+
hardware_requirements = []
|
|
355
|
+
hw_data = data.get("hardware_requirements", [])
|
|
356
|
+
for hw in hw_data:
|
|
357
|
+
hardware_requirements.append(HardwareRequirement.from_dict(hw))
|
|
358
|
+
|
|
359
|
+
# Parse success criteria
|
|
360
|
+
success_criteria = []
|
|
361
|
+
criteria_data = data.get("success_criteria", {})
|
|
362
|
+
for name, criterion_data in criteria_data.items():
|
|
363
|
+
success_criteria.append(SuccessCriterion.from_dict(name, criterion_data))
|
|
364
|
+
|
|
365
|
+
# Parse execution flow
|
|
366
|
+
execution_flow = []
|
|
367
|
+
flow_data = data.get("execution_flow", [])
|
|
368
|
+
for step in flow_data:
|
|
369
|
+
if isinstance(step, str):
|
|
370
|
+
execution_flow.append(PrimitiveCall(primitive=step))
|
|
371
|
+
else:
|
|
372
|
+
execution_flow.append(PrimitiveCall(
|
|
373
|
+
primitive=step.get("primitive", ""),
|
|
374
|
+
parameters=step.get("parameters", {}),
|
|
375
|
+
condition=step.get("condition"),
|
|
376
|
+
on_failure=step.get("on_failure"),
|
|
377
|
+
retries=step.get("retries", 0)
|
|
378
|
+
))
|
|
379
|
+
|
|
380
|
+
# Parse workspace bounds
|
|
381
|
+
workspace_bounds = None
|
|
382
|
+
if "workspace_bounds" in data:
|
|
383
|
+
wb = data["workspace_bounds"]
|
|
384
|
+
workspace_bounds = {
|
|
385
|
+
axis: tuple(bounds) for axis, bounds in wb.items()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return cls(
|
|
389
|
+
name=data["name"],
|
|
390
|
+
version=data.get("version", "1.0.0"),
|
|
391
|
+
description=data.get("description", ""),
|
|
392
|
+
parameters=parameters,
|
|
393
|
+
primitives=data.get("primitives", []),
|
|
394
|
+
hardware_requirements=hardware_requirements,
|
|
395
|
+
success_criteria=success_criteria,
|
|
396
|
+
author=data.get("author"),
|
|
397
|
+
license=data.get("license"),
|
|
398
|
+
category=data.get("category"),
|
|
399
|
+
tags=data.get("tags", []),
|
|
400
|
+
dependencies=data.get("dependencies", []),
|
|
401
|
+
execution_flow=execution_flow,
|
|
402
|
+
max_velocity=data.get("max_velocity"),
|
|
403
|
+
max_force=data.get("max_force"),
|
|
404
|
+
workspace_bounds=workspace_bounds,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
408
|
+
"""Convert to dictionary representation."""
|
|
409
|
+
d = {
|
|
410
|
+
"name": self.name,
|
|
411
|
+
"version": self.version,
|
|
412
|
+
"description": self.description,
|
|
413
|
+
"parameters": {p.name: p.to_dict() for p in self.parameters},
|
|
414
|
+
"primitives": self.primitives,
|
|
415
|
+
"hardware_requirements": [hr.to_dict() for hr in self.hardware_requirements],
|
|
416
|
+
"success_criteria": {sc.name: sc.to_dict() for sc in self.success_criteria},
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if self.author:
|
|
420
|
+
d["author"] = self.author
|
|
421
|
+
if self.license:
|
|
422
|
+
d["license"] = self.license
|
|
423
|
+
if self.category:
|
|
424
|
+
d["category"] = self.category
|
|
425
|
+
if self.tags:
|
|
426
|
+
d["tags"] = self.tags
|
|
427
|
+
if self.dependencies:
|
|
428
|
+
d["dependencies"] = self.dependencies
|
|
429
|
+
if self.execution_flow:
|
|
430
|
+
d["execution_flow"] = [
|
|
431
|
+
{
|
|
432
|
+
"primitive": pc.primitive,
|
|
433
|
+
"parameters": pc.parameters,
|
|
434
|
+
"condition": pc.condition,
|
|
435
|
+
"on_failure": pc.on_failure,
|
|
436
|
+
"retries": pc.retries
|
|
437
|
+
} if pc.parameters or pc.condition else pc.primitive
|
|
438
|
+
for pc in self.execution_flow
|
|
439
|
+
]
|
|
440
|
+
if self.max_velocity:
|
|
441
|
+
d["max_velocity"] = self.max_velocity
|
|
442
|
+
if self.max_force:
|
|
443
|
+
d["max_force"] = self.max_force
|
|
444
|
+
if self.workspace_bounds:
|
|
445
|
+
d["workspace_bounds"] = {
|
|
446
|
+
k: list(v) for k, v in self.workspace_bounds.items()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return d
|
|
450
|
+
|
|
451
|
+
def to_yaml(self, path: Union[str, Path]) -> None:
|
|
452
|
+
"""Save skill specification to YAML file."""
|
|
453
|
+
path = Path(path)
|
|
454
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
455
|
+
|
|
456
|
+
with open(path, "w") as f:
|
|
457
|
+
yaml.dump(self.to_dict(), f, default_flow_style=False, sort_keys=False)
|
|
458
|
+
|
|
459
|
+
def validate(self) -> List[str]:
|
|
460
|
+
"""
|
|
461
|
+
Validate the skill specification.
|
|
462
|
+
|
|
463
|
+
Returns a list of validation errors, or empty list if valid.
|
|
464
|
+
"""
|
|
465
|
+
errors = []
|
|
466
|
+
|
|
467
|
+
# Name validation
|
|
468
|
+
if not self.name:
|
|
469
|
+
errors.append("Skill name cannot be empty")
|
|
470
|
+
elif not self.name.replace("_", "").replace("-", "").isalnum():
|
|
471
|
+
errors.append(
|
|
472
|
+
f"Skill name '{self.name}' contains invalid characters. "
|
|
473
|
+
"Use only alphanumeric characters, underscores, and hyphens."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Version validation
|
|
477
|
+
if not self.version:
|
|
478
|
+
errors.append("Version cannot be empty")
|
|
479
|
+
else:
|
|
480
|
+
# Simple semver check
|
|
481
|
+
parts = self.version.split(".")
|
|
482
|
+
if len(parts) != 3:
|
|
483
|
+
errors.append(
|
|
484
|
+
f"Version '{self.version}' should be in semver format (x.y.z)"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Parameters validation
|
|
488
|
+
param_names = set()
|
|
489
|
+
for param in self.parameters:
|
|
490
|
+
if param.name in param_names:
|
|
491
|
+
errors.append(f"Duplicate parameter name: {param.name}")
|
|
492
|
+
param_names.add(param.name)
|
|
493
|
+
|
|
494
|
+
if not param.type:
|
|
495
|
+
errors.append(f"Parameter '{param.name}' missing type")
|
|
496
|
+
|
|
497
|
+
# Primitives validation
|
|
498
|
+
if not self.primitives:
|
|
499
|
+
errors.append("At least one primitive is required")
|
|
500
|
+
|
|
501
|
+
# Hardware requirements validation
|
|
502
|
+
if not self.hardware_requirements:
|
|
503
|
+
errors.append("At least one hardware requirement is required")
|
|
504
|
+
|
|
505
|
+
# Success criteria validation
|
|
506
|
+
if not self.success_criteria:
|
|
507
|
+
errors.append("At least one success criterion is required")
|
|
508
|
+
|
|
509
|
+
return errors
|
|
510
|
+
|
|
511
|
+
def get_parameter(self, name: str) -> Optional[SkillParameter]:
|
|
512
|
+
"""Get a parameter by name."""
|
|
513
|
+
for param in self.parameters:
|
|
514
|
+
if param.name == name:
|
|
515
|
+
return param
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
def get_required_hardware_types(self) -> List[str]:
|
|
519
|
+
"""Get list of required hardware component types."""
|
|
520
|
+
return [
|
|
521
|
+
req.component_type
|
|
522
|
+
for req in self.hardware_requirements
|
|
523
|
+
if not req.optional
|
|
524
|
+
]
|
|
525
|
+
|
|
526
|
+
def __repr__(self) -> str:
|
|
527
|
+
return (
|
|
528
|
+
f"SkillSpecification(name='{self.name}', version='{self.version}', "
|
|
529
|
+
f"primitives={self.primitives}, "
|
|
530
|
+
f"parameters={[p.name for p in self.parameters]})"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# Convenience type aliases
|
|
535
|
+
SkillSpec = SkillSpecification
|
|
536
|
+
Param = SkillParameter
|
|
537
|
+
HWReq = HardwareRequirement
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Telemetry module for FoodforThought CLI
|
|
3
|
+
|
|
4
|
+
Provides tools for collecting, serializing, and uploading robot telemetry data
|
|
5
|
+
from simulations and hardware executions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
Pose,
|
|
10
|
+
Contact,
|
|
11
|
+
TrajectoryFrame,
|
|
12
|
+
TrajectoryMetadata,
|
|
13
|
+
TrajectoryRecording,
|
|
14
|
+
ExecutionEvent,
|
|
15
|
+
SensorReading,
|
|
16
|
+
)
|
|
17
|
+
from .collector import TelemetryCollector
|
|
18
|
+
from .context import record_trajectory
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# Types
|
|
22
|
+
"Pose",
|
|
23
|
+
"Contact",
|
|
24
|
+
"TrajectoryFrame",
|
|
25
|
+
"TrajectoryMetadata",
|
|
26
|
+
"TrajectoryRecording",
|
|
27
|
+
"ExecutionEvent",
|
|
28
|
+
"SensorReading",
|
|
29
|
+
# Collector
|
|
30
|
+
"TelemetryCollector",
|
|
31
|
+
# Context manager
|
|
32
|
+
"record_trajectory",
|
|
33
|
+
]
|