Skip to content

feat(recording): Go2 + Mid-360 Point-LIO map recording#2557

Open
jeff-hykin wants to merge 214 commits into
mainfrom
jeff/feat/go2_record_clean
Open

feat(recording): Go2 + Mid-360 Point-LIO map recording#2557
jeff-hykin wants to merge 214 commits into
mainfrom
jeff/feat/go2_record_clean

Conversation

@jeff-hykin

@jeff-hykin jeff-hykin commented Jun 22, 2026

Copy link
Copy Markdown
Member

For Ivan

How to record map

export LIDAR_IP=<lidar-ip>
uv run python dimos/mapping/recording/go2_mid360/record.py

Writes recordings/<timestamp>/mem2.db go2 topics and pointlio_odometry/lidar
Pose values are correct.

How to view

Note this is the imperfect but okay-enough post-processing (the good one is in the next PR)

  • Syncs/truncates/aligns the go2 odom and the pointlio odom
  • Uses aprilTag-corrected ground-truth (which is often broken) + a Rerun .rrd:
uv run --no-sync python dimos/mapping/recording/go2_mid360/post_process.py

(no arg = newest recording). Writes recordings/<timestamp>/<timestamp>.rrd.

The one that generates a pc2.lcm is in the next pr

_

For others

  1. Mount the Mid-360 at 45deg on head (see urdf)
  2. set your wired NIC onto the lidar's /24, and get the dog + your computer on the same phone hotspot if you're recording outside or away from wifi:
dimos go2tool connect-wifi --ssid <hotspot> --password <pw>   # provision the dog over BLE

2. Record — drive with WASD; Ctrl+C to stop:

export LIDAR_IP=<lidar-ip>
uv run python dimos/mapping/recording/go2_mid360/record.py

Writes recordings/<timestamp>/mem2.db. Point-LIO odometry + Mid-360 cloud + camera, with the pose baked into each lidar frame.

3. Post-process — AprilTag-corrected ground-truth + a Rerun .rrd:

uv run --no-sync python dimos/mapping/recording/go2_mid360/post_process.py

(no arg = newest recording). Writes recordings/<timestamp>/<timestamp>.rrd.

4. Look at it:

rerun recordings/<timestamp>/<timestamp>.rrd

Full walkthrough: docs/capabilities/navigation/recording_a_map.md.

jeff-hykin added 30 commits June 1, 2026 15:32
Move the recorder + tcpdump pcap logic out of the go2_record blueprint
into dimos/hardware/sensors/lidar/fastlio2/recorder.py. Pcap recording
is now opt-in (record_pcap defaults to False), and the default paths
land under ./go2_recordings/<date_time>/{mem2.db,raw_mid360.pcap}.
Add the offline post-processing pipeline for Go2 + Livox recordings:
- recording/{apriltags,gtsam_gt,lidar_reanchor,build_rrd,camera,rec_check}
- scripts/go2_mid360_post_process.py orchestrator

AprilTag detection now drops distant/oblique glimpses (keep <=1m, head-on
within 45deg), clusters same-id detections within 5s, and emits one medoid
representative per cluster (most spatially/rotationally central).
…process

TARGET positional accepts a mem2.db, a dir containing one (process just that
recording), or a dir to scan. With no TARGET, process the most recently
created recording under --recordings-dir. Folds in the old --db flag.
build_rrd now looks for a main.jsonl next to the mem2.db (else one dir up),
replays each JSON line as a rerun TextLog on the `ts` timeline (level + logger
+ extra fields preserved), and docks a TextLogView below the 3D/camera views
when present.
--check runs only the rec_check report on each recording and writes a
structured summary.json (files, pcap stats, per-stream rows/hz/pose%, fastlio
odometry travel) into the recording dir, skipping GTSAM/re-anchor/.rrd.
Drop the one-dir-up fallback in build_rrd's jsonl discovery.
…lio_native flake/cmake consuming dimos-module-fastlio2 pointlio branch + Estimator/parameters sources)
…y path)

