Skip to content

GO2 MCAP/DDS integration as memory2 backend#2314

Open
leshy wants to merge 21 commits into
mainfrom
feat/ivan/go2dds
Open

GO2 MCAP/DDS integration as memory2 backend#2314
leshy wants to merge 21 commits into
mainfrom
feat/ivan/go2dds

Conversation

@leshy
Copy link
Copy Markdown
Member

@leshy leshy commented May 31, 2026

implements MCAP as memory2 Store, allows for standard memory2 queries

cd dimos/robot/unitree/go2dds
python cli/render.py go2_china_office_indoor.mcap --seconds 100 --out test.rrd

in render.py - world lidar to odom alignment is very bad (single function though) builds full odom list and TS aligns lidar to it - will be changed after mem2 stream alignment is merged #2306

@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

leshy added 12 commits May 31, 2026 13:33
- add dimos/memory2/cli (app + render): `dimos mem rerun <store>` renders any
  memory2 store (mcap or .db) to rerun; no __init__.py (namespace package)
- cdr: type the spec param as Any so the generic decoder is mypy + ruff clean
The go2dds store/reader decode Go2 DDS mcap recordings via the `mcap` package;
add it so CI (and installs) that pull the unitree extra have it.
- move mcap from the unitree extra to unitree-dds (its real go2 DDS home)
- add dimos[unitree-dds] to the tests-self-hosted group so the go2dds store
  tests run on the self-hosted (ROS/cyclonedds) runner; drop the now-redundant
  unitree extra (unitree-dds implies it)
- memory2/store/mcap.py: import mcap lazily so the store module imports without
  the optional dep (keeps non-self-hosted test collection mcap-free)
- test_store.py: skip if mcap isn't installed
The pre-commit "insert license" hook prepended the full Apache header to files
that already carried the short 3-line form, leaving a stray second block. Remove
the duplicate from the 16 affected go2dds files.
The full `unitree-dds` extra pulls `cyclonedds`, whose wheel needs a CycloneDDS
C lib the ros-dev image doesn't expose at build time (and `uv sync` runs before
ROS is sourced). The go2dds store tests only decode mcap (pure-Python), so depend
on `mcap` directly and keep `unitree` for the rest. cyclonedds stays in the
`unitree-dds` extra for live-DDS use.
@leshy leshy marked this pull request as ready for review May 31, 2026 13:27
Comment thread dimos/robot/unitree/go2dds/ros.py
Comment thread dimos/robot/unitree/go2dds/cli/render.py Outdated
This was referenced May 31, 2026
- render_store: skip observations whose payload decoded to None (e.g. a
  truncated/corrupt JPEG from decode_compressed_image) instead of crashing
  on obs.data.to_rerun() (Greptile P1).
- go2dds render: log_lidar annotated PointCloud2, not PoseStamped (Greptile P2).
- CdrStructCodec.decode: assert the decode consumed all bytes — leftover
  bytes mean a wrong fixed-layout spec; fail loud rather than decode garbage.
  Verified end == len against the real recording (lowstate/sportmodestate).
@dimensionalOS dimensionalOS deleted a comment from greptile-apps Bot May 31, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 31, 2026

Greptile Summary

This PR introduces a full MCAP/DDS integration as a memory2 backend, allowing standard memory2 stream queries over Go2 DDS recordings. A generic McapStore / McapObservationStore pair handles read-only MCAP access (codec-injected, lazy-decode), and a Go2-specific Go2McapStore wires the codec set and stream names on top.

  • New MCAP store: McapStore reads the MCAP index at init time, exposes each decodable channel as a named stream with lazy payload decode and cheap O(1) counts for unfiltered queries.
  • Go2 DDS codec stack: cdr.py (generic CDR/XCDR1 decoder), codec.py (per-topic wire codecs), ros.py (CDR wire structs → dimos messages), and message dataclasses for all standard Go2 topics.
  • Render pipelines: cli/render.py composes stream transforms (leg-odom, IMU dead-reckoning, lidar, world-lidar + global voxel map) into a Rerun .rrd file.

Confidence Score: 5/5

The core MCAP store, CDR decoder, codec registry, and stream pipeline all work correctly for well-formed Go2 recordings; robustness gaps only surface on malformed or unclosed MCAP files.

All findings are confined to edge cases that do not occur in normal Go2 recordings. The happy path is sound.

dimos/robot/unitree/go2dds/ros.py (decode_pointcloud2 alignment and field guards) and dimos/memory2/store/mcap.py (silent empty store on missing statistics).

Important Files Changed

