Skip to content

Branch: Feat/ivan/m20 Optimized the adaptation of simple_nav for M20#2807

Open
MeloLong wants to merge 3 commits into
dimensionalOS:feat/ivan/m20from
MeloLong:feat/ivan/m20
Open

Branch: Feat/ivan/m20 Optimized the adaptation of simple_nav for M20#2807
MeloLong wants to merge 3 commits into
dimensionalOS:feat/ivan/m20from
MeloLong:feat/ivan/m20

Conversation

@MeloLong

@MeloLong MeloLong commented Jul 8, 2026

Copy link
Copy Markdown
  1. Costmap Optimization

(1) Introduced local ground height estimation to suppress the influence of elevated structures on costmap generation.

A local ground height is first estimated. If a grid cell only contains points that are significantly higher than the estimated ground, those points are treated as ceiling or elevated structures and ignored, instead of being directly used to generate the traversal cost. This prevents elevated points from being mistakenly classified as ground bumps or obstacles, thereby avoiding the incorrect removal of traversable areas.

(2) Relaxed the ground continuity constraint to improve robustness under sparse point cloud observations.

The original method required a grid cell and all four of its neighboring cells (front, back, left, and right) to have valid observations before slope and traversability costs could be computed reliably. Under sparse point clouds or local ground gaps, this strict requirement caused many valid ground cells to be marked as unknown. We therefore relaxed the criterion from complete neighborhood observation to a sufficient number of valid neighboring observations, reducing the spread of unknown regions caused by sparse measurements and improving the completeness of traversable areas.

  1. Trajectory Controller Optimization

Resolved the M20 velocity command response issue by enforcing the robot's minimum velocity response thresholds in the simple_nav trajectory planner. The minimum command limits are:

Linear velocity (x): ≥ 0.2 m/s

Linear velocity (y): ≥ 0.3 m/s

Angular velocity (yaw): ≥ 0.6 rad/s

When linear and angular motions are commanded simultaneously: yaw ≥ 0.2 rad/s

How to run:

Tips: m20 is a IP of m20 robot, please replace it.

DIMOS_ZENOH_CONNECT=tcp/m20_ip python -m dimos.robot.deeprobotics.m20.zenoh_lcm_bridge
dimos run m20-simple-nav

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adapts the simple_nav stack for the DeepRobotics M20 robot by adding a new m20_simple_nav blueprint, improving costmap robustness, and enforcing the robot's minimum velocity response thresholds.

  • New m20_simple_nav blueprint: wires M20's SLAM outputs into a full navigation stack (ray tracer → height-cost occupancy → A* planner → movement manager), registered as dimos run m20-simple-nav.
  • Costmap improvements: introduces ignore_overhead_only local-floor estimation to suppress ceiling/elevated-structure points, and replaces strict binary erosion with a configurable min_gradient_neighbors count (default 5 preserves old behavior; M20 uses 2 for sparse clouds).
  • Velocity thresholds: M20 model sets _min_angular_velocity = 0.6 rad/s in PController; M20Config.ip is now derived from global_config.robot_ip instead of a hardcoded string.

Confidence Score: 4/5

Safe to merge after fixing the M20TF odometry topic remapping in basic.py.

The m20 base blueprint has M20TF subscribing to slam_odom while the zenoh-LCM bridge and every other module updated in this PR use dimos/slam_odom. M20TF will silently receive no messages and never emit the map → base_link TF transform, breaking the coordinate frame chain used by visualization and downstream consumers in the m20-simple-nav stack.

dimos/robot/deeprobotics/m20/blueprints/basic.py — the M20TF remapping line needs the same slam_odomdimos/slam_odom rename applied to all other modules in this PR.

Important Files Changed

