Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9423ae4
all vis changes
jeff-hykin Apr 13, 2026
6d6151c
fixup
jeff-hykin Apr 13, 2026
ac62445
add a conventions.md to dev docs
jeff-hykin Apr 13, 2026
36492b1
fixup cmd_vel timer edgecase leaks
jeff-hykin Apr 13, 2026
306bd2b
switch cmd_vel_scaling to be on rerun
jeff-hykin Apr 13, 2026
f7277d9
add convention
jeff-hykin Apr 13, 2026
75bf186
fixup
jeff-hykin Apr 13, 2026
29875f8
missed edgecase
jeff-hykin Apr 14, 2026
dd10ae4
fix webvis logic
jeff-hykin Apr 15, 2026
a3c5666
make pydantic happy
jeff-hykin Apr 15, 2026
9683ea6
global config patch
jeff-hykin Apr 15, 2026
247571f
cleanup
jeff-hykin Apr 15, 2026
bffc95d
better helper message
jeff-hykin Apr 17, 2026
43d6f78
Merge branch 'dev' into jeff/fix/rconnect2
jeff-hykin Apr 18, 2026
a009186
-
jeff-hykin Apr 21, 2026
20d455a
Merge branch 'jeff/fix/rconnect2' of github.com:dimensionalOS/dimos i…
jeff-hykin Apr 21, 2026
0b962a3
merge dev
jeff-hykin Apr 21, 2026
a22150d
uv lock was incorrect somehow
jeff-hykin Apr 21, 2026
a8f9653
add conventions
jeff-hykin Apr 23, 2026
769c8b8
switch to rosnav's MovementManager
jeff-hykin Apr 23, 2026
a3a2ab5
refactor: move rerun constants into dimos/visualization/rerun/config.py
jeff-hykin Apr 23, 2026
e7106aa
simplify tests and make them good
jeff-hykin Apr 23, 2026
f6de436
import improve
jeff-hykin Apr 23, 2026
1dce3fe
restore constants.py
jeff-hykin Apr 24, 2026
f41ffc8
refactor(cli): move cast, get_args, RerunOpenOption to top-level imports
jeff-hykin Apr 24, 2026
881fc04
import psutil
jeff-hykin Apr 24, 2026
b35242c
-
jeff-hykin Apr 24, 2026
3b745dc
-
jeff-hykin Apr 24, 2026
009aff9
cleanup test
jeff-hykin Apr 24, 2026
474f10e
clean
jeff-hykin Apr 24, 2026
72500ff
add scaling
jeff-hykin Apr 24, 2026
4e38462
-
jeff-hykin Apr 24, 2026
4591285
add scaling check
jeff-hykin Apr 24, 2026
76d50e4
address main comments
jeff-hykin Apr 24, 2026
073d0a7
refactor(ws-server): add ViewerMsg TypedDict union for typed message …
jeff-hykin Apr 24, 2026
cdd07b5
remove junk comments
jeff-hykin Apr 24, 2026
4d2072b
-
jeff-hykin Apr 24, 2026
39c2769
re-apply the SIGINT fix
jeff-hykin Apr 24, 2026
3d0aa56
Merge remote-tracking branch 'origin/dev' into jeff/fix/rconnect2
jeff-hykin Apr 24, 2026
ef579e8
-
jeff-hykin Apr 24, 2026
83ad060
update docs
jeff-hykin Apr 24, 2026
059564b
fixes
jeff-hykin Apr 24, 2026
293642e
Merge remote-tracking branch 'origin/dev' into jeff/fix/rconnect2
jeff-hykin Apr 24, 2026
38b4e29
test: ignore .ignore.enhance overlay in section-marker scan
jeff-hykin Apr 24, 2026
6165d65
fix: cast pipe() return to satisfy mypy no-any-return check
jeff-hykin Apr 24, 2026
92c02d9
refactor: move websockets imports to top of test files
jeff-hykin Apr 24, 2026
473dc5b
refactor: address remaining review nits
jeff-hykin Apr 25, 2026
c1b845a
fix: correct import sort order for ruff
jeff-hykin Apr 25, 2026
aac49ba
revert .ignore.enhance exclusion, use .hidden instead
jeff-hykin Apr 25, 2026
aab885e
Merge branch 'dev' into jeff/fix/rconnect2
jeff-hykin Apr 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dimos/core/docker_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from dimos.core.rpc_client import ModuleProxyProtocol, RpcCall
from dimos.protocol.rpc.pubsubrpc import LCMRPC
from dimos.utils.logging_config import setup_logger
from dimos.visualization.rerun.bridge import RERUN_GRPC_PORT, RERUN_WEB_PORT
from dimos.visualization.constants import RERUN_GRPC_PORT, RERUN_WEB_PORT

