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.
Files changed (46) hide show
  1. ate/__init__.py +1 -1
  2. ate/bridge_server.py +622 -0
  3. ate/cli.py +2625 -242
  4. ate/compatibility.py +580 -0
  5. ate/generators/__init__.py +19 -0
  6. ate/generators/docker_generator.py +461 -0
  7. ate/generators/hardware_config.py +469 -0
  8. ate/generators/ros2_generator.py +617 -0
  9. ate/generators/skill_generator.py +783 -0
  10. ate/marketplace.py +524 -0
  11. ate/mcp_server.py +1341 -107
  12. ate/primitives.py +1016 -0
  13. ate/robot_setup.py +2222 -0
  14. ate/skill_schema.py +537 -0
  15. ate/telemetry/__init__.py +33 -0
  16. ate/telemetry/cli.py +455 -0
  17. ate/telemetry/collector.py +444 -0
  18. ate/telemetry/context.py +318 -0
  19. ate/telemetry/fleet_agent.py +419 -0
  20. ate/telemetry/formats/__init__.py +18 -0
  21. ate/telemetry/formats/hdf5_serializer.py +503 -0
  22. ate/telemetry/formats/mcap_serializer.py +457 -0
  23. ate/telemetry/types.py +334 -0
  24. foodforthought_cli-0.2.3.dist-info/METADATA +300 -0
  25. foodforthought_cli-0.2.3.dist-info/RECORD +44 -0
  26. foodforthought_cli-0.2.3.dist-info/top_level.txt +6 -0
  27. mechdog_labeled/__init__.py +3 -0
  28. mechdog_labeled/primitives.py +113 -0
  29. mechdog_labeled/servo_map.py +209 -0
  30. mechdog_output/__init__.py +3 -0
  31. mechdog_output/primitives.py +59 -0
  32. mechdog_output/servo_map.py +203 -0
  33. test_autodetect/__init__.py +3 -0
  34. test_autodetect/primitives.py +113 -0
  35. test_autodetect/servo_map.py +209 -0
  36. test_full_auto/__init__.py +3 -0
  37. test_full_auto/primitives.py +113 -0
  38. test_full_auto/servo_map.py +209 -0
  39. test_smart_detect/__init__.py +3 -0
  40. test_smart_detect/primitives.py +113 -0
  41. test_smart_detect/servo_map.py +209 -0
  42. foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
  43. foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
  44. foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
  45. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
  46. {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