Filename Overview
dimos/memory2/store/mcap.py New generic MCAP-backed ObservationStore and Store. count() fast-path correctly detects filters (BeforeFilter is in q.filters). Minor robustness gap: silent empty store when MCAP has no statistics.
dimos/robot/unitree/go2dds/cli/render.py Pipeline compositions for leg-odom, IMU dead-reckoning, lidar, and world-lidar. world_lidar crashes with IndexError when odom has fewer than 2 messages (already noted); camera() correctly guards against None frames from decode_compressed_image.
dimos/robot/unitree/go2dds/ros.py CDR-to-dimos decoders for all Go2 channels. decode_pointcloud2 missing alignment guard before view(dt); could crash on malformed/truncated data where data.size % point_step != 0.
dimos/robot/unitree/go2dds/cdr.py Generic little-endian CDR/XCDR1 decoder driven by cdr_fields specs. Cursor slices do not bounds-check, so a truncated buffer yields silently short strings/arrays; struct.unpack_from would raise on primitives.
dimos/robot/unitree/go2dds/codec.py DDS wire codec registry. CdrStructCodec.decode uses assert for length validation, disabled under python -O.
dimos/robot/unitree/go2dds/store.py Thin Go2 wrapper over McapStore: supplies codec set, stream name map, and path resolution. Clean and minimal.
dimos/memory2/stream.py Adds to_time(), from_time(), range_time() windowing helpers and summary() to Stream. All new methods correctly handle None bounds and empty streams.
dimos/memory2/utils/progress.py New per-observation progress callback. Handles total=0 gracefully.
dimos/robot/unitree/go2dds/extrinsics.py Hardcoded L1 lidar extrinsics and camera mount. Well-commented with calibration provenance.

Sequence Diagram

sequenceDiagram
    participant CLI as render.py
    participant Store as Go2McapStore
    participant McapStore as McapStore
    participant Backend as Backend
    participant Obs as McapObservationStore
    participant MCAP as MCAP file

    CLI->>Store: Go2McapStore(path)
    Store->>McapStore: super().__init__(codecs, streams)
    McapStore->>MCAP: open + get_summary()
    MCAP-->>McapStore: channels, statistics
    McapStore-->>Store: _available, _stream_topic

    CLI->>Store: store.streams.sportmodestate.to_time(seconds)
    Store->>Backend: stream() returns Backend
    Backend->>Obs: "McapObservationStore(count=N)"
    CLI->>Obs: count() returns N index fast-path
    CLI->>Obs: query(q) returns iter
    Obs->>MCAP: open + iter_messages(topic)
    MCAP-->>Obs: schema, channel, message
    Obs-->>CLI: Observation with lazy _loader
    CLI->>CLI: obs.data triggers CDR decode

    CLI->>CLI: tap map_data throttle accumulate_path drain
    CLI->>CLI: rr.log for each observation
Loading

Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

leshy added 2 commits June 1, 2026 07:25
- camera pipeline behind --image (off by default), throttled to --image-hz
  (default 10). Throttle runs before obs.data so thinned frames skip the
  jpeg decode; failed decodes (None) are skipped.
- world_lidar pipeline: per-cloud pose interpolation (_interp_pose) over the
  leg-odom trajectory, transformed lidar -> base -> world into a global voxel
  map. Hoists rerun/Transform imports to module scope.
Comment thread dimos/memory2/store/mcap.py
leshy added a commit that referenced this pull request Jun 1, 2026
Add from_time/to_time (relative to the first observation) and
from_timestamp/to_timestamp (absolute epoch seconds) for windowing a
stream by time. A trailing to_time is a duration measured from the
current start, so from_time(2).to_time(30) reads as "skip 2s, take the
following 30s"; frames mix freely (from_timestamp(ts).to_time(30)).

Shared base for the stream-alignment (#2306) and go2dds (#2314)
branches, which both need this windowing API.
leshy added a commit that referenced this pull request Jun 1, 2026
Add from_time/to_time (relative to the first observation) and
from_timestamp/to_timestamp (absolute epoch seconds) for windowing a
stream by time. A trailing to_time is a duration measured from the
current start, so from_time(2).to_time(30) reads as "skip 2s, take the
following 30s"; frames mix freely (from_timestamp(ts).to_time(30)).

Shared base for the stream-alignment (#2306) and go2dds (#2314)
branches, which both need this windowing API.
# Conflicts:
#	dimos/memory2/stream.py
#	dimos/memory2/test_stream.py
Comment thread dimos/robot/unitree/go2dds/cli/render.py
leshy added 2 commits June 1, 2026 21:09
Return rr.TransformAxes3D(axis_length) instead of a rotation-only
Transform3D, which draws nothing on its own.
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