247 lines
6.6 KiB
Python
247 lines
6.6 KiB
Python
# json_to_urdf.py
|
|
import json
|
|
import math
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
from xml.dom import minidom
|
|
|
|
# ============================================================
|
|
# CONFIG
|
|
# ============================================================
|
|
|
|
ROBOT_JSON = "robot.json"
|
|
OUTPUT_URDF = "robot.urdf"
|
|
|
|
# ============================================================
|
|
# HELPERS
|
|
# ============================================================
|
|
|
|
|
|
def mm_to_m(v):
|
|
return float(v) / 1000.0
|
|
|
|
|
|
def vec_mm_to_m(v):
|
|
return [mm_to_m(x) for x in v]
|
|
|
|
|
|
def deg_to_rad(v):
|
|
return math.radians(float(v))
|
|
|
|
|
|
def vec_deg_to_rad(v):
|
|
return [deg_to_rad(x) for x in v]
|
|
|
|
|
|
def xyz_str(v):
|
|
return f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}"
|
|
|
|
|
|
# ============================================================
|
|
# LOAD JSON
|
|
# ============================================================
|
|
|
|
with open(ROBOT_JSON, "r", encoding="utf-8") as f:
|
|
robot_json = json.load(f)
|
|
|
|
links = robot_json.get("links", {})
|
|
|
|
# ============================================================
|
|
# ROOT
|
|
# ============================================================
|
|
|
|
robot_name = Path(ROBOT_JSON).stem
|
|
robot = ET.Element("robot")
|
|
robot.set("name", robot_name)
|
|
|
|
# ============================================================
|
|
# CREATE LINKS
|
|
# ============================================================
|
|
|
|
for link_name, link_info in links.items():
|
|
|
|
urdf_link = ET.SubElement(robot, "link")
|
|
urdf_link.set("name", link_name)
|
|
|
|
# --------------------------------------------------------
|
|
# VISUALS
|
|
# --------------------------------------------------------
|
|
|
|
models = link_info.get("model", [])
|
|
|
|
for model in models:
|
|
|
|
stl_file = model.get("stlFile")
|
|
if not stl_file:
|
|
continue
|
|
|
|
visual = ET.SubElement(urdf_link, "visual")
|
|
|
|
origin = ET.SubElement(visual, "origin")
|
|
|
|
xyz = vec_mm_to_m(model.get("originOfModel", [0, 0, 0]))
|
|
rpy = vec_deg_to_rad(model.get("rotationOfModelDegree", [0, 0, 0]))
|
|
|
|
origin.set("xyz", xyz_str(xyz))
|
|
origin.set("rpy", xyz_str(rpy))
|
|
|
|
geometry = ET.SubElement(visual, "geometry")
|
|
|
|
mesh = ET.SubElement(geometry, "mesh")
|
|
mesh.set("filename", stl_file)
|
|
mesh.set("scale", "0.001 0.001 0.001")
|
|
|
|
# --------------------------------------------------------
|
|
# SIMPLE COLLISION FROM SKELETON
|
|
# --------------------------------------------------------
|
|
|
|
skeleton = link_info.get("skeleton")
|
|
|
|
if isinstance(skeleton, dict):
|
|
|
|
p1 = skeleton.get("from", [0, 0, 0])
|
|
p2 = skeleton.get("to", [0, 0, 0])
|
|
|
|
dx = p2[0] - p1[0]
|
|
dy = p2[1] - p1[1]
|
|
dz = p2[2] - p1[2]
|
|
|
|
length_mm = math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
length_m = mm_to_m(length_mm)
|
|
|
|
radius_mm = skeleton.get("radius", 4)
|
|
radius_m = mm_to_m(radius_mm)
|
|
|
|
cx = (p1[0] + p2[0]) * 0.5
|
|
cy = (p1[1] + p2[1]) * 0.5
|
|
cz = (p1[2] + p2[2]) * 0.5
|
|
|
|
collision = ET.SubElement(urdf_link, "collision")
|
|
|
|
collision_origin = ET.SubElement(collision, "origin")
|
|
collision_origin.set(
|
|
"xyz",
|
|
xyz_str(vec_mm_to_m([cx, cy, cz]))
|
|
)
|
|
|
|
# ----------------------------------------------------
|
|
# CYLINDER ORIENTATION
|
|
# URDF cylinder points along Z
|
|
# ----------------------------------------------------
|
|
|
|
roll = 0.0
|
|
pitch = 0.0
|
|
yaw = 0.0
|
|
|
|
if abs(dx) >= abs(dy) and abs(dx) >= abs(dz):
|
|
pitch = math.radians(90)
|
|
yaw = 0.0
|
|
|
|
elif abs(dy) >= abs(dx) and abs(dy) >= abs(dz):
|
|
pitch = math.radians(90)
|
|
yaw = math.radians(90)
|
|
|
|
collision_origin.set(
|
|
"rpy",
|
|
xyz_str([roll, pitch, yaw])
|
|
)
|
|
|
|
geometry = ET.SubElement(collision, "geometry")
|
|
|
|
cylinder = ET.SubElement(geometry, "cylinder")
|
|
cylinder.set("radius", f"{radius_m:.6f}")
|
|
cylinder.set("length", f"{length_m:.6f}")
|
|
|
|
# ============================================================
|
|
# CREATE JOINTS
|
|
# ============================================================
|
|
|
|
for link_name, link_info in links.items():
|
|
|
|
parent_name = link_info.get("parent")
|
|
|
|
if parent_name is None:
|
|
continue
|
|
|
|
joint_info = link_info.get("jointToParent", {})
|
|
|
|
joint_name = joint_info.get("name", f"joint_{parent_name}_{link_name}")
|
|
joint_type = joint_info.get("type", "fixed")
|
|
|
|
if joint_type == "linear":
|
|
urdf_joint_type = "prismatic"
|
|
elif joint_type == "revolute":
|
|
urdf_joint_type = "revolute"
|
|
else:
|
|
urdf_joint_type = "fixed"
|
|
|
|
joint = ET.SubElement(robot, "joint")
|
|
joint.set("name", joint_name)
|
|
joint.set("type", urdf_joint_type)
|
|
|
|
# --------------------------------------------------------
|
|
# PARENT / CHILD
|
|
# --------------------------------------------------------
|
|
|
|
parent = ET.SubElement(joint, "parent")
|
|
parent.set("link", parent_name)
|
|
|
|
child = ET.SubElement(joint, "child")
|
|
child.set("link", link_name)
|
|
|
|
# --------------------------------------------------------
|
|
# ORIGIN
|
|
# --------------------------------------------------------
|
|
|
|
origin_xyz = joint_info.get("origin", [0, 0, 0])
|
|
origin_rpy = joint_info.get("rotation", [0, 0, 0])
|
|
|
|
origin = ET.SubElement(joint, "origin")
|
|
origin.set(
|
|
"xyz",
|
|
xyz_str(vec_mm_to_m(origin_xyz))
|
|
)
|
|
origin.set(
|
|
"rpy",
|
|
xyz_str(vec_deg_to_rad(origin_rpy))
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# AXIS
|
|
# --------------------------------------------------------
|
|
|
|
axis = joint_info.get("axis", [1, 0, 0])
|
|
|
|
axis_tag = ET.SubElement(joint, "axis")
|
|
axis_tag.set("xyz", xyz_str(axis))
|
|
|
|
# --------------------------------------------------------
|
|
# DEFAULT LIMITS
|
|
# --------------------------------------------------------
|
|
|
|
if urdf_joint_type in ["revolute", "prismatic"]:
|
|
|
|
limit = ET.SubElement(joint, "limit")
|
|
|
|
if urdf_joint_type == "revolute":
|
|
limit.set("lower", f"{-math.pi:.6f}")
|
|
limit.set("upper", f"{math.pi:.6f}")
|
|
else:
|
|
limit.set("lower", "-1.0")
|
|
limit.set("upper", "1.0")
|
|
|
|
limit.set("effort", "100")
|
|
limit.set("velocity", "10")
|
|
|
|
# ============================================================
|
|
# PRETTY XML
|
|
# ============================================================
|
|
|
|
xml_string = ET.tostring(robot, encoding="utf-8")
|
|
pretty = minidom.parseString(xml_string).toprettyxml(indent=" ")
|
|
|
|
with open(OUTPUT_URDF, "w", encoding="utf-8") as f:
|
|
f.write(pretty)
|
|
|
|
print("URDF written:", OUTPUT_URDF)
|