Skip to content

feat: livox replay#2089

Open
aclauer wants to merge 20 commits into
mainfrom
ivan/feat/livox_recorder
Open

feat: livox replay#2089
aclauer wants to merge 20 commits into
mainfrom
ivan/feat/livox_recorder

Conversation

@aclauer
Copy link
Copy Markdown
Collaborator

@aclauer aclauer commented May 14, 2026

Problem

Replay recorded livox mid360 for offline and reproducible testing

Closes DIM-XXX

Solution

Introduce FastLioReplay blueprint. Can be dropped in place of FastLio to play back recorded data.

How to Test

dimos run mid360-fastlio-memory to record sample
dimos run mid360-fastlio-replay to play back

Contributor License Agreement

  • I have read and approved the CLA.

@aclauer aclauer changed the title Ivan/feat/livox recorder feat: livox recorder May 14, 2026
@aclauer aclauer changed the title feat: livox recorder feat: livox replay May 14, 2026
@aclauer aclauer marked this pull request as ready for review May 14, 2026 23:03
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 14, 2026

Greptile Summary

This PR introduces FastlioReplay, a drop-in replacement for FastLio2 that plays back a recorded Livox Mid-360 session from a SQLite database, and FastlioMemory, a Recorder subclass that records lidar + odometry while also publishing odometry transforms to TF. Supporting infrastructure includes Transform.from_odometry(), frame-ID resolution improvements in Recorder._port_to_stream, and a _native_ignore_fields() helper on NativeModuleConfig.

  • FastlioMemory wraps Recorder with a live odometry→TF subscription so recorded messages carry correct robot-pose anchors.
  • FastlioReplay drives two independent timed_playback observables (lidar + odometry) from the same SQLite store, republishing both streams and TF transforms for downstream consumers.
  • _native_ignore_fields refines which config fields to_cli_args and to_config_dict expose by tracking which NativeModuleConfig fields are re-declared (rather than inherited) in a subclass.

Confidence Score: 4/5

Safe to merge for the replay functionality itself; the native-module CLI-arg exposure bug in native_module.py is a pre-existing concern that was already raised and is not introduced by this PR.

The recording and replay paths are well-structured and the individual components (timed_playback, from_odometry, frame-ID resolution) are correct. The one lingering concern is that _native_ignore_fields() causes FastLio2Config's infrastructure fields (cwd, executable, build_command, cli_exclude) to leak into to_cli_args() output, which was flagged in a prior review round. That issue predates this PR's additions and fastlio-replay/memory don't call to_cli_args() at all, so replay functionality is unaffected. Everything new in this PR is sound.

dimos/core/native_module.py — the _native_ignore_fields logic interacts with FastLio2Config's redeclared infrastructure fields in a way that was flagged previously and remains unresolved.

Important Files Changed

Filename Overview
dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py Adds FastlioMemory and FastlioReplay classes plus four new blueprints; tf subscription is now properly registered as a disposable; two replay blueprints share the same robot_model (acknowledged as intentional)
dimos/core/native_module.py Introduces _native_ignore_fields() to expose only redeclared subclass fields; FastLio2Config redeclares cwd/executable/build_command/cli_exclude causing those infrastructure fields to be appended as binary CLI args (previously flagged)
dimos/memory2/module.py Improves frame-ID resolution in _port_to_stream: prefers child_frame_id for transform messages (Odometry), falls back to frame_id, and overrides "world" with default_frame_id
dimos/msgs/geometry_msgs/Transform.py Adds Transform.from_odometry() classmethod that extracts frame names and pose from an Odometry message; cleanly avoids circular imports via TYPE_CHECKING guard
dimos/protocol/tf/tf.py Raises ValueError on same-frame tf.get() calls to catch caller bugs; breaks the None-returning contract of the rest of get() — callers without try/except will surface an unhandled exception (previously flagged, acknowledged as deferred)
dimos/robot/all_blueprints.py Registers three new blueprint/module entries for fastlio-memory, fastlio-replay, and fastlio-replay-voxels
dimos/hardware/sensors/lidar/fastlio2/module.py One-line comment update clarifying when config_path is resolved; no logic changes

Reviews (5): Last reviewed commit: "Merge branch 'main' into ivan/feat/livox..." | Re-trigger Greptile