The fast-lio input was pinned to file:///Users/jeffhykin/... which only
exists on the Mac. Repoint to the dimensionalOS/dimos-module-fastlio2
pointlio branch on github so the flake builds on Linux. Same locked rev.
Rename the mirrored fastlio_blueprints.py to pointlio_blueprints.py and
wire it to PointLio (was incorrectly using FastLio2). Adds mid360-pointlio
and mid360-pointlio-voxels to the blueprint registry.
…dimos into jeff/feat/go2_record_clean

# Conflicts:
#	dimos/hardware/sensors/lidar/fastlio2/config/mid360.yaml
#	dimos/hardware/sensors/lidar/fastlio2/cpp/main.cpp
#	dimos/hardware/sensors/lidar/fastlio2/module.py
#	dimos/hardware/sensors/lidar/fastlio2/recorder.py
#	dimos/hardware/sensors/lidar/fastlio2/tools/pcap_to_db.py
#	dimos/robot/all_blueprints.py
Replace FastLio2 with PointLio in both recording drivers. PointlioRecorder
stamps each lidar frame with the live odometry pose at record time, so the
drivers drop the FastLio2 TfHack static-transform pose logic entirely — they
just declare the companion In ports and let the recorder handle poses.

Rename the recorded stream names fastlio_* -> pointlio_* throughout the
post-process toolchain (gtsam_gt, build_rrd, rec_check, multi_map_anchor) so
recordings post-process and visualize. multi_map_anchor: drop the dead
lidar_reanchor import (removed in the post-process refactor) and use the
recorder-stamped pointlio_lidar for the map viz.

Add docs/capabilities/navigation/recording_a_map.md guide.

Regenerate all_blueprints.
return f"{self.config.camera_name}_depth_optical_frame"

@property
def _imu_frame(self) -> str:

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.

Recorded IMU with realsense cause I wanted an all-in-one recording

@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1906 1 1905 159
View the top 1 failed test(s) by shortest run time
dimos.agents.skills.test_unitree_skill_container::test_pounce
Stack Traces | 6.97s run time
agent_setup = <function agent_setup.<locals>.fn at 0xffe687baf100>

    def test_pounce(agent_setup) -> None:
>       history = agent_setup(
            blueprints=[
                MockedUnitreeSkill.blueprint(),
                StubNavigation.blueprint(),
                StubGO2Connection.blueprint(),
            ],
            messages=[HumanMessage("Pounce! Use the execute_sport_command tool.")],
        )

agent_setup = <function agent_setup.<locals>.fn at 0xffe687baf100>

