openral-cli 0.1.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.
- openral_cli/__init__.py +6 -0
- openral_cli/_hf_publish.py +125 -0
- openral_cli/_rskill_doc_validator.py +562 -0
- openral_cli/_rskill_intel.py +334 -0
- openral_cli/_rskill_readme.py +217 -0
- openral_cli/_rskill_scaffolder.py +267 -0
- openral_cli/autodetect.py +464 -0
- openral_cli/check.py +397 -0
- openral_cli/collision.py +369 -0
- openral_cli/dataset.py +438 -0
- openral_cli/deploy_sim.py +2664 -0
- openral_cli/install.py +422 -0
- openral_cli/main.py +4108 -0
- openral_cli/prompt.py +128 -0
- openral_cli/py.typed +0 -0
- openral_cli/robot.py +301 -0
- openral_cli-0.1.0.dist-info/METADATA +33 -0
- openral_cli-0.1.0.dist-info/RECORD +20 -0
- openral_cli-0.1.0.dist-info/WHEEL +4 -0
- openral_cli-0.1.0.dist-info/entry_points.txt +2 -0
openral_cli/prompt.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""``openral prompt`` CLI adapter.
|
|
2
|
+
|
|
3
|
+
Publishes a one-shot ``openral_msgs/PromptStamped`` onto
|
|
4
|
+
``/openral/prompt_in/cli`` and exits. The ``prompt_router_node``
|
|
5
|
+
(``packages/openral_prompt_router``) fans the message out onto
|
|
6
|
+
``/openral/prompt`` after stamping ``{"source": "cli", "priority": 100}``
|
|
7
|
+
onto ``metadata_json``; the F4 reasoner consumes ``/openral/prompt``.
|
|
8
|
+
|
|
9
|
+
Wire shape:
|
|
10
|
+
|
|
11
|
+
* QoS: ``RELIABLE + VOLATILE + KEEP_LAST=10`` (matches the router's
|
|
12
|
+
subscription so the message survives a one-shot publish-and-exit
|
|
13
|
+
even if the router was a hair late to subscribe).
|
|
14
|
+
* ``metadata_json``: ``{"source_cli": true}`` — minimal; the router
|
|
15
|
+
appends the canonical ``source`` / ``priority`` fields.
|
|
16
|
+
|
|
17
|
+
``rclpy`` is imported lazily inside the typer command so ``openral --help``
|
|
18
|
+
stays sub-second even when ROS is not sourced.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
import typer
|
|
27
|
+
|
|
28
|
+
__all__ = ["prompt_command"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prompt_command(
|
|
32
|
+
text: str = typer.Argument(..., help='Prompt text, e.g. "pick the red cube"'),
|
|
33
|
+
topic: str = typer.Option(
|
|
34
|
+
"/openral/prompt_in/cli",
|
|
35
|
+
help="ROS topic to publish on; defaults to the prompt-router's CLI input.",
|
|
36
|
+
),
|
|
37
|
+
wait_s: float = typer.Option(
|
|
38
|
+
1.0,
|
|
39
|
+
help=(
|
|
40
|
+
"Seconds to wait after publish before exiting. Gives the prompt-router "
|
|
41
|
+
"time to receive on a fresh DDS discovery; 0 exits immediately."
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
discovery_wait_s: float = typer.Option(
|
|
45
|
+
5.0,
|
|
46
|
+
help=(
|
|
47
|
+
"Seconds to spin waiting for the prompt-router to discover the publisher "
|
|
48
|
+
"before giving up and publishing anyway. Cold-boot deploys with stale "
|
|
49
|
+
"Fast-DDS shared-memory remnants need 10-15 s; the default 5 s matches "
|
|
50
|
+
"the interactive use case where the launch has been up for a while."
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Publish a one-shot PromptStamped on the prompt-router CLI input topic.
|
|
55
|
+
|
|
56
|
+
Requires a sourced ROS 2 install. Exits 0 on success, 2 when
|
|
57
|
+
``rclpy`` / ``openral_msgs`` are not importable (after pointing
|
|
58
|
+
the user at ``just ros2-build`` + ``source install/setup.bash``).
|
|
59
|
+
|
|
60
|
+
Example::
|
|
61
|
+
|
|
62
|
+
openral prompt "pick the red cube"
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
import rclpy # noqa: PLC0415 # reason: heavy ROS dep deferred
|
|
66
|
+
from openral_msgs.msg import ( # noqa: PLC0415 # reason: heavy ROS dep deferred
|
|
67
|
+
PromptStamped,
|
|
68
|
+
)
|
|
69
|
+
from rclpy.qos import ( # noqa: PLC0415
|
|
70
|
+
QoSDurabilityPolicy,
|
|
71
|
+
QoSHistoryPolicy,
|
|
72
|
+
QoSProfile,
|
|
73
|
+
QoSReliabilityPolicy,
|
|
74
|
+
)
|
|
75
|
+
except ImportError as exc:
|
|
76
|
+
typer.echo(
|
|
77
|
+
f"openral prompt: cannot import rclpy / openral_msgs ({exc!s}). "
|
|
78
|
+
"Run `just ros2-build` then `source install/setup.bash` first.",
|
|
79
|
+
err=True,
|
|
80
|
+
)
|
|
81
|
+
raise typer.Exit(code=2) from exc
|
|
82
|
+
|
|
83
|
+
rclpy.init()
|
|
84
|
+
try:
|
|
85
|
+
node = rclpy.create_node("openral_cli_prompt")
|
|
86
|
+
qos = QoSProfile(
|
|
87
|
+
history=QoSHistoryPolicy.KEEP_LAST,
|
|
88
|
+
depth=10,
|
|
89
|
+
reliability=QoSReliabilityPolicy.RELIABLE,
|
|
90
|
+
durability=QoSDurabilityPolicy.VOLATILE,
|
|
91
|
+
)
|
|
92
|
+
pub = node.create_publisher(PromptStamped, topic, qos)
|
|
93
|
+
|
|
94
|
+
msg = PromptStamped()
|
|
95
|
+
msg.header.stamp = node.get_clock().now().to_msg()
|
|
96
|
+
msg.header.frame_id = "openral_cli_prompt"
|
|
97
|
+
msg.text = text
|
|
98
|
+
msg.metadata_json = json.dumps({"source_cli": True}, sort_keys=True)
|
|
99
|
+
|
|
100
|
+
# Spin until a subscriber matches so the one-shot publish is
|
|
101
|
+
# not delivered into the void. 0.5 s was too tight on hosts
|
|
102
|
+
# where the shared-memory transport falls back to UDP
|
|
103
|
+
# discovery (we saw `RTPS_TRANSPORT_SHM Error: Failed init_port`
|
|
104
|
+
# on this box, which adds ~1 s before UDP picks up the
|
|
105
|
+
# prompt_router subscription). 5 s covers normal interactive
|
|
106
|
+
# use; cold-boot deploys (where the prompt is sent within the
|
|
107
|
+
# same shell session as the launch start) can need 10-15 s,
|
|
108
|
+
# exposed via ``--discovery-wait-s``.
|
|
109
|
+
deadline = time.monotonic() + discovery_wait_s
|
|
110
|
+
while time.monotonic() < deadline and pub.get_subscription_count() == 0:
|
|
111
|
+
rclpy.spin_once(node, timeout_sec=0.05)
|
|
112
|
+
n_subs = pub.get_subscription_count()
|
|
113
|
+
if n_subs == 0:
|
|
114
|
+
typer.echo(
|
|
115
|
+
f"openral prompt: no subscriber on {topic} after {discovery_wait_s:.1f} s; "
|
|
116
|
+
"is the launch up?",
|
|
117
|
+
err=True,
|
|
118
|
+
)
|
|
119
|
+
pub.publish(msg)
|
|
120
|
+
typer.echo(
|
|
121
|
+
f"openral prompt: published on {topic} text={text!r} (matched_subs={n_subs})",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if wait_s > 0:
|
|
125
|
+
time.sleep(wait_s)
|
|
126
|
+
node.destroy_node()
|
|
127
|
+
finally:
|
|
128
|
+
rclpy.shutdown()
|
openral_cli/py.typed
ADDED
|
File without changes
|
openral_cli/robot.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""``openral robot vendor-urdf <id>`` — expand an upstream xacro to a flat URDF.
|
|
2
|
+
|
|
3
|
+
The flat, committed URDF means end users need no xacro tooling at runtime.
|
|
4
|
+
The xacro-only arms (ur5e/ur10e/rizon4) ship a ``XACRO_PATH`` in
|
|
5
|
+
``robot_descriptions``; we let ``robot_descriptions``' ``yourdfpy`` loader run
|
|
6
|
+
``xacrodoc`` to expand every ``${…}`` substitution, then serialize the resulting
|
|
7
|
+
flat URDF with a provenance header. ``openarm`` ships only MJCF upstream, so its
|
|
8
|
+
flattened URDF is cloned separately and passed as a ``file:`` upstream; the
|
|
9
|
+
``--rename`` hook strips the ``openarm_`` joint/link prefix to the OpenRAL HAL
|
|
10
|
+
convention (``left_joint1..7`` / ``right_joint1..7``).
|
|
11
|
+
|
|
12
|
+
**Portable mesh refs.** The yourdfpy round-trip absolutizes every mesh
|
|
13
|
+
``filename`` into the vendoring machine's ``robot_descriptions`` cache — paths
|
|
14
|
+
that resolve nowhere else, which once broke collision lowering for the whole
|
|
15
|
+
xacro fleet in CI (empty ACMs). For ``rd:`` upstreams the round-trip output is
|
|
16
|
+
therefore post-processed by :func:`_portable_mesh_refs`: cache-absolute paths
|
|
17
|
+
become ``rd:<module>:<path-relative-to-repository>`` refs, expanded at load
|
|
18
|
+
time by ``openral_safety.urdf_lowering`` through the same pinned clone.
|
|
19
|
+
|
|
20
|
+
**Raw-text mode** (``raw_text=True``). The same yourdfpy round-trip also
|
|
21
|
+
mangles ``package://`` mesh paths, which is fatal for already-flat upstream
|
|
22
|
+
URDFs that ship relative or ``package://`` meshes (so100/so101/gr1/h1). For
|
|
23
|
+
those we copy the upstream text verbatim and apply joint-name renames with
|
|
24
|
+
``re.sub`` directly on the raw XML — preserving every mesh path byte-for-byte.
|
|
25
|
+
The renames target **joint names only** (``<joint name="X"`` and any
|
|
26
|
+
``joint="X"`` mimic/transmission references); link names are never touched. A
|
|
27
|
+
:class:`list` of ``(pattern, repl)`` pairs is applied in order, so so100/so101
|
|
28
|
+
take six numeric renames and gr1/h1 take one ``_joint``-suffix strip.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import re
|
|
34
|
+
from collections.abc import Sequence
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import cast
|
|
37
|
+
|
|
38
|
+
_PROVENANCE = (
|
|
39
|
+
"<!-- Vendored by `openral robot vendor-urdf {id}` from {src}. Upstream license applies. -->\n"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# SO-ARM numeric-joint → semantic HAL-name map, applied by joint NUMBER (the
|
|
43
|
+
# upstream so100/so101 URDFs name joints "1".."6"; the SO-ARM motor convention
|
|
44
|
+
# and the manifest use these semantic names). Both follower arms share the map.
|
|
45
|
+
_SO_ARM_JOINT_NAMES: tuple[str, ...] = (
|
|
46
|
+
"shoulder_pan", # 1
|
|
47
|
+
"shoulder_lift", # 2
|
|
48
|
+
"elbow_flex", # 3
|
|
49
|
+
"wrist_flex", # 4
|
|
50
|
+
"wrist_roll", # 5
|
|
51
|
+
"gripper", # 6
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Per-robot raw-text joint renames (regex pattern, replacement), applied in
|
|
55
|
+
# order. Patterns are scoped to the joint-name context so no <link …> is hit:
|
|
56
|
+
# * so100/so101: rewrite ``name="N"`` (only ever a <joint> in these URDFs;
|
|
57
|
+
# links are semantic, transmissions are ``N_trans`` / ``motorN``).
|
|
58
|
+
# * gr1/h1: strip the ``_joint`` suffix from every ``name="…_joint"`` (no link
|
|
59
|
+
# ends in ``_joint`` in either URDF — verified).
|
|
60
|
+
# * gr1 also collapses ``*_elbow_pitch`` → ``*_elbow`` to match the manifest's
|
|
61
|
+
# HAL joint name (the upstream URDF spells the single-DoF elbow joint
|
|
62
|
+
# ``*_elbow_pitch_joint``; the manifest/control contract calls it ``*_elbow``).
|
|
63
|
+
# Applied AFTER the ``_joint`` strip; link-safe (no ``*_elbow_pitch`` link
|
|
64
|
+
# exists — only the two ``*_elbow_pitch_joint`` joints — verified).
|
|
65
|
+
_RAW_RENAMES: dict[str, list[tuple[str, str]]] = {
|
|
66
|
+
"so100_follower": [
|
|
67
|
+
(rf'name="{n}"', f'name="{sem}"') for n, sem in enumerate(_SO_ARM_JOINT_NAMES, start=1)
|
|
68
|
+
],
|
|
69
|
+
"so101_follower": [
|
|
70
|
+
(rf'name="{n}"', f'name="{sem}"') for n, sem in enumerate(_SO_ARM_JOINT_NAMES, start=1)
|
|
71
|
+
],
|
|
72
|
+
"gr1": [
|
|
73
|
+
(r'name="([^"]*)_joint"', r'name="\1"'),
|
|
74
|
+
(r'name="(left|right)_elbow_pitch"', r'name="\1_elbow"'),
|
|
75
|
+
],
|
|
76
|
+
"h1": [(r'name="([^"]*)_joint"', r'name="\1"')],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Joint-name normalization to the OpenRAL HAL convention. openarm: strip the
|
|
80
|
+
# "openarm_" prefix so joints become left_joint1..7 / right_joint1..7.
|
|
81
|
+
_RENAME: dict[str, tuple[str, str]] = {"openarm": (r'"openarm_', '"')}
|
|
82
|
+
|
|
83
|
+
# A ``(pattern, repl)`` rename is a 2-tuple; named to satisfy the magic-value lint.
|
|
84
|
+
_PAIR_LEN = 2
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _model_to_xml(model: object) -> str:
|
|
88
|
+
"""Serialize a loaded ``yourdfpy.URDF`` to a pretty-printed flat URDF string.
|
|
89
|
+
|
|
90
|
+
yourdfpy ≥0.0.56 exposes ``write_xml_string()`` returning the serialized URDF
|
|
91
|
+
as ASCII ``bytes`` on a single line (verified against the installed package —
|
|
92
|
+
it wraps ``lxml.etree.tostring``; its ``**kwargs`` forwarding is broken so we
|
|
93
|
+
cannot pass ``pretty_print`` through it). We re-parse and pretty-print via lxml
|
|
94
|
+
so the committed file is human-reviewable + git-diffable.
|
|
95
|
+
"""
|
|
96
|
+
from lxml import etree
|
|
97
|
+
|
|
98
|
+
# ``write_xml_string`` is the public serializer in yourdfpy ≥0.0.56; one line.
|
|
99
|
+
raw = model.write_xml_string() # type: ignore[attr-defined] # reason: yourdfpy.URDF has no stubs
|
|
100
|
+
root = etree.fromstring(raw if isinstance(raw, bytes) else raw.encode("utf-8"))
|
|
101
|
+
pretty: bytes = etree.tostring(root, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
|
102
|
+
return pretty.decode("utf-8")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _load_model(upstream: str) -> object:
|
|
106
|
+
"""Load an upstream description into a ``yourdfpy.URDF``.
|
|
107
|
+
|
|
108
|
+
``rd:<module>`` resolves through ``robot_descriptions`` (xacro expanded via
|
|
109
|
+
``xacrodoc``); ``file:<path>`` loads an already-flat URDF directly.
|
|
110
|
+
"""
|
|
111
|
+
if upstream.startswith("file:"):
|
|
112
|
+
import yourdfpy
|
|
113
|
+
|
|
114
|
+
path = Path(upstream[len("file:") :])
|
|
115
|
+
# build_scene_graph/build_collision_scene_graph are off so an
|
|
116
|
+
# upstream that references missing meshes still parses for re-export.
|
|
117
|
+
return yourdfpy.URDF.load(
|
|
118
|
+
str(path),
|
|
119
|
+
build_scene_graph=False,
|
|
120
|
+
build_collision_scene_graph=False,
|
|
121
|
+
load_meshes=False,
|
|
122
|
+
load_collision_meshes=False,
|
|
123
|
+
)
|
|
124
|
+
from robot_descriptions.loaders.yourdfpy import load_robot_description
|
|
125
|
+
|
|
126
|
+
module = upstream[len("rd:") :] if upstream.startswith("rd:") else upstream
|
|
127
|
+
return load_robot_description(module) # expands xacro via xacrodoc
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def vendor_urdf(
|
|
131
|
+
robot_id: str,
|
|
132
|
+
*,
|
|
133
|
+
upstream: str,
|
|
134
|
+
out_dir: Path,
|
|
135
|
+
rename: tuple[str, str] | Sequence[tuple[str, str]] | None = None,
|
|
136
|
+
raw_text: bool = False,
|
|
137
|
+
) -> Path:
|
|
138
|
+
"""Expand ``upstream`` to a flat URDF at ``out_dir/<robot_id>.urdf``.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
robot_id: OpenRAL robot id; names the output file and selects the
|
|
142
|
+
default rename rule.
|
|
143
|
+
upstream: ``rd:<robot_descriptions module>`` for xacro arms, or
|
|
144
|
+
``file:<path>`` for an already-flat upstream URDF (openarm).
|
|
145
|
+
out_dir: Directory to write ``<robot_id>.urdf`` into (created if absent).
|
|
146
|
+
rename: A ``(pattern, repl)`` pair, or a sequence of them, applied with
|
|
147
|
+
``re.sub`` in order. ``None`` selects the per-robot default
|
|
148
|
+
(``_RAW_RENAMES[robot_id]`` in raw-text mode, else ``_RENAME``).
|
|
149
|
+
raw_text: When ``True``, copy the already-flat upstream URDF text
|
|
150
|
+
verbatim and apply ``rename`` directly to the raw XML — no yourdfpy
|
|
151
|
+
round-trip — so ``package://`` / relative mesh paths are preserved
|
|
152
|
+
byte-for-byte. Only valid for ``file:`` / ``rd:`` URDFs that are
|
|
153
|
+
already flat (so100/so101/gr1/h1). Mutually exclusive with xacro
|
|
154
|
+
expansion: the upstream must contain no ``${…}``.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Path to the written URDF.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> from pathlib import Path
|
|
161
|
+
>>> import tempfile
|
|
162
|
+
>>> d = Path(tempfile.mkdtemp())
|
|
163
|
+
>>> # vendor-urdf needs the optional `lowering` group (yourdfpy/xacrodoc):
|
|
164
|
+
>>> out = vendor_urdf( # doctest: +SKIP
|
|
165
|
+
... "ur5e", upstream="rd:ur5e_description", out_dir=d
|
|
166
|
+
... )
|
|
167
|
+
>>> out.name # doctest: +SKIP
|
|
168
|
+
'ur5e.urdf'
|
|
169
|
+
"""
|
|
170
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
out = out_dir / f"{robot_id}.urdf"
|
|
172
|
+
if raw_text:
|
|
173
|
+
xml = _read_raw_text(upstream)
|
|
174
|
+
renames = _resolve_renames(robot_id, rename, default=_RAW_RENAMES.get(robot_id))
|
|
175
|
+
for pat, repl in renames:
|
|
176
|
+
xml = re.sub(pat, repl, xml)
|
|
177
|
+
# Preserve the upstream's exact bytes apart from the renamed joint names
|
|
178
|
+
# and the inserted provenance comment — including its newline style — so
|
|
179
|
+
# the only diff vs upstream is the joint renames (gate: lowering must
|
|
180
|
+
# stay byte-identical, mesh paths verbatim). newline="" disables write-side
|
|
181
|
+
# newline translation (Path.write_text gained the kwarg only in 3.13).
|
|
182
|
+
with out.open("w", encoding="utf-8", newline="") as fh:
|
|
183
|
+
fh.write(_with_provenance_raw(xml, id=robot_id, src=upstream))
|
|
184
|
+
return out
|
|
185
|
+
model = _load_model(upstream)
|
|
186
|
+
xml = _model_to_xml(model)
|
|
187
|
+
if upstream.startswith("rd:"):
|
|
188
|
+
xml = _portable_mesh_refs(xml, module=upstream[len("rd:") :])
|
|
189
|
+
default = [_RENAME[robot_id]] if robot_id in _RENAME else None
|
|
190
|
+
renames = _resolve_renames(robot_id, rename, default=default)
|
|
191
|
+
for pat, repl in renames:
|
|
192
|
+
xml = re.sub(pat, repl, xml)
|
|
193
|
+
out.write_text(_with_provenance(xml, id=robot_id, src=upstream))
|
|
194
|
+
return out
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _portable_mesh_refs(xml: str, *, module: str) -> str:
|
|
198
|
+
"""Rewrite cache-absolutized mesh paths to portable ``rd:<module>:<relpath>`` refs.
|
|
199
|
+
|
|
200
|
+
The yourdfpy round-trip resolves every mesh ``filename`` to an absolute path
|
|
201
|
+
inside the vendoring machine's ``robot_descriptions`` clone
|
|
202
|
+
(``~/.cache/robot_descriptions/<repo>/…``) — paths that exist nowhere else, so
|
|
203
|
+
committing them breaks collision lowering on every other host (CI's re-lower
|
|
204
|
+
then finds no meshes and emits an empty ACM). Rewrite them to
|
|
205
|
+
``rd:<module>:<path-relative-to-repository>``, which
|
|
206
|
+
``openral_safety.urdf_lowering`` expands at load time through the same pinned
|
|
207
|
+
``robot_descriptions`` clone on any machine.
|
|
208
|
+
"""
|
|
209
|
+
import importlib
|
|
210
|
+
|
|
211
|
+
mod = importlib.import_module(f"robot_descriptions.{module}")
|
|
212
|
+
repo = str(Path(mod.REPOSITORY_PATH))
|
|
213
|
+
return xml.replace(f'filename="{repo}/', f'filename="rd:{module}:')
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _resolve_renames(
|
|
217
|
+
robot_id: str,
|
|
218
|
+
rename: tuple[str, str] | Sequence[tuple[str, str]] | None,
|
|
219
|
+
*,
|
|
220
|
+
default: list[tuple[str, str]] | None,
|
|
221
|
+
) -> list[tuple[str, str]]:
|
|
222
|
+
"""Normalize the ``rename`` argument to a flat list of ``(pattern, repl)``.
|
|
223
|
+
|
|
224
|
+
A bare ``(pattern, repl)`` pair (two strings) is wrapped into a one-element
|
|
225
|
+
list; a sequence of pairs is passed through; ``None`` falls back to the
|
|
226
|
+
per-robot ``default``.
|
|
227
|
+
"""
|
|
228
|
+
if rename is None:
|
|
229
|
+
return list(default) if default else []
|
|
230
|
+
# A bare ``(pattern, repl)`` is a 2-tuple of strings; a sequence of such pairs
|
|
231
|
+
# is anything else iterable. ``_PAIR_LEN`` names the 2 to satisfy PLR2004.
|
|
232
|
+
if (
|
|
233
|
+
isinstance(rename, tuple)
|
|
234
|
+
and len(rename) == _PAIR_LEN
|
|
235
|
+
and all(isinstance(x, str) for x in rename)
|
|
236
|
+
):
|
|
237
|
+
return [cast("tuple[str, str]", rename)]
|
|
238
|
+
return list(cast("Sequence[tuple[str, str]]", rename))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _read_raw_text(upstream: str) -> str:
|
|
242
|
+
"""Read an already-flat upstream URDF's text verbatim (no XML round-trip).
|
|
243
|
+
|
|
244
|
+
``file:<path>`` reads that path; ``rd:<module>`` resolves the cached
|
|
245
|
+
upstream ``.urdf`` via ``robot_descriptions`` and reads it directly. The
|
|
246
|
+
upstream must already be flat — raising if any ``${…}`` xacro substitution
|
|
247
|
+
survives, since raw-text mode does no expansion.
|
|
248
|
+
"""
|
|
249
|
+
if upstream.startswith("file:"):
|
|
250
|
+
path = Path(upstream[len("file:") :])
|
|
251
|
+
elif upstream.startswith("rd:"):
|
|
252
|
+
import importlib
|
|
253
|
+
|
|
254
|
+
module = importlib.import_module(f"robot_descriptions.{upstream[len('rd:') :]}")
|
|
255
|
+
path = Path(module.URDF_PATH)
|
|
256
|
+
else:
|
|
257
|
+
raise ValueError(f"raw_text mode needs a 'file:' or 'rd:' upstream, got {upstream!r}")
|
|
258
|
+
# newline="" disables universal-newline translation so an upstream that uses
|
|
259
|
+
# CRLF (e.g. gr1) is preserved verbatim rather than silently rewritten to LF.
|
|
260
|
+
# (Path.read_text gained a newline kwarg only in 3.13; open() is the 3.12 path.)
|
|
261
|
+
with path.open("r", encoding="utf-8", newline="") as fh:
|
|
262
|
+
text = fh.read()
|
|
263
|
+
if "${" in text:
|
|
264
|
+
raise ValueError(f"raw_text mode requires a flat URDF; {path} still has '${{…}}' xacro")
|
|
265
|
+
return text
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _with_provenance_raw(xml: str, *, id: str, src: str) -> str:
|
|
269
|
+
"""Insert the provenance comment into a raw-text URDF, preserving its bytes.
|
|
270
|
+
|
|
271
|
+
Unlike :func:`_with_provenance` (which rewrites the XML declaration emitted
|
|
272
|
+
by the yourdfpy round-trip), this keeps the upstream document verbatim and
|
|
273
|
+
only inserts the provenance comment on the line *after* the declaration (or
|
|
274
|
+
at the top if there is none), matching the document's own newline style so
|
|
275
|
+
the diff vs upstream is exactly the renamed joint names plus this comment.
|
|
276
|
+
"""
|
|
277
|
+
nl = "\r\n" if "\r\n" in xml else "\n"
|
|
278
|
+
header = _PROVENANCE.format(id=id, src=src).rstrip("\n")
|
|
279
|
+
decl_match = re.match(r"^\s*<\?xml[^>]*\?>[ \t]*\r?\n?", xml)
|
|
280
|
+
if decl_match:
|
|
281
|
+
return xml[: decl_match.end()] + header + nl + xml[decl_match.end() :]
|
|
282
|
+
return header + nl + xml
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _with_provenance(xml: str, *, id: str, src: str) -> str:
|
|
286
|
+
"""Prepend the provenance comment without breaking XML well-formedness.
|
|
287
|
+
|
|
288
|
+
An XML declaration (``<?xml …?>``) must be byte 0 of the document — a
|
|
289
|
+
comment may not precede it. When the serialized URDF leads with one, the
|
|
290
|
+
provenance comment is inserted on the line *after* the declaration; otherwise
|
|
291
|
+
it goes at the top.
|
|
292
|
+
"""
|
|
293
|
+
header = _PROVENANCE.format(id=id, src=src)
|
|
294
|
+
decl_match = re.match(r"^\s*<\?xml[^>]*\?>\s*\n?", xml)
|
|
295
|
+
if decl_match:
|
|
296
|
+
# The provenance comment carries a non-ASCII em-dash, so normalize the
|
|
297
|
+
# declared encoding to UTF-8 (yourdfpy emits ``encoding='ASCII'``) to
|
|
298
|
+
# keep the document well-formed.
|
|
299
|
+
body = xml[decl_match.end() :]
|
|
300
|
+
return '<?xml version="1.0" encoding="utf-8"?>\n' + header + body
|
|
301
|
+
return header + xml
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openral-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenRAL command-line tool
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: <3.13,>=3.12
|
|
7
|
+
Requires-Dist: opencv-python-headless<5,>=4.9
|
|
8
|
+
Requires-Dist: openral-core
|
|
9
|
+
Requires-Dist: openral-detect
|
|
10
|
+
Requires-Dist: openral-observability[dashboard]
|
|
11
|
+
Requires-Dist: openral-rskill
|
|
12
|
+
Requires-Dist: openral-sensors
|
|
13
|
+
Requires-Dist: openral-sim
|
|
14
|
+
Requires-Dist: pyudev>=0.24.4
|
|
15
|
+
Requires-Dist: rich>=13.7
|
|
16
|
+
Requires-Dist: typer>=0.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# openral-cli
|
|
20
|
+
|
|
21
|
+
OpenRAL command-line tool
|
|
22
|
+
|
|
23
|
+
Part of [**OpenRAL**](https://github.com/OpenRAL/openral) — the open Robot
|
|
24
|
+
Abstraction Layer for vision-language-action robotics. This package is one
|
|
25
|
+
member of the OpenRAL Python workspace; see the architecture overview and the
|
|
26
|
+
eight-layer model in the project docs.
|
|
27
|
+
|
|
28
|
+
- **Docs:** https://openral.github.io/openral/
|
|
29
|
+
- **Source:** https://github.com/OpenRAL/openral
|
|
30
|
+
- **License:** Apache-2.0
|
|
31
|
+
|
|
32
|
+
> All OpenRAL workspace packages move in lockstep at `0.1.x` until the first
|
|
33
|
+
> public release.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
openral_cli/__init__.py,sha256=m3UphUxCyvMi46_Hk0z9TUEHUJsEzvaCupzu_g3uq8Q,128
|
|
2
|
+
openral_cli/_hf_publish.py,sha256=OKAdqRPF0DLutocQAzBACrP3-ND0DxiYqY56xPmBams,4649
|
|
3
|
+
openral_cli/_rskill_doc_validator.py,sha256=Qx4CqXEuMkZY4fLozLMMe4i_qidEe94StJItFPxXiqg,22578
|
|
4
|
+
openral_cli/_rskill_intel.py,sha256=iLyn1qSZdtf2PlECE9xEo8M6lzL9NjH_xJCpffs3gmk,13203
|
|
5
|
+
openral_cli/_rskill_readme.py,sha256=cHmeTYUGxyBWOXXoZtTs1oCCOwcEeG0UVC-_C3yF1g0,9368
|
|
6
|
+
openral_cli/_rskill_scaffolder.py,sha256=bQV53SyIt8lojETTsDnmq4HohBTOFT7-Hfw-EbRcuNw,10122
|
|
7
|
+
openral_cli/autodetect.py,sha256=olVg6UMLvxXvu24HSgQvgNbzEYEEcQ6JIHqo8bbm-1k,16521
|
|
8
|
+
openral_cli/check.py,sha256=Fjfla9BFfQDnrjkl8-_YN4YKpyNLjabhe6dhvaL98v8,14892
|
|
9
|
+
openral_cli/collision.py,sha256=6g-C4FrqnKvLMTKD_J6M-8FywpxZ92mDYiXd-6R6mEw,15685
|
|
10
|
+
openral_cli/dataset.py,sha256=1ayK3DHmjHJn8TXZV2ucaX04ru-gdVcyllZkxMBzgtk,15451
|
|
11
|
+
openral_cli/deploy_sim.py,sha256=Wp1CXJpRBa59HPWkvWUMmEPzOWL2mLS0Jcbtq5I39Y4,125143
|
|
12
|
+
openral_cli/install.py,sha256=HKxuxwIcm6yf9JFwGK6XpCpHkOgPRVnta3MzfHE34Gk,14793
|
|
13
|
+
openral_cli/main.py,sha256=ExGdLcmP2YLZik15K7fux5jojKdq3CXsjbItSR1FxkI,158280
|
|
14
|
+
openral_cli/prompt.py,sha256=FH6mCLRNUDwsobY11vv8CG-j6qwwwXHd26z4jqMj308,4794
|
|
15
|
+
openral_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
openral_cli/robot.py,sha256=3axrbc1lcnynDd4ns5ncUA3gf3ByVA9kuebRgvtgNEo,14069
|
|
17
|
+
openral_cli-0.1.0.dist-info/METADATA,sha256=q3NT7VxZigWtgG0aQgesvI_ZdJzVB7KzAOxwoBvUkQ0,1034
|
|
18
|
+
openral_cli-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
19
|
+
openral_cli-0.1.0.dist-info/entry_points.txt,sha256=MA7LRsTMvTgm_JLd-6QjU9tRzWaNipFGZlyaFNLGNIU,49
|
|
20
|
+
openral_cli-0.1.0.dist-info/RECORD,,
|