if TYPE_CHECKING:
from collections.abc import Callable
Expand Down
11 changes: 8 additions & 3 deletions dimos/core/global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
# limitations under the License.

import re
from typing import Literal, TypeAlias

from pydantic_settings import BaseSettings, SettingsConfigDict

from dimos.models.vl.types import VlModelName

ViewerBackend: TypeAlias = Literal["rerun", "rerun-web", "rerun-connect", "foxglove", "none"]
from dimos.visualization.constants import (
RERUN_ENABLE_WEB,
RERUN_OPEN_DEFAULT,
RerunOpenOption,
ViewerBackend,
)


def _get_all_numbers(s: str) -> list[float]:
Expand All @@ -37,6 +40,8 @@ class GlobalConfig(BaseSettings):
replay_dir: str = "go2_sf_office"
Comment thread
jeff-hykin marked this conversation as resolved.
Outdated
new_memory: bool = False
viewer: ViewerBackend = "rerun"
rerun_open: RerunOpenOption = RERUN_OPEN_DEFAULT
rerun_web: bool = RERUN_ENABLE_WEB
Comment thread
jeff-hykin marked this conversation as resolved.
n_workers: int = 2
memory_limit: str = "auto"
mujoco_camera_position: str | None = None
Expand Down
5 changes: 3 additions & 2 deletions dimos/hardware/sensors/camera/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from dimos.agents.annotation import skill
from dimos.core.coordination.blueprints import autoconnect
from dimos.core.core import rpc
from dimos.core.global_config import global_config
from dimos.core.module import Module, ModuleConfig
from dimos.core.stream import Out
from dimos.hardware.sensors.camera.spec import CameraHardware
Expand All @@ -31,7 +32,7 @@
from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier
from dimos.spec import perception
from dimos.visualization.rerun.bridge import RerunBridgeModule
from dimos.visualization.vis_module import vis_module


def default_transform() -> Transform:
Expand Down Expand Up @@ -120,5 +121,5 @@ def stop(self) -> None:

demo_camera = autoconnect(
CameraModule.blueprint(),
RerunBridgeModule.blueprint(),
vis_module(viewer_backend=global_config.viewer),
)
35 changes: 25 additions & 10 deletions dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,45 @@
from dimos.core.coordination.blueprints import autoconnect
from dimos.hardware.sensors.lidar.fastlio2.module import FastLio2
from dimos.mapping.voxels import VoxelGridMapper
from dimos.visualization.rerun.bridge import RerunBridgeModule
from dimos.visualization.vis_module import vis_module

voxel_size = 0.05

mid360_fastlio = autoconnect(
FastLio2.blueprint(voxel_size=voxel_size, map_voxel_size=voxel_size, map_freq=-1),
RerunBridgeModule.blueprint(),
vis_module(
"rerun",
rerun_config={
"visual_override": {
"world/lidar": lambda grid: grid.to_rerun(voxel_size=voxel_size, mode="boxes"),
},
},
),
).global_config(n_workers=2, robot_model="mid360_fastlio2")

