Skip to content

go2 dds decoding improvements#2521

Open
leshy wants to merge 3 commits into
mainfrom
fix/ivan/go2_dds
Open

go2 dds decoding improvements#2521
leshy wants to merge 3 commits into
mainfrom
fix/ivan/go2_dds

Conversation

@leshy

@leshy leshy commented Jun 17, 2026

Copy link
Copy Markdown
Member

dimos mem rerun --root /sportmodestate go2_dds_stairs.mcap

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR improves Go2 DDS recording decode coverage: it adds CDR message types for LowCmd/MotorCmd/BmsCmd/CompressedVideo, a stateful H264Decoder transformer for rt/frontvideo/h264, height-map decoding for rt/utlidar/height_map_array, and a rolling time-window voxel map mode. CLI plumbing is consolidated into a new dataset.py dispatcher and the mem summary/mem rerun --root commands are added.

  • HeightMap is imported at module level in both codec.py and ros.py, but HeightMap.py was not committed — every import of either file raises ImportError, disabling all Go2 DDS stream decoding.
  • The dataset.py dispatcher centralises .db vs .mcap routing for all mapping CLIs; render.py gains an entity-path --root option for namespaced rerun playback.
  • VoxelGrid gains a time_window parameter that evicts stale frames by timestamp, enabling a rolling local-surface view alongside the existing global and column-carving modes.

Confidence Score: 2/5

Not safe to merge: the missing HeightMap.py causes an ImportError at load time for codec.py and ros.py, breaking all Go2 DDS stream decoding.

Both codec.py and ros.py import HeightMap from a module that was never committed. These are top-level imports, so loading either file — which happens the moment any Go2 DDS topic is decoded — raises ImportError. This makes the entire DDS codec pipeline non-functional, including all the other new decoders added in this PR.

dimos/robot/unitree/go2/dds/codec.py and dimos/robot/unitree/go2/dds/ros.py — both reference the missing HeightMap class.

Important Files Changed

