Source code for embodichain_robots.the_robot_studio.so101.assets

# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

"""Pinned official SO101 follower descriptions, meshes and motor references."""

from __future__ import annotations

import hashlib
import os
from pathlib import Path
from tempfile import NamedTemporaryFile
from urllib.request import urlopen

import open3d as o3d

from embodichain.data.constants import EMBODICHAIN_DEFAULT_DATA_ROOT
from embodichain.data.dataset import EmbodiChainDataset, _dataset_download_lock

__all__ = ["SO101FollowerAssets"]

_REVISION = "a758567c3978dfeefe282ede0500085a48fe8f78"
_SOURCE_ROOT = f"https://raw.githubusercontent.com/TheRobotStudio/SO-ARM100/{_REVISION}"
_URDF = "so101_new_calib.urdf"
_URDF_MD5 = "308645ed050642dbbfc21814588a286c"
_FILES = {
    "README.md": "c6df213e471e4573ba6384cb957e9eee9644f5d671272f98f67d28866333f747",
    "assets/base_motor_holder_so101_v1.stl": "8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424",
    "assets/base_so101_v2.stl": "bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d",
    "assets/motor_holder_so101_base_v1.stl": "31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b",
    "assets/motor_holder_so101_wrist_v1.stl": "887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011",
    "assets/moving_jaw_so101_v1.stl": "785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d",
    "assets/rotation_pitch_so101_v1.stl": "9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34",
    "assets/sts3215_03a_no_horn_v1.stl": "75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887",
    "assets/sts3215_03a_v1.stl": "a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0",
    "assets/under_arm_so101_v1.stl": "d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71",
    "assets/upper_arm_so101_v1.stl": "475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f",
    "assets/waveshare_mounting_plate_so101_v2.stl": "e197e24005a07d01bbc06a8c42311664eaeda415bf859f68fa247884d0f1a6e9",
    "assets/wrist_roll_follower_so101_v1.stl": "4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4",
    "assets/wrist_roll_pitch_so101_v2.stl": "6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae",
    "joints_properties.xml": "abbcc92761943befe3a24b1c13e82c62c5b8c2914303818f3c870b1f51cd45ed",
    "so101_new_calib.urdf": "3a65d2d35e68a8d2f0c2cc176d19b884506543c93ba72980145b80abe276022c",
    "LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
}


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def _ensure_file(url: str, destination: Path, expected: str) -> None:
    if destination.is_file() and _sha256(destination) == expected:
        return
    destination.parent.mkdir(parents=True, exist_ok=True)
    temporary: Path | None = None
    try:
        with NamedTemporaryFile(dir=destination.parent, delete=False) as output:
            temporary = Path(output.name)
            with urlopen(url, timeout=60) as source:
                for block in iter(lambda: source.read(1024 * 1024), b""):
                    output.write(block)
        actual = _sha256(temporary)
        if actual != expected:
            raise ValueError(
                f"SO101 asset checksum mismatch for {destination.name}: {actual}"
            )
        os.replace(temporary, destination)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)


[docs] class SO101FollowerAssets(EmbodiChainDataset): """Download the official follower URDF and its dependencies at a fixed revision. The original Apache-2.0 license, CAD inertials, joint frames, limits and yellow/black URDF materials are preserved. Each source file is checked against its pinned SHA256, including when repairing an existing cache. Args: data_root: Optional asset cache root. Omission uses ``EMBODICHAIN_DATA_ROOT`` through the shared data constants. """ cache_prefix = f"SO101Follower_{_REVISION[:12]}"
[docs] def __init__(self, data_root: str | None = None) -> None: root = str( Path(EMBODICHAIN_DEFAULT_DATA_ROOT if data_root is None else data_root) .expanduser() .resolve() ) descriptor = o3d.data.DataDescriptor( f"{_SOURCE_ROOT}/Simulation/SO101/{_URDF}", _URDF_MD5 ) super().__init__(self.cache_prefix, descriptor, root) with _dataset_download_lock(root, self.cache_prefix): for relative, digest in _FILES.items(): if relative == _URDF: url = (Path(self.download_dir) / _URDF).as_uri() else: source = ( relative if relative == "LICENSE" else f"Simulation/SO101/{relative}" ) url = f"{_SOURCE_ROOT}/{source}" _ensure_file(url, Path(self.extract_dir) / relative, digest)