mid360_fastlio_voxels = autoconnect(
FastLio2.blueprint(),
VoxelGridMapper.blueprint(voxel_size=voxel_size, carve_columns=False),
RerunBridgeModule.blueprint(
visual_override={
"world/lidar": None,
}
vis_module(
"rerun",
rerun_config={
"visual_override": {
"world/global_map": lambda grid: grid.to_rerun(voxel_size=voxel_size, mode="boxes"),
"world/lidar": None,
},
},
),
).global_config(n_workers=3, robot_model="mid360_fastlio2_voxels")

mid360_fastlio_voxels_native = autoconnect(
FastLio2.blueprint(voxel_size=voxel_size, map_voxel_size=voxel_size, map_freq=3.0),
RerunBridgeModule.blueprint(
visual_override={
"world/lidar": None,
}
vis_module(
"rerun",
rerun_config={
"visual_override": {
"world/lidar": None,
"world/global_map": lambda grid: grid.to_rerun(voxel_size=voxel_size, mode="boxes"),
},
},
),
).global_config(n_workers=2, robot_model="mid360_fastlio2")
4 changes: 2 additions & 2 deletions dimos/hardware/sensors/lidar/livox/livox_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

from dimos.core.coordination.blueprints import autoconnect
from dimos.hardware.sensors.lidar.livox.module import Mid360
from dimos.visualization.rerun.bridge import RerunBridgeModule
from dimos.visualization.vis_module import vis_module

mid360 = autoconnect(
Mid360.blueprint(),
RerunBridgeModule.blueprint(),
vis_module("rerun"),
).global_config(n_workers=2, robot_model="mid360")
10 changes: 5 additions & 5 deletions dimos/manipulation/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from dimos.msgs.sensor_msgs.JointState import JointState
from dimos.perception.object_scene_registration import ObjectSceneRegistrationModule
from dimos.robot.catalog.ufactory import xarm6 as _catalog_xarm6, xarm7 as _catalog_xarm7
from dimos.robot.foxglove_bridge import FoxgloveBridge # TODO: migrate to rerun
from dimos.visualization.vis_module import vis_module

# Single XArm6 planner (standalone, no coordinator)
_xarm6_planner_cfg = _catalog_xarm6(
Expand Down Expand Up @@ -196,14 +196,14 @@
use_aabb=True,
max_obstacle_width=0.06,
),
FoxgloveBridge.blueprint(), # TODO: migrate to rerun
vis_module("foxglove"),
)
.transports(
{
("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState),
}
)
.global_config(viewer="foxglove", n_workers=4)
.global_config(n_workers=4)
)


Expand Down Expand Up @@ -289,7 +289,7 @@

from dimos.robot.catalog.ufactory import XARM7_SIM_PATH
from dimos.simulation.engines.mujoco_sim_module import MujocoSimModule
from dimos.visualization.rerun.bridge import RerunBridgeModule, _resolve_viewer_mode
from dimos.visualization.rerun.bridge import RerunBridgeModule

_xarm7_sim_cfg = _catalog_xarm7(
name="arm",
Expand Down Expand Up @@ -323,7 +323,7 @@
hardware=[_xarm7_sim_cfg.to_hardware_component()],
tasks=[_xarm7_sim_cfg.to_task_config()],
),
RerunBridgeModule.blueprint(viewer_mode=_resolve_viewer_mode()),
RerunBridgeModule.blueprint(),
).transports(
{
("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState),
Expand Down
4 changes: 2 additions & 2 deletions dimos/manipulation/grasping/demo_grasping.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from dimos.manipulation.grasping.grasping import GraspingModule
from dimos.perception.detection.detectors.yoloe import YoloePromptMode
from dimos.perception.object_scene_registration import ObjectSceneRegistrationModule
from dimos.robot.foxglove_bridge import FoxgloveBridge
from dimos.visualization.vis_module import vis_module

camera_module = RealSenseCamera.blueprint(enable_pointcloud=False)

Expand All @@ -44,7 +44,7 @@
("/tmp", "/tmp", "rw")
], # Grasp visualization debug standalone: python -m dimos.manipulation.grasping.visualize_grasps
),
FoxgloveBridge.blueprint(),
vis_module("foxglove"),
McpServer.blueprint(),
McpClient.blueprint(),
).global_config(viewer="foxglove")
166 changes: 166 additions & 0 deletions dimos/navigation/cmd_vel_mux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Copyright 2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""CmdVelMux: merges nav and teleop velocity commands.

Teleop (tele_cmd_vel) takes priority over autonomous navigation
(nav_cmd_vel). When teleop is active, nav commands are suppressed
and a stop_movement signal is published. After a cooldown period
with no teleop input, nav commands resume.
"""

from __future__ import annotations

import threading
from typing import Any
import weakref

from dimos_lcm.std_msgs import Bool
from reactivex.disposable import Disposable

from dimos.constants import DEFAULT_THREAD_JOIN_TIMEOUT
from dimos.core.core import rpc
from dimos.core.module import Module, ModuleConfig
from dimos.core.stream import In, Out
from dimos.msgs.geometry_msgs.Twist import Twist
from dimos.utils.logging_config import setup_logger

logger = setup_logger()


class CmdVelMuxConfig(ModuleConfig):
tele_cooldown_sec: float = 1.0


class CmdVelMux(Module):
"""Multiplexes nav_cmd_vel and tele_cmd_vel into a single cmd_vel output.

When teleop input arrives, stop_movement is published so downstream
modules (planner, explorer) can cancel their active goals.

config.tele_cooldown_sec
nav_cmd_vel will be ignored for tele_cooldown_sec seconds after
the last teleop command

dev notes: each new tele_cmd_vel message restarts the cooldown
so under continuous teleop (e.g. 50 Hz joystick) the cooldown
is never actually reached; it only fires once the operator stops.

