Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions data/.lfs/cmu_unity_sim_x86.tar.gz
Git LFS file not shown
41 changes: 36 additions & 5 deletions dimos/core/native_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,25 @@ def start(self) -> None:
env = {**os.environ, **self.config.extra_env}
cwd = self.config.cwd or str(Path(self.config.executable).resolve().parent)

logger.info("Starting native process", cmd=" ".join(cmd), cwd=cwd)
module_name = type(self).__name__
logger.info(
f"Starting native process: {module_name}",
module=module_name,
cmd=" ".join(cmd),
cwd=cwd,
)
Copy link
Member Author

@jeff-hykin jeff-hykin Mar 12, 2026

Choose a reason for hiding this comment

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

Changes to native modules here cause I'm testing the unity sim with the livox native modules and got VERY undescriptive error messages

self._process = subprocess.Popen(
cmd,
env=env,
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
logger.info("Native process started", pid=self._process.pid)
logger.info(
f"Native process started: {module_name}",
module=module_name,
pid=self._process.pid,
)

self._stopping = False
self._watchdog = threading.Thread(target=self._watch_process, daemon=True)
Expand Down Expand Up @@ -193,10 +203,27 @@ def _watch_process(self) -> None:

if self._stopping:
return

module_name = type(self).__name__
exe_name = Path(self.config.executable).name if self.config.executable else "unknown"

# Collect any remaining stderr for the crash report
last_stderr = ""
if self._process.stderr and not self._process.stderr.closed:
try:
remaining = self._process.stderr.read()
if remaining:
last_stderr = remaining.decode("utf-8", errors="replace").strip()
except Exception:
pass
Comment on lines +210 to +218
Copy link
Contributor

Choose a reason for hiding this comment

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

last_stderr will always be empty — crash report has no stderr

The stderr is already fully consumed and closed by the time this read is attempted. _read_log_stream (called via _start_reader) iterates over the entire stream and explicitly calls stream.close() at the end. After stderr_t.join(timeout=2) completes, the stream is exhausted and closed, so self._process.stderr.read() will always return b"".

As a result, last_stderr will always be None in the crash log, making the new crash-report feature a no-op.

A common fix for this pattern is to buffer the last N lines inside the reader thread itself and expose them via a shared variable, rather than trying to re-read the already-closed pipe.

# Example fix: buffer the last few lines in _read_log_stream
# and expose via an instance variable, e.g. self._last_stderr_lines


logger.error(
"Native process died unexpectedly",
f"Native process crashed: {module_name} ({exe_name})",
module=module_name,
executable=exe_name,
pid=self._process.pid,
returncode=rc,
last_stderr=last_stderr[:500] if last_stderr else None,
)
self.stop()

Expand Down Expand Up @@ -265,12 +292,16 @@ def _maybe_build(self) -> None:
if line.strip():
logger.warning(line)
if proc.returncode != 0:
stderr_tail = stderr.decode("utf-8", errors="replace").strip()[-1000:]
raise RuntimeError(
f"Build command failed (exit {proc.returncode}): {self.config.build_command}"
f"Build command failed (exit {proc.returncode}): {self.config.build_command}\n"
f"stderr: {stderr_tail}"
)
if not exe.exists():
raise FileNotFoundError(
f"Build command succeeded but executable still not found: {exe}"
f"Build command succeeded but executable still not found: {exe}\n"
f"Build output may have been written to a different path. "
f"Check that build_command produces the executable at the expected location."
)

def _collect_topics(self) -> dict[str, str]:
Expand Down
1 change: 1 addition & 0 deletions dimos/robot/all_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"unitree-go2-spatial": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial:unitree_go2_spatial",
"unitree-go2-temporal-memory": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_temporal_memory:unitree_go2_temporal_memory",
"unitree-go2-vlm-stream-test": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_vlm_stream_test:unitree_go2_vlm_stream_test",
"unity-sim": "dimos.simulation.unity.blueprint:unity_sim",
"xarm-perception": "dimos.manipulation.blueprints:xarm_perception",
"xarm-perception-agent": "dimos.manipulation.blueprints:xarm_perception_agent",
"xarm6-planner-only": "dimos.manipulation.blueprints:xarm6_planner_only",
Expand Down
Empty file.
61 changes: 61 additions & 0 deletions dimos/simulation/unity/blueprint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.

"""Standalone Unity sim blueprint — interactive test of the Unity bridge.

Launches the Unity simulator, displays lidar + camera in Rerun, and accepts
keyboard teleop via TUI. No navigation stack — just raw sim data.

Usage:
dimos run unity-sim
"""

from __future__ import annotations

from typing import Any

from dimos.core.blueprints import autoconnect
from dimos.protocol.pubsub.impl.lcmpubsub import LCM
from dimos.simulation.unity.module import UnityBridgeModule
from dimos.visualization.rerun.bridge import _resolve_viewer_mode, rerun_bridge


def _rerun_blueprint() -> Any:
import rerun.blueprint as rrb

return rrb.Blueprint(
rrb.Vertical(
rrb.Spatial3DView(origin="world", name="3D"),
rrb.Spatial2DView(origin="world/color_image", name="Camera"),
row_shares=[2, 1],
),
)


rerun_config = {
"blueprint": _rerun_blueprint,
"pubsubs": [LCM()],
"visual_override": {
"world/camera_info": UnityBridgeModule.rerun_suppress_camera_info,
},
"static": {
"world/color_image": UnityBridgeModule.rerun_static_pinhole,
},
}


unity_sim = autoconnect(
UnityBridgeModule.blueprint(),
rerun_bridge(viewer_mode=_resolve_viewer_mode(), **rerun_config),
)
Loading
Loading