Filename Overview
dimos/robot/unitree/go2/dds/codec.py Adds HeightMap, CompressedVideo, LowCmd codecs to GO2_CODECS registry, but imports HeightMap from a file that does not exist — will raise ImportError at load time.
dimos/robot/unitree/go2/dds/ros.py Adds decode_compressed_video and decode_height_map decoders; also imports the missing HeightMap — same import failure as codec.py.
dimos/robot/unitree/go2/dds/video.py New H264Decoder transformer wrapping PyAV; silently drops FFmpegErrors for out-of-GOP packets. Missing end-of-stream flush (already flagged in prior review).
dimos/robot/unitree/go2/dds/msgs/LowCmd.py New CDR message struct for rt/lowcmd with per-joint MotorCmd and BmsCmd; documents absence of trailing CRC field.
dimos/robot/unitree/go2/dds/msgs/CompressedVideo.py New message dataclass for foxglove CompressedVideo packets; includes to_rerun() for viewer-side decoding via rr.VideoStream.
dimos/memory2/cli/dataset.py New central dispatcher: open_store dispatches on .db/.mcap extension; stream_payload_types still accesses ._source private attribute (flagged in prior review).
dimos/memory2/cli/render.py Adds root entity path nesting; entity() function correctly collapses matching stream names to root path. open_store moved to dataset.py.
dimos/memory2/cli/app.py Adds --root option to mem rerun and a new mem summary command; safely uses getattr guard for uncodec_channels.
dimos/memory2/store/mcap.py Tracks uncodec channels (topics without a registered codec) so they appear in summary instead of being silently dropped.
dimos/mapping/voxels.py Adds time_window rolling voxel eviction; _add_windowed rebuilds the hashmap from buffered frame keys on each call. Clean logic; edge case when ts is always 0 (no timestamps) means frames never evict, but that's benign for replay use.
dimos/mapping/utils/cli/replay.py Migrates from SqliteStore direct construction to open_store/resolve_dataset; moves rerun setup inside the store context manager.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["mcap file"] --> B["Go2McapStore"]
    B --> C{"stream name"}
    C -->|"rt/utlidar/cloud"| D["PointCloud2 → FnCodec(decode_pointcloud2)"]
    C -->|"rt/utlidar/cloud_deskewed"| D
    C -->|"rt/utlidar/height_map_array"| E["HeightMap → FnCodec(decode_height_map)\n⚠️ ImportError: HeightMap.py missing"]
    C -->|"rt/frontvideo"| F["Image → FnCodec(decode_compressed_image)"]
    C -->|"rt/frontvideo/h264"| G["CompressedVideo → FnCodec(decode_compressed_video)"]
    G --> H["H264Decoder transformer\n(stateful PyAV decode)"]
    H --> I["Image (BGR)"]
    C -->|"rt/lowstate"| J["LowState → CdrStructCodec"]
    C -->|"rt/lowcmd"| K["LowCmd → CdrStructCodec\n(MotorCmd × 20 + BmsCmd)"]
    C -->|"rt/sportmodestate"| L["SportModeState → CdrStructCodec"]
    B --> M["uncodec_channels()\n(topics with no codec)"]
    M --> N["mem summary output"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["mcap file"] --> B["Go2McapStore"]
    B --> C{"stream name"}
    C -->|"rt/utlidar/cloud"| D["PointCloud2 → FnCodec(decode_pointcloud2)"]
    C -->|"rt/utlidar/cloud_deskewed"| D
    C -->|"rt/utlidar/height_map_array"| E["HeightMap → FnCodec(decode_height_map)\n⚠️ ImportError: HeightMap.py missing"]
    C -->|"rt/frontvideo"| F["Image → FnCodec(decode_compressed_image)"]
    C -->|"rt/frontvideo/h264"| G["CompressedVideo → FnCodec(decode_compressed_video)"]
    G --> H["H264Decoder transformer\n(stateful PyAV decode)"]
    H --> I["Image (BGR)"]
    C -->|"rt/lowstate"| J["LowState → CdrStructCodec"]
    C -->|"rt/lowcmd"| K["LowCmd → CdrStructCodec\n(MotorCmd × 20 + BmsCmd)"]
    C -->|"rt/sportmodestate"| L["SportModeState → CdrStructCodec"]
    B --> M["uncodec_channels()\n(topics with no codec)"]
    M --> N["mem summary output"]
Loading

Reviews (2): Last reviewed commit: "perception of height" | Re-trigger Greptile

Comment on lines +56 to +68
for obs in upstream:
packet = obs.data
if packet is None:
continue
try:
frames = decoder.decode(av.packet.Packet(packet.data.tobytes()))
except av.error.FFmpegError:
continue # P-frame with no reference yet (e.g. seeked past a keyframe)
for frame in frames:
bgr = frame.to_ndarray(format="bgr24")
yield obs.derive(
data=Image.from_numpy(bgr, ImageFormat.BGR, packet.frame_id, obs.ts)
)

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 H264Decoder silently drops buffered frames at end of stream

After the last packet is consumed, the codec context may still hold decoded frames waiting on temporal dependencies (B-frames / delayed output). Without a flush step, those frames are never yielded. For a typical h264 GOP of 30–120 frames, the last 1–4 seconds of a recording can disappear without any error. The fix is to drive a final decoder.flush() call and yield any remaining frames after the upstream loop exits.

Comment on lines +59 to +64
def stream_payload_types(store: Store) -> dict[str, type]:
"""Map each stream name in *store* to its payload type (any backend)."""
return {
name: cast("Backend[Any]", store.stream(name)._source).data_type
for name in store.list_streams()
}

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.

P2 stream_payload_types reaches into private _source attribute

store.stream(name)._source is not part of the public Store/Stream API. Any refactor that renames or removes _source (or changes how Backend exposes data_type) will produce a silent AttributeError at runtime rather than a type error. replay.py calls this path every time a recording is replayed. Consider exposing data_type through the public stream() return value or through a dedicated Store.payload_type(name) accessor.

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1880 1 1879 155
View the top 1 failed test(s) by shortest run time
::dimos.robot.unitree.go2.dds.test_store
Stack Traces | 0s run time
ImportError while importing test module '.../go2/dds/test_store.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
.../go2/dds/test_store.py:35: in <module>
    from dimos.robot.unitree.go2.dds.store import Go2McapStore
.../go2/dds/store.py:34: in <module>
    from dimos.robot.unitree.go2.dds.codec import GO2_CODECS
.../go2/dds/codec.py:38: in <module>
    from dimos.robot.unitree.go2.dds import cdr, ros
.../go2/dds/ros.py:48: in <module>
    from dimos.robot.unitree.go2.dds.msgs.HeightMap import HeightMap
E   ModuleNotFoundError: No module named 'dimos.robot.unitree.go2.dds.msgs.HeightMap'

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jun 17, 2026
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jun 17, 2026
from dimos.robot.unitree.go2.dds import cdr, ros
from dimos.robot.unitree.go2.dds.msgs.CompressedVideo import CompressedVideo
from dimos.robot.unitree.go2.dds.msgs.ControlEvent import ControlEvent
from dimos.robot.unitree.go2.dds.msgs.HeightMap import HeightMap

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 Missing HeightMap.py — module-level ImportError breaks all Go2 DDS decoding

HeightMap is imported at module level from dimos.robot.unitree.go2.dds.msgs.HeightMap, but no such file exists in the msgs/ directory (neither a HeightMap.py nor an __init__.py that exports the class). The same import appears in ros.py line 48. Because these are top-level imports, any attempt to load codec.py or ros.py — which happens as soon as the Go2 codec registry is used — raises ImportError, silently disabling all Go2 DDS stream decoding. The BmsCmd.py, CompressedVideo.py, LowCmd.py, and MotorCmd.py additions all have corresponding files, but HeightMap.py was not included in the PR.

Comment thread dimos/utils/cli/map.py
@@ -0,0 +1,14 @@
#!/usr/bin/env python3

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.

Did you mean to commit this?

# See the License for the specific language governing permissions and
# limitations under the License.

"""foxglove_msgs::msg::CompressedVideo_ — one encoded video packet (rt/frontvideo/h264).

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.

Are we still using foxglove?

Comment thread dimos/memory2/cli/app.py
store = open_dataset(dataset)
with store:
for name in store.list_streams():
store.streams[name] # register so summary() includes it

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.

This kinda indicates a deficiency in the API if you have to include empty statements to get .summary() to work.

Shouldn't .summary() loop over the streams instead?

Comment thread dimos/memory2/cli/app.py

# mcap files may carry channels we have no codec for (e.g. h264 video);
# list them so the inventory is complete rather than silently filtered.
uncodec = getattr(store, "uncodec_channels", None)

@paul-nechifor paul-nechifor Jun 18, 2026

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.

I don't like this approach of getting random methods by name.

If you specifically need McapStore.uncodec_channels, then check if store is a McapStore. If multiple stores are expected to have uncodec_channels, then declare uncodec_channels on Store and set it to return [] by default and let McapStore.uncodec_channels implement a different functionality.

Comment thread dimos/memory2/cli/app.py
# list them so the inventory is complete rather than silently filtered.
uncodec = getattr(store, "uncodec_channels", None)
for topic, (count, schema) in sorted(uncodec().items()) if uncodec else []:
print(f" (no codec) {topic}: {count} msgs [{schema or '?'}]")

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.

Better to use typer.echo(...).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants