Skip to content

feat(recording): add CDR MCAP store and native recording - #4732

Open
TomCC7 wants to merge 2 commits into
cc/fix/cdr-native-sdkfrom
cc/feat/cdr-unit-recording
Open

TomCC7 wants to merge 2 commits into
cc/fix/cdr-native-sdkfrom
cc/feat/cdr-unit-recording

Conversation

@TomCC7

@TomCC7 TomCC7 commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

What this adds

  • Record generated messages from LCM or Zenoh into SQLite or MCAP with Rust, without decoding the payload.
  • Embed complete ros2msg schemas in MCAP and mark channels as cdr.
  • Read and replay CDR MCAP files through the separate CdrMcapStore.

Base: #4774 (shared native SDK), after #4722. Recording uses reception time; the original header.stamp stays in the payload.

How to test

From this PR's checkout, with Linux, uv, Rust and the native build dependencies:

uv venv .venv-cdr-recording --python 3.12
DIMOS_ALLOW_MISSING_COCKPIT=1 uv pip install --python .venv-cdr-recording/bin/python -e . -e examples/cdr-recording
source .venv-cdr-recording/bin/activate
cargo build --locked --manifest-path dimos/experimental/memory/rust/Cargo.toml --target-dir build/cdr-recorder -j2

dimos --transport zenoh --viewer none --n-workers 1 \
  --record mcap --record-engine rust --record-topics pose \
  --record-executable "$PWD/build/cdr-recorder/debug/dimos-memory-recorder" \
  run dimos-cdr-recording-example.pose

Wait for recorder readiness, record for about 15 seconds, then press Ctrl+C once. Open the saved artifact_path in Foxglove and view pose in frame map. It should move in a circle. No robot, ROS installation or API key is needed.

Prerequisites, expected output and Python readback.

WIP: local installation, recording and Python readback were checked. Foxglove UI rendering and Nix were not verified. Business blueprint migration, compression and default cutover remain later work.

@github-actions github-actions Bot left a comment •

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.

go2 replay realtime (arm64)

Details
Benchmark suite Current: 12c81e0 Previous: ecc2b8f Ratio
peak memory 1588.184 MB 1570.996 MB 1.01
peak threads 347 threads 355 threads 0.98
network (transport) 2724.766 MB 2730.387 MB 1.00
disk write 1.742 MB 1.496 MB 1.16
instructions 115.685 G 115 G 1.01

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
7312 1 7311 70
View the top 1 failed test(s) by shortest run time
dimos.perception.detection.type.test_detection3d::test_guess_projection
Stack Traces | 0.286s run time
get_moment_2d = <functools._lru_cache_wrapper object at 0x3bf4c2980>
publish_moment = <function publish_moment.<locals>.publisher at 0x3aff42de0>

    def test_guess_projection(get_moment_2d, publish_moment) -> None:
        moment = get_moment_2d()
        for key, value in moment.items():
            print(key, "====================================")
            print(value)
    
        moment.get("camera_info")
        detection2d = moment.get("detections2d")[0]
        tf = moment.get("tf")
        tf.get("camera_optical", "world", detection2d.ts, 5.0)
    
        # for stash
        # detection3d = Detection3D.from_2d(detection2d, 1.5, camera_info, transform)
        # print(detection3d)
    
>       publish_moment(moment)