Comment thread dimos/memory2/module.py Outdated
Comment thread dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py
Comment thread dimos/protocol/tf/tf.py
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
1741 2 1739 27
View the top 2 failed test(s) by shortest run time
dimos.core.test_native_module::test_manual
Stack Traces | 6.48s run time
dimos_cluster = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0x7f066ab98e30>
args_file = '.../popen-gw1/test_manual0/native_echo_args.json'

    def test_manual(dimos_cluster: ModuleCoordinator, args_file: str) -> None:
        native_module = dimos_cluster.deploy(
            StubNativeModule,
            some_param=2.5,
            output_file=args_file,
        )
    
        native_module.set_transport("pointcloud", LCMTransport("........./my/custom/lidar", PointCloud2))
        native_module.set_transport("cmd_vel", LCMTransport("/cmd_vel", Twist))
        native_module.start()
        time.sleep(1)
        native_module.stop()
    
>       assert read_json_file(args_file) == {
            "cmd_vel": "/cmd_vel#geometry_msgs.Twist",
            "pointcloud": "........./my/custom/lidar#sensor_msgs.PointCloud2",
            "output_file": args_file,
            "some_param": "2.5",
        }
E       AssertionError: assert {'cmd_vel': '...tCloud2', ...} == {'cmd_vel': '...param': '2.5'}
E         
E         Omitting 4 identical items, use -vv to show
E         Left contains 1 more item:
E         #x1B[0m{#x1B[33m'#x1B[39;49;00m#x1B[33mexecutable#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m: #x1B[33m'#x1B[39;49;00m#x1B[.../core/tests/native_echo.py#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m}#x1B[90m#x1B[39;49;00m
E         
E         Full diff:
E         #x1B[0m#x1B[90m #x1B[39;49;00m {#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'cmd_vel': '/cmd_vel#geometry_msgs.Twist',#x1B[90m#x1B[39;49;00m
E         #x1B[92m+     'executable': '.../core/tests/native_echo.py',#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'output_file': '.../popen-gw1/test_manual0/native_echo_args.json',#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'pointcloud': '........./my/custom/lidar#sensor_msgs.PointCloud2',#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'some_param': '2.5',#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m }#x1B[90m#x1B[39;49;00m

args_file  = '.../popen-gw1/test_manual0/native_echo_args.json'
dimos_cluster = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0x7f066ab98e30>
native_module = <dimos.core.rpc_client.RPCClient object at 0x7f066ab98d40>

dimos/core/test_native_module.py:140: AssertionError
dimos.core.test_native_module::test_autoconnect
Stack Traces | 7.52s run time
args_file = '.../popen-gw1/test_autoconnect0/native_echo_args.json'

    def test_autoconnect(args_file: str) -> None:
        """autoconnect passes correct topic args to the native subprocess."""
        blueprint = autoconnect(
            StubNativeModule.blueprint(
                some_param=2.5,
                output_file=args_file,
            ),
            StubConsumer.blueprint(),
            StubProducer.blueprint(),
        ).transports(
            {
                ("pointcloud", PointCloud2): LCMTransport("............/my/custom/lidar", PointCloud2),
            },
        )
    
        coordinator = ModuleCoordinator.build(blueprint.global_config(viewer="none"))
        try:
            # Validate blueprint wiring: all modules deployed
            native = coordinator.get_instance(StubNativeModule)
            consumer = coordinator.get_instance(StubConsumer)
            producer = coordinator.get_instance(StubProducer)
            assert native is not None
            assert consumer is not None
            assert producer is not None
    
            # Out→In topics match between connected modules
            assert native.pointcloud.transport.topic == consumer.pointcloud.transport.topic
            assert native.imu.transport.topic == consumer.imu.transport.topic
            assert producer.cmd_vel.transport.topic == native.cmd_vel.transport.topic
    
            # Custom transport was applied
            assert native.pointcloud.transport.topic.topic == "............/my/custom/lidar"
    
            # Wait for the native subprocess to write the output file
            for _ in range(50):
                if Path(args_file).exists():
                    break
                time.sleep(_WATCHDOG_POLL_INTERVAL)
        finally:
            coordinator.stop()
    
>       assert read_json_file(args_file) == {
            "cmd_vel": "/cmd_vel#geometry_msgs.Twist",
            "pointcloud": "............/my/custom/lidar#sensor_msgs.PointCloud2",
            "imu": "/imu#sensor_msgs.Imu",
            "output_file": args_file,
            "some_param": "2.5",
        }