Filename Overview
dimos/robot/deeprobotics/m20/blueprints/basic.py Moves M20Connection into the base m20 blueprint and updates topic names to use dimos/ prefix throughout — but M20TF was not updated and still subscribes to bare slam_odom, silently dropping all SLAM odometry messages in any stack built on m20.
dimos/mapping/pointclouds/occupancy.py Adds local-floor estimation to suppress overhead-only structures and relaxes the gradient validity mask from full-erosion to a configurable neighbor-count threshold; logic is sound and well-commented.
dimos/navigation/replanning_a_star/controllers.py Adds a robot-model-specific minimum angular velocity (0.6 rad/s) for M20; the per-model conditional in __init__ is minimal and clear.
dimos/robot/deeprobotics/m20/nav/m20_simple_nav.py New blueprint wiring together the M20 base stack with the SLAM ray tracer, height-cost occupancy, A* planner, and movement manager; inherits the M20TF topic bug from m20.
dimos/robot/deeprobotics/m20/connection.py Switches M20Config.ip from a hardcoded default to a global_config-driven factory, consistent with other robot connection configs in the repo.
dimos/robot/all_blueprints.py Registers the new m20-simple-nav entry point; functionally correct.
.gitignore Restructures data/ ignore rules but accidentally duplicates five data/.lfs/*.tar.gz lines.
dimos/mapping/ray_tracing/rust/flake.lock Replaces a pinned git rev with a dirty working-tree reference; this is a development artifact and won't reproduce on a clean checkout.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Bridge["zenoh_lcm_bridge\n(publishes dimos/slam_*)"]
    M20Conn["M20Connection\n(video + cmd_vel)"]
    M20TF["M20TF\nremapped to slam_odom WARNING\n(should be dimos/slam_odom)"]
    RayTracer["_m20_slam_ray_tracer\nremapped to dimos/slam_odom OK"]
    Occupancy["CostMapper / HeightCostConfig\nignore_overhead_only=True\nmin_gradient_neighbors=2"]
    AStar["ReplanningAStarPlanner\nremapped to dimos/slam_odom OK"]
    PCtrl["PController\nM20: min_angular=0.6 rad/s"]
    MM["MovementManager"]
    Robot["M20 Robot"]

    Bridge -->|dimos/slam_aligned_points| RayTracer
    Bridge -->|dimos/slam_odom| RayTracer
    Bridge -.->|slam_odom no match| M20TF
    Bridge -->|dimos/slam_odom| AStar
    M20Conn -->|color_image / status| Rerun["RerunBridgeModule"]
    RayTracer -->|local_map| Occupancy
    Occupancy -->|costmap| AStar
    AStar --> PCtrl
    PCtrl --> MM
    MM -->|cmd_vel| M20Conn
    M20Conn --> Robot
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
    Bridge["zenoh_lcm_bridge\n(publishes dimos/slam_*)"]
    M20Conn["M20Connection\n(video + cmd_vel)"]
    M20TF["M20TF\nremapped to slam_odom WARNING\n(should be dimos/slam_odom)"]
    RayTracer["_m20_slam_ray_tracer\nremapped to dimos/slam_odom OK"]
    Occupancy["CostMapper / HeightCostConfig\nignore_overhead_only=True\nmin_gradient_neighbors=2"]
    AStar["ReplanningAStarPlanner\nremapped to dimos/slam_odom OK"]
    PCtrl["PController\nM20: min_angular=0.6 rad/s"]
    MM["MovementManager"]
    Robot["M20 Robot"]

    Bridge -->|dimos/slam_aligned_points| RayTracer
    Bridge -->|dimos/slam_odom| RayTracer
    Bridge -.->|slam_odom no match| M20TF
    Bridge -->|dimos/slam_odom| AStar
    M20Conn -->|color_image / status| Rerun["RerunBridgeModule"]
    RayTracer -->|local_map| Occupancy
    Occupancy -->|costmap| AStar
    AStar --> PCtrl
    PCtrl --> MM
    MM -->|cmd_vel| M20Conn
    M20Conn --> Robot
Loading

Reviews (2): Last reviewed commit: "Remove accidentally committed result sym..." | Re-trigger Greptile

Comment on lines +53 to +54
if global_config.robot_model == "m20":
self._min_angular_velocity = 0.6

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 Yaw Floor Overrides Proportional Control

When robot_model is m20, the controller clips angular speed to self._speed before applying this new 0.6 minimum. The default planner speed is 0.55, so any non-zero heading error becomes ±0.6 rad/s, including tiny corrections while driving forward; the M20 path follower can zig-zag or overshoot instead of making proportional yaw corrections.

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!

can_climb=m20_max_step_height,
ignore_noise=0.08,
smoothing=1.5,
min_gradient_neighbors=2,

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 Sparse Cells Become Known Terrain

This M20 config accepts a gradient when only 2 of the center/N/S/E/W cells are observed. In sparse SLAM clouds, a boundary cell with mostly unknown neighbors now receives a finite cost from smoothed or zero-filled heights instead of staying unknown, so A* can prefer it as known traversable ground and route the robot through poorly observed obstacles or drop-offs.

Comment on lines +20 to +23
#include <limits>
#include <mutex>
#include <string>
#include <vector>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Missing Iostream Include

This new native target uses std::cout and std::endl, but the file does not include <iostream>. Building m20_pointcloud_map_save will fail as soon as this module is enabled or compiled directly.

Suggested change
#include <limits>
#include <mutex>
#include <string>
#include <vector>
#include <iostream>
#include <limits>
#include <mutex>
#include <string>
#include <vector>

@@ -0,0 +1 @@
/nix/store/nk8n7m2h8lcanp77aypjsq1pa6xs2fh0-smartnav-terrain-analysis-0.1.0 No newline at end of file

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 Host-Local Result Symlink

This commits a result symlink to an absolute /nix/store/... path. On a fresh checkout, CI runner, or another robot without that exact store path, the link is dangling and any terrain-analysis build or runtime path that expects this artifact will fail with a missing file.

Comment thread .gitignore

# Deepwork local progress state
.slim/deepwork/
data/*

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.

We already have /data/* in the file. But data/.lfs should not be ignored.

Some of the files you're ignoring here are already committed. Also you have duplicates.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will delete it

]
)

# _m20_pointcloud_map_save = PointCloudMapSave.blueprint(

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.

delete commented code

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
2023 4 2019 160
View the top 3 failed test(s) by shortest run time
dimos.codebase_checks.test_no_init_files::test_no_init_files
Stack Traces | 0.018s run time
def test_no_init_files():
        dimos_dir = DIMOS_PROJECT_ROOT / "dimos"
        init_files = sorted(dimos_dir.rglob("__init__.py"))
        # The root dimos/__init__.py is allowed for the porcelain lazy import.
        init_files = [f for f in init_files if f != dimos_dir / "__init__.py"]
        if init_files:
            listing = "\n".join(f"  - {f.relative_to(dimos_dir)}" for f in init_files)
>           raise AssertionError(
                f"Found __init__.py files in dimos/:\n{listing}\n\n"
                "__init__.py files are not allowed because they lead to unnecessary "
                "extraneous imports. Everything should be imported straight from the "
                "source module."
            )
E           AssertionError: Found __init__.py files in dimos/:
E             - .../cli/spy/__init__.py
E           
E           __init__.py files are not allowed because they lead to unnecessary extraneous imports. Everything should be imported straight from the source module.

dimos_dir  = PosixPath('.../dimos/dimos/dimos')
init_files = [PosixPath('.../dimos/dimos/dimos/.../cli/spy/__init__.py')]
listing    = '  - .../cli/spy/__init__.py'

dimos/codebase_checks/test_no_init_files.py:25: AssertionError
dimos.codebase_checks.test_no_sections::test_no_section_markers
Stack Traces | 0.515s run time
def test_no_section_markers():
        """
        Fail if any file contains section-style comment markers.
    
        If a file is too complicated to be understood without sections, then the
        sections should be files. We don't need "subfiles".
        """
        violations = find_section_markers()
        if violations:
            report_lines = [
                f"Found {len(violations)} section marker(s). "
                "If a file is too complicated to be understood without sections, "
                'then the sections should be files. We don\'t need "subfiles".',
                "",
            ]
            for path, lineno, text in violations:
                report_lines.append(f"  {path}:{lineno}: {text.strip()}")
>           raise AssertionError("\n".join(report_lines))
E           AssertionError: Found 2 section marker(s). If a file is too complicated to be understood without sections, then the sections should be files. We don't need "subfiles".
E           
E             ....../deeprobotics/m20/connection.py:66: # --- Patrol protocol (manual §1.1) --------------------------------------------
E             ....../deeprobotics/m20/connection.py:273: # --- dimos module -------------------------------------------------------------

lineno     = 273
path       = '....../deeprobotics/m20/connection.py'
report_lines = ['Found 2 section marker(s). If a file is too complicated to be understood without sections, then the sections should ....../deeprobotics/m20/connection.py:273: # --- dimos module -------------------------------------------------------------']
text       = '# --- dimos module -------------------------------------------------------------'
violations = [('....../deeprobotics/m20/connection.py', 66, '# --- Patrol protocol (manual §1.1) -----------------------------...probotics/m20/connection.py', 273, '# --- dimos module -------------------------------------------------------------')]

dimos/codebase_checks/test_no_sections.py:145: AssertionError
dimos.robot.test_all_blueprints_generation::test_all_blueprints_is_current
Stack Traces | 2.83s run time
def test_all_blueprints_is_current() -> None:
        root = DIMOS_PROJECT_ROOT / "dimos"
        all_blueprints, all_modules = _scan_for_blueprints(root)
    
        common = set(all_blueprints.keys()) & set(all_modules.keys())
        assert not common, (
            f"Names must be unique across blueprints and modules, "
            f"but these appear in both: {sorted(common)}"
        )
    
        generated_content = _generate_all_blueprints_content(all_blueprints, all_modules)
    
        file_path = root / "robot" / "all_blueprints.py"
    
        if "CI" in os.environ:
            if not file_path.exists():
                pytest.fail(f"all_blueprints.py does not exist at {file_path}")
    
            current_content = file_path.read_text()
            if current_content != generated_content:
                diff = difflib.unified_diff(
                    current_content.splitlines(keepends=True),
                    generated_content.splitlines(keepends=True),
                    fromfile="all_blueprints.py (current)",
                    tofile="all_blueprints.py (generated)",
                )
                diff_str = "".join(diff)
>               pytest.fail(
                    f"all_blueprints.py is out of date. Run "
                    f"`pytest dimos/robot/test_all_blueprints_generation.py` locally to update.\n\n"
                    f"Diff:\n{diff_str}"
                )
E               Failed: all_blueprints.py is out of date. Run `pytest dimos/robot/test_all_blueprints_generation.py` locally to update.
E               
E               Diff:
E               --- all_blueprints.py (current)
E               +++ all_blueprints.py (generated)
E               @@ -227,6 +227,7 @@
E                    "pgo": "dimos.navigation.cmu_nav.modules.pgo.pgo.PGO",
E                    "phone-teleop-module": "dimos.teleop.phone.phone_teleop_module.PhoneTeleopModule",
E                    "pick-and-place-module": "dimos.manipulation.pick_and_place_module.PickAndPlaceModule",
E               +    "point-cloud-map-save": "dimos.robot.deeprobotics.m20.nav.map_save.map_save.PointCloudMapSave",
E                    "point-lio": "dimos.hardware.sensors.lidar.pointlio.module.PointLio",
E                    "pointlio-recorder": "dimos.hardware.sensors.lidar.pointlio.recorder.PointlioRecorder",
E                    "quest-teleop-module": "dimos.teleop.quest.quest_teleop_module.QuestTeleopModule",

all_blueprints = {'alfred-nav': 'dimos.robot.diy.alfred.blueprints.alfred_nav:alfred_nav', 'coordinator-basic': 'dimos.control.blueprin...oordinator-cartesian-ik-piper': 'dimos.robot.manipulators.piper.blueprints.teleop:coordinator_cartesian_ik_piper', ...}
all_modules = {'alfred-high-level': 'dimos.robot.diy.alfred.effector_high_level.AlfredHighLevel', 'arm-teleop-module': 'dimos.teleop..._navigation.BBoxNavigationModule', 'b1-connection-module': 'dimos.robot.unitree.b1.connection.B1ConnectionModule', ...}
common     = set()
current_content = '# Copyright 2025-2026 Dimensional Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the "License");\n# you m...ebsocket_vis_module.WebsocketVisModule",\n    "zed-camera": "dimos.hardware.sensors.camera.zed.camera.ZEDCamera",\n}\n'
diff       = <generator object unified_diff at 0xff2a766dd080>
diff_str   = '--- all_blueprints.py (current)\n+++ all_blueprints.py (generated)\n@@ -227,6 +227,7 @@\n     "pgo": "dimos.navigatio...recorder.PointlioRecorder",\n     "quest-teleop-module": "dimos.teleop.quest.quest_teleop_module.QuestTeleopModule",\n'
file_path  = PosixPath('.../dimos/robot/all_blueprints.py')
generated_content = '# Copyright 2025-2026 Dimensional Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the "License");\n# you m...ebsocket_vis_module.WebsocketVisModule",\n    "zed-camera": "dimos.hardware.sensors.camera.zed.camera.ZEDCamera",\n}\n'
root       = PosixPath('.../dimos/dimos/dimos')

dimos/robot/test_all_blueprints_generation.py:76: Failed
dimos.codebase_checks.test_no_all::test_no_all
Stack Traces | 4.5s run time
def test_no_all():
        """Fail if any file defines `__all__`."""
        dimos_dir = DIMOS_PROJECT_ROOT / "dimos"
        hits = find_all_definitions()
        if hits:
            listing = "\n".join(f"  - {p.relative_to(dimos_dir)}:{lineno}" for p, lineno in hits)
>           raise AssertionError(
                f"Found __all__ definition(s) in dimos/:\n{listing}\n\n"
                "__all__ is not allowed. We don't use `from x import *`, so __all__ "
                "lists serve no purpose and are tedious to maintain. Remove them. For "
                "an import that exists purely to be re-exported, use `# noqa: F401`."
            )
E           AssertionError: Found __all__ definition(s) in dimos/:
E             - mapping/ray_tracing/voxel_map.py:27
E             - .../pubsub/impl/zenohpubsub.py:295
E             - .../blueprints/navigation/unitree_go2_nav_3d.py:136
E           
E           __all__ is not allowed. We don't use `from x import *`, so __all__ lists serve no purpose and are tedious to maintain. Remove them. For an import that exists purely to be re-exported, use `# noqa: F401`.

dimos_dir  = PosixPath('.../dimos/dimos/dimos')
hits       = [(PosixPath('.../dimos/dimos/dimos/mapping/ray_tracing/voxel_map.py'), 27), (PosixPath('.../home/runner/wor... (PosixPath('.../dimos/dimos/dimos/.../blueprints/navigation/unitree_go2_nav_3d.py'), 136)]
listing    = '  - mapping/ray_tracing/voxel_map.py:27\n  - .../pubsub/impl/zenohpubsub.py:295\n  - .../blueprints/navigation/unitree_go2_nav_3d.py:136'

dimos/codebase_checks/test_no_all.py:51: 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/robot/deeprobotics/m20/nav/m20_simple_nav.py
global_emit_every=10,
# M20's onboard SLAM publishes this cloud already registered in map frame.
registered_clouds=True,
).remappings(

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.

Why remap?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the LiDAR and odometry topics published by the M20 are not named lidar and odometry, but as shown in the current code.

from dimos.msgs.geometry_msgs.Twist import Twist


def main() -> None:

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 this for? If you just need to publish the same Twist, you can do it from the command line: uv run dimos topic send /nav/cmd_vel "Twist(Vector3(0.5, 0, 0), Vector3(0, 0, 0))"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll remove it. It was just some test code from when I first started working on this.


# Verify protocol port compliance (mypy will flag missing ports)
if TYPE_CHECKING:
PointCloudMapSave()

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.

Why create and discard this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this code. It's a deprecated feature.

@@ -0,0 +1 @@
/nix/store/nk8n7m2h8lcanp77aypjsq1pa6xs2fh0-smartnav-terrain-analysis-0.1.0 No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this was accidentally commited?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants