From 5824b99150f3f137838f66c1a5b8836fe3aff647 Mon Sep 17 00:00:00 2001 From: Alpenj <29724960+Alpenj@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:55:55 +0900 Subject: [PATCH] Validate repaired Onshape 4DOF export --- .gitignore | 2 + onshape/jdcobot100/README.md | 42 +++++++++++++------- onshape/jdcobot100/export_working.sh | 21 ++++++++++ onshape/jdcobot100/normalize_joint_names.py | 44 ++++++++++++++------- 4 files changed, 80 insertions(+), 29 deletions(-) create mode 100755 onshape/jdcobot100/export_working.sh diff --git a/.gitignore b/.gitignore index 8e1545c2..3c2c2fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ **/*.part onshape/jdcobot100/assets/ onshape/jdcobot100/jdcobot100.urdf +onshape/jdcobot100/jdcobot100.xml +onshape/jdcobot100/scene.xml diff --git a/onshape/jdcobot100/README.md b/onshape/jdcobot100/README.md index 414c3b50..78b177dc 100644 --- a/onshape/jdcobot100/README.md +++ b/onshape/jdcobot100/README.md @@ -4,33 +4,38 @@ 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 @@ -38,15 +43,24 @@ 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 diff --git a/onshape/jdcobot100/export_working.sh b/onshape/jdcobot100/export_working.sh new file mode 100755 index 00000000..35f0e914 --- /dev/null +++ b/onshape/jdcobot100/export_working.sh @@ -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" diff --git a/onshape/jdcobot100/normalize_joint_names.py b/onshape/jdcobot100/normalize_joint_names.py index 8a40b05f..85de41df 100755 --- a/onshape/jdcobot100/normalize_joint_names.py +++ b/onshape/jdcobot100/normalize_joint_names.py @@ -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", @@ -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}")