152 lines
4.6 KiB
Markdown
152 lines
4.6 KiB
Markdown
# SDF generator contract
|
|
|
|
Use this reference when creating or editing Python sources that generate SDFormat/SDF files.
|
|
|
|
## Source of truth
|
|
|
|
The Python source that defines `gen_sdf()` is the source of truth. The configured `.sdf` output is generated and should not be hand-edited.
|
|
|
|
## Required shape
|
|
|
|
`gen_sdf()` must be a top-level zero-argument function. Prefer returning the root `xml.etree.ElementTree.Element` for a complete SDFormat document.
|
|
|
|
```python
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
def gen_sdf():
|
|
sdf = ET.Element("sdf", {"version": "1.12"})
|
|
model = ET.SubElement(sdf, "model", {"name": "sample_robot"})
|
|
ET.SubElement(model, "link", {"name": "base_link"})
|
|
return sdf
|
|
```
|
|
|
|
The root must be `<sdf>` and must include a non-empty `version` attribute. Default new outputs to `version="1.12"` unless the user or target simulator requires another version.
|
|
|
|
## Accepted return values
|
|
|
|
Preferred:
|
|
|
|
```python
|
|
def gen_sdf():
|
|
return sdf_root_element
|
|
```
|
|
|
|
Accepted for compatibility:
|
|
|
|
```python
|
|
def gen_sdf():
|
|
return """<?xml version="1.0"?>
|
|
<sdf version="1.12">...</sdf>
|
|
"""
|
|
```
|
|
|
|
Envelope form:
|
|
|
|
```python
|
|
def gen_sdf():
|
|
return {
|
|
"xml": sdf_root_element,
|
|
"assumptions": [
|
|
{"code": "mesh_units", "message": "Assumed mesh units are meters."},
|
|
"Assumed lidar frame is coincident with lidar_link.",
|
|
],
|
|
"warnings": [
|
|
{"code": "plugin_unverified", "message": "Camera plugin filename was not verified in the target simulator."}
|
|
],
|
|
"metadata": {
|
|
"target_consumer": "Gazebo Harmonic",
|
|
"sdf_version": "1.12",
|
|
},
|
|
}
|
|
```
|
|
|
|
Envelope rules:
|
|
|
|
- `xml` is required and may be an XML element or XML string.
|
|
- `assumptions`, when present, must be a list of strings or dicts with `code`, `message`, and optional `source`.
|
|
- `warnings`, when present, must be a list of strings or dicts with `code`, `message`, and optional `source`.
|
|
- `metadata`, when present, must be a JSON-serializable dict with scalar values.
|
|
- Output-path fields such as `sdf_output` are unsupported; select output paths with CLI targets.
|
|
- Unsupported envelope fields should fail with a clear error.
|
|
|
|
Use envelope assumptions to make spatial, physical, simulator, and resource assumptions auditable. Do not hide guesses in XML literals.
|
|
|
|
## Output path
|
|
|
|
The generated `.sdf` output path is selected by the CLI:
|
|
|
|
- plain source target: sibling `.sdf` beside the source;
|
|
- `-o` / `--output`: override for one plain target;
|
|
- `SOURCE.py=OUTPUT.sdf`: per-target override for multi-target generation.
|
|
|
|
The generator should not write the output file itself.
|
|
|
|
## Source-relative asset paths
|
|
|
|
Do not rely on the shell working directory for mesh or resource paths inside generator code. Prefer source-relative constants:
|
|
|
|
```python
|
|
from pathlib import Path
|
|
|
|
SOURCE_DIR = Path(__file__).resolve().parent
|
|
MESH_DIR = SOURCE_DIR / "meshes"
|
|
```
|
|
|
|
For model packages, prefer URI conventions understood by the target simulator, such as `model://...`, `package://...`, or stable relative paths from the generated `.sdf` location.
|
|
|
|
## Pose and frame discipline
|
|
|
|
Before emitting `<pose>`, `<frame>`, `<joint>`, `<axis>`, `<visual>`, `<collision>`, sensor, or plugin placement elements, fill out the design ledger:
|
|
|
|
- pose value;
|
|
- pose frame or `relative_to`;
|
|
- joint axis and `expressed_in` frame;
|
|
- positive-motion convention;
|
|
- mesh units and scale;
|
|
- source of every value.
|
|
|
|
Use constants with names that expose assumptions:
|
|
|
|
```python
|
|
ASSUMED_BASE_TO_LIDAR_Z_M = 0.18
|
|
ASSUMED_LIDAR_YAW_RAD = 0.0
|
|
```
|
|
|
|
Do not hide spatial guesses inside XML literals.
|
|
|
|
## Minimal model example with explicit pose intent
|
|
|
|
```python
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
def text(parent, tag, value, attrib=None):
|
|
child = ET.SubElement(parent, tag, attrib or {})
|
|
child.text = str(value)
|
|
return child
|
|
|
|
|
|
def pose(parent, xyz=(0, 0, 0), rpy=(0, 0, 0), *, relative_to=None):
|
|
attrib = {"relative_to": relative_to} if relative_to else {}
|
|
return text(parent, "pose", " ".join(str(v) for v in (*xyz, *rpy)), attrib)
|
|
|
|
|
|
def gen_sdf():
|
|
sdf = ET.Element("sdf", {"version": "1.12"})
|
|
model = ET.SubElement(sdf, "model", {"name": "sample_robot"})
|
|
pose(model, relative_to="world")
|
|
|
|
base = ET.SubElement(model, "link", {"name": "base_link"})
|
|
visual = ET.SubElement(base, "visual", {"name": "base_visual"})
|
|
geometry = ET.SubElement(visual, "geometry")
|
|
box = ET.SubElement(geometry, "box")
|
|
text(box, "size", "0.4 0.3 0.1")
|
|
|
|
return {
|
|
"xml": sdf,
|
|
"assumptions": ["base_link is coincident with the model frame."],
|
|
"metadata": {"target_consumer": "visualization"},
|
|
}
|
|
```
|