sw2robot 0.4.1__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.
- sw2robot/__init__.py +37 -0
- sw2robot/editor/__init__.py +21 -0
- sw2robot/editor/__main__.py +4 -0
- sw2robot/editor/_autolimits_cli.py +106 -0
- sw2robot/editor/_vendor/__init__.py +1 -0
- sw2robot/editor/_vendor/rc_config/__init__.py +26 -0
- sw2robot/editor/_vendor/rc_config/export.py +188 -0
- sw2robot/editor/_vendor/rc_config/gazebo_generator.py +121 -0
- sw2robot/editor/_vendor/rc_config/imitation_generator.py +123 -0
- sw2robot/editor/_vendor/rc_config/moveit_generator.py +124 -0
- sw2robot/editor/_vendor/rc_config/servo_mapping.py +100 -0
- sw2robot/editor/_vendor/rc_config/urdf_parser.py +160 -0
- sw2robot/editor/autoinit.py +82 -0
- sw2robot/editor/cli.py +53 -0
- sw2robot/editor/core.py +894 -0
- sw2robot/editor/package_uri.py +82 -0
- sw2robot/editor/state.py +107 -0
- sw2robot/editor/update.py +415 -0
- sw2robot/editor/web/axis-markers.js +309 -0
- sw2robot/editor/web/batch-look.js +311 -0
- sw2robot/editor/web/boot.js +168 -0
- sw2robot/editor/web/bootstrap.js +72 -0
- sw2robot/editor/web/box-select.js +142 -0
- sw2robot/editor/web/bulk-edit.js +234 -0
- sw2robot/editor/web/camera-reroot.js +234 -0
- sw2robot/editor/web/capture-progress.js +362 -0
- sw2robot/editor/web/coacd-preview.js +315 -0
- sw2robot/editor/web/diagnostics.js +154 -0
- sw2robot/editor/web/dom.js +29 -0
- sw2robot/editor/web/endcoords-gizmo.js +408 -0
- sw2robot/editor/web/export-box.js +149 -0
- sw2robot/editor/web/export-names.js +95 -0
- sw2robot/editor/web/face-pick.js +524 -0
- sw2robot/editor/web/file-browser.js +265 -0
- sw2robot/editor/web/frames.js +306 -0
- sw2robot/editor/web/i18n.js +1027 -0
- sw2robot/editor/web/index.html +610 -0
- sw2robot/editor/web/joint-rows.js +127 -0
- sw2robot/editor/web/joint-type.js +300 -0
- sw2robot/editor/web/keycast.js +92 -0
- sw2robot/editor/web/link-info.js +377 -0
- sw2robot/editor/web/link-look.js +248 -0
- sw2robot/editor/web/lists.js +164 -0
- sw2robot/editor/web/load.js +309 -0
- sw2robot/editor/web/main.js +76 -0
- sw2robot/editor/web/mass-editor.js +280 -0
- sw2robot/editor/web/mass-heatmap.js +213 -0
- sw2robot/editor/web/mimic.js +159 -0
- sw2robot/editor/web/play-mode.js +180 -0
- sw2robot/editor/web/root-frame.js +161 -0
- sw2robot/editor/web/selection.js +391 -0
- sw2robot/editor/web/session-log.js +277 -0
- sw2robot/editor/web/state.js +128 -0
- sw2robot/editor/web/style.css +433 -0
- sw2robot/editor/web/subassembly-choices.js +613 -0
- sw2robot/editor/web/subassembly-preview.js +381 -0
- sw2robot/editor/web/three-setup.js +27 -0
- sw2robot/editor/web/tree.js +586 -0
- sw2robot/editor/webserver.py +7750 -0
- sw2robot/exporter/__init__.py +1 -0
- sw2robot/exporter/build.py +43 -0
- sw2robot/exporter/export.py +871 -0
- sw2robot/exporter/extract.py +62 -0
- sw2robot/exporter/geometry.py +135 -0
- sw2robot/exporter/inertia.py +56 -0
- sw2robot/exporter/jointcfg.py +170 -0
- sw2robot/exporter/mesh.py +936 -0
- sw2robot/exporter/model.py +4639 -0
- sw2robot/exporter/ros_export.py +1571 -0
- sw2robot/exporter/state.py +210 -0
- sw2robot/exporter/sw2urdf_import/__init__.py +38 -0
- sw2robot/exporter/sw2urdf_import/_common.py +162 -0
- sw2robot/exporter/sw2urdf_import/bridge.py +598 -0
- sw2robot/exporter/sw2urdf_import/geometric.py +318 -0
- sw2robot/exporter/sw2urdf_import/naming.py +255 -0
- sw2robot/exporter/sw2urdf_import/payload.py +484 -0
- sw2robot/exporter/swcom.py +907 -0
- sw2robot/exporter/urdf_writer.py +845 -0
- sw2robot/exporter/validate.py +236 -0
- sw2robot-0.4.1.dist-info/METADATA +452 -0
- sw2robot-0.4.1.dist-info/RECORD +84 -0
- sw2robot-0.4.1.dist-info/WHEEL +4 -0
- sw2robot-0.4.1.dist-info/entry_points.txt +4 -0
- sw2robot-0.4.1.dist-info/licenses/LICENSE +202 -0
sw2robot/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""SolidWorks assembly -> URDF, plus a browser editor.
|
|
2
|
+
|
|
3
|
+
Sub-packages:
|
|
4
|
+
- ``sw2robot.exporter`` -- the exporter (SolidWorks .sldasm -> graph.json -> URDF)
|
|
5
|
+
- ``sw2robot.editor`` -- the browser editor on top of the graph
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _detect_version() -> str:
|
|
10
|
+
"""The running version, single-sourced from ``pyproject.toml``.
|
|
11
|
+
|
|
12
|
+
Order: installed package metadata (works in a wheel/pip install AND in the
|
|
13
|
+
frozen .exe, which bundles the dist-info via build_exe.py's
|
|
14
|
+
``--copy-metadata sw2robot``) -> the repo's pyproject.toml (a bare source
|
|
15
|
+
checkout that was never installed, e.g. the test run with pythonpath=".")
|
|
16
|
+
-> a sentinel. The self-update check (``sw2robot.editor.update``) compares
|
|
17
|
+
this against the latest GitHub Release tag, so it must reflect the build."""
|
|
18
|
+
try:
|
|
19
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
20
|
+
try:
|
|
21
|
+
return version("sw2robot")
|
|
22
|
+
except PackageNotFoundError:
|
|
23
|
+
pass
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
try:
|
|
27
|
+
import os
|
|
28
|
+
import tomllib
|
|
29
|
+
pp = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
|
30
|
+
"pyproject.toml")
|
|
31
|
+
with open(pp, "rb") as f:
|
|
32
|
+
return tomllib.load(f)["project"]["version"]
|
|
33
|
+
except Exception:
|
|
34
|
+
return "0.0.0+unknown"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__version__ = _detect_version()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""sw2robot.editor -- the headless core bridging sw2robot.exporter
|
|
2
|
+
(CAD->module) and the ROS config export.
|
|
3
|
+
|
|
4
|
+
Design (a standing directive):
|
|
5
|
+
- **State**: ``RobotCompilerState`` (Pydantic) is the single source of truth for
|
|
6
|
+
a CAD-derived module being configured. It serializes to JSON, so it moves to
|
|
7
|
+
a REST/WebSocket payload unchanged.
|
|
8
|
+
- **Core API** (``core``): pure functions that mutate State / produce artifacts.
|
|
9
|
+
A web-editor handler or a FastAPI endpoint is a *thin* wrapper over these.
|
|
10
|
+
- **Headless**: the whole pipeline (CAD build -> edit -> ROS export) runs from
|
|
11
|
+
the CLI (``python -m sw2robot.editor``) with no GUI and no SolidWorks.
|
|
12
|
+
|
|
13
|
+
It uses the *pure* halves: ``sw2robot.exporter.export.build`` (no SolidWorks)
|
|
14
|
+
and the vendored ``sw2robot.editor._vendor.rc_config`` (ROS/MoveIt/Gazebo
|
|
15
|
+
config generators).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from . import core
|
|
19
|
+
from .state import JointEdit, RobotCompilerState
|
|
20
|
+
|
|
21
|
+
__all__ = ["JointEdit", "RobotCompilerState", "core"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Self-collision joint-limit sweep as a standalone subprocess (JSON on stdout).
|
|
2
|
+
|
|
3
|
+
The webserver shells out to this instead of sweeping in-process: the sweep is
|
|
4
|
+
CPU-bound and releases the GIL (numpy / python-fcl), so running it inside the
|
|
5
|
+
threaded HTTP server makes it thrash the GIL against the browser's idle
|
|
6
|
+
keep-alive connection threads -- the classic CPython convoy, which inflated an
|
|
7
|
+
8 s sweep to ~90 s just by having a page open. A fresh process has its own GIL
|
|
8
|
+
and no server threads, so it stays at the true ~8 s (+~3 s to load the model).
|
|
9
|
+
|
|
10
|
+
python -m sw2robot.editor._autolimits_cli <urdf> <step_deg> <max_deg> \
|
|
11
|
+
[margin_deg] [margin_mm]
|
|
12
|
+
-> stdout: {"results": [{"child","lower","upper","continuous"}, ...]}
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _fast_urdf(urdf):
|
|
25
|
+
"""Return a URDF path whose mesh refs prefer the ``.3dxml.glb`` caches.
|
|
26
|
+
|
|
27
|
+
skrobot loads the SolidWorks ``.3dxml`` (mm, slow XML parse -- the heavy
|
|
28
|
+
ones are ~135 ms each and dominate load) and scales mm->m. The web server
|
|
29
|
+
already converts each to a ``.3dxml.glb`` (metres) on package open; loading
|
|
30
|
+
those is ~5x faster and skrobot reads them at native scale, so the geometry
|
|
31
|
+
is identical. We rewrite the URDF to point at the .glb wherever the cache
|
|
32
|
+
exists (falling back to .3dxml otherwise) and write it beside the original
|
|
33
|
+
so the ``../meshes`` relative paths still resolve. Returns (path, is_temp).
|
|
34
|
+
"""
|
|
35
|
+
txt = open(urdf, encoding="utf-8").read()
|
|
36
|
+
base = os.path.dirname(urdf)
|
|
37
|
+
|
|
38
|
+
def repl(m):
|
|
39
|
+
fn = m.group(1)
|
|
40
|
+
full = os.path.normpath(os.path.join(base, fn))
|
|
41
|
+
return f'filename="{fn}.glb"' if os.path.exists(full + ".glb") \
|
|
42
|
+
else m.group(0)
|
|
43
|
+
|
|
44
|
+
txt2 = re.sub(r'filename="([^"]+\.3dxml)"', repl, txt)
|
|
45
|
+
if txt2 == txt:
|
|
46
|
+
return urdf, False
|
|
47
|
+
fd, tmp = tempfile.mkstemp(suffix=".urdf", dir=base)
|
|
48
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
49
|
+
f.write(txt2)
|
|
50
|
+
return tmp, True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _emit(**ev):
|
|
54
|
+
"""One progress event as a JSON line on STDERR (stdout is reserved for the
|
|
55
|
+
final results JSON the caller parses). The webserver reads these live to
|
|
56
|
+
drive the UI's per-joint progress bar."""
|
|
57
|
+
sys.stderr.write(json.dumps(ev) + "\n")
|
|
58
|
+
sys.stderr.flush()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
urdf, step_deg, max_deg = sys.argv[1], float(sys.argv[2]), float(sys.argv[3])
|
|
63
|
+
# optional backoff margins: degrees (revolute), mm (prismatic)
|
|
64
|
+
margin_deg = float(sys.argv[4]) if len(sys.argv) > 4 else 2.0
|
|
65
|
+
margin_mm = float(sys.argv[5]) if len(sys.argv) > 5 else 2.0
|
|
66
|
+
from skrobot.models.urdf import RobotModelFromURDF
|
|
67
|
+
|
|
68
|
+
from sw2robot.editor import autoinit
|
|
69
|
+
|
|
70
|
+
_emit(event="loading") # model load is the slow first ~2 s
|
|
71
|
+
# skrobot (>=0.3.16) resolves package:// meshes itself, so load the URDF
|
|
72
|
+
# (glb-accelerated) directly -- no pre-resolved temp copy needed.
|
|
73
|
+
load_urdf, is_tmp = _fast_urdf(urdf)
|
|
74
|
+
try:
|
|
75
|
+
robot = RobotModelFromURDF(urdf_file=load_urdf)
|
|
76
|
+
finally:
|
|
77
|
+
if is_tmp:
|
|
78
|
+
try:
|
|
79
|
+
os.remove(load_urdf)
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
meshes = autoinit.link_meshes(robot)
|
|
83
|
+
|
|
84
|
+
total = sum(1 for j in robot.joint_list
|
|
85
|
+
if type(j).__name__ in ("RotationalJoint", "LinearJoint"))
|
|
86
|
+
_emit(event="start", total=total)
|
|
87
|
+
|
|
88
|
+
def on_start(name, i, n):
|
|
89
|
+
# emit BEFORE each joint's sweep so the bar advances and names the joint
|
|
90
|
+
# currently being worked (a single joint can take seconds on a big model)
|
|
91
|
+
_emit(event="joint", i=i, total=n, joint=name)
|
|
92
|
+
|
|
93
|
+
results = autoinit.sweep_limits(
|
|
94
|
+
robot, meshes, step_deg=step_deg, max_deg=max_deg,
|
|
95
|
+
margin_deg=margin_deg, margin_mm=margin_mm,
|
|
96
|
+
refine=True, on_start=on_start)
|
|
97
|
+
out = [{"child": v["child"], "lower": v["lower"], "upper": v["upper"],
|
|
98
|
+
"continuous": v["continuous"]}
|
|
99
|
+
for v in results.values() if v.get("child")]
|
|
100
|
+
# ONLY the JSON goes to stdout (skrobot's URDF warnings go to stderr)
|
|
101
|
+
sys.stdout.write(json.dumps({"results": out}))
|
|
102
|
+
sys.stdout.flush()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vendored third-party code (kept verbatim)."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration file generators for robot URDFs.
|
|
3
|
+
|
|
4
|
+
This module provides generators for:
|
|
5
|
+
- MoveIt2 configuration (SRDF, controllers, etc.)
|
|
6
|
+
- Gazebo configuration (world, plugins, etc.)
|
|
7
|
+
- Imitation Learning configuration (observation/action spaces)
|
|
8
|
+
- Servo mapping configuration (joint to servo ID mapping)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .export import export_all_configs
|
|
12
|
+
from .gazebo_generator import generate_gazebo_config
|
|
13
|
+
from .imitation_generator import generate_il_config
|
|
14
|
+
from .moveit_generator import generate_controllers_yaml, generate_srdf
|
|
15
|
+
from .servo_mapping import generate_servo_mapping_yaml
|
|
16
|
+
from .urdf_parser import parse_urdf_content
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"export_all_configs",
|
|
20
|
+
"generate_controllers_yaml",
|
|
21
|
+
"generate_gazebo_config",
|
|
22
|
+
"generate_il_config",
|
|
23
|
+
"generate_servo_mapping_yaml",
|
|
24
|
+
"generate_srdf",
|
|
25
|
+
"parse_urdf_content",
|
|
26
|
+
]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration export utilities.
|
|
3
|
+
|
|
4
|
+
Creates ZIP archives containing all configuration files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import zipfile
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .gazebo_generator import generate_gazebo_config, generate_ros2_control_xacro
|
|
12
|
+
from .imitation_generator import generate_il_config
|
|
13
|
+
from .moveit_generator import generate_controllers_yaml, generate_srdf
|
|
14
|
+
from .servo_mapping import generate_servo_mapping_yaml
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def export_all_configs(
|
|
18
|
+
urdf_content: str,
|
|
19
|
+
joints: list[dict[str, Any]],
|
|
20
|
+
servo_mappings: list[dict[str, Any]],
|
|
21
|
+
planning_groups: list[dict[str, Any]],
|
|
22
|
+
controllers: list[dict[str, Any]],
|
|
23
|
+
disabled_collision_pairs: list[tuple[str, str]],
|
|
24
|
+
gazebo_physics: dict[str, Any],
|
|
25
|
+
gazebo_plugins: list[dict[str, Any]],
|
|
26
|
+
il_observation: dict[str, Any],
|
|
27
|
+
il_action: dict[str, Any],
|
|
28
|
+
robot_name: str = "robot",
|
|
29
|
+
export_options: dict[str, bool] | None = None,
|
|
30
|
+
) -> bytes:
|
|
31
|
+
"""
|
|
32
|
+
Export all configuration files as a ZIP archive.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
urdf_content : str
|
|
37
|
+
Original URDF content.
|
|
38
|
+
joints : list
|
|
39
|
+
Parsed joint information.
|
|
40
|
+
servo_mappings : list
|
|
41
|
+
Servo mapping configurations.
|
|
42
|
+
planning_groups : list
|
|
43
|
+
MoveIt planning group configurations.
|
|
44
|
+
controllers : list
|
|
45
|
+
Controller configurations.
|
|
46
|
+
disabled_collision_pairs : list
|
|
47
|
+
List of disabled collision link pairs.
|
|
48
|
+
gazebo_physics : dict
|
|
49
|
+
Gazebo physics settings.
|
|
50
|
+
gazebo_plugins : list
|
|
51
|
+
Gazebo plugin configurations.
|
|
52
|
+
il_observation : dict
|
|
53
|
+
IL observation space configuration.
|
|
54
|
+
il_action : dict
|
|
55
|
+
IL action space configuration.
|
|
56
|
+
robot_name : str
|
|
57
|
+
Name of the robot for file naming.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
bytes
|
|
62
|
+
ZIP file contents as bytes.
|
|
63
|
+
"""
|
|
64
|
+
# Default export options if not provided
|
|
65
|
+
if export_options is None:
|
|
66
|
+
export_options = {
|
|
67
|
+
"includeUrdf": True,
|
|
68
|
+
"includeServoMapping": True,
|
|
69
|
+
"includeMoveIt": True,
|
|
70
|
+
"includeGazebo": True,
|
|
71
|
+
"includeImitationLearning": True,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Create in-memory ZIP file
|
|
75
|
+
zip_buffer = io.BytesIO()
|
|
76
|
+
|
|
77
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
78
|
+
# URDF (original)
|
|
79
|
+
if export_options.get("includeUrdf", True):
|
|
80
|
+
zf.writestr(f"{robot_name}/urdf/{robot_name}.urdf", urdf_content)
|
|
81
|
+
|
|
82
|
+
# Servo mapping
|
|
83
|
+
if export_options.get("includeServoMapping", True):
|
|
84
|
+
servo_yaml = generate_servo_mapping_yaml(servo_mappings)
|
|
85
|
+
zf.writestr(f"{robot_name}/config/servo_mapping.yaml", servo_yaml)
|
|
86
|
+
|
|
87
|
+
# MoveIt SRDF and controllers
|
|
88
|
+
if export_options.get("includeMoveIt", True):
|
|
89
|
+
srdf_content = generate_srdf(
|
|
90
|
+
robot_name=robot_name,
|
|
91
|
+
planning_groups=planning_groups,
|
|
92
|
+
disabled_collision_pairs=[tuple(p) for p in disabled_collision_pairs],
|
|
93
|
+
)
|
|
94
|
+
zf.writestr(f"{robot_name}/config/{robot_name}.srdf", srdf_content)
|
|
95
|
+
|
|
96
|
+
controllers_yaml = generate_controllers_yaml(controllers)
|
|
97
|
+
zf.writestr(f"{robot_name}/config/controllers.yaml", controllers_yaml)
|
|
98
|
+
|
|
99
|
+
# Gazebo config
|
|
100
|
+
if export_options.get("includeGazebo", True):
|
|
101
|
+
gazebo_config = generate_gazebo_config(gazebo_physics, gazebo_plugins)
|
|
102
|
+
zf.writestr(f"{robot_name}/config/gazebo.xml", gazebo_config)
|
|
103
|
+
|
|
104
|
+
ros2_control_xacro = generate_ros2_control_xacro(joints)
|
|
105
|
+
zf.writestr(f"{robot_name}/urdf/ros2_control.xacro", ros2_control_xacro)
|
|
106
|
+
|
|
107
|
+
# IL config
|
|
108
|
+
if export_options.get("includeImitationLearning", True):
|
|
109
|
+
il_config = generate_il_config(il_observation, il_action, joints)
|
|
110
|
+
zf.writestr(f"{robot_name}/config/il_config.yaml", il_config)
|
|
111
|
+
|
|
112
|
+
# README (always include)
|
|
113
|
+
readme = _generate_readme(robot_name, export_options)
|
|
114
|
+
zf.writestr(f"{robot_name}/README.md", readme)
|
|
115
|
+
|
|
116
|
+
return zip_buffer.getvalue()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _generate_readme(robot_name: str, export_options: dict[str, bool]) -> str:
|
|
120
|
+
"""Generate a README file for the configuration package."""
|
|
121
|
+
contents = []
|
|
122
|
+
usage_sections = []
|
|
123
|
+
|
|
124
|
+
if export_options.get("includeUrdf", True):
|
|
125
|
+
contents.append(f"- `urdf/{robot_name}.urdf` - Robot URDF description")
|
|
126
|
+
|
|
127
|
+
if export_options.get("includeGazebo", True):
|
|
128
|
+
contents.append("- `urdf/ros2_control.xacro` - ros2_control hardware interface configuration")
|
|
129
|
+
contents.append("- `config/gazebo.xml` - Gazebo physics and plugin configuration")
|
|
130
|
+
usage_sections.append(f"""### Gazebo
|
|
131
|
+
|
|
132
|
+
Include the ros2_control xacro in your robot description:
|
|
133
|
+
|
|
134
|
+
```xml
|
|
135
|
+
<xacro:include filename="$(find {robot_name})/urdf/ros2_control.xacro" />
|
|
136
|
+
```""")
|
|
137
|
+
|
|
138
|
+
if export_options.get("includeServoMapping", True):
|
|
139
|
+
contents.append("- `config/servo_mapping.yaml` - Joint to servo ID mapping")
|
|
140
|
+
|
|
141
|
+
if export_options.get("includeMoveIt", True):
|
|
142
|
+
contents.append(f"- `config/{robot_name}.srdf` - MoveIt2 SRDF (semantic robot description)")
|
|
143
|
+
contents.append("- `config/controllers.yaml` - ros2_control controller configuration")
|
|
144
|
+
usage_sections.append(f"""### MoveIt2
|
|
145
|
+
|
|
146
|
+
Include the SRDF in your MoveIt2 launch files:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
srdf_file = os.path.join(pkg_share, 'config', '{robot_name}.srdf')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Controllers
|
|
153
|
+
|
|
154
|
+
Load the controller configuration:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
ros2_control_node:
|
|
158
|
+
ros__parameters:
|
|
159
|
+
robot_description: $(command 'cat $(find {robot_name})/urdf/{robot_name}.urdf')
|
|
160
|
+
```""")
|
|
161
|
+
|
|
162
|
+
if export_options.get("includeImitationLearning", True):
|
|
163
|
+
contents.append("- `config/il_config.yaml` - Imitation Learning configuration")
|
|
164
|
+
usage_sections.append("""### Imitation Learning
|
|
165
|
+
|
|
166
|
+
Load the IL configuration in your training script:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import yaml
|
|
170
|
+
with open('config/il_config.yaml') as f:
|
|
171
|
+
config = yaml.safe_load(f)
|
|
172
|
+
```""")
|
|
173
|
+
|
|
174
|
+
contents_str = "\n".join(contents)
|
|
175
|
+
usage_str = "\n\n".join(usage_sections)
|
|
176
|
+
|
|
177
|
+
return f"""# {robot_name} Configuration Package
|
|
178
|
+
|
|
179
|
+
Generated by Robot Compiler.
|
|
180
|
+
|
|
181
|
+
## Contents
|
|
182
|
+
|
|
183
|
+
{contents_str}
|
|
184
|
+
|
|
185
|
+
## Usage
|
|
186
|
+
|
|
187
|
+
{usage_str}
|
|
188
|
+
"""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gazebo configuration file generators.
|
|
3
|
+
|
|
4
|
+
Generates world and plugin configuration files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_gazebo_config(
|
|
11
|
+
physics: dict[str, Any],
|
|
12
|
+
plugins: list[dict[str, Any]],
|
|
13
|
+
) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Generate Gazebo world configuration snippet.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
physics : dict
|
|
20
|
+
Physics configuration with:
|
|
21
|
+
- solver: Physics engine (ode, bullet, simbody, dart)
|
|
22
|
+
- stepSize: Simulation step size in seconds
|
|
23
|
+
- realTimeFactor: Real-time factor
|
|
24
|
+
plugins : list
|
|
25
|
+
List of plugin configurations with:
|
|
26
|
+
- name: Plugin name
|
|
27
|
+
- filename: Plugin library filename
|
|
28
|
+
- enabled: Whether plugin is enabled
|
|
29
|
+
- parameters: Dict of plugin parameters
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
str
|
|
34
|
+
Gazebo SDF/world configuration snippet.
|
|
35
|
+
"""
|
|
36
|
+
solver = physics.get("solver", "ode")
|
|
37
|
+
step_size = physics.get("stepSize", 0.001)
|
|
38
|
+
rtf = physics.get("realTimeFactor", 1.0)
|
|
39
|
+
|
|
40
|
+
lines = [
|
|
41
|
+
"<!-- Gazebo Configuration Generated by Robot Compiler -->",
|
|
42
|
+
"",
|
|
43
|
+
f'<physics name="default_physics" type="{solver}">',
|
|
44
|
+
f" <max_step_size>{step_size}</max_step_size>",
|
|
45
|
+
f" <real_time_factor>{rtf}</real_time_factor>",
|
|
46
|
+
f" <real_time_update_rate>{int(1 / step_size)}</real_time_update_rate>",
|
|
47
|
+
"</physics>",
|
|
48
|
+
"",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Plugins
|
|
52
|
+
for plugin in plugins:
|
|
53
|
+
if not plugin.get("enabled", True):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
name = plugin["name"]
|
|
57
|
+
filename = plugin["filename"]
|
|
58
|
+
params = plugin.get("parameters", {})
|
|
59
|
+
|
|
60
|
+
lines.append(f'<plugin filename="{filename}" name="{name}">')
|
|
61
|
+
for key, value in params.items():
|
|
62
|
+
lines.append(f" <{key}>{value}</{key}>")
|
|
63
|
+
lines.append("</plugin>")
|
|
64
|
+
lines.append("")
|
|
65
|
+
|
|
66
|
+
return "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def generate_ros2_control_xacro(
|
|
70
|
+
joints: list[dict[str, Any]],
|
|
71
|
+
hardware_interface: str = "gazebo_ros2_control/GazeboSystem",
|
|
72
|
+
) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Generate ros2_control URDF/Xacro snippet for Gazebo.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
joints : list
|
|
79
|
+
List of joint configurations with:
|
|
80
|
+
- name: Joint name
|
|
81
|
+
- type: Joint type (revolute, prismatic, etc.)
|
|
82
|
+
hardware_interface : str
|
|
83
|
+
Hardware interface plugin name.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
str
|
|
88
|
+
ros2_control Xacro snippet.
|
|
89
|
+
"""
|
|
90
|
+
lines = [
|
|
91
|
+
"<!-- ros2_control Configuration for Gazebo -->",
|
|
92
|
+
"<!-- Generated by Robot Compiler -->",
|
|
93
|
+
"",
|
|
94
|
+
'<ros2_control name="GazeboSystem" type="system">',
|
|
95
|
+
" <hardware>",
|
|
96
|
+
f" <plugin>{hardware_interface}</plugin>",
|
|
97
|
+
" </hardware>",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
for joint in joints:
|
|
101
|
+
if joint.get("type") == "fixed":
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
name = joint["name"]
|
|
105
|
+
lines.append(f' <joint name="{name}">')
|
|
106
|
+
lines.append(' <command_interface name="position"/>')
|
|
107
|
+
lines.append(' <command_interface name="velocity"/>')
|
|
108
|
+
lines.append(' <state_interface name="position"/>')
|
|
109
|
+
lines.append(' <state_interface name="velocity"/>')
|
|
110
|
+
lines.append(' <state_interface name="effort"/>')
|
|
111
|
+
lines.append(" </joint>")
|
|
112
|
+
|
|
113
|
+
lines.append("</ros2_control>")
|
|
114
|
+
lines.append("")
|
|
115
|
+
lines.append("<gazebo>")
|
|
116
|
+
lines.append(' <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">')
|
|
117
|
+
lines.append(" <parameters>$(find robot_config)/config/controllers.yaml</parameters>")
|
|
118
|
+
lines.append(" </plugin>")
|
|
119
|
+
lines.append("</gazebo>")
|
|
120
|
+
|
|
121
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Imitation Learning configuration generators.
|
|
3
|
+
|
|
4
|
+
Generates configuration files for RL/IL training.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_il_config(
|
|
11
|
+
observation: dict[str, Any],
|
|
12
|
+
action: dict[str, Any],
|
|
13
|
+
joints: list[dict[str, Any]],
|
|
14
|
+
) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Generate Imitation Learning configuration YAML.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
observation : dict
|
|
21
|
+
Observation space configuration with:
|
|
22
|
+
- includeJointPositions: bool
|
|
23
|
+
- includeJointVelocities: bool
|
|
24
|
+
- includeEndEffectorPose: bool
|
|
25
|
+
- customObservations: list of custom observation names
|
|
26
|
+
action : dict
|
|
27
|
+
Action space configuration with:
|
|
28
|
+
- actionType: 'joint_positions', 'joint_velocities', or 'end_effector_delta'
|
|
29
|
+
- selectedJoints: list of joint names
|
|
30
|
+
- actionScale: float multiplier for actions
|
|
31
|
+
joints : list
|
|
32
|
+
List of joint configurations for dimension calculation.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
str
|
|
37
|
+
YAML configuration content.
|
|
38
|
+
"""
|
|
39
|
+
# Calculate dimensions
|
|
40
|
+
actuated_joints = [j for j in joints if j.get("type") != "fixed"]
|
|
41
|
+
n_joints = len(actuated_joints)
|
|
42
|
+
|
|
43
|
+
obs_dim = 0
|
|
44
|
+
if observation.get("includeJointPositions", True):
|
|
45
|
+
obs_dim += n_joints
|
|
46
|
+
if observation.get("includeJointVelocities", True):
|
|
47
|
+
obs_dim += n_joints
|
|
48
|
+
if observation.get("includeEndEffectorPose", False):
|
|
49
|
+
obs_dim += 7 # position (3) + quaternion (4)
|
|
50
|
+
|
|
51
|
+
custom_obs = observation.get("customObservations", [])
|
|
52
|
+
obs_dim += len(custom_obs)
|
|
53
|
+
|
|
54
|
+
action_joints = action.get("selectedJoints", [])
|
|
55
|
+
action_dim = len(action_joints)
|
|
56
|
+
|
|
57
|
+
lines = [
|
|
58
|
+
"# Imitation Learning Configuration",
|
|
59
|
+
"# Generated by Robot Compiler",
|
|
60
|
+
"",
|
|
61
|
+
"robot:",
|
|
62
|
+
" observation_space:",
|
|
63
|
+
f" dim: {obs_dim}",
|
|
64
|
+
f" include_joint_positions: {str(observation.get('includeJointPositions', True)).lower()}",
|
|
65
|
+
f" include_joint_velocities: {str(observation.get('includeJointVelocities', True)).lower()}",
|
|
66
|
+
f" include_end_effector_pose: {str(observation.get('includeEndEffectorPose', False)).lower()}",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
if custom_obs:
|
|
70
|
+
lines.append(" custom_observations:")
|
|
71
|
+
for obs in custom_obs:
|
|
72
|
+
lines.append(f" - {obs}")
|
|
73
|
+
|
|
74
|
+
lines.extend(
|
|
75
|
+
[
|
|
76
|
+
"",
|
|
77
|
+
" action_space:",
|
|
78
|
+
f" dim: {action_dim}",
|
|
79
|
+
f" type: {action.get('actionType', 'joint_positions')}",
|
|
80
|
+
f" scale: {action.get('actionScale', 1.0)}",
|
|
81
|
+
" joints:",
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
for joint in action_joints:
|
|
86
|
+
lines.append(f" - {joint}")
|
|
87
|
+
|
|
88
|
+
# Add joint limits for reference
|
|
89
|
+
lines.extend(
|
|
90
|
+
[
|
|
91
|
+
"",
|
|
92
|
+
" joint_limits:",
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
for joint in actuated_joints:
|
|
97
|
+
if joint["name"] in action_joints:
|
|
98
|
+
lines.append(f" {joint['name']}:")
|
|
99
|
+
lines.append(f" lower: {joint.get('lowerLimit', 0.0)}")
|
|
100
|
+
lines.append(f" upper: {joint.get('upperLimit', 0.0)}")
|
|
101
|
+
lines.append(f" velocity: {joint.get('velocityLimit', 0.0)}")
|
|
102
|
+
|
|
103
|
+
# Add training hints
|
|
104
|
+
lines.extend(
|
|
105
|
+
[
|
|
106
|
+
"",
|
|
107
|
+
"# Training Configuration",
|
|
108
|
+
"training:",
|
|
109
|
+
" horizon: 1000",
|
|
110
|
+
" batch_size: 256",
|
|
111
|
+
" learning_rate: 3.0e-4",
|
|
112
|
+
" gamma: 0.99",
|
|
113
|
+
" clip_range: 0.2",
|
|
114
|
+
"",
|
|
115
|
+
"# Normalization",
|
|
116
|
+
"normalization:",
|
|
117
|
+
" normalize_observations: true",
|
|
118
|
+
" normalize_rewards: true",
|
|
119
|
+
" clip_observations: 10.0",
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return "\n".join(lines)
|