E       AssertionError: assert {'cmd_vel': '...gs.json', ...} == {'cmd_vel': '...tCloud2', ...}
E         
E         Omitting 5 identical items, use -vv to show
E         Left contains 1 more item:
E         #x1B[0m{#x1B[33m'#x1B[39;49;00m#x1B[33mexecutable#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m: #x1B[33m'#x1B[39;49;00m#x1B[.../core/tests/native_echo.py#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m}#x1B[90m#x1B[39;49;00m
E         
E         Full diff:
E         #x1B[0m#x1B[90m #x1B[39;49;00m {#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'cmd_vel': '/cmd_vel#geometry_msgs.Twist',#x1B[90m#x1B[39;49;00m
E         #x1B[92m+     'executable': '.../core/tests/native_echo.py',#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'imu': '/imu#sensor_msgs.Imu',#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'output_file': '.../popen-gw1/test_autoconnect0/native_echo_args.json',#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'pointcloud': '............/my/custom/lidar#sensor_msgs.PointCloud2',#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m     'some_param': '2.5',#x1B[90m#x1B[39;49;00m
E         #x1B[90m #x1B[39;49;00m }#x1B[90m#x1B[39;49;00m

_          = 1
args_file  = '.../popen-gw1/test_autoconnect0/native_echo_args.json'
blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={'some_param': 2.5, 'output_file': '.../pytest-of-runner/pytest-0/popen-gw...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
consumer   = <dimos.core.rpc_client.RPCClient object at 0x7f0654609040>
coordinator = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0x7f0654dea030>
native     = <dimos.core.rpc_client.RPCClient object at 0x7f0654debd10>
producer   = <dimos.core.rpc_client.RPCClient object at 0x7f065460b260>

dimos/core/test_native_module.py:189: AssertionError

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

Comment thread dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py Outdated
Comment on lines +119 to +122
for klass in self.__class__.__mro__:
if klass is NativeModuleConfig:
break
ignore -= set(getattr(klass, "__annotations__", {}))
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 _native_ignore_fields exposes infrastructure fields as binary CLI args

FastLio2Config re-declares cwd, executable, build_command, and cli_exclude in its class body (to set different defaults). Because those names appear in both NativeModuleConfig.model_fields and FastLio2Config.__annotations__, the new logic removes them from ignore_fields. to_cli_args() then appends --executable result/bin/fastlio2_native --cwd cpp --build_command "nix build .#fastlio2_native" --cli_exclude "frozenset({'config', 'mount'})" to the subprocess command, which the binary will likely reject as unrecognised arguments. The old code was safe because ignore_fields retained every NativeModuleConfig field (with only an explicit frame_id carve-out). A targeted fix would add "cwd", "executable", "build_command", "cli_exclude", and "cli_name_override" to FastLio2Config.cli_exclude, or restrict _native_ignore_fields to only strip fields that are not control/infrastructure fields of the base config.

return self.inverse()

@classmethod
def from_odometry(cls, odom: Odometry) -> Transform: # type: ignore[name-defined]
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.

What is the type: ignore for? Looks like Odometry and Transform are imported.

Comment thread dimos/protocol/tf/tf.py
Comment on lines +158 to +159
parent_frame = args[0] if args else kwargs.get("parent_frame")
child_frame = args[1] if len(args) > 1 else kwargs.get("child_frame")
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 is an odd way of parsing arguments. Just replace *args, **kwargs with what get_transform wants:

        parent_frame: str,
        child_frame: str,
        time_point: float | None = None,
        time_tolerance: float | None = None,

speed: float = 1.0


class FastlioReplay(MemoryModule):
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 file, fastlio_blueprints, implies it's just for blueprints. Move these new modules to fastlio_modules.py or similar.

@paul-nechifor
Copy link
Copy Markdown
Contributor

Looks good. Added some small comments.

But I'm not sure how this works with @leshy 's replay stuff: #1976

For example, instead of using different blueprints to record:

dimos run mid360-fastlio-memory #to record sample
dimos run mid360-fastlio-replay #to play back

That uses the same blueprint with some arguments.

# 1) Start a stack and record all configured output streams
dimos --record-path /tmp/recording.db run unitree-go2

# 2) Stop the run, then replay the same file
dimos --replay-file /tmp/recording.db --viewer rerun-web run unitree-go2

@leshy
Copy link
Copy Markdown
Contributor

leshy commented May 15, 2026

Looks good. Added some small comments.

But I'm not sure how this works with @leshy 's replay stuff: #1976

For example, instead of using different blueprints to record:

dimos run mid360-fastlio-memory #to record sample
dimos run mid360-fastlio-replay #to play back

That uses the same blueprint with some arguments.

# 1) Start a stack and record all configured output streams
dimos --record-path /tmp/recording.db run unitree-go2

# 2) Stop the run, then replay the same file
dimos --replay-file /tmp/recording.db --viewer rerun-web run unitree-go2

yeah this is temporary before replay stuff is mature

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.

3 participants