Skip to content

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

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

feat(recording): Go2 + Mid-360 Point-LIO map recording#2557
jeff-hykin wants to merge 213 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.
ZedSimple: UVC color (YUYV side-by-side, left eye) plus ZED-M IMU over
USB-HID via the zed-open-capture protocol; used when pyzed is absent.
Color and IMU run as independent fail-soft loops. (hid is an optional
runtime dep; the module logs and skips IMU if it or libhidapi is missing.)

record.py:
- Select ZEDCamera (SDK) or ZedSimple, remap color_image->zed_color_image
  and imu->zed_imu; record both. Add an imu Out stream to ZEDCamera too.
- Replace the in-process TUI teleop with the KeyboardTeleop module
  (cmd_vel->tele_cmd_vel); sit/stand via the auto-wired GO2ConnectionSpec.

Mid360: auto-correct host_ip to a local interface on the lidar subnet
(mirrors FastLio2), fixing the native bind failure when the configured
host_ip is not assigned to this machine.
…go2_record_clean

# Conflicts:
#	dimos/hardware/sensors/lidar/livox/module.py
…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.9s run time
agent_setup = <function agent_setup.<locals>.fn at 0xffa34f78a7a0>

    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 0xffa34f78a7a0>

.../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:23059/mcp', 'model_fixture': '.../agents/fixtures/test_pounce.json', 'system_prompt': None}
        agent_transport = <dimos.core.transport.pLCMTransport object at 0xffa355dde990>
        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 0xffa359db8560: unset>
        finished_transport = <dimos.core.transport.pLCMTransport object at 0xffa355dfc3e0>
        fixture    = None
        fixture_path = PosixPath('.../agents/fixtures/test_pounce.json')
        history    = []
        lcm_url    = 'udpm://239.255.76.67:10759?ttl=0'
        mcp_url    = 'http://localhost:23059/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 0xffa34f775080>
        recording  = False
        request    = <SubRequest 'agent_setup' for <Function test_pounce>>
        system_prompt = None
        transports = [<dimos.core.transport.pLCMTransport object at 0xffa355dde990>, <dimos.core.transport.pLCMTransport object at 0xffa355dfc3e0>]
        unsubs     = [<function LCMPubSubBase.subscribe.<locals>.unsubscribe at 0xffa34f78be20>, <function LCMPubSubBase.subscribe.<locals>.unsubscribe at 0xffa34f788fe0>]
.../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 0xffa355dfca40>
.../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 0xffa428e1f7e0>
        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 0xffa355dfca40>
        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

This PR adds a complete Go2 + Livox Mid-360 map-recording pipeline: a recording script (record.py), offline post-processing (post_process.py), and a shared utility library covering AprilTag detection, GTSAM landmark-SLAM drift correction, lidar Scan Context loop closure, and Rerun .rrd visualization. It also adds a TUI keyboard teleop module and a multi_map_anchor.py tool for aligning multiple recordings in a common world frame.

  • Recording: Go2Recorder (extends PointlioRecorder) wires PointLIO, Mid-360, Go2Connection, and KeyboardTeleop into a single autoconnect blueprint; each lidar frame is stamped with the latest odometry pose at record time via the new @pose_setter_for decorator in memory2.
  • Post-processing: post_process.py chains rec_check → AprilTag detection → GTSAM solve → Rerun .rrd export; the same runner is reused by both the Go2 and mid360_realsense rigs.
  • Utilities: Scan Context + GICP loop closure (lidar_loop_closure.py), GTSAM factor-graph solver (gtsam_gt.py), AprilTag detector (apriltags.py), and RRD builder (build_rrd.py) are all new, self-contained modules under dimos/mapping/recording/utils/.

Confidence Score: 4/5

Safe to merge with one fix: a break in the camera-frame logging loop truncates the .rrd output at the first decode error instead of skipping that frame.

The recording path itself is unaffected — data is written correctly to mem2.db. The issue is in the offline post-processing step: build_rrd.py uses break inside the color-image loop, so a single frame that fails to_rerun() silently discards every subsequent frame from the .rrd visualization. All other exception handlers in the same file use continue/pass for exactly this reason. The fix is a one-character change.

dimos/mapping/recording/utils/build_rrd.py (lines 499–509) and dimos/mapping/recording/utils/rec_check.py (odometry_travel / pointlio_odometry hardcoding).

Important Files Changed