detection2d = Detection2DBBox(bbox=(503.437255859375, 249.89385986328125, 655.950439453125, 469.82879638671875), track_id=1, class_i...70.490248, image=Image(shape=(720, 1280, 3), format=BGR, dtype=uint8, frame_id='camera_optical', ts=1757960670.490248))
get_moment_2d = <functools._lru_cache_wrapper object at 0x3bf4c2980>
key        = 'detections2d'
moment     = {'camera_info': CameraInfo(height=720, width=1280, distortion_model='equidistant', frame_id='camera_optical', ts=17926...optical', ts=1757960670.490248), 'lidar_frame': PointCloud(points=31284, frame_id='world', ts=1757960670.4755309), ...}
publish_moment = <function publish_moment.<locals>.publisher at 0x3aff42de0>
tf         = <dimos.protocol.tf.tf.TF object at 0x4153e93a0>
value      = <dimos.perception.detection.type.detection2d.imageDetections2D.ImageDetections2D object at 0x41510c6e0>

.../detection/type/test_detection3d.py:37: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../perception/detection/conftest.py:126: in publisher
    detections.publish(detections2d_val.to_ros_detection2d_array())
        detections = <dimos.core.transport.LCMTransport object at 0x4154c2ea0>
        detections2d_val = <dimos.perception.detection.type.detection2d.imageDetections2D.ImageDetections2D object at 0x41510c6e0>
        moment     = {'camera_info': CameraInfo(height=720, width=1280, distortion_model='equidistant', frame_id='camera_optical', ts=17926...optical', ts=1757960670.490248), 'lidar_frame': PointCloud(points=31284, frame_id='world', ts=1757960670.4755309), ...}
dimos/core/stream.py:107: in publish
    self.broadcast(None, msg)
        msg        = <dimos_lcm.vision_msgs.Detection2DArray.Detection2DArray object at 0x3afeffdc0>
        self       = <dimos.core.transport.LCMTransport object at 0x4154c2ea0>
dimos/core/transport.py:164: in broadcast
    self.lcm.publish(self.topic, msg)
        _          = None
        msg        = <dimos_lcm.vision_msgs.Detection2DArray.Detection2DArray object at 0x3afeffdc0>
        self       = <dimos.core.transport.LCMTransport object at 0x4154c2ea0>
.../protocol/pubsub/encoders.py:68: in publish
    encoded_message = self.encode(message, topic)
        __class__  = <class 'dimos.protocol.pubsub.encoders.PubSubEncoderMixin'>
        message    = <dimos_lcm.vision_msgs.Detection2DArray.Detection2DArray object at 0x3afeffdc0>
        self       = <dimos.protocol.pubsub.impl.lcmpubsub.LCM object at 0x414e6f050>
        topic      = Topic(topic='/detections', lcm_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>, message_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>)
.../protocol/pubsub/encoders.py:149: in encode
    return descriptor.codec.encode(msg)
        descriptor = MessageDescriptor(message_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>, encoding='lcm', nam...sgs.Detection2DArray', codec=LCMCodec(message_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>))
        msg        = <dimos_lcm.vision_msgs.Detection2DArray.Detection2DArray object at 0x3afeffdc0>
        self       = <dimos.protocol.pubsub.impl.lcmpubsub.LCM object at 0x414e6f050>
        topic      = Topic(topic='/detections', lcm_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>, message_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = LCMCodec(message_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>)
message = <dimos_lcm.vision_msgs.Detection2DArray.Detection2DArray object at 0x3afeffdc0>

    def encode(self, message: T) -> bytes:
        if not isinstance(message, self.message_type):
>           raise TypeError("Message does not match the declared type")
E           TypeError: Message does not match the declared type

message    = <dimos_lcm.vision_msgs.Detection2DArray.Detection2DArray object at 0x3afeffdc0>
self       = LCMCodec(message_type=<class 'dimos.msgs.vision_msgs.Detection2DArray.Detection2DArray'>)

.../protocol/pubsub/codec.py:41: TypeError

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

@TomCC7
TomCC7 added this pull request to stack #4743 October 20, 2026 22:26
@TomCC7
TomCC7 force-pushed the cc/feat/cdr-unit-recording branch from 59b492b to 3ce7f4e Compare October 21, 2026 02:08
@TomCC7 TomCC7 changed the title feat(recording): consolidate CDR capture replay and collection feat(recording): add CDR storage and replay on shared codecs Oct 21, 2026
@TomCC7 TomCC7 changed the title feat(recording): add CDR storage and replay on shared codecs feat(recording): add CDR MCAP store and native recording Oct 21, 2026

@TomCC7 TomCC7 left a comment

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.

need to think about an example used in this pr

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.

verify that we do need this and can't be solved by rust's own cdr lib

Comment thread dimos/memory/store/cdr_mcap.py
@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The PR extends the recording and replay workflow for generated CDR messages.

The confirmed optional-metadata readback limitation is non-blocking. The mixed-file readback assessment depends on PR #4733 landing.

Findings

  1. P2 Valid recordings appear empty ▶

Summary

Adds native CDR recording to SQLite and MCAP, plus Python readback through CdrMcapStore.

  • The Rust recorder saves generated CDR messages with their schemas.
  • Python reads CDR MCAP files as generated messages or raw bytes.
  • Operators can select a built recorder and run a CDR recording example.

Reviews (1) · Last reviewed commit: "Merge branch 'cc/feat/cdr-shared-codec' ..." · Reviewed by Greptile

Comment on lines +178 to +179
if summary is not None and summary.statistics is not None:
for cid, ch in summary.channels.items():

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 Valid recordings appear empty

CdrMcapStore silently reports no streams when a valid MCAP file omits statistics or its summary. With Writer(use_statistics=False), a recording containing three directly readable CDR messages opens successfully but exposes no streams or observations. This is a non-blocking readback limitation for recordings that omit this optional metadata. Scan the messages in this case, or raise a clear unsupported-file error instead of presenting the recording as empty.

Artifacts

Executed reproduction script for recordings with optional metadata omitted

  • The complete authored and executed Python source writes three MCAP variants and compares direct reader availability with store readback, isolating the optional-metadata failure.

Readback log with statistics enabled

  • Executed the statistics-enabled control and captured its command, working directory, exit code, installed source quotes, and three decoded store observations, showing normal readback works.

Readback log with statistics or summary absent

  • Executed both missing-metadata cases and captured three directly readable messages versus zero store streams in each case, confirming valid recordings silently appear empty.

View artifacts

TREX Ran code and verified through TREX

@TomCC7
TomCC7 force-pushed the cc/feat/cdr-unit-recording branch from 16906b4 to fb2d4e8 Compare October 21, 2026 04:39
@TomCC7
TomCC7 removed this pull request from stack #4743 October 22, 2026 18:51
@TomCC7
TomCC7 changed the base branch from cc/feat/cdr-unit-transports to cc/fix/cdr-native-sdk October 22, 2026 18:52
@TomCC7
TomCC7 added this pull request to stack #4776 October 22, 2026 18:52

This branch was successfully deployed

1 active (outdated) deployment
cachix — 12c81e08 Deployed Oct 21, 2026 by TomCC7 via cachix-build #13843
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.

1 participant