Skip to content

Clean Go2 Static Transform frames#2236

Open
jeff-hykin wants to merge 19 commits into
mainfrom
jeff/fix/go2_tf
Open

Clean Go2 Static Transform frames#2236
jeff-hykin wants to merge 19 commits into
mainfrom
jeff/fix/go2_tf

Conversation

@jeff-hykin
Copy link
Copy Markdown
Member

@jeff-hykin jeff-hykin commented May 23, 2026

  • Uses self.frame_id
  • Uses urdf instead of constants, adds config.py to match g1
  • Publishes static stuff in the exisitng static loop instead of every Odom event
  • Makes the other frames overideable

NOTE: I don't want this to become a whole static-publishing system - just trying to make go2 a bit cleaner

How to Test

dimos --replay run unitree-go2

Contributor License Agreement

  • I have read and approved the CLA.



# base_link → camera_optical extrinsics (applied at render time for image observations)
_BASE_TO_OPTICAL = Transform(
Copy link
Copy Markdown
Member Author

@jeff-hykin jeff-hykin May 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: I'm not fixing this hardcoded problem, just trying to clean up the go2 side


self._camera_info_thread = Thread(
target=self.publish_camera_info,
self._static_publish_thread = Thread(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upgrade from just camera to all static info


# Static camera mount chain: base_link -> camera_link -> camera_optical.
# TODO we need a standardized way to specify this for all cameras in dimos
BASE_TO_OPTICAL: Transform = Transform(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these were moved to the urdf

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 23, 2026

Greptile Summary

This PR centralises Go2 frame definitions into a new dimos/robot/unitree/go2/config.py and a richer go2.urdf, replacing hardcoded quaternion/translation constants across connection.py, dimsim_connection.py, and rerun.py with URDF-parsed transforms via RobotConfig.static_transforms. The static transform loop is also separated from the odometry callback, publishing camera/lidar frames at a configurable rate instead of on every odom event.

  • New config.py: exposes Go2Config (a RobotConfig singleton), camera_info_static(), and odom_to_tf(), consolidating logic that was scattered across three files.
  • RobotConfig enhancements: adds body_frame (skips floating joints), all_frame_ids, and static_transforms (reads fixed-joint origins from the URDF), all as cached_property.
  • connection.py cleanup: _publish_tf now only publishes the dynamic world→base_link transform; a new _static_publish daemon thread handles the static chain (camera_link, camera_optical, lidar_link) and camera info at a configurable rate, with frame IDs overrideable via ConnectionConfig.frame_mapping.

Confidence Score: 5/5

Safe to merge — the previously reported crashes and broken imports are all resolved, and the new URDF-driven transform logic is mathematically equivalent to the constants it replaces.

The refactor cleanly separates dynamic (odom) and static (camera/lidar) TF publishing, the RPY→quaternion conversion in go2.urdf produces the same values as the removed hardcoded constants, and all previously broken import/classmethod signatures are fixed. Only minor annotation and header nits remain.

dimos/robot/unitree/go2/config.py — misleading parameter name and duplicate license header; dimos/robot/unitree/dimsim_connection.py — passes PoseStamped to a function typed Odometry.

Important Files Changed

Filename Overview
dimos/robot/unitree/go2/config.py New Go2-specific config module: exposes camera_info_static(), Go2Config singleton, and odom_to_tf(); parameter name cls and type annotation Odometry are misleading since callers (dimsim_connection, test_moment) pass PoseStamped; duplicate Apache license header at lines 1–27.
dimos/robot/unitree/go2/connection.py Refactors static transform publishing into a single _static_publish thread; removes duplicated per-Odom static TF publishing; uses ConnectionConfig fields for frame_id, parent_frame_id, static_transforms, and frame_mapping.
dimos/robot/config.py Adds body_frame (skips past floating joints), all_frame_ids, and static_transforms cached properties to RobotConfig; the math (RPY→quaternion via Quaternion.from_euler) is consistent with the previous hardcoded values.
dimos/robot/model_parser.py Adds origin_xyz and origin_rpy fields to JointDescription and parses them from URDF; _parse_triple helper added with appropriate error message.
dimos/robot/unitree/go2/go2.urdf Adds camera_link, camera_optical, and lidar_link joints with correct transforms; camera_optical rpy matches the previously hardcoded quaternion (-0.5, 0.5, -0.5, 0.5).
dimos/robot/unitree/dimsim_connection.py Removes duplicate local _odom_to_tf; calls the shared odom_to_tf from config.py, but passes a PoseStamped while the function is typed Odometry.
dimos/utils/testing/test_moment.py Import and call sites updated to use the new config-level odom_to_tf and camera_info_static; previously broken import is now correct.
dimos/perception/detection/conftest.py Updated to import odom_to_tf and camera_info_static from config.py; odom_frame is Odometry here so the type matches correctly.
dimos/memory2/vis/space/rerun.py Replaces the module-level _BASE_TO_OPTICAL constant with Go2Config.static_transforms lookups; hardcoded string keys remain a latent KeyError risk flagged in a prior review.
dimos/constants.py Adds DEFAULT_WORLD_FRAME and DEFAULT_ROBOT_FRAME constants; straightforward.

Sequence Diagram

sequenceDiagram
    participant GO2Connection
    participant _static_publish thread
    participant TF
    participant camera_info Out

    GO2Connection->>_static_publish thread: start (daemon)
    Note over _static_publish thread: build remap dict once<br/>(Go2Config.body_frame → self.frame_id,<br/>**frame_mapping)
    Note over _static_publish thread: build stamped_statics list once<br/>(apply remap to frame IDs)

    loop every 1/static_publish_rate seconds
        _static_publish thread->>TF: publish(camera_link, camera_optical, lidar_link transforms with ts=now)
        _static_publish thread->>camera_info Out: publish(camera_info_static)
    end

    GO2Connection->>GO2Connection: _publish_tf(PoseStamped)
    GO2Connection->>TF: publish(world→base_link with ts from msg)
Loading

Reviews (9): Last reviewed commit: "-" | Re-trigger Greptile

Comment thread dimos/memory2/vis/space/rerun.py Outdated
Comment thread dimos/robot/unitree/go2/connection.py Outdated
Comment thread dimos/robot/config.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 30 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/robot/unitree/go2/connection.py 41.66% 14 Missing ⚠️
dimos/robot/config.py 53.57% 12 Missing and 1 partial ⚠️
dimos/robot/model_parser.py 76.92% 1 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@jeff-hykin jeff-hykin marked this pull request as draft May 24, 2026 00:06
Comment thread dimos/robot/unitree/go2/connection.py
jeff-hykin added a commit that referenced this pull request May 24, 2026
…removed GO2Connection._odom_to_tf

PR #2236 removed the GO2Connection._odom_to_tf classmethod in favor of
URDF-derived static transforms via Go2Config. Also removed the
connection._camera_info_static() module-level helper in favor of
camera_info_static() in dimos.robot.unitree.go2.config. The detection conftest
still called both, so the get_moment fixture broke (locally, since
test_detection2d carries @pytest.mark.skipif_in_ci).

Build the transform list inline (world/base_link from the odom pose plus the
static fixed-joint transforms from Go2Config.static_transforms with the odom
timestamp stamped on) and switch to the new camera_info_static() location.
jeff-hykin added a commit that referenced this pull request May 24, 2026
Records solutions for the four greptile-apps review comments on this PR:
- P1 #3293654340 (dict merge order in _static_publish) — fixed
- P2 #3293617061 (typo 'remape') — naturally resolved by Jeff's 'improve'
  commit which rewrote the surrounding code, comment line no longer exists
- P2 #3293617047 (hardcoded key in rerun.py) — skipped, Jeff already
  acknowledged inline that this is an out-of-scope cleanup
- P2 #3293617084 (path_to walks non-fixed joints) — skipped, only affects
  non-Go2 robots with floating-joint URDFs, belongs in a follow-up RobotConfig PR
@jeff-hykin jeff-hykin marked this pull request as ready for review May 24, 2026 16:26
@jeff-hykin jeff-hykin enabled auto-merge (squash) May 24, 2026 16:27
Use Go2Config.static_transforms + local _odom_to_tf helper, and the
public camera_info_static() (was the removed _camera_info_static).
if img is not None:
# Apply base→optical extrinsics for camera frustum rendering
world_T_optical = Transform.from_pose("world", obs.pose_stamped) + _BASE_TO_OPTICAL
# TODO: fix there should be a standard way to set camera frames
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this is not a new TODO)

Comment thread dimos/utils/testing/test_moment.py Outdated
from dimos.protocol.tf.tf import TF
from dimos.robot.unitree.go2 import connection
from dimos.robot.unitree.go2.config import camera_info_static
from dimos.robot.unitree.go2.connection import connection
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 ImportError at module load — connection is not a name in the module

from dimos.robot.unitree.go2.connection import connection looks for a symbol named connection inside the connection module. No such module-level name exists there (the connection local only appears inside deploy()). This will raise ImportError: cannot import name 'connection' from 'dimos.robot.unitree.go2.connection' before a single test runs. The original form from dimos.robot.unitree.go2 import connection (importing the module itself) is what the subsequent connection.GO2Connection._odom_to_tf call actually needs.

Comment thread dimos/robot/unitree/go2/connection.py Outdated
Comment on lines +263 to +286
@classmethod
def _odom_to_tf(cls, odom: PoseStamped) -> list[Transform]:
camera_link = Transform(
translation=Vector3(0.3, 0.0, 0.0),
rotation=Quaternion(0.0, 0.0, 0.0, 1.0),
frame_id="base_link",
child_frame_id="camera_link",
ts=odom.ts,
def _odom_to_tf(cls: Odometry) -> list[Transform]:
"""[world→base_link, base_link→camera_link, camera_link→camera_optical] for tests."""
base_link = Transform(
translation=cls.position,
rotation=cls.orientation,
frame_id=cls.frame_id or DEFAULT_WORLD_FRAME,
child_frame_id=DEFAULT_ROBOT_FRAME,
ts=cls.ts,
)

camera_optical = Transform(
translation=Vector3(0.0, 0.0, 0.0),
rotation=Quaternion(-0.5, 0.5, -0.5, 0.5),
frame_id="camera_link",
child_frame_id="camera_optical",
ts=odom.ts,
)

return [
Transform.from_pose("base_link", odom),
camera_link,
camera_optical,
statics = [
Transform(
translation=t.translation,
rotation=t.rotation,
frame_id=t.frame_id,
child_frame_id=t.child_frame_id,
ts=cls.ts,
)
for t in (
Go2Config.static_transforms["camera_link"],
Go2Config.static_transforms["camera_optical"],
)
]
return [base_link, *statics]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 @classmethod + cls: Odometry is a broken signature — TypeError at call site

@classmethod causes Python to automatically pass the class (GO2Connection) as the first argument. The method has only one declared parameter (cls), so any call like GO2Connection._odom_to_tf(odom) raises TypeError: _odom_to_tf() takes 1 positional argument but 2 were given. The type annotation cls: Odometry makes it look like a regular function but the decorator means cls is always the class. test_moment.py:55 still calls this method directly, so this failure is live. The decorator should be @staticmethod and the first parameter renamed to odom: Odometry.

Suggested change
@classmethod
def _odom_to_tf(cls, odom: PoseStamped) -> list[Transform]:
camera_link = Transform(
translation=Vector3(0.3, 0.0, 0.0),
rotation=Quaternion(0.0, 0.0, 0.0, 1.0),
frame_id="base_link",
child_frame_id="camera_link",
ts=odom.ts,
def _odom_to_tf(cls: Odometry) -> list[Transform]:
"""[world→base_link, base_link→camera_link, camera_link→camera_optical] for tests."""
base_link = Transform(
translation=cls.position,
rotation=cls.orientation,
frame_id=cls.frame_id or DEFAULT_WORLD_FRAME,
child_frame_id=DEFAULT_ROBOT_FRAME,
ts=cls.ts,
)
camera_optical = Transform(
translation=Vector3(0.0, 0.0, 0.0),
rotation=Quaternion(-0.5, 0.5, -0.5, 0.5),
frame_id="camera_link",
child_frame_id="camera_optical",
ts=odom.ts,
)
return [
Transform.from_pose("base_link", odom),
camera_link,
camera_optical,
statics = [
Transform(
translation=t.translation,
rotation=t.rotation,
frame_id=t.frame_id,
child_frame_id=t.child_frame_id,
ts=cls.ts,
)
for t in (
Go2Config.static_transforms["camera_link"],
Go2Config.static_transforms["camera_optical"],
)
]
return [base_link, *statics]
@staticmethod
def _odom_to_tf(odom: Odometry) -> list[Transform]:
"""[world→base_link, base_link→camera_link, camera_link→camera_optical] for tests."""
base_link = Transform(
translation=odom.position,
rotation=odom.orientation,
frame_id=odom.frame_id or DEFAULT_WORLD_FRAME,
child_frame_id=DEFAULT_ROBOT_FRAME,
ts=odom.ts,
)
statics = [
Transform(
translation=t.translation,
rotation=t.rotation,
frame_id=t.frame_id,
child_frame_id=t.child_frame_id,
ts=odom.ts,
)
for t in (
Go2Config.static_transforms["camera_link"],
Go2Config.static_transforms["camera_optical"],
)
]
return [base_link, *statics]

Comment thread dimos/robot/unitree/go2/config.py Outdated
from dimos.msgs.nav_msgs.Odometry import Odometry
from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
from dimos.robot.config import RobotConfig
from dimos.robot.unitree.go2.config import Go2Config, camera_info_static
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Self-import crashes the entire module on load

Line 39 imports Go2Config and camera_info_static from dimos.robot.unitree.go2.config — which is this very file. At the time Python executes this statement, neither name is defined yet (camera_info_static is at line 46, Go2Config at line 77). Python raises ImportError: cannot import name 'Go2Config' from partially initialized module 'dimos.robot.unitree.go2.config', which cascades to connection.py, conftest.py, rerun.py, and test_moment.py — every file that imports from this module. The line should simply be deleted; everything needed is already defined in the same file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant