devagent-physical-engine 0.10.0__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.
- devagent_physical_engine/__init__.py +44 -0
- devagent_physical_engine/agent/__init__.py +40 -0
- devagent_physical_engine/agent/compiler.py +285 -0
- devagent_physical_engine/agent/contracts.py +129 -0
- devagent_physical_engine/agent/coordinator.py +108 -0
- devagent_physical_engine/agent/critic.py +72 -0
- devagent_physical_engine/agent/evidence.py +34 -0
- devagent_physical_engine/agent/interpreter.py +179 -0
- devagent_physical_engine/agent/planner.py +105 -0
- devagent_physical_engine/agent/recovery.py +54 -0
- devagent_physical_engine/agent/routing.py +76 -0
- devagent_physical_engine/agent/runtime.py +270 -0
- devagent_physical_engine/agent/semantic.py +304 -0
- devagent_physical_engine/agent/structured.py +423 -0
- devagent_physical_engine/ai_cli.py +226 -0
- devagent_physical_engine/cli.py +392 -0
- devagent_physical_engine/doctor.py +20 -0
- devagent_physical_engine/engineering_agent.py +243 -0
- devagent_physical_engine/engineering_request.py +630 -0
- devagent_physical_engine/execution.py +90 -0
- devagent_physical_engine/models.py +143 -0
- devagent_physical_engine/operating_envelope.py +120 -0
- devagent_physical_engine/optimization/__init__.py +50 -0
- devagent_physical_engine/optimization/benchmark.py +122 -0
- devagent_physical_engine/optimization/candidates.py +198 -0
- devagent_physical_engine/optimization/contracts.py +235 -0
- devagent_physical_engine/optimization/evaluator.py +107 -0
- devagent_physical_engine/optimization/evidence.py +53 -0
- devagent_physical_engine/optimization/experience.py +105 -0
- devagent_physical_engine/optimization/measured.py +125 -0
- devagent_physical_engine/optimization/optimizer.py +215 -0
- devagent_physical_engine/optimization/orchestrator.py +155 -0
- devagent_physical_engine/physical_campaign.py +413 -0
- devagent_physical_engine/physical_evidence.py +214 -0
- devagent_physical_engine/physical_motion.py +196 -0
- devagent_physical_engine/planning.py +80 -0
- devagent_physical_engine/preexecution_contract.py +65 -0
- devagent_physical_engine/provider_adapters/__init__.py +22 -0
- devagent_physical_engine/provider_adapters/anthropic.py +112 -0
- devagent_physical_engine/provider_adapters/common.py +187 -0
- devagent_physical_engine/provider_adapters/factory.py +20 -0
- devagent_physical_engine/provider_adapters/gemini.py +126 -0
- devagent_physical_engine/provider_adapters/openai.py +95 -0
- devagent_physical_engine/provider_qualification.py +268 -0
- devagent_physical_engine/providers.py +94 -0
- devagent_physical_engine/qualification.py +44 -0
- devagent_physical_engine/qualification_cli.py +195 -0
- devagent_physical_engine/qualification_harness.py +917 -0
- devagent_physical_engine/robot_platform.py +411 -0
- devagent_physical_engine/robots.py +76 -0
- devagent_physical_engine/ros2/__init__.py +35 -0
- devagent_physical_engine/ros2/acceptance.py +324 -0
- devagent_physical_engine/ros2/commands.py +175 -0
- devagent_physical_engine/ros2/doctor.py +116 -0
- devagent_physical_engine/ros2/fk_probe.py +83 -0
- devagent_physical_engine/ros2/frame_alignment.py +61 -0
- devagent_physical_engine/ros2/gazebo_world.py +125 -0
- devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
- devagent_physical_engine/ros2/measured_motion.py +233 -0
- devagent_physical_engine/ros2/moveit_scene.py +121 -0
- devagent_physical_engine/ros2/preexecution.py +113 -0
- devagent_physical_engine/ros2/qualification.py +81 -0
- devagent_physical_engine/ros2/qualification_v10.py +252 -0
- devagent_physical_engine/ros2/scene_probe.py +219 -0
- devagent_physical_engine/ros2/state_validity_probe.py +125 -0
- devagent_physical_engine/ros2/tf_probe.py +51 -0
- devagent_physical_engine/ros2/trajectory.py +188 -0
- devagent_physical_engine/ros2/ur5e.py +59 -0
- devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
- devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
- devagent_physical_engine/setup_profile.py +356 -0
- devagent_physical_engine/simulation.py +32 -0
- devagent_physical_engine/simulation_platform.py +269 -0
- devagent_physical_engine/trajectory_qualification.py +201 -0
- devagent_physical_engine/twin.py +939 -0
- devagent_physical_engine/twin_builder.py +309 -0
- devagent_physical_engine/twin_materialization.py +404 -0
- devagent_physical_engine/verification.py +46 -0
- devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
- devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
- devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
- devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
- devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
- devagent_physical_engine-0.10.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
import json
|
|
6
|
+
from math import ceil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _hash(payload: object) -> str:
|
|
12
|
+
text = json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
13
|
+
return sha256(text.encode("utf-8")).hexdigest()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _sample_motion(motion: dict[str, Any], *, max_joint_step_rad: float, max_samples: int) -> list[tuple[float, ...]]:
|
|
17
|
+
points = motion["points"]
|
|
18
|
+
samples: list[tuple[float, ...]] = []
|
|
19
|
+
for index in range(len(points) - 1):
|
|
20
|
+
start = tuple(float(value) for value in points[index]["positions_rad"])
|
|
21
|
+
end = tuple(float(value) for value in points[index + 1]["positions_rad"])
|
|
22
|
+
if len(start) != len(end):
|
|
23
|
+
raise ValueError("trajectory_joint_shape_mismatch")
|
|
24
|
+
maximum = max(abs(first - second) for first, second in zip(start, end))
|
|
25
|
+
steps = max(1, int(ceil(maximum / max_joint_step_rad)))
|
|
26
|
+
for step in range(steps):
|
|
27
|
+
ratio = step / steps
|
|
28
|
+
sample = tuple(first + (second - first) * ratio for first, second in zip(start, end))
|
|
29
|
+
if not samples or sample != samples[-1]:
|
|
30
|
+
samples.append(sample)
|
|
31
|
+
if len(samples) > max_samples:
|
|
32
|
+
raise ValueError("preexecution_sample_limit_exceeded")
|
|
33
|
+
final = tuple(float(value) for value in points[-1]["positions_rad"])
|
|
34
|
+
if not samples or final != samples[-1]:
|
|
35
|
+
samples.append(final)
|
|
36
|
+
if len(samples) > max_samples:
|
|
37
|
+
raise ValueError("preexecution_sample_limit_exceeded")
|
|
38
|
+
return samples
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check(path: Path, *, timeout_s: float, max_joint_step_rad: float, max_samples: int, group_name: str) -> dict[str, Any]:
|
|
42
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
43
|
+
motion = payload["motion"]
|
|
44
|
+
joint_names = tuple(str(value) for value in motion["joint_names"])
|
|
45
|
+
samples = _sample_motion(motion, max_joint_step_rad=max_joint_step_rad, max_samples=max_samples)
|
|
46
|
+
|
|
47
|
+
import rclpy
|
|
48
|
+
from moveit_msgs.srv import GetStateValidity
|
|
49
|
+
|
|
50
|
+
rclpy.init()
|
|
51
|
+
node = rclpy.create_node("devagent_state_validity_probe")
|
|
52
|
+
client = node.create_client(GetStateValidity, "/check_state_validity")
|
|
53
|
+
try:
|
|
54
|
+
if not client.wait_for_service(timeout_sec=timeout_s):
|
|
55
|
+
return {"success": False, "code": "state_validity_service_unavailable", "sample_count": 0}
|
|
56
|
+
checked = 0
|
|
57
|
+
for index, positions in enumerate(samples):
|
|
58
|
+
request = GetStateValidity.Request()
|
|
59
|
+
request.group_name = group_name
|
|
60
|
+
request.robot_state.is_diff = True
|
|
61
|
+
request.robot_state.joint_state.name = list(joint_names)
|
|
62
|
+
request.robot_state.joint_state.position = list(positions)
|
|
63
|
+
future = client.call_async(request)
|
|
64
|
+
rclpy.spin_until_future_complete(node, future, timeout_sec=timeout_s)
|
|
65
|
+
if not future.done() or future.result() is None:
|
|
66
|
+
return {"success": False, "code": "state_validity_timeout", "sample_count": checked, "first_invalid": index}
|
|
67
|
+
response = future.result()
|
|
68
|
+
checked += 1
|
|
69
|
+
if not response.valid:
|
|
70
|
+
evidence = {"index": index, "positions_rad": list(positions), "contact_count": len(response.contacts)}
|
|
71
|
+
return {
|
|
72
|
+
"success": False,
|
|
73
|
+
"code": "collision_or_invalid_state",
|
|
74
|
+
"sample_count": checked,
|
|
75
|
+
"first_invalid": evidence,
|
|
76
|
+
"group_name": group_name,
|
|
77
|
+
"max_joint_step_rad": max_joint_step_rad,
|
|
78
|
+
"evidence_hash": _hash(evidence),
|
|
79
|
+
}
|
|
80
|
+
evidence = {
|
|
81
|
+
"motion_hash": payload["motion_hash"],
|
|
82
|
+
"materialization_hash": payload["materialization_hash"],
|
|
83
|
+
"sample_count": checked,
|
|
84
|
+
"group_name": group_name,
|
|
85
|
+
"max_joint_step_rad": max_joint_step_rad,
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
"success": True,
|
|
89
|
+
"code": "sampled_state_validity_pass",
|
|
90
|
+
"sample_count": checked,
|
|
91
|
+
"group_name": group_name,
|
|
92
|
+
"max_joint_step_rad": max_joint_step_rad,
|
|
93
|
+
"evidence_hash": _hash(evidence),
|
|
94
|
+
}
|
|
95
|
+
finally:
|
|
96
|
+
node.destroy_node()
|
|
97
|
+
rclpy.shutdown()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
parser = argparse.ArgumentParser()
|
|
102
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
103
|
+
command = sub.add_parser("check")
|
|
104
|
+
command.add_argument("--input", required=True)
|
|
105
|
+
command.add_argument("--timeout", type=float, default=10.0)
|
|
106
|
+
command.add_argument("--max-joint-step", type=float, default=0.03)
|
|
107
|
+
command.add_argument("--max-samples", type=int, default=4000)
|
|
108
|
+
command.add_argument("--group", default="ur_manipulator")
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
try:
|
|
111
|
+
result = check(
|
|
112
|
+
Path(args.input).expanduser().resolve(),
|
|
113
|
+
timeout_s=float(args.timeout),
|
|
114
|
+
max_joint_step_rad=float(args.max_joint_step),
|
|
115
|
+
max_samples=int(args.max_samples),
|
|
116
|
+
group_name=str(args.group),
|
|
117
|
+
)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
result = {"success": False, "code": f"state_validity_probe_error:{type(exc).__name__}", "sample_count": 0}
|
|
120
|
+
print(json.dumps(result, sort_keys=True))
|
|
121
|
+
return 0 if result.get("success") else 2
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> int:
|
|
8
|
+
parser = argparse.ArgumentParser()
|
|
9
|
+
parser.add_argument("--parent", required=True)
|
|
10
|
+
parser.add_argument("--child", required=True)
|
|
11
|
+
parser.add_argument("--timeout", type=float, default=10.0)
|
|
12
|
+
args = parser.parse_args()
|
|
13
|
+
payload: dict[str, object]
|
|
14
|
+
try:
|
|
15
|
+
import rclpy
|
|
16
|
+
from rclpy.duration import Duration
|
|
17
|
+
from rclpy.time import Time
|
|
18
|
+
from tf2_ros import Buffer, TransformListener
|
|
19
|
+
|
|
20
|
+
rclpy.init()
|
|
21
|
+
node = rclpy.create_node("devagent_tf_probe")
|
|
22
|
+
buffer = Buffer()
|
|
23
|
+
listener = TransformListener(buffer, node, spin_thread=True)
|
|
24
|
+
try:
|
|
25
|
+
transform = buffer.lookup_transform(
|
|
26
|
+
args.parent,
|
|
27
|
+
args.child,
|
|
28
|
+
Time(),
|
|
29
|
+
timeout=Duration(seconds=float(args.timeout)),
|
|
30
|
+
)
|
|
31
|
+
translation = transform.transform.translation
|
|
32
|
+
rotation = transform.transform.rotation
|
|
33
|
+
payload = {
|
|
34
|
+
"success": True,
|
|
35
|
+
"code": "tf_verified",
|
|
36
|
+
"parent_frame": args.parent,
|
|
37
|
+
"child_frame": args.child,
|
|
38
|
+
"translation_m": [translation.x, translation.y, translation.z],
|
|
39
|
+
"quaternion_xyzw": [rotation.x, rotation.y, rotation.z, rotation.w],
|
|
40
|
+
}
|
|
41
|
+
finally:
|
|
42
|
+
node.destroy_node()
|
|
43
|
+
rclpy.shutdown()
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
payload = {"success": False, "code": f"tf_probe_error:{type(exc).__name__}"}
|
|
46
|
+
print(json.dumps(payload, sort_keys=True))
|
|
47
|
+
return 0 if payload.get("success") else 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
from math import isfinite
|
|
6
|
+
import re
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
|
|
9
|
+
from ..physical_motion import JointTrajectoryPoint, PhysicalMotionPlan
|
|
10
|
+
from .commands import CommandResult, RosCommandRunner
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RosTrajectoryError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class JointStateSnapshot:
|
|
19
|
+
names: tuple[str, ...]
|
|
20
|
+
positions_rad: tuple[float, ...]
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
if not self.names or len(self.names) != len(self.positions_rad):
|
|
24
|
+
raise RosTrajectoryError("joint_state_shape_invalid")
|
|
25
|
+
if len(set(self.names)) != len(self.names):
|
|
26
|
+
raise RosTrajectoryError("joint_state_duplicate_name")
|
|
27
|
+
if any(not name.strip() for name in self.names):
|
|
28
|
+
raise RosTrajectoryError("joint_state_empty_name")
|
|
29
|
+
if any(not isfinite(value) for value in self.positions_rad):
|
|
30
|
+
raise RosTrajectoryError("joint_state_non_finite")
|
|
31
|
+
|
|
32
|
+
def ordered(self, joint_names: tuple[str, ...]) -> tuple[float, ...]:
|
|
33
|
+
values = dict(zip(self.names, self.positions_rad))
|
|
34
|
+
missing = [name for name in joint_names if name not in values]
|
|
35
|
+
if missing:
|
|
36
|
+
raise RosTrajectoryError(
|
|
37
|
+
"joint_state_missing:" + ",".join(sorted(missing))
|
|
38
|
+
)
|
|
39
|
+
return tuple(values[name] for name in joint_names)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def read_joint_state(
|
|
43
|
+
runner: RosCommandRunner,
|
|
44
|
+
*,
|
|
45
|
+
topic: str = "/joint_states",
|
|
46
|
+
timeout_s: float = 8.0,
|
|
47
|
+
) -> JointStateSnapshot:
|
|
48
|
+
result = runner.run(
|
|
49
|
+
["ros2", "topic", "echo", topic, "--once"],
|
|
50
|
+
timeout_s=timeout_s,
|
|
51
|
+
)
|
|
52
|
+
if not result.ok:
|
|
53
|
+
raise RosTrajectoryError(
|
|
54
|
+
"joint_state_snapshot_failed:"
|
|
55
|
+
+ (result.stderr.strip()[-300:] or "ros2_topic_echo_failed")
|
|
56
|
+
)
|
|
57
|
+
try:
|
|
58
|
+
import yaml
|
|
59
|
+
|
|
60
|
+
payload = yaml.safe_load(result.stdout)
|
|
61
|
+
if not isinstance(payload, Mapping):
|
|
62
|
+
raise TypeError("joint state payload is not a mapping")
|
|
63
|
+
names_raw = payload.get("name")
|
|
64
|
+
positions_raw = payload.get("position")
|
|
65
|
+
if not isinstance(names_raw, list) or not isinstance(positions_raw, list):
|
|
66
|
+
raise TypeError("name/position fields are not arrays")
|
|
67
|
+
return JointStateSnapshot(
|
|
68
|
+
tuple(str(item) for item in names_raw),
|
|
69
|
+
tuple(float(item) for item in positions_raw),
|
|
70
|
+
)
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
raise RosTrajectoryError("joint_state_snapshot_invalid") from exc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _duration_payload(seconds: float) -> dict[str, int]:
|
|
76
|
+
whole = int(seconds)
|
|
77
|
+
nanos = int(round((seconds - whole) * 1_000_000_000))
|
|
78
|
+
if nanos >= 1_000_000_000:
|
|
79
|
+
whole += 1
|
|
80
|
+
nanos -= 1_000_000_000
|
|
81
|
+
return {"sec": whole, "nanosec": nanos}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _point_payload(point: JointTrajectoryPoint) -> dict:
|
|
85
|
+
payload = {
|
|
86
|
+
"positions": list(point.positions_rad),
|
|
87
|
+
"time_from_start": _duration_payload(point.time_from_start_s),
|
|
88
|
+
}
|
|
89
|
+
if point.velocities_rad_s:
|
|
90
|
+
payload["velocities"] = list(point.velocities_rad_s)
|
|
91
|
+
if point.accelerations_rad_s2:
|
|
92
|
+
payload["accelerations"] = list(point.accelerations_rad_s2)
|
|
93
|
+
return payload
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def follow_joint_trajectory_command(
|
|
97
|
+
motion: PhysicalMotionPlan,
|
|
98
|
+
*,
|
|
99
|
+
action_name: str = "/joint_trajectory_controller/follow_joint_trajectory",
|
|
100
|
+
) -> tuple[str, ...]:
|
|
101
|
+
if not action_name.strip().startswith("/"):
|
|
102
|
+
raise RosTrajectoryError("trajectory_action_must_be_absolute")
|
|
103
|
+
points = motion.points[1:]
|
|
104
|
+
goal = {
|
|
105
|
+
"trajectory": {
|
|
106
|
+
"joint_names": list(motion.joint_names),
|
|
107
|
+
"points": [_point_payload(point) for point in points],
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
"ros2",
|
|
112
|
+
"action",
|
|
113
|
+
"send_goal",
|
|
114
|
+
action_name,
|
|
115
|
+
"control_msgs/action/FollowJointTrajectory",
|
|
116
|
+
json.dumps(goal, separators=(",", ":"), allow_nan=False),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def single_point_motion(
|
|
121
|
+
*,
|
|
122
|
+
motion_id: str,
|
|
123
|
+
robot_profile_key: str,
|
|
124
|
+
joint_names: tuple[str, ...],
|
|
125
|
+
current_positions_rad: tuple[float, ...],
|
|
126
|
+
target_positions_rad: tuple[float, ...],
|
|
127
|
+
duration_s: float,
|
|
128
|
+
source_graph_hash: str,
|
|
129
|
+
twin_hash: str,
|
|
130
|
+
planner_id: str = "devagent-reset",
|
|
131
|
+
) -> PhysicalMotionPlan:
|
|
132
|
+
if duration_s <= 0:
|
|
133
|
+
raise RosTrajectoryError("reset_duration_must_be_positive")
|
|
134
|
+
return PhysicalMotionPlan(
|
|
135
|
+
motion_id=motion_id,
|
|
136
|
+
robot_profile_key=robot_profile_key,
|
|
137
|
+
joint_names=joint_names,
|
|
138
|
+
points=(
|
|
139
|
+
JointTrajectoryPoint(current_positions_rad, 0.0),
|
|
140
|
+
JointTrajectoryPoint(target_positions_rad, duration_s),
|
|
141
|
+
),
|
|
142
|
+
source_graph_hash=source_graph_hash,
|
|
143
|
+
planner_id=planner_id,
|
|
144
|
+
twin_hash=twin_hash,
|
|
145
|
+
constraints=("simulation_reset_only",),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _confirm_successful_action_output(output: str) -> None:
|
|
150
|
+
normalized = output.lower()
|
|
151
|
+
if "goal rejected" in normalized:
|
|
152
|
+
raise RosTrajectoryError("trajectory_goal_rejected")
|
|
153
|
+
for failure in ("status: aborted", "status: canceled", "status: cancelled"):
|
|
154
|
+
if failure in normalized:
|
|
155
|
+
raise RosTrajectoryError("trajectory_action_not_succeeded")
|
|
156
|
+
|
|
157
|
+
error_codes = [int(value) for value in re.findall(r"error_code\s*[=:]\s*(-?\d+)", normalized)]
|
|
158
|
+
if any(code != 0 for code in error_codes):
|
|
159
|
+
raise RosTrajectoryError("trajectory_result_error_code")
|
|
160
|
+
|
|
161
|
+
success_markers = (
|
|
162
|
+
"status: succeeded",
|
|
163
|
+
"goal finished with status: succeeded",
|
|
164
|
+
"successful",
|
|
165
|
+
)
|
|
166
|
+
if error_codes and all(code == 0 for code in error_codes):
|
|
167
|
+
return
|
|
168
|
+
if any(marker in normalized for marker in success_markers):
|
|
169
|
+
return
|
|
170
|
+
raise RosTrajectoryError("trajectory_result_unconfirmed")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def execute_joint_trajectory(
|
|
174
|
+
runner: RosCommandRunner,
|
|
175
|
+
motion: PhysicalMotionPlan,
|
|
176
|
+
*,
|
|
177
|
+
action_name: str = "/joint_trajectory_controller/follow_joint_trajectory",
|
|
178
|
+
timeout_s: float | None = None,
|
|
179
|
+
) -> CommandResult:
|
|
180
|
+
timeout = timeout_s or max(15.0, motion.planned_duration_s + 15.0)
|
|
181
|
+
result = runner.run(
|
|
182
|
+
follow_joint_trajectory_command(motion, action_name=action_name),
|
|
183
|
+
timeout_s=timeout,
|
|
184
|
+
)
|
|
185
|
+
if not result.ok:
|
|
186
|
+
raise RosTrajectoryError("trajectory_action_failed")
|
|
187
|
+
_confirm_successful_action_output(result.stdout + "\n" + result.stderr)
|
|
188
|
+
return result
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .commands import RosCommandRunner, RunningProcess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
UR5E = "ur5e"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def official_ur5e_moveit_command(*, world_file: Path | str | None = None) -> tuple[str, ...]:
|
|
13
|
+
command = [
|
|
14
|
+
"ros2",
|
|
15
|
+
"launch",
|
|
16
|
+
"ur_simulation_gz",
|
|
17
|
+
"ur_sim_moveit.launch.py",
|
|
18
|
+
"ur_type:=ur5e",
|
|
19
|
+
]
|
|
20
|
+
if world_file is not None:
|
|
21
|
+
world = Path(world_file).expanduser().resolve()
|
|
22
|
+
if not world.is_file():
|
|
23
|
+
raise FileNotFoundError(f"world_file_not_found:{world}")
|
|
24
|
+
command.append(f"world_file:={world}")
|
|
25
|
+
return tuple(command)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def official_ur_motion_smoke_command() -> tuple[str, ...]:
|
|
29
|
+
return ("ros2", "run", "ur_robot_driver", "example_move.py")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class UR5eSimulation:
|
|
34
|
+
runner: RosCommandRunner
|
|
35
|
+
process: RunningProcess
|
|
36
|
+
log_path: Path
|
|
37
|
+
command: tuple[str, ...]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def running(self) -> bool:
|
|
41
|
+
return self.process.poll() is None
|
|
42
|
+
|
|
43
|
+
def stop(self, *, timeout_s: float = 10.0) -> None:
|
|
44
|
+
self.runner.stop(self.process, timeout_s=timeout_s)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UR5eSimulationLauncher:
|
|
48
|
+
def __init__(self, runner: RosCommandRunner) -> None:
|
|
49
|
+
self.runner = runner
|
|
50
|
+
|
|
51
|
+
def launch(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
log_path: Path,
|
|
55
|
+
world_file: Path | str | None = None,
|
|
56
|
+
) -> UR5eSimulation:
|
|
57
|
+
command = official_ur5e_moveit_command(world_file=world_file)
|
|
58
|
+
process = self.runner.start(command, log_path=log_path)
|
|
59
|
+
return UR5eSimulation(self.runner, process, log_path, command)
|