Ports:
nav_cmd_vel (In[Twist]): Velocity from the autonomous planner.
tele_cmd_vel (In[Twist]): Velocity from keyboard/joystick teleop.
cmd_vel (Out[Twist]): Merged output — teleop wins when active.
stop_movement (Out[Bool]): Published once per cooldown window, on
the first teleop message; downstream nav modules should cancel
their active goal when they see it.
"""

config: CmdVelMuxConfig

nav_cmd_vel: In[Twist]
tele_cmd_vel: In[Twist]
cmd_vel: Out[Twist]
stop_movement: Out[Bool]

def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._teleop_active = False
self._lock = threading.Lock()
self._timer: threading.Timer | None = None
# Monotonic token identifying the current cooldown timer. Each new
# _on_teleop bumps this; _end_teleop short-circuits if its captured
# generation doesn't match — a cheap fix for stale Timer callbacks.
self._timer_gen = 0

def __getstate__(self) -> dict[str, Any]:
state: dict[str, Any] = super().__getstate__() # type: ignore[no-untyped-call]
state.pop("_lock", None)
state.pop("_timer", None)
return state

def __setstate__(self, state: dict[str, Any]) -> None:
super().__setstate__(state)
self._lock = threading.Lock()
self._timer = None
self._timer_gen = 0

def __del__(self) -> None:
# Cancel any pending cooldown timer so the daemon thread doesn't
# outlive the mux and trip pytest's thread-leak detector.
timer = getattr(self, "_timer", None)
if timer is not None:
timer.cancel()
timer.join(timeout=DEFAULT_THREAD_JOIN_TIMEOUT)
Comment thread
jeff-hykin marked this conversation as resolved.
Outdated

@rpc
def start(self) -> None:
super().start()
self.register_disposable(Disposable(self.nav_cmd_vel.subscribe(self._on_nav)))
self.register_disposable(Disposable(self.tele_cmd_vel.subscribe(self._on_teleop)))

@rpc
def stop(self) -> None:
with self._lock:
self._timer_gen += 1 # invalidate any pending _end_teleop
if self._timer is not None:
self._timer.cancel()
self._timer = None
super().stop()

def _on_nav(self, msg: Twist) -> None:
with self._lock:
if self._teleop_active:
return
self.cmd_vel.publish(msg)

def _on_teleop(self, msg: Twist) -> None:
was_active: bool
with self._lock:
was_active = self._teleop_active
self._teleop_active = True
if self._timer is not None:
# Cancel + join so the superseded Timer thread exits promptly
# rather than accumulating under rapid teleop (50 Hz) and
# tripping pytest's thread-leak detector.
self._timer.cancel()
self._timer.join(timeout=DEFAULT_THREAD_JOIN_TIMEOUT)
self._timer_gen += 1
my_gen = self._timer_gen
# weakref prevents the Timer thread from keeping the mux alive
# via a bound-method reference — otherwise mux.__del__ can't
# run at test scope exit.
self_ref = weakref.ref(self)
Comment thread
jeff-hykin marked this conversation as resolved.
Outdated

def _end() -> None:
obj = self_ref()
if obj is not None:
obj._end_teleop(my_gen)

self._timer = threading.Timer(self.config.tele_cooldown_sec, _end)
self._timer.daemon = True
self._timer.start()
Comment thread
jeff-hykin marked this conversation as resolved.
Outdated

if not was_active:
self.stop_movement.publish(Bool(data=True))
logger.info("Teleop active — published stop_movement")

self.cmd_vel.publish(msg)

def _end_teleop(self, expected_gen: int) -> None:
with self._lock:
if expected_gen != self._timer_gen:
# Superseded by a newer timer (or cleared by stop()).
return
self._teleop_active = False
self._timer = None
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class WavefrontFrontierExplorer(Module):
goal_reached: In[Bool]
explore_cmd: In[Bool]
stop_explore_cmd: In[Bool]
stop_movement: In[Bool]

# LCM outputs
goal_request: Out[PoseStamped]
Expand Down Expand Up @@ -171,6 +172,10 @@ def start(self) -> None:
unsub = self.stop_explore_cmd.subscribe(self._on_stop_explore_cmd)
self.register_disposable(Disposable(unsub))

if self.stop_movement.transport is not None:
unsub = self.stop_movement.subscribe(self._on_stop_movement)
self.register_disposable(Disposable(unsub))

@rpc
def stop(self) -> None:
self.stop_exploration()
Expand Down Expand Up @@ -201,6 +206,12 @@ def _on_stop_explore_cmd(self, msg: Bool) -> None:
logger.info("Received exploration stop command via LCM")
self.stop_exploration()

def _on_stop_movement(self, msg: Bool) -> None:
"""Handle stop movement from teleop — cancel active exploration."""
if msg.data and self.exploration_active:
logger.info("WavefrontFrontierExplorer: stop_movement received, stopping exploration")
self.stop_exploration()

def _count_costmap_information(self, costmap: OccupancyGrid) -> int:
"""
Count the amount of information in a costmap (free space + obstacles).
Expand Down
Loading
Loading