.../agents/skills/test_unitree_skill_container.py:56: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/agents/conftest.py:90: in fn
    coordinator = ModuleCoordinator.build(blueprint)
        agent_kwargs = {'mcp_server_url': 'http://localhost:23617/mcp', 'model_fixture': '.../agents/fixtures/test_pounce.json', 'system_prompt': None}
        agent_transport = <dimos.core.transport.pLCMTransport object at 0xffe677fed2b0>
        blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedU...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        blueprints = [Blueprint(blueprints=(BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.Mocked...obal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())]
        coordinator = None
        finished_event = <threading.Event at 0xffe677fec9b0: unset>
        finished_transport = <dimos.core.transport.pLCMTransport object at 0xffe677fed250>
        fixture    = None
        fixture_path = PosixPath('.../agents/fixtures/test_pounce.json')
        history    = []
        lcm_url    = 'udpm://239.255.76.67:11317?ttl=0'
        mcp_url    = 'http://localhost:23617/mcp'
        messages   = [HumanMessage(content='Pounce! Use the execute_sport_command tool.', additional_kwargs={}, response_metadata={})]
        on_message = <function agent_setup.<locals>.fn.<locals>.on_message at 0xffe687baea20>
        recording  = False
        request    = <SubRequest 'agent_setup' for <Function test_pounce>>
        system_prompt = None
        transports = [<dimos.core.transport.pLCMTransport object at 0xffe677fed2b0>, <dimos.core.transport.pLCMTransport object at 0xffe677fed250>]
        unsubs     = [<function LCMPubSubBase.subscribe.<locals>.unsubscribe at 0xffe687bacb80>, <function LCMPubSubBase.subscribe.<locals>.unsubscribe at 0xffe687baed40>]
.../core/coordination/module_coordinator.py:304: in build
    _connect_module_refs(blueprint, coordinator)
        blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedU...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        blueprint_args = {}
        cls        = <class 'dimos.core.coordination.module_coordinator.ModuleCoordinator'>
        coordinator = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0xffe676146f30>
.../core/coordination/module_coordinator.py:830: in _connect_module_refs
    result = _resolve_single_ref(
        AsyncSpecProxy = <class 'dimos.core.rpc_client.AsyncSpecProxy'>
        DisabledModuleProxy = <class 'dimos.core.coordination.blueprints.DisabledModuleProxy'>
        blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedU...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        bp         = BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedUnitreeSkill'>, streams...duleRef(name='_connection', spec=<class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>, optional=False)))
        declared_spec = {(<class 'dimos.agents.skills.test_unitree_skill_container.MockedUnitreeSkill'>, '_connection'): <class 'dimos.robot.u...ill_container.MockedUnitreeSkill'>, '_navigation'): <class 'dimos.navigation.navigation_spec.NavigationInterfaceSpec'>}
        disabled_ref_proxies = {}
        disabled_set = set()
        existing_modules = None
        is_module_type = <function is_module_type at 0xffe7497df7e0>
        mod_and_mod_ref_to_proxy = {(<class 'dimos.agents.skills.test_unitree_skill_container.MockedUnitreeSkill'>, '_navigation'): <class 'dimos.agents.skills.test_unitree_skill_container.StubNavigation'>}
        module_coordinator = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0xffe676146f30>
        module_ref = ModuleRef(name='_connection', spec=<class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>, optional=False)
        result     = <class 'dimos.agents.skills.test_unitree_skill_container.StubNavigation'>
        spec       = <class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

bp = BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedUnitreeSkill'>, streams...duleRef(name='_connection', spec=<class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>, optional=False)))
module_ref = ModuleRef(name='_connection', spec=<class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>, optional=False)
spec = <class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>
blueprint = Blueprint(blueprints=(BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedU...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
disabled_set = set(), existing_modules = None

    def _resolve_single_ref(
        bp: Any,
        module_ref: Any,
        spec: Any,
        blueprint: Blueprint,
        disabled_set: set[type],
        existing_modules: set[type[ModuleBase]] | None = None,
    ) -> Any:
        """Resolve a module ref to its provider.
    
        Returns a module type, a ``DisabledModuleProxy``, or *None* (skip).
        """
        from dimos.core.coordination.blueprints import DisabledModuleProxy
    
        m = bp.module.__name__
        s = module_ref.spec.__name__
    
        possible = [
            other.module
            for other in blueprint.active_blueprints
            if other != bp and spec_structural_compliance(other.module, spec)
        ]
        if existing_modules:
            bp_module_set = {o.module for o in blueprint.active_blueprints}
            for mod_cls in existing_modules:
                if (
                    mod_cls != bp.module
                    and mod_cls not in bp_module_set
                    and spec_structural_compliance(mod_cls, spec)
                ):
                    possible.append(mod_cls)
        valid = [c for c in possible if spec_annotation_compliance(c, spec)]
    
        if not possible:
            if module_ref.optional:
                return None
            disabled = next(
                (
                    other.module
                    for other in blueprint.blueprints
                    if other.module in disabled_set and spec_structural_compliance(other.module, spec)
                ),
                None,
            )
            if disabled is not None:
                logger.warning(
                    "Module ref unsatisfied because provider is disabled; installing no-op proxy",
                    ref=module_ref.name,
                    consumer=m,
                    disabled_provider=disabled.__name__,
                    spec=s,
                )
                return DisabledModuleProxy(s)
>           raise Exception(_ref_msg(m, module_ref, s, "No module met that spec."))
E           Exception: MockedUnitreeSkill has a module reference (ModuleRef(name='_connection', spec=<class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>, optional=False)) requesting a module that satisfies the GO2ConnectionSpec spec. No module met that spec.

DisabledModuleProxy = <class 'dimos.core.coordination.blueprints.DisabledModuleProxy'>
blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedU...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
bp         = BlueprintAtom(kwargs={}, module=<class 'dimos.agents.skills.test_unitree_skill_container.MockedUnitreeSkill'>, streams...duleRef(name='_connection', spec=<class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>, optional=False)))
disabled   = None
disabled_set = set()
existing_modules = None
m          = 'MockedUnitreeSkill'
module_ref = ModuleRef(name='_connection', spec=<class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>, optional=False)
possible   = []
s          = 'GO2ConnectionSpec'
spec       = <class 'dimos.robot.unitree.go2.connection_spec.GO2ConnectionSpec'>
valid      = []

.../core/coordination/module_coordinator.py:756: Exception

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

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds end-to-end map recording for Go2 + Livox Mid-360 using Point-LIO odometry, with a companion mid360_realsense rig, offline post-processing, AprilTag-corrected ground-truth, and a multi-map anchor tool for cross-recording alignment.

  • Point-LIO config overhaul: YAML file replaced with direct CLI arg passthrough; frame_id semantics changed (now the odometry parent frame, formerly the sensor frame) with a new sensor_frame_id field for the cloud stamp header — a breaking rename that all callers in the PR correctly update.
  • RealSense IMU stream: New enable_imu path adds a second rs.pipeline for accel/gyro, publishes fused Imu messages, and derives depth→IMU extrinsics from the main pipeline's device graph.
  • Post-processing pipeline: New build_rrd.py, apriltags.py, gtsam_gt.py, and multi_map_anchor.py implement the full offline flow — tag detection, GTSAM factor-graph solve, Kabsch alignment between recordings, and Rerun visualisation.

Confidence Score: 3/5

Two concrete defects need fixing before this is reliable on hardware: a crash in RealSense camera start and a NIC mismatch that silently prevents Point-LIO from receiving lidar data on the Go2 rig.

The RealSense IMU initialisation crashes with a bare StopIteration if the device's accel profile isn't found in the extrinsics graph — the IMU pipeline is already running when the exception propagates, leaving the camera in a partially-started state. Separately, go2_mid360/record.py passes host_ip=_LIDAR_HOST_IP (default 192.168.1.100) to Mid360 but no host_ip to PointLio, which falls back to the C++ binary default 192.168.1.5. On a recording machine whose NIC is at .100, Point-LIO silently binds the wrong interface and produces no odometry.

dimos/hardware/sensors/camera/realsense/camera.py (_start_imu extrinsics query) and dimos/mapping/recording/go2_mid360/record.py (PointLio host_ip)

Important Files Changed

Filename Overview
dimos/hardware/sensors/camera/realsense/camera.py Adds IMU stream support (accel+gyro via a separate rs.pipeline) and publishes Imu messages driven by gyro frames. Contains a StopIteration crash when the accel profile is not found in the device's extrinsics graph.
dimos/mapping/recording/go2_mid360/record.py New recording script for Go2 + Mid-360. PointLio.blueprint() omits host_ip, which defaults to a different NIC address than the explicit host_ip passed to Mid360.blueprint(), causing PointLio to silently bind the wrong network interface.
dimos/hardware/sensors/lidar/pointlio/module.py Replaces YAML config with direct CLI arg passthrough; renames frame fields (frame_id now the odom parent, sensor_frame_id for the cloud stamp, child_frame_id for body). Clean refactor aligned with the C++ changes.
dimos/hardware/sensors/lidar/pointlio/recorder.py Simplified to use the new Recorder base class with pose_setter_for decorators; removed custom time-alignment logic in favour of the framework's Recorder infra. Straightforward and correct.
dimos/hardware/sensors/lidar/pointlio/cpp/main.cpp Removes YAML config loading; all tuning params are now parsed from CLI args with sensible defaults. Adds sensor_frame_id arg and renames body_frame_id to child_frame_id consistently.
dimos/mapping/recording/multi_map_anchor.py New multi-recording anchor tool using Kabsch alignment over shared AprilTag positions. Degenerate case (collinear tags) produces a singular rotation matrix without warning, but is unlikely in practice.
dimos/mapping/recording/utils/build_rrd.py New Rerun .rrd builder with memory-efficient incremental voxel map accumulation and per-cloud height shading. The O(n_images x n_targets) loop in _log_cam_frustums was flagged in a prior review.
dimos/mapping/recording/utils/apriltags.py New AprilTag detection pipeline: raw detections → quality gate (distance + view angle) → time-clustered medoid representative per tag. Clean and well-structured.
dimos/mapping/recording/go2_mid360/static_transforms.py New static transform tree for Go2 + Mid-360 rig; composes rigid mounts from URDF/manual measurements and visualises them in Rerun. No functional issues found.
dimos/mapping/recording/mid360_realsense/record.py New recording script for Mid-360 + RealSense rig. Both Mid360 and PointLio blueprints omit host_ip, so they consistently fall back to the same default — no NIC mismatch unlike the go2_mid360 script.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Record["record.py (runtime)"]
        Mid360["Mid360\n(raw livox + IMU)"]
        PointLio["PointLio\n(SLAM odometry + cloud)"]
        RealSense["RealSenseCamera\n(color + depth + IMU)"]
        Go2["GO2Connection\n(go2_lidar + go2_odom)"]
        Recorder["Go2Recorder / RealsenseRecorder\n(mem2.db)"]
        Mid360 -->|livox_lidar, livox_imu| Recorder
        PointLio -->|pointlio_odometry, pointlio_lidar| Recorder
        RealSense -->|color_image, realsense_imu, ...| Recorder
        Go2 -->|go2_lidar, go2_odom| Recorder
    end

    subgraph PostProcess["post_process.py (offline)"]
        DB["mem2.db"]
        Tags["detect_apriltags\n(apriltags.py)"]
        GTSAM["build_gtsam_gt\n(gtsam_gt.py)"]
        RRD["build_rrd\n(build_rrd.py)"]
        DB --> Tags --> GTSAM --> RRD
    end

    subgraph Anchor["multi_map_anchor.py (offline)"]
        AnchorDB["mid360_realsense\nmem2.db"]
        TargetDB["go2_mid360\nmem2.db"]
        Kabsch["Kabsch alignment\n(shared AprilTags)"]
        CombinedRRD["combined .rrd"]
        AnchorDB --> Kabsch
        TargetDB --> Kabsch
        Kabsch --> CombinedRRD
    end

    Recorder --> DB
    PostProcess --> Anchor
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
    subgraph Record["record.py (runtime)"]
        Mid360["Mid360\n(raw livox + IMU)"]
        PointLio["PointLio\n(SLAM odometry + cloud)"]
        RealSense["RealSenseCamera\n(color + depth + IMU)"]
        Go2["GO2Connection\n(go2_lidar + go2_odom)"]
        Recorder["Go2Recorder / RealsenseRecorder\n(mem2.db)"]
        Mid360 -->|livox_lidar, livox_imu| Recorder
        PointLio -->|pointlio_odometry, pointlio_lidar| Recorder
        RealSense -->|color_image, realsense_imu, ...| Recorder
        Go2 -->|go2_lidar, go2_odom| Recorder
    end

    subgraph PostProcess["post_process.py (offline)"]
        DB["mem2.db"]
        Tags["detect_apriltags\n(apriltags.py)"]
        GTSAM["build_gtsam_gt\n(gtsam_gt.py)"]
        RRD["build_rrd\n(build_rrd.py)"]
        DB --> Tags --> GTSAM --> RRD
    end

    subgraph Anchor["multi_map_anchor.py (offline)"]
        AnchorDB["mid360_realsense\nmem2.db"]
        TargetDB["go2_mid360\nmem2.db"]
        Kabsch["Kabsch alignment\n(shared AprilTags)"]
        CombinedRRD["combined .rrd"]
        AnchorDB --> Kabsch
        TargetDB --> Kabsch
        Kabsch --> CombinedRRD
    end

    Recorder --> DB
    PostProcess --> Anchor
Loading

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

Comment on lines +44 to +47
def _default_recording_dir() -> Path:
now = datetime.now()
stamp = now.strftime("%Y-%m-%d") + "_" + now.strftime("%I-%M%p").lower() + "-PST"
return Path("recordings") / stamp

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 The directory timestamp bakes in -PST unconditionally, regardless of the system's actual timezone. A user running this on a UTC or EST machine gets a directory labeled with the wrong timezone, which is confusing when correlating recordings with wall-clock logs. Using datetime.now().astimezone() and %z produces the real local-offset string instead.

Suggested change
def _default_recording_dir() -> Path:
now = datetime.now()
stamp = now.strftime("%Y-%m-%d") + "_" + now.strftime("%I-%M%p").lower() + "-PST"
return Path("recordings") / stamp
def _default_recording_dir() -> Path:
now = datetime.now().astimezone()
stamp = now.strftime("%Y-%m-%d") + "_" + now.strftime("%H-%M") + now.strftime("%z")
return Path("recordings") / stamp

Comment on lines +74 to +75
Prefer the SDK-backed ``ZEDCamera`` (depth/imu/pointcloud); fall back to the
UVC-only ``ZedSimple`` (color only) when ``pyzed`` is not installed.

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 The docstring describes the ZedSimple fallback as "color only" but ZedSimple publishes both color_image and imu (USB-HID path from zed-open-capture). The remapping already wires ZedSimple.imu -> zed_imu, so it works correctly — the comment just misleads future readers about what the fallback provides.

Suggested change
Prefer the SDK-backed ``ZEDCamera`` (depth/imu/pointcloud); fall back to the
UVC-only ``ZedSimple`` (color only) when ``pyzed`` is not installed.
Prefer the SDK-backed ``ZEDCamera`` (depth/imu/pointcloud); fall back to the
``ZedSimple`` (UVC color + USB-HID IMU, no depth/pointcloud) when ``pyzed`` is not installed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +133 to +134
pose_non_null = cur.execute(f'SELECT COUNT(pose_x) FROM "{name}"').fetchone()[0]
return n, t0, t1, pose_non_null

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 COUNT(pose_x) will throw sqlite3.OperationalError: table "X" has no column named pose_x if the table was ever created without the standard dimos stream schema. There is no try/except around these two queries inside stream_rows, so any such table in the DB would cause the entire report() call to raise an exception. The outer try/except in process_db catches it, but the rec_check output is then silently skipped with a terse error message. Wrapping the second query in its own try/except sqlite3.OperationalError and defaulting to pose_non_null = 0 would make the function robust to schema variations (e.g. legacy or manually created tables).

Comment on lines +328 to +334
nearest = [(1e18, None) for _ in camera_targets] # (time delta, image obs) per target
for image_obs in store.stream("color_image", Image):
for target_index, (_entity, _pose, target_ts) in enumerate(camera_targets):
delta = abs(image_obs.ts - target_ts)
if delta < nearest[target_index][0]:
nearest[target_index] = (delta, image_obs)
logged = 0

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 The nearest-image scan is O(n_images × n_camera_targets). n_camera_targets grows as max_views_per_tag × n_unique_tags, so for a long recording with many tags (e.g. 10 tags × 40 views = 400 targets) and a full-rate color stream (e.g. 30 Hz × 300 s = 9 000 frames), this inner double loop performs ~3.6 M comparisons and also materialises every image from the store in memory simultaneously. Sorting or binary-searching the image timestamps per target would reduce this to O((n_images + n_targets) × log n_images).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

base_transform: Transform | None = Field(default_factory=default_base_transform)
align_depth_to_color: bool = True
enable_depth: bool = True
enable_imu: bool = True

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 weekend I did a zed recording and wanted IMU

_GYRO_SCALE = (1000.0 / 32768.0) * (math.pi / 180.0) # raw -> rad/s (+-1000 deg/s)


def autodetect_zed_device() -> str | None:

@jeff-hykin jeff-hykin Jun 22, 2026

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.

Treat this file as disposable. Go2 was struggling to see the april tags with limited light, did a Zed recording this weekend. ~2Gb zed SDK kept failing, this module uses the Zed without their SDK which is less overhead (e.g. good for recording) anyways.

Comment thread dimos/robot/unitree/keyboard_teleop.py Outdated

cmd_vel: Out[Twist]

_go2: GO2ConnectionSpec | None = None

@jeff-hykin jeff-hykin Jun 22, 2026

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 should be reverted/cleaned later, just needed to be able to make the dog lie-down to save battery on the huge loop (which was right around 100% battery usage)

}


class KeyboardTeleopTUI(Module):

@jeff-hykin jeff-hykin Jun 22, 2026

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.

The UX on this is horrible, so hard to control the dog. Needed for when running a jetson headless and tele-oping and avoiding rerun for performance reasons.

This module should be considered disposable/scaffolding, completely vibed.

Comment thread pyproject.toml Outdated
"mcap",
"mcap.*",
"mujoco",
"hid",

@jeff-hykin jeff-hykin Jun 22, 2026

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.

zed imu dependecy

def trajectory_task_name(hardware_id: str) -> TaskName:
return f"traj_{hardware_id}"
# Mid-360 mount pose on the FlowBase (position + orientation) in the base frame.
FLOWBASE_MID360_MOUNT = Pose(0.20, -0.20, 0.10, *Quaternion.from_euler(Vector3(0, 0, 0)))

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.

will be needed to fix the flowbase blueprint(s) later

Drop ZedSimple (the import-hid module), revert ZEDCamera changes to main, and
remove the ZED color/imu streams from the go2 recorder. Drop the now-orphaned
hid mypy override and regenerate all_blueprints.
Comment on lines +137 to +154
def odometry_travel(cur: sqlite3.Cursor) -> dict | None:
rows = cur.execute(
"SELECT pose_x, pose_y, pose_z FROM pointlio_odometry WHERE pose_x IS NOT NULL ORDER BY ts"
).fetchall()
if not rows:
return None
xs, ys, zs = zip(*rows, strict=False)
path_length = sum(math.dist(rows[i - 1], rows[i]) for i in range(1, len(rows)))
return {
"rows": len(rows),
"start": rows[0],
"end": rows[-1],
"path_length": path_length,
"straight_line": math.dist(rows[0], rows[-1]),
"bbox_x": (min(xs), max(xs)),
"bbox_y": (min(ys), max(ys)),
"bbox_z": (min(zs), max(zs)),
}

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 odometry_travel crashes on FastLIO recordings

odometry_travel queries FROM pointlio_odometry with no existence check. The mid360_realsense rig in this same PR (using FastLIO) produces databases without pointlio_odometry. Both summarize() (line 208) and report() (line 279) call odometry_travel with no surrounding try/except for this specific call — the outer catch in process_db swallows the entire output. Wrapping the SELECT in a try/except that returns None on OperationalError, or checking tables membership first (as stream_rows already does), would fix this.

Removes this branch's additions to the pygame KeyboardTeleop (the _go2
GO2ConnectionSpec, _call_go2_pose, Z/X liedown/standup key handlers, and help
text) — reverted to main. The Z=lie-down hack was handy for saving Go2 battery
on long loops; tagged RESTORE so it's easy to git-revert this commit to bring
it back later.
Comment on lines +218 to +224
accel_stream = next(
profile
for sensor in self._profile.get_device().query_sensors()
for profile in sensor.get_stream_profiles()
if profile.stream_type() == rs.stream.accel
)
self._depth_to_imu_extrinsics = depth_stream.get_extrinsics_to(accel_stream)

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 StopIteration crash when accel profile not found

next() with no default raises StopIteration if no sensor on the device exposes an rs.stream.accel profile. This exception is outside the surrounding try/except RuntimeError block, so it propagates up through _start_imu() into start(), crashing camera initialisation with a confusing traceback. The IMU pipeline has already been started and stored in self._imu_pipeline at this point, so the state is partially initialised when the crash occurs. Using next(..., None) and guarding the extrinsics call prevents this.

Suggested change
accel_stream = next(
profile
for sensor in self._profile.get_device().query_sensors()
for profile in sensor.get_stream_profiles()
if profile.stream_type() == rs.stream.accel
)
self._depth_to_imu_extrinsics = depth_stream.get_extrinsics_to(accel_stream)
accel_stream = next(
(
profile
for sensor in self._profile.get_device().query_sensors()
for profile in sensor.get_stream_profiles()
if profile.stream_type() == rs.stream.accel
),
None,
)
if accel_stream is not None:
self._depth_to_imu_extrinsics = depth_stream.get_extrinsics_to(accel_stream)
else:
print("RealSense: no accel profile found in device graph; IMU->depth TF will be skipped")

Comment thread dimos/mapping/recording/go2_mid360/record.py
@leshy

leshy commented Jun 22, 2026

Copy link
Copy Markdown
Member

blueprint plz :) remove post processing

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