Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
**/*.part
onshape/jdcobot100/assets/
onshape/jdcobot100/jdcobot100.urdf
onshape/jdcobot100/jdcobot100.xml
onshape/jdcobot100/scene.xml
42 changes: 28 additions & 14 deletions onshape/jdcobot100/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,63 @@ This directory contains reproducible `onshape-to-robot` configurations for the
jdcobot100 four-DOF arm. Credentials are read from environment variables and
must never be committed.

The root configs point at the in-progress DAPIER assembly. It currently exports
zero joints because the parts are separate root nodes and the revolute mates
have not been added yet. The `reference/` configs point at the completed
four-joint assembly used by the JD-edu exercise and provide the expected
topology:
The root configs point at the repaired DAPIER assembly. On 2026-08-03 it was
rebuilt around five subassemblies and verified to export one rooted, four-joint
kinematic tree. The `reference/` configs point at the JD-edu exercise assembly
used as the topology reference:

```text
base_sub_assembly
-> dof_base (Onshape mate: base_shoulder)
-> dof_base
shoulder_sub_assembly
-> dof_shoulder (Onshape mate: shoulder_arm1)
-> dof_shoulder
arm_1_sub_asssembly
-> dof_elbow (Onshape mate: arm1_arm2)
-> dof_elbow
arm_2_sub_asssembly_copy_1
-> dof_wrist_pitch (Onshape mate: arn2_end_arm)
-> dof_wrist_pitch
end_arm_sub_assembly
```

The local credentials file is loaded from
`~/.config/onshape-to-robot/env`. Regenerate and normalize both reference
`~/.config/onshape-to-robot/env`. Regenerate and normalize the repaired working
models with:

```bash
onshape/jdcobot100/export_working.sh
```

Regenerate and normalize both reference models with:

```bash
onshape/jdcobot100/export_reference.sh
```

To inspect the unfinished DAPIER workspace directly:
To inspect the repaired DAPIER workspace directly without normalization:

```bash
onshape-to-robot --safe onshape/jdcobot100/config.json
onshape-to-robot --safe onshape/jdcobot100/config.mujoco.json
```

The first export generates `jdcobot100.urdf`; the second generates
`jdcobot100.xml`. Meshes are written below `assets/`. The wrapper then maps the
Onshape mate names to the stable simulator names shown above.
`jdcobot100.xml`. Meshes are written below `assets/`. The working wrapper maps
the exporter names `base`, `shoulder`, `elbow`, and `wrist_pitch` to the stable
simulator names shown above. The raw Onshape mate names are `dof_base`,
`dof_shoulder`, `dof_elbow`, and `dof_wrist_pitch`; onshape-to-robot removes the
`dof_` prefix while exporting.

The repaired Onshape assembly contains eight hidden explicit mate connectors
and four Revolute mates. Two legacy `ParametricPartStudio` features remain
suppressed so that they do not regenerate the old flattened servo instances.

Run the headless engine checks with the Python environment installed for
`onshape-to-robot`:

```bash
~/.local/share/uv/tools/onshape-to-robot/bin/python \\
onshape/jdcobot100/validate_models.py
onshape/jdcobot100/validate_models.py \\
--urdf onshape/jdcobot100/jdcobot100.urdf \\
--mjcf onshape/jdcobot100/jdcobot100.xml
```

The check loads the URDF in PyBullet and the MJCF in MuJoCo, verifies all four
Expand Down
21 changes: 21 additions & 0 deletions onshape/jdcobot100/export_working.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
credential_file="${XDG_CONFIG_HOME:-${HOME}/.config}/onshape-to-robot/env"
exporter="${ONSHAPE_TO_ROBOT_BIN:-${HOME}/.local/bin/onshape-to-robot}"

if [[ -f "${credential_file}" ]]; then
# shellcheck disable=SC1090
source "${credential_file}"
fi

: "${ONSHAPE_API:?ONSHAPE_API is not set}"
: "${ONSHAPE_ACCESS_KEY:?ONSHAPE_ACCESS_KEY is not set}"
: "${ONSHAPE_SECRET_KEY:?ONSHAPE_SECRET_KEY is not set}"

"${exporter}" --safe "${script_dir}/config.json"
"${exporter}" --safe "${script_dir}/config.mujoco.json"
python3 "${script_dir}/normalize_joint_names.py" \
"${script_dir}/jdcobot100.urdf" \
"${script_dir}/jdcobot100.xml"
44 changes: 29 additions & 15 deletions onshape/jdcobot100/normalize_joint_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@

import argparse
from pathlib import Path
import xml.etree.ElementTree as ET


JOINT_NAME_MAP = {
# Current DAPIER assembly. onshape-to-robot removes the dof_ prefix.
"base": "dof_base",
"shoulder": "dof_shoulder",
"elbow": "dof_elbow",
"wrist_pitch": "dof_wrist_pitch",
# Original JD-edu reference assembly.
"base_shoulder": "dof_base",
"shoulder_arm1": "dof_shoulder",
"arm1_arm2": "dof_elbow",
Expand All @@ -16,24 +23,31 @@


def normalize(path: Path) -> None:
"""Replace quoted joint identifiers without touching link or mesh names."""
contents = path.read_text(encoding="utf-8")

for old_name, new_name in JOINT_NAME_MAP.items():
old_token = f'"{old_name}"'
new_token = f'"{new_name}"'
if old_token in contents:
contents = contents.replace(old_token, new_token)

missing = [
new_name
for new_name in JOINT_NAME_MAP.values()
if f'"{new_name}"' not in contents
]
"""Normalize joint attributes without changing links, meshes, or materials."""
tree = ET.parse(path)
root = tree.getroot()

for element in root.iter():
if element.tag == "joint":
name = element.attrib.get("name")
if name in JOINT_NAME_MAP:
element.attrib["name"] = JOINT_NAME_MAP[name]

for attribute in ("joint", "joint1", "joint2"):
name = element.attrib.get(attribute)
if name in JOINT_NAME_MAP:
element.attrib[attribute] = JOINT_NAME_MAP[name]

joint_names = {
element.attrib["name"]
for element in root.iter("joint")
if "name" in element.attrib
}
missing = sorted(set(JOINT_NAME_MAP.values()) - joint_names)
if missing:
raise ValueError(f"{path}: normalized joints not found: {', '.join(missing)}")

path.write_text(contents, encoding="utf-8")
tree.write(path, encoding="unicode", xml_declaration=True)
print(f"normalized {path}")


Expand Down