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,617 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ROS2 Package Generator - Generate ROS2-compatible package structure.
|
|
3
|
+
|
|
4
|
+
This generator creates:
|
|
5
|
+
- package.xml: Package manifest with dependencies
|
|
6
|
+
- CMakeLists.txt: Build configuration
|
|
7
|
+
- setup.py: Python package setup
|
|
8
|
+
- launch/: Launch files
|
|
9
|
+
- action/: Action definitions for async execution
|
|
10
|
+
- srv/: Service definitions for sync execution
|
|
11
|
+
- config/: Parameter files
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional, Set
|
|
18
|
+
|
|
19
|
+
from ..skill_schema import SkillSpecification, SkillParameter
|
|
20
|
+
from .skill_generator import to_pascal_case, to_snake_case
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ros2_type(param_type: str) -> str:
|
|
24
|
+
"""Convert skill parameter type to ROS2 message type."""
|
|
25
|
+
type_map = {
|
|
26
|
+
"Pose": "geometry_msgs/Pose",
|
|
27
|
+
"float": "float64",
|
|
28
|
+
"int": "int32",
|
|
29
|
+
"bool": "bool",
|
|
30
|
+
"string": "string",
|
|
31
|
+
"array": "float64[]",
|
|
32
|
+
"JointState": "sensor_msgs/JointState",
|
|
33
|
+
"Trajectory": "trajectory_msgs/JointTrajectory",
|
|
34
|
+
"Point": "geometry_msgs/Point",
|
|
35
|
+
"Quaternion": "geometry_msgs/Quaternion",
|
|
36
|
+
"Transform": "geometry_msgs/Transform",
|
|
37
|
+
}
|
|
38
|
+
return type_map.get(param_type, "string")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ros2_msg_type(param_type: str) -> str:
|
|
42
|
+
"""Convert to simple ROS2 message field type."""
|
|
43
|
+
type_map = {
|
|
44
|
+
"Pose": "geometry_msgs/msg/Pose",
|
|
45
|
+
"float": "float64",
|
|
46
|
+
"int": "int32",
|
|
47
|
+
"bool": "bool",
|
|
48
|
+
"string": "string",
|
|
49
|
+
"array": "float64[]",
|
|
50
|
+
"JointState": "sensor_msgs/msg/JointState",
|
|
51
|
+
"Trajectory": "trajectory_msgs/msg/JointTrajectory",
|
|
52
|
+
"Point": "geometry_msgs/msg/Point",
|
|
53
|
+
"Quaternion": "geometry_msgs/msg/Quaternion",
|
|
54
|
+
"Transform": "geometry_msgs/msg/Transform",
|
|
55
|
+
}
|
|
56
|
+
return type_map.get(param_type, "string")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ROS2PackageGenerator:
|
|
60
|
+
"""
|
|
61
|
+
Generate a ROS2-compatible package structure for a skill.
|
|
62
|
+
|
|
63
|
+
Creates all necessary files for a ROS2 package including:
|
|
64
|
+
- Package manifest (package.xml)
|
|
65
|
+
- Build configuration (CMakeLists.txt, setup.py)
|
|
66
|
+
- Launch files
|
|
67
|
+
- Action/Service definitions
|
|
68
|
+
- Configuration files
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, spec: SkillSpecification):
|
|
72
|
+
"""
|
|
73
|
+
Initialize the generator.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
spec: The skill specification to generate ROS2 package for
|
|
77
|
+
"""
|
|
78
|
+
self.spec = spec
|
|
79
|
+
self.package_name = to_snake_case(spec.name) + "_skill"
|
|
80
|
+
self.class_name = to_pascal_case(spec.name)
|
|
81
|
+
|
|
82
|
+
def get_dependencies(self) -> Set[str]:
|
|
83
|
+
"""Get ROS2 package dependencies based on skill specification."""
|
|
84
|
+
deps = {
|
|
85
|
+
"rclpy",
|
|
86
|
+
"std_msgs",
|
|
87
|
+
"std_srvs",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Add dependencies based on parameter types
|
|
91
|
+
for param in self.spec.parameters:
|
|
92
|
+
if param.type == "Pose":
|
|
93
|
+
deps.add("geometry_msgs")
|
|
94
|
+
elif param.type == "JointState":
|
|
95
|
+
deps.add("sensor_msgs")
|
|
96
|
+
elif param.type == "Trajectory":
|
|
97
|
+
deps.add("trajectory_msgs")
|
|
98
|
+
elif param.type in ("Point", "Quaternion", "Transform"):
|
|
99
|
+
deps.add("geometry_msgs")
|
|
100
|
+
|
|
101
|
+
# Add dependencies based on hardware requirements
|
|
102
|
+
for req in self.spec.hardware_requirements:
|
|
103
|
+
if req.component_type == "arm":
|
|
104
|
+
deps.add("moveit_msgs")
|
|
105
|
+
deps.add("control_msgs")
|
|
106
|
+
elif req.component_type == "gripper":
|
|
107
|
+
deps.add("control_msgs")
|
|
108
|
+
elif req.component_type == "camera":
|
|
109
|
+
deps.add("sensor_msgs")
|
|
110
|
+
deps.add("cv_bridge")
|
|
111
|
+
elif req.component_type == "force_sensor":
|
|
112
|
+
deps.add("geometry_msgs")
|
|
113
|
+
|
|
114
|
+
return deps
|
|
115
|
+
|
|
116
|
+
def generate_package_xml(self) -> str:
|
|
117
|
+
"""Generate package.xml with dependencies."""
|
|
118
|
+
deps = self.get_dependencies()
|
|
119
|
+
|
|
120
|
+
dep_lines = "\n".join(f" <depend>{dep}</depend>" for dep in sorted(deps))
|
|
121
|
+
exec_deps = "\n".join(f" <exec_depend>{dep}</exec_depend>" for dep in sorted(deps))
|
|
122
|
+
|
|
123
|
+
return f'''<?xml version="1.0"?>
|
|
124
|
+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
|
125
|
+
<package format="3">
|
|
126
|
+
<name>{self.package_name}</name>
|
|
127
|
+
<version>{self.spec.version}</version>
|
|
128
|
+
<description>{self.spec.description}</description>
|
|
129
|
+
<maintainer email="{self.spec.author or 'maintainer'}@todo.todo">{self.spec.author or 'TODO'}</maintainer>
|
|
130
|
+
<license>{self.spec.license or 'Apache-2.0'}</license>
|
|
131
|
+
|
|
132
|
+
<buildtool_depend>ament_cmake</buildtool_depend>
|
|
133
|
+
<buildtool_depend>ament_cmake_python</buildtool_depend>
|
|
134
|
+
|
|
135
|
+
{dep_lines}
|
|
136
|
+
|
|
137
|
+
<test_depend>ament_lint_auto</test_depend>
|
|
138
|
+
<test_depend>ament_lint_common</test_depend>
|
|
139
|
+
<test_depend>ament_cmake_pytest</test_depend>
|
|
140
|
+
|
|
141
|
+
<export>
|
|
142
|
+
<build_type>ament_cmake</build_type>
|
|
143
|
+
</export>
|
|
144
|
+
</package>
|
|
145
|
+
'''
|
|
146
|
+
|
|
147
|
+
def generate_cmakelists(self) -> str:
|
|
148
|
+
"""Generate CMakeLists.txt."""
|
|
149
|
+
deps = self.get_dependencies()
|
|
150
|
+
find_packages = "\n".join(f"find_package({dep} REQUIRED)" for dep in sorted(deps))
|
|
151
|
+
|
|
152
|
+
return f'''cmake_minimum_required(VERSION 3.8)
|
|
153
|
+
project({self.package_name})
|
|
154
|
+
|
|
155
|
+
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
156
|
+
add_compile_options(-Wall -Wextra -Wpedantic)
|
|
157
|
+
endif()
|
|
158
|
+
|
|
159
|
+
# Find dependencies
|
|
160
|
+
find_package(ament_cmake REQUIRED)
|
|
161
|
+
find_package(ament_cmake_python REQUIRED)
|
|
162
|
+
find_package(rosidl_default_generators REQUIRED)
|
|
163
|
+
{find_packages}
|
|
164
|
+
|
|
165
|
+
# Generate interfaces
|
|
166
|
+
rosidl_generate_interfaces(${{PROJECT_NAME}}
|
|
167
|
+
"action/{self.class_name}.action"
|
|
168
|
+
"srv/Execute{self.class_name}.srv"
|
|
169
|
+
DEPENDENCIES std_msgs geometry_msgs
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Install Python modules
|
|
173
|
+
ament_python_install_package(${{PROJECT_NAME}})
|
|
174
|
+
|
|
175
|
+
# Install Python executables
|
|
176
|
+
install(PROGRAMS
|
|
177
|
+
scripts/{self.package_name}_node.py
|
|
178
|
+
DESTINATION lib/${{PROJECT_NAME}}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Install launch files
|
|
182
|
+
install(DIRECTORY
|
|
183
|
+
launch
|
|
184
|
+
DESTINATION share/${{PROJECT_NAME}}/
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Install config files
|
|
188
|
+
install(DIRECTORY
|
|
189
|
+
config
|
|
190
|
+
DESTINATION share/${{PROJECT_NAME}}/
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if(BUILD_TESTING)
|
|
194
|
+
find_package(ament_lint_auto REQUIRED)
|
|
195
|
+
ament_lint_auto_find_test_dependencies()
|
|
196
|
+
|
|
197
|
+
find_package(ament_cmake_pytest REQUIRED)
|
|
198
|
+
ament_add_pytest_test(test_skill test/test_skill.py)
|
|
199
|
+
endif()
|
|
200
|
+
|
|
201
|
+
ament_package()
|
|
202
|
+
'''
|
|
203
|
+
|
|
204
|
+
def generate_setup_py(self) -> str:
|
|
205
|
+
"""Generate setup.py for Python package."""
|
|
206
|
+
return f'''from setuptools import setup, find_packages
|
|
207
|
+
|
|
208
|
+
package_name = '{self.package_name}'
|
|
209
|
+
|
|
210
|
+
setup(
|
|
211
|
+
name=package_name,
|
|
212
|
+
version='{self.spec.version}',
|
|
213
|
+
packages=find_packages(),
|
|
214
|
+
data_files=[
|
|
215
|
+
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
|
|
216
|
+
('share/' + package_name, ['package.xml']),
|
|
217
|
+
('share/' + package_name + '/launch', ['launch/skill.launch.py']),
|
|
218
|
+
('share/' + package_name + '/config', ['config/skill_params.yaml']),
|
|
219
|
+
],
|
|
220
|
+
install_requires=['setuptools'],
|
|
221
|
+
zip_safe=True,
|
|
222
|
+
maintainer='{self.spec.author or "TODO"}',
|
|
223
|
+
maintainer_email='todo@todo.todo',
|
|
224
|
+
description='{self.spec.description}',
|
|
225
|
+
license='{self.spec.license or "Apache-2.0"}',
|
|
226
|
+
tests_require=['pytest'],
|
|
227
|
+
entry_points={{
|
|
228
|
+
'console_scripts': [
|
|
229
|
+
'{self.package_name}_node = {self.package_name}.skill_node:main',
|
|
230
|
+
],
|
|
231
|
+
}},
|
|
232
|
+
)
|
|
233
|
+
'''
|
|
234
|
+
|
|
235
|
+
def generate_launch_file(self) -> str:
|
|
236
|
+
"""Generate launch/skill.launch.py."""
|
|
237
|
+
return f'''"""
|
|
238
|
+
Launch file for {self.spec.name} skill.
|
|
239
|
+
|
|
240
|
+
Generated by Skill Compiler v1.0.0
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
from launch import LaunchDescription
|
|
244
|
+
from launch.actions import DeclareLaunchArgument
|
|
245
|
+
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
|
|
246
|
+
from launch_ros.actions import Node
|
|
247
|
+
from launch_ros.substitutions import FindPackageShare
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def generate_launch_description():
|
|
251
|
+
# Declare launch arguments
|
|
252
|
+
use_sim = DeclareLaunchArgument(
|
|
253
|
+
'use_sim',
|
|
254
|
+
default_value='false',
|
|
255
|
+
description='Use simulation mode'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
config_file = DeclareLaunchArgument(
|
|
259
|
+
'config_file',
|
|
260
|
+
default_value=PathJoinSubstitution([
|
|
261
|
+
FindPackageShare('{self.package_name}'),
|
|
262
|
+
'config',
|
|
263
|
+
'skill_params.yaml'
|
|
264
|
+
]),
|
|
265
|
+
description='Path to skill parameters file'
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Skill node
|
|
269
|
+
skill_node = Node(
|
|
270
|
+
package='{self.package_name}',
|
|
271
|
+
executable='{self.package_name}_node.py',
|
|
272
|
+
name='{self.package_name}',
|
|
273
|
+
parameters=[LaunchConfiguration('config_file')],
|
|
274
|
+
output='screen',
|
|
275
|
+
emulate_tty=True,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return LaunchDescription([
|
|
279
|
+
use_sim,
|
|
280
|
+
config_file,
|
|
281
|
+
skill_node,
|
|
282
|
+
])
|
|
283
|
+
'''
|
|
284
|
+
|
|
285
|
+
def generate_action_definition(self) -> str:
|
|
286
|
+
"""Generate action/{ClassName}.action for async execution."""
|
|
287
|
+
# Goal fields
|
|
288
|
+
goal_fields = []
|
|
289
|
+
for param in self.spec.parameters:
|
|
290
|
+
ros_type = ros2_msg_type(param.type)
|
|
291
|
+
goal_fields.append(f"{ros_type} {param.name}")
|
|
292
|
+
|
|
293
|
+
goal_section = "\n".join(goal_fields) if goal_fields else "# No parameters"
|
|
294
|
+
|
|
295
|
+
# Result fields
|
|
296
|
+
result_fields = [
|
|
297
|
+
"bool success",
|
|
298
|
+
"string message",
|
|
299
|
+
"string error_code",
|
|
300
|
+
"float64 execution_time",
|
|
301
|
+
]
|
|
302
|
+
for criterion in self.spec.success_criteria:
|
|
303
|
+
result_fields.append(f"bool {criterion.name}")
|
|
304
|
+
|
|
305
|
+
result_section = "\n".join(result_fields)
|
|
306
|
+
|
|
307
|
+
# Feedback fields
|
|
308
|
+
feedback_fields = [
|
|
309
|
+
"string current_state",
|
|
310
|
+
"float32 progress",
|
|
311
|
+
"string status_message",
|
|
312
|
+
]
|
|
313
|
+
feedback_section = "\n".join(feedback_fields)
|
|
314
|
+
|
|
315
|
+
return f'''# {self.spec.name} Action Definition
|
|
316
|
+
# {self.spec.description}
|
|
317
|
+
# Generated by Skill Compiler v1.0.0
|
|
318
|
+
|
|
319
|
+
# Goal: Input parameters for skill execution
|
|
320
|
+
{goal_section}
|
|
321
|
+
---
|
|
322
|
+
# Result: Outcome of skill execution
|
|
323
|
+
{result_section}
|
|
324
|
+
---
|
|
325
|
+
# Feedback: Progress during execution
|
|
326
|
+
{feedback_section}
|
|
327
|
+
'''
|
|
328
|
+
|
|
329
|
+
def generate_service_definition(self) -> str:
|
|
330
|
+
"""Generate srv/Execute{ClassName}.srv for sync execution."""
|
|
331
|
+
# Request fields
|
|
332
|
+
request_fields = []
|
|
333
|
+
for param in self.spec.parameters:
|
|
334
|
+
ros_type = ros2_msg_type(param.type)
|
|
335
|
+
request_fields.append(f"{ros_type} {param.name}")
|
|
336
|
+
|
|
337
|
+
request_section = "\n".join(request_fields) if request_fields else "# No parameters"
|
|
338
|
+
|
|
339
|
+
# Response fields
|
|
340
|
+
response_fields = [
|
|
341
|
+
"bool success",
|
|
342
|
+
"string message",
|
|
343
|
+
"string error_code",
|
|
344
|
+
]
|
|
345
|
+
for criterion in self.spec.success_criteria:
|
|
346
|
+
response_fields.append(f"bool {criterion.name}")
|
|
347
|
+
|
|
348
|
+
response_section = "\n".join(response_fields)
|
|
349
|
+
|
|
350
|
+
return f'''# {self.spec.name} Service Definition
|
|
351
|
+
# {self.spec.description}
|
|
352
|
+
# Generated by Skill Compiler v1.0.0
|
|
353
|
+
|
|
354
|
+
# Request
|
|
355
|
+
{request_section}
|
|
356
|
+
---
|
|
357
|
+
# Response
|
|
358
|
+
{response_section}
|
|
359
|
+
'''
|
|
360
|
+
|
|
361
|
+
def generate_skill_node(self) -> str:
|
|
362
|
+
"""Generate the main ROS2 node script."""
|
|
363
|
+
return f'''#!/usr/bin/env python3
|
|
364
|
+
"""
|
|
365
|
+
{self.spec.name} Skill ROS2 Node
|
|
366
|
+
|
|
367
|
+
Provides the skill as both an Action Server and Service Server.
|
|
368
|
+
Generated by Skill Compiler v1.0.0
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
import rclpy
|
|
372
|
+
from rclpy.node import Node
|
|
373
|
+
from rclpy.action import ActionServer, CancelResponse, GoalResponse
|
|
374
|
+
from rclpy.callback_groups import ReentrantCallbackGroup
|
|
375
|
+
|
|
376
|
+
from {self.package_name}.skill import {self.class_name}Skill, {self.class_name}Input, {self.class_name}Output
|
|
377
|
+
|
|
378
|
+
# Import generated interfaces (uncomment after building)
|
|
379
|
+
# from {self.package_name}.action import {self.class_name}
|
|
380
|
+
# from {self.package_name}.srv import Execute{self.class_name}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class {self.class_name}SkillNode(Node):
|
|
384
|
+
"""ROS2 node for {self.spec.name} skill."""
|
|
385
|
+
|
|
386
|
+
def __init__(self):
|
|
387
|
+
super().__init__('{self.package_name}')
|
|
388
|
+
|
|
389
|
+
# Declare parameters
|
|
390
|
+
self.declare_parameter('use_sim', False)
|
|
391
|
+
self.declare_parameter('hardware_config_file', '')
|
|
392
|
+
|
|
393
|
+
# Load configuration
|
|
394
|
+
self.use_sim = self.get_parameter('use_sim').value
|
|
395
|
+
config_file = self.get_parameter('hardware_config_file').value
|
|
396
|
+
|
|
397
|
+
# Initialize skill
|
|
398
|
+
hardware_config = self._load_hardware_config(config_file)
|
|
399
|
+
self.skill = {self.class_name}Skill(hardware_config)
|
|
400
|
+
|
|
401
|
+
# Create callback group for concurrent execution
|
|
402
|
+
self.callback_group = ReentrantCallbackGroup()
|
|
403
|
+
|
|
404
|
+
# TODO: Create action server (uncomment after building interfaces)
|
|
405
|
+
# self._action_server = ActionServer(
|
|
406
|
+
# self,
|
|
407
|
+
# {self.class_name},
|
|
408
|
+
# '{self.package_name}',
|
|
409
|
+
# execute_callback=self._execute_callback,
|
|
410
|
+
# goal_callback=self._goal_callback,
|
|
411
|
+
# cancel_callback=self._cancel_callback,
|
|
412
|
+
# callback_group=self.callback_group
|
|
413
|
+
# )
|
|
414
|
+
|
|
415
|
+
# TODO: Create service server (uncomment after building interfaces)
|
|
416
|
+
# self._service = self.create_service(
|
|
417
|
+
# Execute{self.class_name},
|
|
418
|
+
# '{self.package_name}/execute',
|
|
419
|
+
# self._service_callback,
|
|
420
|
+
# callback_group=self.callback_group
|
|
421
|
+
# )
|
|
422
|
+
|
|
423
|
+
self.get_logger().info('{self.class_name} skill node initialized')
|
|
424
|
+
|
|
425
|
+
def _load_hardware_config(self, config_file: str) -> dict:
|
|
426
|
+
"""Load hardware configuration from file or use defaults."""
|
|
427
|
+
if config_file and config_file != '':
|
|
428
|
+
import yaml
|
|
429
|
+
with open(config_file, 'r') as f:
|
|
430
|
+
return yaml.safe_load(f)
|
|
431
|
+
|
|
432
|
+
# Default configuration for simulation
|
|
433
|
+
return {{
|
|
434
|
+
'''
|
|
435
|
+
# Add default hardware config
|
|
436
|
+
hw_config_lines = []
|
|
437
|
+
for req in self.spec.hardware_requirements:
|
|
438
|
+
hw_config_lines.append(f''' "{req.component_type}": {{
|
|
439
|
+
"driver": "mock" if self.use_sim else "real",
|
|
440
|
+
"controller": "/{req.component_type}_controller",
|
|
441
|
+
}},''')
|
|
442
|
+
|
|
443
|
+
hw_config = "\n".join(hw_config_lines)
|
|
444
|
+
|
|
445
|
+
return f'''{hw_config}
|
|
446
|
+
}}
|
|
447
|
+
|
|
448
|
+
def _goal_callback(self, goal_request):
|
|
449
|
+
"""Accept or reject a goal request."""
|
|
450
|
+
self.get_logger().info('Received goal request')
|
|
451
|
+
return GoalResponse.ACCEPT
|
|
452
|
+
|
|
453
|
+
def _cancel_callback(self, goal_handle):
|
|
454
|
+
"""Accept or reject a cancel request."""
|
|
455
|
+
self.get_logger().info('Received cancel request')
|
|
456
|
+
return CancelResponse.ACCEPT
|
|
457
|
+
|
|
458
|
+
async def _execute_callback(self, goal_handle):
|
|
459
|
+
"""Execute the skill action."""
|
|
460
|
+
self.get_logger().info('Executing skill...')
|
|
461
|
+
|
|
462
|
+
# Convert ROS2 goal to skill input
|
|
463
|
+
goal = goal_handle.request
|
|
464
|
+
skill_input = self._goal_to_input(goal)
|
|
465
|
+
|
|
466
|
+
# Execute skill
|
|
467
|
+
result = self.skill.execute(skill_input)
|
|
468
|
+
|
|
469
|
+
# Convert skill output to ROS2 result
|
|
470
|
+
action_result = self._output_to_result(result)
|
|
471
|
+
|
|
472
|
+
if result.success:
|
|
473
|
+
goal_handle.succeed()
|
|
474
|
+
else:
|
|
475
|
+
goal_handle.abort()
|
|
476
|
+
|
|
477
|
+
return action_result
|
|
478
|
+
|
|
479
|
+
def _service_callback(self, request, response):
|
|
480
|
+
"""Handle synchronous service call."""
|
|
481
|
+
self.get_logger().info('Received service request')
|
|
482
|
+
|
|
483
|
+
# Convert ROS2 request to skill input
|
|
484
|
+
skill_input = self._request_to_input(request)
|
|
485
|
+
|
|
486
|
+
# Execute skill
|
|
487
|
+
result = self.skill.execute(skill_input)
|
|
488
|
+
|
|
489
|
+
# Convert skill output to ROS2 response
|
|
490
|
+
self._output_to_response(result, response)
|
|
491
|
+
|
|
492
|
+
return response
|
|
493
|
+
|
|
494
|
+
def _goal_to_input(self, goal) -> {self.class_name}Input:
|
|
495
|
+
"""Convert ROS2 action goal to skill input."""
|
|
496
|
+
return {self.class_name}Input(
|
|
497
|
+
# TODO: Map goal fields to input fields
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def _request_to_input(self, request) -> {self.class_name}Input:
|
|
501
|
+
"""Convert ROS2 service request to skill input."""
|
|
502
|
+
return {self.class_name}Input(
|
|
503
|
+
# TODO: Map request fields to input fields
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def _output_to_result(self, output: {self.class_name}Output):
|
|
507
|
+
"""Convert skill output to ROS2 action result."""
|
|
508
|
+
# TODO: Return proper action result type
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
def _output_to_response(self, output: {self.class_name}Output, response):
|
|
512
|
+
"""Convert skill output to ROS2 service response."""
|
|
513
|
+
response.success = output.success
|
|
514
|
+
response.message = output.message
|
|
515
|
+
response.error_code = output.error_code or ''
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def main(args=None):
|
|
519
|
+
rclpy.init(args=args)
|
|
520
|
+
node = {self.class_name}SkillNode()
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
rclpy.spin(node)
|
|
524
|
+
except KeyboardInterrupt:
|
|
525
|
+
pass
|
|
526
|
+
finally:
|
|
527
|
+
node.destroy_node()
|
|
528
|
+
rclpy.shutdown()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
if __name__ == '__main__':
|
|
532
|
+
main()
|
|
533
|
+
'''
|
|
534
|
+
|
|
535
|
+
def generate_params_yaml(self) -> str:
|
|
536
|
+
"""Generate config/skill_params.yaml."""
|
|
537
|
+
lines = [
|
|
538
|
+
f"# {self.spec.name} Skill Parameters",
|
|
539
|
+
f"# Generated by Skill Compiler v1.0.0",
|
|
540
|
+
"",
|
|
541
|
+
f"{self.package_name}:",
|
|
542
|
+
" ros__parameters:",
|
|
543
|
+
" use_sim: false",
|
|
544
|
+
"",
|
|
545
|
+
" # Hardware configuration",
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
for req in self.spec.hardware_requirements:
|
|
549
|
+
lines.extend([
|
|
550
|
+
f" {req.component_type}:",
|
|
551
|
+
f" driver: real",
|
|
552
|
+
f" controller: /{req.component_type}_controller",
|
|
553
|
+
])
|
|
554
|
+
|
|
555
|
+
lines.extend([
|
|
556
|
+
"",
|
|
557
|
+
" # Skill parameters (defaults)",
|
|
558
|
+
])
|
|
559
|
+
|
|
560
|
+
for param in self.spec.parameters:
|
|
561
|
+
if param.default is not None:
|
|
562
|
+
lines.append(f" {param.name}: {param.default}")
|
|
563
|
+
elif not param.required:
|
|
564
|
+
lines.append(f" # {param.name}: null # Optional")
|
|
565
|
+
|
|
566
|
+
if self.spec.max_velocity:
|
|
567
|
+
lines.append(f" max_velocity: {self.spec.max_velocity}")
|
|
568
|
+
if self.spec.max_force:
|
|
569
|
+
lines.append(f" max_force: {self.spec.max_force}")
|
|
570
|
+
|
|
571
|
+
return "\n".join(lines)
|
|
572
|
+
|
|
573
|
+
def generate(self, output_dir: Path) -> Dict[str, str]:
|
|
574
|
+
"""
|
|
575
|
+
Generate complete ROS2 package structure.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
output_dir: Directory to write package to
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
Dict mapping filenames to generated content
|
|
582
|
+
"""
|
|
583
|
+
output_dir = Path(output_dir)
|
|
584
|
+
package_dir = output_dir / self.package_name
|
|
585
|
+
|
|
586
|
+
# Create directory structure
|
|
587
|
+
(package_dir / "launch").mkdir(parents=True, exist_ok=True)
|
|
588
|
+
(package_dir / "config").mkdir(parents=True, exist_ok=True)
|
|
589
|
+
(package_dir / "action").mkdir(parents=True, exist_ok=True)
|
|
590
|
+
(package_dir / "srv").mkdir(parents=True, exist_ok=True)
|
|
591
|
+
(package_dir / "scripts").mkdir(parents=True, exist_ok=True)
|
|
592
|
+
(package_dir / "test").mkdir(parents=True, exist_ok=True)
|
|
593
|
+
(package_dir / "resource").mkdir(parents=True, exist_ok=True)
|
|
594
|
+
(package_dir / self.package_name).mkdir(parents=True, exist_ok=True)
|
|
595
|
+
|
|
596
|
+
files = {
|
|
597
|
+
"package.xml": self.generate_package_xml(),
|
|
598
|
+
"CMakeLists.txt": self.generate_cmakelists(),
|
|
599
|
+
"setup.py": self.generate_setup_py(),
|
|
600
|
+
"launch/skill.launch.py": self.generate_launch_file(),
|
|
601
|
+
f"action/{self.class_name}.action": self.generate_action_definition(),
|
|
602
|
+
f"srv/Execute{self.class_name}.srv": self.generate_service_definition(),
|
|
603
|
+
f"scripts/{self.package_name}_node.py": self.generate_skill_node(),
|
|
604
|
+
"config/skill_params.yaml": self.generate_params_yaml(),
|
|
605
|
+
f"resource/{self.package_name}": "", # Marker file
|
|
606
|
+
f"{self.package_name}/__init__.py": f'"""{self.spec.name} skill package."""',
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
# Write files
|
|
610
|
+
for rel_path, content in files.items():
|
|
611
|
+
file_path = package_dir / rel_path
|
|
612
|
+
file_path.write_text(content)
|
|
613
|
+
if rel_path.endswith(".py") and "scripts" in rel_path:
|
|
614
|
+
# Make scripts executable
|
|
615
|
+
file_path.chmod(0o755)
|
|
616
|
+
|
|
617
|
+
return files
|