Filename Overview
dimos/mapping/recording/utils/build_rrd.py New file: produces Rerun .rrd visualizations from mem2.db recordings. Contains a break in the camera-frame logging loop that silently truncates output on the first decode error; all other error handling in the file uses continue/pass.
dimos/mapping/recording/utils/rec_check.py New file: recording sanity-checker. odometry_travel hardcodes pointlio_odometry without checking table existence, which will raise sqlite3.OperationalError on FastLIO (mid360_realsense) recordings where that table is absent.
dimos/mapping/recording/go2_mid360/record.py New file: Go2 + Mid-360 recording script. Wires PointLio, Mid360, GO2Connection, KeyboardTeleop, and Go2Recorder into a blueprint. Logic is clean.
dimos/mapping/recording/utils/gtsam_gt.py New file: GTSAM landmark-SLAM to drift-correct odometry via AprilTag landmarks and lidar loop closures. Well-structured with proper per-DOF noise weighting.
dimos/mapping/recording/utils/lidar_loop_closure.py New file: Scan Context + GICP lidar loop closure detection. Correct keyframe selection, robust candidate filtering, and sensor-frame transform conversion.
dimos/mapping/recording/utils/post_process.py New file: shared post-process runner that orchestrates rec_check, AprilTag detection, GTSAM solve, and rrd generation per recording. Clean error isolation around each step.
dimos/mapping/recording/utils/apriltags.py New file: AprilTag detection over a color stream with quality-gating (distance/view-angle) and time-cluster medoid selection. Clean implementation.
dimos/hardware/sensors/lidar/pointlio/recorder.py New file: PointlioRecorder that stamps each lidar frame with the latest odometry pose at record time via @pose_setter_for. Clean and self-contained.
dimos/robot/unitree/keyboard_teleop_tui.py New file: TUI keyboard teleop using raw terminal input (no display). Terminal state restoration in finally-block, turbo-toggle debounce, and sit/stand action serialisation all look correct.
dimos/mapping/recording/multi_map_anchor.py New file: anchors one recording's trajectory onto another via shared AprilTag landmarks. Imports private _log_map / _log_path_gradient from build_rrd for the visualization step.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant R as record.py
    participant G as Go2Recorder
    participant PL as PointLio (C++)
    participant M as Mid360
    participant DB as mem2.db

    R->>G: start blueprint
    M-->>G: livox_lidar / livox_imu
    PL-->>G: pointlio_odometry (Odometry)
    PL-->>G: pointlio_lidar (PointCloud2)
    G->>G: "@pose_setter_for caches latest Pose"
    G->>DB: write pointlio_odometry (with pose cols)
    G->>DB: write pointlio_lidar (with pose cols baked in)
    G->>DB: write go2_odom / go2_lidar / color_image

    Note over R,DB: Ctrl+C stops recording

    participant PP as post_process.py
    participant RC as rec_check
    participant AT as apriltags
    participant GT as gtsam_gt
    participant RRD as build_rrd

    PP->>RC: report + write_summary
    PP->>AT: "detect_apriltags -> april_tags stream"
    PP->>GT: build_gtsam_gt (odom + tag landmarks + loop closures)
    GT-->>DB: write gtsam_odom stream
    PP->>RRD: "build_rrd -> .rrd file"
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"}}}%%
sequenceDiagram
    participant R as record.py
    participant G as Go2Recorder
    participant PL as PointLio (C++)
    participant M as Mid360
    participant DB as mem2.db

    R->>G: start blueprint
    M-->>G: livox_lidar / livox_imu
    PL-->>G: pointlio_odometry (Odometry)
    PL-->>G: pointlio_lidar (PointCloud2)
    G->>G: "@pose_setter_for caches latest Pose"
    G->>DB: write pointlio_odometry (with pose cols)
    G->>DB: write pointlio_lidar (with pose cols baked in)
    G->>DB: write go2_odom / go2_lidar / color_image

    Note over R,DB: Ctrl+C stops recording

    participant PP as post_process.py
    participant RC as rec_check
    participant AT as apriltags
    participant GT as gtsam_gt
    participant RRD as build_rrd

    PP->>RC: report + write_summary
    PP->>AT: "detect_apriltags -> april_tags stream"
    PP->>GT: build_gtsam_gt (odom + tag landmarks + loop closures)
    GT-->>DB: write gtsam_odom stream
    PP->>RRD: "build_rrd -> .rrd file"
Loading

Reviews (4): Last reviewed commit: "RESTORE: revert KeyboardTeleop lie-down/..." | 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.
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