From 8fcbfa29a97b7c8c039063203872d98b4eaa3698 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 13:04:16 -0600 Subject: [PATCH 01/45] provide alternative organization --- flake.lock | 56 +++++++++++++++++++++++++-- flake.nix | 111 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 130 insertions(+), 37 deletions(-) diff --git a/flake.lock b/flake.lock index e6d920a293..6235e35c44 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,17 @@ { "nodes": { + "core": { + "locked": { + "lastModified": 1, + "narHash": "sha256-lWX5DUltOFcS57I8wHH0Sz3J++zMORxHf+CXoZZLQzU=", + "path": "./helpers/builtins", + "type": "path" + }, + "original": { + "path": "./helpers/builtins", + "type": "path" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -18,13 +30,50 @@ "type": "github" } }, + "lib": { + "inputs": { + "core": "core", + "flakeUtils": [ + "flake-utils" + ], + "libSource": "libSource" + }, + "locked": { + "lastModified": 1762804113, + "narHash": "sha256-8JsbXhJY4pREh0MHEQCbpmJAwdLYdQJ0dz5G9izCOaM=", + "owner": "jeff-hykin", + "repo": "quick-nix-toolkits", + "rev": "6c6112ec4aabbc43320c0a25d935404f7bab002e", + "type": "github" + }, + "original": { + "owner": "jeff-hykin", + "repo": "quick-nix-toolkits", + "type": "github" + } + }, + "libSource": { + "locked": { + "lastModified": 1762650614, + "narHash": "sha256-tPIUJjNeNs3LMWH8w2nHLx0trZJdOJ54mN2UQhcjZ9g=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "91ea24e62ff55f95939f32432fa5def2d6d24d2a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1748929857, - "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", + "lastModified": 1762977756, + "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", + "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", "type": "github" }, "original": { @@ -37,6 +86,7 @@ "root": { "inputs": { "flake-utils": "flake-utils", + "lib": "lib", "nixpkgs": "nixpkgs" } }, diff --git a/flake.nix b/flake.nix index 0061153089..01e6a9a2de 100644 --- a/flake.nix +++ b/flake.nix @@ -4,70 +4,113 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + lib.url = "github:jeff-hykin/quick-nix-toolkits"; + lib.inputs.flakeUtils.follows = "flake-utils"; }; - outputs = { self, nixpkgs, flake-utils, ... }: + outputs = { self, nixpkgs, flake-utils, lib, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; - + # ------------------------------------------------------------ # 1. Shared package list (tool-chain + project deps) # ------------------------------------------------------------ - devPackages = with pkgs; [ + # we "flag" each package with what we need it for (e.g. LD_LIBRARY_PATH, nativeBuildInputs vs buildInputs, etc) + aggregation = lib.aggregator [ ### Core shell & utils - bashInteractive coreutils gh - stdenv.cc.cc.lib pcre2 + { vals.pkg=pkgs.bashInteractive; flags={}; } + { vals.pkg=pkgs.coreutils; flags={}; } + { vals.pkg=pkgs.gh; flags={}; } + { vals.pkg=pkgs.stdenv.cc.cc.lib; flags={}; } + { vals.pkg=pkgs.pcre2; flags.ldLibraryGroup=true; } + { vals.pkg=pkgs.git-lfs; flags={}; } + { vals.pkg=pkgs.unixtools.ifconfig; flags={}; } ### Python + static analysis - python312 python312Packages.pip python312Packages.setuptools - python312Packages.virtualenv pre-commit + { vals.pkg=pkgs.python312; flags={}; } + { vals.pkg=pkgs.python312Packages.pip; flags={}; } + { vals.pkg=pkgs.python312Packages.setuptools; flags={}; } + { vals.pkg=pkgs.python312Packages.virtualenv; flags={}; } + { vals.pkg=pkgs.pre-commit; flags={}; } ### Runtime deps - python312Packages.pyaudio portaudio ffmpeg_6 ffmpeg_6.dev - + { vals.pkg=pkgs.python312Packages.pyaudio; flags={}; } + { vals.pkg=pkgs.portaudio; flags={}; } + { vals.pkg=pkgs.ffmpeg_6; flags={}; } + { vals.pkg=pkgs.ffmpeg_6.dev; flags={}; } + ### Graphics / X11 stack - libGL libGLU mesa glfw - xorg.libX11 xorg.libXi xorg.libXext xorg.libXrandr xorg.libXinerama - xorg.libXcursor xorg.libXfixes xorg.libXrender xorg.libXdamage - xorg.libXcomposite xorg.libxcb xorg.libXScrnSaver xorg.libXxf86vm - - udev SDL2 SDL2.dev zlib + { vals.pkg=pkgs.libGL; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.libGLU; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.mesa; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.glfw; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libX11; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXi; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXext; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXrandr; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXinerama; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXcursor; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXfixes; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXrender; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXdamage; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXcomposite; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libxcb; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXScrnSaver; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.xorg.libXxf86vm; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.udev; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.SDL2; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.SDL2.dev; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.zlib; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } ### GTK / OpenCV helpers - glib gtk3 gdk-pixbuf gobject-introspection + { vals.pkg=pkgs.glib; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gtk3; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gdk-pixbuf; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gobject-introspection; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } ### GStreamer - gst_all_1.gstreamer gst_all_1.gst-plugins-base gst_all_1.gst-plugins-good - gst_all_1.gst-plugins-bad gst_all_1.gst-plugins-ugly - python312Packages.gst-python + { vals.pkg=pkgs.gst_all_1.gstreamer; flags.ldLibraryGroup=true; flags.giTypelibGroup=true; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-base; flags.ldLibraryGroup=true; flags.giTypelibGroup=true; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-good; flags={}; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-bad; flags={}; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.gst_all_1.gst-plugins-ugly; flags={}; onlyIf=pkgs.stdenv.isLinux; } + { vals.pkg=pkgs.python312Packages.gst-python; flags={}; onlyIf=pkgs.stdenv.isLinux; } ### Open3D & build-time - eigen cmake ninja jsoncpp libjpeg libpng + { vals.pkg=pkgs.eigen; flags={}; } + { vals.pkg=pkgs.cmake; flags={}; } + { vals.pkg=pkgs.ninja; flags={}; } + { vals.pkg=pkgs.jsoncpp; flags={}; } + { vals.pkg=pkgs.libjpeg; flags={}; } + { vals.pkg=pkgs.libpng; flags={}; } ### LCM (Lightweight Communications and Marshalling) - lcm + { vals.pkg=pkgs.lcm; flags.ldLibraryGroup=true; } ]; + + # ------------------------------------------------------------ + # 2. group / aggregate our packages + # ------------------------------------------------------------ + devPackages = aggregation.getAll { attrPath=[ "pkg" ]; }; + ldLibraryPackages = aggregation.getAll { hasAllFlags=[ "ldLibraryGroup" ]; attrPath=[ "pkg" ]; }; + giTypelibPackagesString = aggregation.getAll { + hasAllFlags=[ "giTypelibGroup" ]; + attrPath=[ "pkg" ]; + strAppend="/lib/girepository-1.0"; + strJoin=":"; + }; + ldGroupAndStdlib = [ pkgs.stdenv.cc.cc.lib ] ++ ldLibraryPackages; # ------------------------------------------------------------ - # 2. Host interactive shell → `nix develop` + # 3. Host interactive shell → `nix develop` # ------------------------------------------------------------ devShell = pkgs.mkShell { packages = devPackages; shellHook = '' - export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ - pkgs.stdenv.cc.cc.lib pkgs.libGL pkgs.libGLU pkgs.mesa pkgs.glfw - pkgs.xorg.libX11 pkgs.xorg.libXi pkgs.xorg.libXext pkgs.xorg.libXrandr - pkgs.xorg.libXinerama pkgs.xorg.libXcursor pkgs.xorg.libXfixes - pkgs.xorg.libXrender pkgs.xorg.libXdamage pkgs.xorg.libXcomposite - pkgs.xorg.libxcb pkgs.xorg.libXScrnSaver pkgs.xorg.libXxf86vm - pkgs.udev pkgs.portaudio pkgs.SDL2.dev pkgs.zlib pkgs.glib pkgs.gtk3 - pkgs.gdk-pixbuf pkgs.gobject-introspection pkgs.lcm pkgs.pcre2 - pkgs.gst_all_1.gstreamer pkgs.gst_all_1.gst-plugins-base]}:$LD_LIBRARY_PATH" - + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath ldGroupAndStdlib}:$LD_LIBRARY_PATH" export DISPLAY=:0 - export GI_TYPELIB_PATH="${pkgs.gst_all_1.gstreamer}/lib/girepository-1.0:${pkgs.gst_all_1.gst-plugins-base}/lib/girepository-1.0:$GI_TYPELIB_PATH" - + export GI_TYPELIB_PATH="${giTypelibPackagesString}:$GI_TYPELIB_PATH" PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") if [ -f "$PROJECT_ROOT/env/bin/activate" ]; then . "$PROJECT_ROOT/env/bin/activate" From a38b6f67f43aa64a139c3627468a29593c3ad0b7 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 13:28:19 -0600 Subject: [PATCH 02/45] clean up mistakes --- flake.nix | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 01e6a9a2de..72faf2c3d4 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,7 @@ { vals.pkg=pkgs.bashInteractive; flags={}; } { vals.pkg=pkgs.coreutils; flags={}; } { vals.pkg=pkgs.gh; flags={}; } - { vals.pkg=pkgs.stdenv.cc.cc.lib; flags={}; } + { vals.pkg=pkgs.stdenv.cc.cc.lib; flags.ldLibraryGroup=true; } { vals.pkg=pkgs.pcre2; flags.ldLibraryGroup=true; } { vals.pkg=pkgs.git-lfs; flags={}; } { vals.pkg=pkgs.unixtools.ifconfig; flags={}; } @@ -100,7 +100,6 @@ strAppend="/lib/girepository-1.0"; strJoin=":"; }; - ldGroupAndStdlib = [ pkgs.stdenv.cc.cc.lib ] ++ ldLibraryPackages; # ------------------------------------------------------------ # 3. Host interactive shell → `nix develop` @@ -108,7 +107,7 @@ devShell = pkgs.mkShell { packages = devPackages; shellHook = '' - export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath ldGroupAndStdlib}:$LD_LIBRARY_PATH" + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath ldLibraryPackages}:$LD_LIBRARY_PATH" export DISPLAY=:0 export GI_TYPELIB_PATH="${giTypelibPackagesString}:$GI_TYPELIB_PATH" PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") @@ -122,7 +121,7 @@ }; # ------------------------------------------------------------ - # 3. Closure copied into the OCI image rootfs + # 4. Closure copied into the OCI image rootfs # ------------------------------------------------------------ imageRoot = pkgs.buildEnv { name = "dimos-image-root"; From bbc5416b0dc530567d8a2b1ed654be9293e9d376 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 14:11:37 -0600 Subject: [PATCH 03/45] get lcm working on macos, needed 25.05 for old MacOS --- flake.lock | 8 ++++---- flake.nix | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 6235e35c44..13758c1e2a 100644 --- a/flake.lock +++ b/flake.lock @@ -69,16 +69,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1762977756, - "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", + "lastModified": 1748026580, + "narHash": "sha256-rWtXrcIzU5wm/C8F9LWvUfBGu5U5E7cFzPYT1pHIJaQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", + "rev": "11cb3517b3af6af300dd6c055aeda73c9bf52c48", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "25.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 72faf2c3d4..e9d79f72d7 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Project dev environment as Nix shell + DockerTools layered image"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/25.05"; flake-utils.url = "github:numtide/flake-utils"; lib.url = "github:jeff-hykin/quick-nix-toolkits"; lib.inputs.flakeUtils.follows = "flake-utils"; @@ -23,7 +23,8 @@ { vals.pkg=pkgs.coreutils; flags={}; } { vals.pkg=pkgs.gh; flags={}; } { vals.pkg=pkgs.stdenv.cc.cc.lib; flags.ldLibraryGroup=true; } - { vals.pkg=pkgs.pcre2; flags.ldLibraryGroup=true; } + { vals.pkg=pkgs.pcre2; flags={ ldLibraryGroup=true; flags.packageConfGroup=pkgs.stdenv.isDarwin; }; } + { vals.pkg=pkgs.libsysprof-capture; flags.packageConfGroup=true; onlyIf=pkgs.stdenv.isDarwin; } { vals.pkg=pkgs.git-lfs; flags={}; } { vals.pkg=pkgs.unixtools.ifconfig; flags={}; } @@ -86,7 +87,34 @@ { vals.pkg=pkgs.libpng; flags={}; } ### LCM (Lightweight Communications and Marshalling) - { vals.pkg=pkgs.lcm; flags.ldLibraryGroup=true; } + { vals.pkg=pkgs.lcm; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } + # lcm works on darwin, but only after two fixes (1. pkg-config, 2. fsync) + { + onlyIf=pkgs.stdenv.isDarwin; + flags.ldLibraryGroup=true; + vals.pkg=pkgs.lcm.overrideAttrs (old: + let + # 1. fix pkg-config on darwin + pkgConfPackages = aggregation.getAll { hasAllFlags=[ "packageConfGroup" ]; attrPath=[ "pkg" ]; }; + packageConfPackagesString = lib.print { prefix="packageConfPackagesString"; } (aggregation.getAll { + hasAllFlags=[ "packageConfGroup" ]; + attrPath=[ "pkg" ]; + strAppend="/lib/pkgconfig"; + strJoin=":"; + }); + in + { + buildInputs = (old.buildInputs or []) ++ pkgConfPackages; + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.pkg-config ]; + # 1. fix pkg-config on darwin + env.PKG_CONFIG_PATH = packageConfPackagesString; + # 2. Fix fsync on darwin + patches = [ + (pkgs.writeText "lcm-darwin-fsync.patch" "--- ./lcm-logger/lcm_logger.c 2025-11-14 09:46:01.000000000 -0600\n+++ ./lcm-logger/lcm_logger.c 2025-11-14 09:47:05.000000000 -0600\n@@ -428,9 +428,13 @@\n if (needs_flushed) {\n fflush(logger->log->f);\n #ifndef WIN32\n+#ifdef __APPLE__\n+ fsync(fileno(logger->log->f));\n+#else\n // Perform a full fsync operation after flush\n fdatasync(fileno(logger->log->f));\n #endif\n+#endif\n logger->last_fflush_time = log_event->timestamp;\n }\n") + ]; + } + ); + } ]; # ------------------------------------------------------------ From 0f99eb7cdcb043ce2de9d8ecc4200b46f2c92a68 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 14:13:44 -0600 Subject: [PATCH 04/45] fix pytest command in flake --- flake.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index e9d79f72d7..625506e8a8 100644 --- a/flake.nix +++ b/flake.nix @@ -142,7 +142,9 @@ if [ -f "$PROJECT_ROOT/env/bin/activate" ]; then . "$PROJECT_ROOT/env/bin/activate" fi - + + # without this alias, the pytest uses the non-venv python and fails + alias pytest="python -m pytest" [ -f "$PROJECT_ROOT/motd" ] && cat "$PROJECT_ROOT/motd" [ -f "$PROJECT_ROOT/.pre-commit-config.yaml" ] && pre-commit install --install-hooks ''; From b9d1155bacaf12c27bf0da82ffc1540332b08679 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 14:14:06 -0600 Subject: [PATCH 05/45] fix SharedMemory on MacOS --- dimos/protocol/pubsub/shm/ipc_factory.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dimos/protocol/pubsub/shm/ipc_factory.py b/dimos/protocol/pubsub/shm/ipc_factory.py index 9aedbfa1c4..48a6308594 100644 --- a/dimos/protocol/pubsub/shm/ipc_factory.py +++ b/dimos/protocol/pubsub/shm/ipc_factory.py @@ -17,6 +17,7 @@ from abc import ABC, abstractmethod from multiprocessing.shared_memory import SharedMemory import os +import sys import time import numpy as np @@ -127,6 +128,12 @@ def __init__( self._shape = tuple(shape) self._dtype = np.dtype(dtype) self._nbytes = int(self._dtype.itemsize * np.prod(self._shape)) + # on MacOS the name (only for SharedMemory) can only be 30 chars for some stupid reason, so we truncate + if sys.platform == "darwin": + if data_name: + data_name = data_name[0:30] + if ctrl_name: + ctrl_name = ctrl_name[0:30] def _create_or_open(name: str, size: int): try: From 69ca925e05a9f7310dd3fcce9176fd8f2a83fcb9 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 14:14:49 -0600 Subject: [PATCH 06/45] modify check_multicast, and check_buffers for MacOS (could use improvement) --- dimos/protocol/service/lcmservice.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 1b19a5cfeb..9fa95d4179 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -50,6 +50,9 @@ def check_multicast() -> list[str]: # Check if loopback interface has multicast enabled try: + # Skip on macOS + if sys.platform == "darwin": + return [] result = subprocess.run(["ip", "link", "show", "lo"], capture_output=True, text=True) if "MULTICAST" not in result.stdout: commands_needed.append(f"{sudo}ifconfig lo multicast") @@ -82,6 +85,13 @@ def check_buffers() -> tuple[list[str], int | None]: # Check current buffer settings try: + # macOS + if sys.platform == "darwin": + result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) + current_max = ( + int(result.stdout.split(" ")[1].strip()) if result.returncode == 0 else None + ) + return [], current_max result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) current_max = int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None if not current_max or current_max < 2097152: From 4d5b07ea18c4e35b31ca1b4fea2ee0a3bf2f739c Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 14:15:28 -0600 Subject: [PATCH 07/45] disable most of the multicast and check_buffer tests for macos (could use improvement) --- dimos/protocol/service/test_lcmservice.py | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py index 1c9a51b2e5..86b31424f8 100644 --- a/dimos/protocol/service/test_lcmservice.py +++ b/dimos/protocol/service/test_lcmservice.py @@ -14,6 +14,7 @@ import os import subprocess +import sys from unittest.mock import patch import pytest @@ -34,6 +35,10 @@ def get_sudo_prefix() -> str: def test_check_multicast_all_configured() -> None: """Test check_multicast when system is properly configured.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock successful checks with realistic output format mock_run.side_effect = [ type( @@ -54,6 +59,10 @@ def test_check_multicast_all_configured() -> None: def test_check_multicast_missing_multicast_flag() -> None: """Test check_multicast when loopback interface lacks multicast.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock interface without MULTICAST flag (realistic current system state) mock_run.side_effect = [ type( @@ -75,6 +84,10 @@ def test_check_multicast_missing_multicast_flag() -> None: def test_check_multicast_missing_route() -> None: """Test check_multicast when multicast route is missing.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock missing route - interface has multicast but no route mock_run.side_effect = [ type( @@ -96,6 +109,10 @@ def test_check_multicast_missing_route() -> None: def test_check_multicast_all_missing() -> None: """Test check_multicast when both multicast flag and route are missing (current system state).""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock both missing - matches actual current system state mock_run.side_effect = [ type( @@ -121,6 +138,10 @@ def test_check_multicast_all_missing() -> None: def test_check_multicast_subprocess_exception() -> None: """Test check_multicast when subprocess calls fail.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock subprocess exceptions mock_run.side_effect = Exception("Command failed") @@ -136,6 +157,10 @@ def test_check_multicast_subprocess_exception() -> None: def test_check_buffers_all_configured() -> None: """Test check_buffers when system is properly configured.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock sufficient buffer sizes mock_run.side_effect = [ type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), @@ -152,6 +177,10 @@ def test_check_buffers_all_configured() -> None: def test_check_buffers_low_max_buffer() -> None: """Test check_buffers when rmem_max is too low.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock low rmem_max mock_run.side_effect = [ type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), @@ -169,6 +198,10 @@ def test_check_buffers_low_max_buffer() -> None: def test_check_buffers_low_default_buffer() -> None: """Test check_buffers when rmem_default is too low.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock low rmem_default mock_run.side_effect = [ type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), @@ -186,6 +219,9 @@ def test_check_buffers_low_default_buffer() -> None: def test_check_buffers_both_low() -> None: """Test check_buffers when both buffer sizes are too low.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return # Mock both low mock_run.side_effect = [ type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), @@ -207,6 +243,9 @@ def test_check_buffers_both_low() -> None: def test_check_buffers_subprocess_exception() -> None: """Test check_buffers when subprocess calls fail.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return # Mock subprocess exceptions mock_run.side_effect = Exception("Command failed") @@ -223,6 +262,9 @@ def test_check_buffers_subprocess_exception() -> None: def test_check_buffers_parsing_error() -> None: """Test check_buffers when output parsing fails.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return # Mock malformed output mock_run.side_effect = [ type("MockResult", (), {"stdout": "invalid output", "returncode": 0})(), @@ -242,6 +284,9 @@ def test_check_buffers_parsing_error() -> None: def test_check_buffers_dev_container() -> None: """Test check_buffers in dev container where sysctl fails.""" with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return # Mock dev container behavior - sysctl returns non-zero mock_run.side_effect = [ type( @@ -277,6 +322,9 @@ def test_autoconf_no_config_needed() -> None: # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return # Mock all checks passing mock_run.side_effect = [ # check_multicast calls @@ -313,6 +361,9 @@ def test_autoconf_with_config_needed_success() -> None: # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return # Mock checks failing, then mock the execution succeeding mock_run.side_effect = [ # check_multicast calls @@ -368,6 +419,10 @@ def test_autoconf_with_command_failures() -> None: # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Skip on macOS for now + if sys.platform == "darwin": + return + # Mock checks failing, then mock some commands failing mock_run.side_effect = [ # check_multicast calls From 32749e8c57d56ebc1ed09459724612c84e6ef3f5 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 14:23:53 -0600 Subject: [PATCH 08/45] switch back to unstable --- flake.lock | 8 ++++---- flake.nix | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 13758c1e2a..6235e35c44 100644 --- a/flake.lock +++ b/flake.lock @@ -69,16 +69,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748026580, - "narHash": "sha256-rWtXrcIzU5wm/C8F9LWvUfBGu5U5E7cFzPYT1pHIJaQ=", + "lastModified": 1762977756, + "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "11cb3517b3af6af300dd6c055aeda73c9bf52c48", + "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", "type": "github" }, "original": { "owner": "NixOS", - "ref": "25.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 625506e8a8..0b7361e9e6 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Project dev environment as Nix shell + DockerTools layered image"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/25.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; lib.url = "github:jeff-hykin/quick-nix-toolkits"; lib.inputs.flakeUtils.follows = "flake-utils"; From 57bb639a8812c600af529f700ab8e1471f6b40f0 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 15:46:08 -0600 Subject: [PATCH 09/45] grab changes from MacOS PR --- dimos/core/transport.py | 77 ++- dimos/protocol/pubsub/shmpubsub.py | 5 +- dimos/protocol/service/lcmservice.py | 179 +++-- dimos/protocol/service/test_lcmservice.py | 767 ++++++++++++---------- dimos/robot/unitree_webrtc/unitree_go2.py | 212 ++++-- 5 files changed, 747 insertions(+), 493 deletions(-) diff --git a/dimos/core/transport.py b/dimos/core/transport.py index 32f75e6c33..b5600fdeaa 100644 --- a/dimos/core/transport.py +++ b/dimos/core/transport.py @@ -15,7 +15,7 @@ from __future__ import annotations import traceback -from typing import TypeVar +from typing import Any, TypeVar import dimos.core.colors as colors @@ -38,10 +38,11 @@ class PubSubTransport(Transport[T]): - topic: any + topic: Any - def __init__(self, topic: any) -> None: + def __init__(self, topic: Any) -> None: self.topic = topic + self._start_lock = threading.Lock() def __str__(self) -> str: return ( @@ -62,16 +63,18 @@ def __reduce__(self): return (pLCMTransport, (self.topic,)) def broadcast(self, _, msg) -> None: - if not self._started: - self.lcm.start() - self._started = True + with self._start_lock: + if not self._started: + self.lcm.start() + self._started = True self.lcm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - if not self._started: - self.lcm.start() - self._started = True + with self._start_lock: + if not self._started: + self.lcm.start() + self._started = True return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) @@ -87,23 +90,26 @@ def __reduce__(self): return (LCMTransport, (self.topic.topic, self.topic.lcm_type)) def broadcast(self, _, msg) -> None: - if not self._started: - self.lcm.start() - self._started = True + with self._start_lock: + if not self._started: + self.lcm.start() + self._started = True self.lcm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - if not self._started: - self.lcm.start() - self._started = True + with self._start_lock: + if not self._started: + self.lcm.start() + self._started = True return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) class JpegLcmTransport(LCMTransport): - def __init__(self, topic: str, type: type, **kwargs): + def __init__(self, topic: str, type: type, **kwargs) -> None: self.lcm = JpegLCM(**kwargs) super().__init__(topic, type) + self._start_lock = threading.Lock() def __reduce__(self): return (JpegLcmTransport, (self.topic.topic, self.topic.lcm_type)) @@ -120,16 +126,18 @@ def __reduce__(self): return (pSHMTransport, (self.topic,)) def broadcast(self, _, msg) -> None: - if not self._started: - self.shm.start() - self._started = True + with self._start_lock: + if not self._started: + self.shm.start() + self._started = True self.shm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - if not self._started: - self.shm.start() - self._started = True + with self._start_lock: + if not self._started: + self.shm.start() + self._started = True return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) @@ -144,23 +152,25 @@ def __reduce__(self): return (SHMTransport, (self.topic,)) def broadcast(self, _, msg) -> None: - if not self._started: - self.shm.start() - self._started = True + with self._start_lock: + if not self._started: + self.shm.start() + self._started = True self.shm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - if not self._started: - self.shm.start() - self._started = True + with self._start_lock: + if not self._started: + self.shm.start() + self._started = True return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) class JpegShmTransport(PubSubTransport[T]): _started: bool = False - def __init__(self, topic: str, quality: int = 75, **kwargs): + def __init__(self, topic: str, quality: int = 75, **kwargs) -> None: super().__init__(topic) self.shm = JpegSharedMemory(quality=quality, **kwargs) self.quality = quality @@ -168,7 +178,7 @@ def __init__(self, topic: str, quality: int = 75, **kwargs): def __reduce__(self): return (JpegShmTransport, (self.topic, self.quality)) - def broadcast(self, _, msg): + def broadcast(self, _, msg) -> None: if not self._started: self.shm.start() self._started = True @@ -176,9 +186,10 @@ def broadcast(self, _, msg): self.shm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - if not self._started: - self.shm.start() - self._started = True + with self._start_lock: + if not self._started: + self.shm.start() + self._started = True return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) diff --git a/dimos/protocol/pubsub/shmpubsub.py b/dimos/protocol/pubsub/shmpubsub.py index bbbf2192d7..8347af28c8 100644 --- a/dimos/protocol/pubsub/shmpubsub.py +++ b/dimos/protocol/pubsub/shmpubsub.py @@ -233,8 +233,9 @@ def _ensure_topic(self, topic: str) -> _TopicState: cap = int(self.config.default_capacity) def _names_for_topic(topic: str, capacity: int) -> tuple[str, str]: - # Python’s SharedMemory requires names without a leading '/' - h = hashlib.blake2b(f"{topic}:{capacity}".encode(), digest_size=12).hexdigest() + # Python's SharedMemory requires names without a leading '/' + # Use shorter digest to avoid macOS shared memory name length limits + h = hashlib.blake2b(f"{topic}:{capacity}".encode(), digest_size=8).hexdigest() return f"psm_{h}_data", f"psm_{h}_ctrl" data_name, ctrl_name = _names_for_topic(topic, cap) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 9fa95d4179..097ad606ee 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -18,6 +18,7 @@ from dataclasses import dataclass from functools import cache import os +import platform import subprocess import sys import threading @@ -48,26 +49,52 @@ def check_multicast() -> list[str]: sudo = "" if check_root() else "sudo " - # Check if loopback interface has multicast enabled - try: - # Skip on macOS - if sys.platform == "darwin": - return [] - result = subprocess.run(["ip", "link", "show", "lo"], capture_output=True, text=True) - if "MULTICAST" not in result.stdout: - commands_needed.append(f"{sudo}ifconfig lo multicast") - except Exception: - commands_needed.append(f"{sudo}ifconfig lo multicast") - - # Check if multicast route exists - try: - result = subprocess.run( - ["ip", "route", "show", "224.0.0.0/4"], capture_output=True, text=True - ) - if not result.stdout.strip(): - commands_needed.append(f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo") - except Exception: - commands_needed.append(f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo") + system = platform.system() + + if system == "Linux": + # Linux commands + loopback_interface = "lo" + # Check if loopback interface has multicast enabled + try: + result = subprocess.run( + ["ip", "link", "show", loopback_interface], capture_output=True, text=True + ) + if "MULTICAST" not in result.stdout: + commands_needed.append(f"{sudo}ifconfig {loopback_interface} multicast") + except Exception: + commands_needed.append(f"{sudo}ifconfig {loopback_interface} multicast") + + # Check if multicast route exists + try: + result = subprocess.run( + ["ip", "route", "show", "224.0.0.0/4"], capture_output=True, text=True + ) + if not result.stdout.strip(): + commands_needed.append( + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev {loopback_interface}" + ) + except Exception: + commands_needed.append( + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev {loopback_interface}" + ) + + elif system == "Darwin": # macOS + loopback_interface = "lo0" + # Check if multicast route exists + try: + result = subprocess.run(["netstat", "-nr"], capture_output=True, text=True) + if "224.0.0.0/4" not in result.stdout: + commands_needed.append( + f"{sudo}route add -net 224.0.0.0/4 -interface {loopback_interface}" + ) + except Exception: + commands_needed.append( + f"{sudo}route add -net 224.0.0.0/4 -interface {loopback_interface}" + ) + + else: + # For other systems, skip multicast configuration + logger.warning(f"Multicast configuration not supported on {system}") return commands_needed @@ -82,32 +109,73 @@ def check_buffers() -> tuple[list[str], int | None]: current_max = None sudo = "" if check_root() else "sudo " + system = platform.system() - # Check current buffer settings - try: - # macOS - if sys.platform == "darwin": + if system == "Linux": + # Linux buffer configuration + try: result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) current_max = ( - int(result.stdout.split(" ")[1].strip()) if result.returncode == 0 else None + int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None ) - return [], current_max - result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) - current_max = int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None - if not current_max or current_max < 2097152: + if not current_max or current_max < 2097152: + commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") + except: commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") - except: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") - try: - result = subprocess.run(["sysctl", "net.core.rmem_default"], capture_output=True, text=True) - current_default = ( - int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None - ) - if not current_default or current_default < 2097152: + try: + result = subprocess.run( + ["sysctl", "net.core.rmem_default"], capture_output=True, text=True + ) + current_default = ( + int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None + ) + if not current_default or current_default < 2097152: + commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") + except: commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") - except: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") + + elif system == "Darwin": # macOS + # macOS buffer configuration - check and set UDP buffer related sysctls + try: + result = subprocess.run( + ["sysctl", "kern.ipc.maxsockbuf"], capture_output=True, text=True + ) + current_max = ( + int(result.stdout.split(":")[1].strip()) if result.returncode == 0 else None + ) + if not current_max or current_max < 8388608: + commands_needed.append(f"{sudo}sysctl -w kern.ipc.maxsockbuf=8388608") + except: + commands_needed.append(f"{sudo}sysctl -w kern.ipc.maxsockbuf=8388608") + + try: + result = subprocess.run( + ["sysctl", "net.inet.udp.recvspace"], capture_output=True, text=True + ) + current_recvspace = ( + int(result.stdout.split(":")[1].strip()) if result.returncode == 0 else None + ) + if not current_recvspace or current_recvspace < 2097152: + commands_needed.append(f"{sudo}sysctl -w net.inet.udp.recvspace=2097152") + except: + commands_needed.append(f"{sudo}sysctl -w net.inet.udp.recvspace=2097152") + + try: + result = subprocess.run( + ["sysctl", "net.inet.udp.maxdgram"], capture_output=True, text=True + ) + current_maxdgram = ( + int(result.stdout.split(":")[1].strip()) if result.returncode == 0 else None + ) + if not current_maxdgram or current_maxdgram < 65535: + commands_needed.append(f"{sudo}sysctl -w net.inet.udp.maxdgram=65535") + except: + commands_needed.append(f"{sudo}sysctl -w net.inet.udp.maxdgram=65535") + + else: + # For other systems, skip buffer configuration + logger.warning(f"Buffer configuration not supported on {system}") return commands_needed, current_max @@ -155,6 +223,11 @@ def autoconf() -> None: logger.info("CI environment detected: Skipping automatic system configuration.") return + system = platform.system() + if system == "Darwin": + logger.info("macOS detected: Skipping automatic system configuration.") + return + commands_needed = [] # Check multicast configuration @@ -204,6 +277,11 @@ class LCMConfig: autoconf: bool = True lcm: lcm.LCM | None = None + def __post_init__(self): + if self.url is None and platform.system() == "Darwin": + # On macOS, use multicast with TTL=0 to keep traffic local + self.url = "udpm://239.255.76.67:7667?ttl=0" + @runtime_checkable class LCMMsg(Protocol): @@ -243,14 +321,9 @@ def __init__(self, **kwargs) -> None: super().__init__(**kwargs) # we support passing an existing LCM instance - if self.config.lcm: - # TODO: If we pass LCM in, it's unsafe to use in this thread and the _loop thread. - self.l = self.config.lcm - else: - self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() + self.l = self.config.lcm self._l_lock = threading.Lock() - self._stop_event = threading.Event() self._thread = None @@ -278,16 +351,16 @@ def __setstate__(self, state) -> None: self._call_thread_pool_lock = threading.RLock() def start(self) -> None: - # Reinitialize LCM if it's None (e.g., after unpickling) + # Run autoconf before LCM initialization if not already initialized + if self.config.autoconf and self.l is None: + autoconf() + + # Reinitialize LCM if it's None (e.g., after unpickling or deferred init) if self.l is None: - if self.config.lcm: - self.l = self.config.lcm - else: - self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() + self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() - if self.config.autoconf: - autoconf() - else: + # Fallback to check_system if autoconf is disabled + if not self.config.autoconf: try: check_system() except Exception as e: diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py index 86b31424f8..7b7038e16e 100644 --- a/dimos/protocol/service/test_lcmservice.py +++ b/dimos/protocol/service/test_lcmservice.py @@ -34,444 +34,505 @@ def get_sudo_prefix() -> str: def test_check_multicast_all_configured() -> None: """Test check_multicast when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock successful checks with realistic output format - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0})(), - ] - - result = check_multicast() - assert result == [] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock successful checks with realistic output format + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0} + )(), + ] + + result = check_multicast() + assert result == [] def test_check_multicast_missing_multicast_flag() -> None: """Test check_multicast when loopback interface lacks multicast.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock interface without MULTICAST flag (realistic current system state) - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0})(), - ] - - result = check_multicast() - sudo = get_sudo_prefix() - assert result == [f"{sudo}ifconfig lo multicast"] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock interface without MULTICAST flag (realistic current system state) + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0} + )(), + ] + + result = check_multicast() + sudo = get_sudo_prefix() + assert result == [f"{sudo}ifconfig lo multicast"] def test_check_multicast_missing_route() -> None: """Test check_multicast when multicast route is missing.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock missing route - interface has multicast but no route - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), # Empty output - no route - ] - - result = check_multicast() - sudo = get_sudo_prefix() - assert result == [f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock missing route - interface has multicast but no route + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "", "returncode": 0} + )(), # Empty output - no route + ] + + result = check_multicast() + sudo = get_sudo_prefix() + assert result == [f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"] def test_check_multicast_all_missing() -> None: """Test check_multicast when both multicast flag and route are missing (current system state).""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock both missing - matches actual current system state - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", - "returncode": 0, - }, - )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), # Empty output - no route - ] - - result = check_multicast() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}ifconfig lo multicast", - f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", - ] - assert result == expected + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock both missing - matches actual current system state + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\n link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "returncode": 0, + }, + )(), + type( + "MockResult", (), {"stdout": "", "returncode": 0} + )(), # Empty output - no route + ] + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}ifconfig lo multicast", + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", + ] + assert result == expected def test_check_multicast_subprocess_exception() -> None: """Test check_multicast when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock subprocess exceptions + mock_run.side_effect = Exception("Command failed") + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}ifconfig lo multicast", + f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", + ] + assert result == expected - # Mock subprocess exceptions - mock_run.side_effect = Exception("Command failed") - result = check_multicast() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}ifconfig lo multicast", - f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo", - ] - assert result == expected +def test_check_multicast_macos() -> None: + """Test check_multicast on macOS when configuration is needed.""" + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock netstat -nr to not contain the multicast route + mock_run.side_effect = [ + type( + "MockResult", + (), + { + "stdout": "default 192.168.1.1 UGScg en0", + "returncode": 0, + }, + )(), + ] + + result = check_multicast() + sudo = get_sudo_prefix() + expected = [f"{sudo}route add -net 224.0.0.0/4 -interface lo0"] + assert result == expected def test_check_buffers_all_configured() -> None: """Test check_buffers when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock sufficient buffer sizes - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} - )(), - ] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock sufficient buffer sizes + mock_run.side_effect = [ + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + )(), + ] - commands, buffer_size = check_buffers() - assert commands == [] - assert buffer_size == 2097152 + commands, buffer_size = check_buffers() + assert commands == [] + assert buffer_size == 2097152 def test_check_buffers_low_max_buffer() -> None: """Test check_buffers when rmem_max is too low.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock low rmem_max - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} - )(), - ] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock low rmem_max + mock_run.side_effect = [ + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + )(), + ] - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - assert commands == [f"{sudo}sysctl -w net.core.rmem_max=2097152"] - assert buffer_size == 1048576 + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + assert commands == [f"{sudo}sysctl -w net.core.rmem_max=2097152"] + assert buffer_size == 1048576 def test_check_buffers_low_default_buffer() -> None: """Test check_buffers when rmem_default is too low.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock low rmem_default - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} - )(), - ] + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock low rmem_default + mock_run.side_effect = [ + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + )(), + ] - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - assert commands == [f"{sudo}sysctl -w net.core.rmem_default=2097152"] - assert buffer_size == 2097152 + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + assert commands == [f"{sudo}sysctl -w net.core.rmem_default=2097152"] + assert buffer_size == 2097152 def test_check_buffers_both_low() -> None: """Test check_buffers when both buffer sizes are too low.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - # Mock both low - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0})(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} - )(), - ] - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size == 1048576 + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock both low + mock_run.side_effect = [ + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + )(), + type( + "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + )(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size == 1048576 def test_check_buffers_subprocess_exception() -> None: """Test check_buffers when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - # Mock subprocess exceptions - mock_run.side_effect = Exception("Command failed") - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size is None + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock subprocess exceptions + mock_run.side_effect = Exception("Command failed") + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size is None def test_check_buffers_parsing_error() -> None: """Test check_buffers when output parsing fails.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - # Mock malformed output - mock_run.side_effect = [ - type("MockResult", (), {"stdout": "invalid output", "returncode": 0})(), - type("MockResult", (), {"stdout": "also invalid", "returncode": 0})(), - ] - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size is None + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock malformed output + mock_run.side_effect = [ + type("MockResult", (), {"stdout": "invalid output", "returncode": 0})(), + type("MockResult", (), {"stdout": "also invalid", "returncode": 0})(), + ] + + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size is None def test_check_buffers_dev_container() -> None: """Test check_buffers in dev container where sysctl fails.""" - with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - # Mock dev container behavior - sysctl returns non-zero - mock_run.side_effect = [ - type( - "MockResult", - (), - { - "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_max: No such file or directory", - "returncode": 255, - }, - )(), - type( - "MockResult", - (), - { - "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_default: No such file or directory", - "returncode": 255, - }, - )(), - ] - - commands, buffer_size = check_buffers() - sudo = get_sudo_prefix() - expected = [ - f"{sudo}sysctl -w net.core.rmem_max=2097152", - f"{sudo}sysctl -w net.core.rmem_default=2097152", - ] - assert commands == expected - assert buffer_size is None - - -def test_autoconf_no_config_needed() -> None: - """Test autoconf when no configuration is needed.""" - # Clear CI environment variable for this test - with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - # Mock all checks passing + # Mock dev container behavior - sysctl returns non-zero mock_run.side_effect = [ - # check_multicast calls type( "MockResult", (), { - "stdout": "1: lo: mtu 65536", - "returncode": 0, + "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_max: No such file or directory", + "returncode": 255, }, )(), type( - "MockResult", (), {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0} - )(), - # check_buffers calls - type( - "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} - )(), - type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + "MockResult", + (), + { + "stdout": "sysctl: cannot stat /proc/sys/net/core/rmem_default: No such file or directory", + "returncode": 255, + }, )(), ] - with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: - autoconf() - # Should not log anything when no config is needed - mock_logger.info.assert_not_called() - mock_logger.error.assert_not_called() - mock_logger.warning.assert_not_called() + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w net.core.rmem_max=2097152", + f"{sudo}sysctl -w net.core.rmem_default=2097152", + ] + assert commands == expected + assert buffer_size is None -def test_autoconf_with_config_needed_success() -> None: - """Test autoconf when configuration is needed and commands succeed.""" - # Clear CI environment variable for this test - with patch.dict(os.environ, {"CI": ""}, clear=False): +def test_check_buffers_macos_all_configured() -> None: + """Test check_buffers on macOS when system is properly configured.""" + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - # Mock checks failing, then mock the execution succeeding + # Mock sufficient buffer sizes for macOS mock_run.side_effect = [ - # check_multicast calls type( - "MockResult", - (), - {"stdout": "1: lo: mtu 65536", "returncode": 0}, + "MockResult", (), {"stdout": "kern.ipc.maxsockbuf: 8388608", "returncode": 0} )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), - # check_buffers calls type( - "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + "MockResult", (), {"stdout": "net.inet.udp.recvspace: 2097152", "returncode": 0} )(), type( - "MockResult", (), {"stdout": "net.core.rmem_default = 1048576", "returncode": 0} + "MockResult", (), {"stdout": "net.inet.udp.maxdgram: 65535", "returncode": 0} )(), - # Command execution calls - type( - "MockResult", (), {"stdout": "success", "returncode": 0} - )(), # ifconfig lo multicast - type("MockResult", (), {"stdout": "success", "returncode": 0})(), # route add... - type("MockResult", (), {"stdout": "success", "returncode": 0})(), # sysctl rmem_max - type( - "MockResult", (), {"stdout": "success", "returncode": 0} - )(), # sysctl rmem_default ] - from unittest.mock import call - - with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: - autoconf() - - sudo = get_sudo_prefix() - # Verify the expected log calls - expected_info_calls = [ - call("System configuration required. Executing commands..."), - call(f" Running: {sudo}ifconfig lo multicast"), - call(" ✓ Success"), - call(f" Running: {sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"), - call(" ✓ Success"), - call(f" Running: {sudo}sysctl -w net.core.rmem_max=2097152"), - call(" ✓ Success"), - call(f" Running: {sudo}sysctl -w net.core.rmem_default=2097152"), - call(" ✓ Success"), - call("System configuration completed."), - ] - - mock_logger.info.assert_has_calls(expected_info_calls) + commands, buffer_size = check_buffers() + assert commands == [] + assert buffer_size == 8388608 -def test_autoconf_with_command_failures() -> None: - """Test autoconf when some commands fail.""" - # Clear CI environment variable for this test - with patch.dict(os.environ, {"CI": ""}, clear=False): +def test_check_buffers_macos_needs_config() -> None: + """Test check_buffers on macOS when configuration is needed.""" + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: - # Skip on macOS for now - if sys.platform == "darwin": - return - - # Mock checks failing, then mock some commands failing + # Mock low buffer sizes for macOS mock_run.side_effect = [ - # check_multicast calls type( - "MockResult", - (), - {"stdout": "1: lo: mtu 65536", "returncode": 0}, + "MockResult", (), {"stdout": "kern.ipc.maxsockbuf: 4194304", "returncode": 0} )(), - type("MockResult", (), {"stdout": "", "returncode": 0})(), - # check_buffers calls (no buffer issues for simpler test) type( - "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + "MockResult", (), {"stdout": "net.inet.udp.recvspace: 1048576", "returncode": 0} )(), type( - "MockResult", (), {"stdout": "net.core.rmem_default = 2097152", "returncode": 0} + "MockResult", (), {"stdout": "net.inet.udp.maxdgram: 32768", "returncode": 0} )(), - # Command execution calls - first succeeds, second fails - type( - "MockResult", (), {"stdout": "success", "returncode": 0} - )(), # ifconfig lo multicast - subprocess.CalledProcessError( - 1, - [ - *get_sudo_prefix().split(), - "route", - "add", - "-net", - "224.0.0.0", - "netmask", - "240.0.0.0", - "dev", - "lo", - ], - "Permission denied", - "Operation not permitted", - ), ] - with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: - # The function should raise on multicast/route failures - with pytest.raises(subprocess.CalledProcessError): + commands, buffer_size = check_buffers() + sudo = get_sudo_prefix() + expected = [ + f"{sudo}sysctl -w kern.ipc.maxsockbuf=8388608", + f"{sudo}sysctl -w net.inet.udp.recvspace=2097152", + f"{sudo}sysctl -w net.inet.udp.maxdgram=65535", + ] + assert commands == expected + assert buffer_size == 4194304 + + +def test_autoconf_no_config_needed() -> None: + """Test autoconf when no configuration is needed.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock all checks passing + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + { + "stdout": "1: lo: mtu 65536", + "returncode": 0, + }, + )(), + type( + "MockResult", + (), + {"stdout": "224.0.0.0/4 dev lo scope link", "returncode": 0}, + )(), + # check_buffers calls + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + )(), + type( + "MockResult", + (), + {"stdout": "net.core.rmem_default = 2097152", "returncode": 0}, + )(), + ] + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: autoconf() + # Should not log anything when no config is needed + mock_logger.info.assert_not_called() + mock_logger.error.assert_not_called() + mock_logger.warning.assert_not_called() + + +def test_autoconf_with_config_needed_success() -> None: + """Test autoconf when configuration is needed and commands succeed.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock checks failing, then mock the execution succeeding + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + {"stdout": "1: lo: mtu 65536", "returncode": 0}, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), + # check_buffers calls + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 1048576", "returncode": 0} + )(), + type( + "MockResult", + (), + {"stdout": "net.core.rmem_default = 1048576", "returncode": 0}, + )(), + # Command execution calls + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # ifconfig lo multicast + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # route add... + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # sysctl rmem_max + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # sysctl rmem_default + ] + + from unittest.mock import call + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: + autoconf() + + sudo = get_sudo_prefix() + # Verify the expected log calls + expected_info_calls = [ + call("System configuration required. Executing commands..."), + call(f" Running: {sudo}ifconfig lo multicast"), + call(" ✓ Success"), + call(f" Running: {sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev lo"), + call(" ✓ Success"), + call(f" Running: {sudo}sysctl -w net.core.rmem_max=2097152"), + call(" ✓ Success"), + call(f" Running: {sudo}sysctl -w net.core.rmem_default=2097152"), + call(" ✓ Success"), + call("System configuration completed."), + ] + + mock_logger.info.assert_has_calls(expected_info_calls) + + +def test_autoconf_with_command_failures() -> None: + """Test autoconf when some commands fail.""" + # Clear CI environment variable for this test + with patch.dict(os.environ, {"CI": ""}, clear=False): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: + # Mock checks failing, then mock some commands failing + mock_run.side_effect = [ + # check_multicast calls + type( + "MockResult", + (), + {"stdout": "1: lo: mtu 65536", "returncode": 0}, + )(), + type("MockResult", (), {"stdout": "", "returncode": 0})(), + # check_buffers calls (no buffer issues for simpler test) + type( + "MockResult", (), {"stdout": "net.core.rmem_max = 2097152", "returncode": 0} + )(), + type( + "MockResult", + (), + {"stdout": "net.core.rmem_default = 2097152", "returncode": 0}, + )(), + # Command execution calls - first succeeds, second fails + type( + "MockResult", (), {"stdout": "success", "returncode": 0} + )(), # ifconfig lo multicast + subprocess.CalledProcessError( + 1, + [ + *get_sudo_prefix().split(), + "route", + "add", + "-net", + "224.0.0.0", + "netmask", + "240.0.0.0", + "dev", + "lo", + ], + "Permission denied", + "Operation not permitted", + ), + ] + + with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: + # The function should raise on multicast/route failures + with pytest.raises(subprocess.CalledProcessError): + autoconf() - # Verify it logged the failure before raising - info_calls = [call[0][0] for call in mock_logger.info.call_args_list] - error_calls = [call[0][0] for call in mock_logger.error.call_args_list] + # Verify it logged the failure before raising + info_calls = [call[0][0] for call in mock_logger.info.call_args_list] + error_calls = [call[0][0] for call in mock_logger.error.call_args_list] - assert "System configuration required. Executing commands..." in info_calls - assert " ✓ Success" in info_calls # First command succeeded - assert any( - "✗ Failed to configure multicast" in call for call in error_calls - ) # Second command failed + assert "System configuration required. Executing commands..." in info_calls + assert " ✓ Success" in info_calls # First command succeeded + assert any( + "✗ Failed to configure multicast" in call for call in error_calls + ) # Second command failed diff --git a/dimos/robot/unitree_webrtc/unitree_go2.py b/dimos/robot/unitree_webrtc/unitree_go2.py index b91433ead8..d6ea9c05a5 100644 --- a/dimos/robot/unitree_webrtc/unitree_go2.py +++ b/dimos/robot/unitree_webrtc/unitree_go2.py @@ -38,8 +38,9 @@ from dimos.msgs.sensor_msgs import Image from dimos.msgs.std_msgs import Header from dimos.msgs.vision_msgs import Detection2DArray +from dimos.navigation.base import NavigationState from dimos.navigation.bbox_navigation import BBoxNavigationModule -from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator, NavigatorState +from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer from dimos.navigation.global_planner import AstarPlanner from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner @@ -54,7 +55,6 @@ from dimos.protocol.pubsub.lcmpubsub import LCM from dimos.protocol.tf import TF from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.robot.robot import UnitreeRobot from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.robot.unitree_webrtc.type.map import Map @@ -145,6 +145,7 @@ class ConnectionModule(Module): _odom: PoseStamped = None _lidar: LidarMessage = None _last_image: Image = None + _global_config: GlobalConfig def __init__( self, @@ -155,10 +156,10 @@ def __init__( *args, **kwargs, ) -> None: - cfg = global_config or GlobalConfig() - self.ip = ip if ip is not None else cfg.robot_ip - self.connection_type = connection_type or cfg.unitree_connection_type - self.rectify_image = not cfg.use_simulation + self._global_config = global_config or GlobalConfig() + self.ip = ip if ip is not None else self._global_config.robot_ip + self.connection_type = connection_type or self._global_config.unitree_connection_type + self.rectify_image = not self._global_config.simulation self.tf = TF() self.connection = None @@ -198,14 +199,14 @@ def start(self) -> None: case "mujoco": from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection - self.connection = MujocoConnection() + self.connection = MujocoConnection(self._global_config) case _: raise ValueError(f"Unknown connection type: {self.connection_type}") self.connection.start() # Connect sensor streams to outputs - unsub = self.connection.lidar_stream().subscribe(self.lidar.publish) + unsub = self.connection.lidar_stream().subscribe(self._on_lidar) self._disposables.add(unsub) unsub = self.connection.odom_stream().subscribe(self._publish_tf) @@ -227,16 +228,22 @@ def stop(self) -> None: self.connection.stop() super().stop() + def _on_lidar(self, msg: LidarMessage) -> None: + if self.lidar.transport: + self.lidar.publish(msg) + def _on_video(self, msg: Image) -> None: """Handle incoming video frames and publish synchronized camera data.""" # Apply rectification if enabled if self.rectify_image: rectified_msg = rectify_image(msg, self.camera_matrix, self.dist_coeffs) self._last_image = rectified_msg - self.color_image.publish(rectified_msg) + if self.color_image.transport: + self.color_image.publish(rectified_msg) else: self._last_image = msg - self.color_image.publish(msg) + if self.color_image.transport: + self.color_image.publish(msg) # Publish camera info and pose synchronized with video timestamp = msg.ts if msg.ts else time.time() @@ -248,8 +255,11 @@ def _publish_gps_location(self, msg: LatLon) -> None: def _publish_tf(self, msg) -> None: self._odom = msg - self.odom.publish(msg) + if self.odom.transport: + self.odom.publish(msg) self.tf.publish(Transform.from_pose("base_link", msg)) + + # Publish camera_link transform camera_link = Transform( translation=Vector3(0.3, 0.0, 0.0), rotation=Quaternion(0.0, 0.0, 0.0, 1.0), @@ -257,12 +267,22 @@ def _publish_tf(self, msg) -> None: child_frame_id="camera_link", ts=time.time(), ) - self.tf.publish(camera_link) + + map_to_world = Transform( + translation=Vector3(0.0, 0.0, 0.0), + rotation=Quaternion(0.0, 0.0, 0.0, 1.0), + frame_id="map", + child_frame_id="world", + ts=time.time(), + ) + + self.tf.publish(camera_link, map_to_world) def _publish_camera_info(self, timestamp: float) -> None: header = Header(timestamp, "camera_link") self.lcm_camera_info.header = header - self.camera_info.publish(self.lcm_camera_info) + if self.camera_info.transport: + self.camera_info.publish(self.lcm_camera_info) def _publish_camera_pose(self, timestamp: float) -> None: """Publish camera pose from TF lookup.""" @@ -282,7 +302,8 @@ def _publish_camera_pose(self, timestamp: float) -> None: position=transform.translation, orientation=transform.rotation, ) - self.camera_pose.publish(pose_msg) + if self.camera_pose.transport: + self.camera_pose.publish(pose_msg) else: logger.debug("Could not find transform from world to camera_link") @@ -328,7 +349,7 @@ def publish_request(self, topic: str, data: dict): connection = ConnectionModule.blueprint -class UnitreeGo2(UnitreeRobot, Resource): +class UnitreeGo2(Resource): """Full Unitree Go2 robot with navigation and perception capabilities.""" _dimos: ModuleCoordinator @@ -360,6 +381,7 @@ def __init__( self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") self.websocket_port = websocket_port self.lcm = LCM() + self._transports = [] # Initialize skill library if skill_library is None: @@ -430,15 +452,34 @@ def _deploy_connection(self) -> None: ConnectionModule, self.ip, connection_type=self.connection_type ) - self.connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) - self.connection.odom.transport = core.LCMTransport("/odom", PoseStamped) - self.connection.gps_location.transport = core.pLCMTransport("/gps_location") - self.connection.color_image.transport = core.pSHMTransport( + self.connection.lidar.transport = lidar_transport = core.LCMTransport( + "/lidar", LidarMessage + ) + self.connection.odom.transport = odom_transport = core.LCMTransport("/odom", PoseStamped) + self.connection.gps_location.transport = gps_location_transport = core.pLCMTransport( + "/gps_location" + ) + self.connection.color_image.transport = color_image_transport = core.pSHMTransport( "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE ) - self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - self.connection.camera_info.transport = core.LCMTransport("/go2/camera_info", CameraInfo) - self.connection.camera_pose.transport = core.LCMTransport("/go2/camera_pose", PoseStamped) + self.connection.cmd_vel.transport = cmd_vel_transport = core.LCMTransport("/cmd_vel", Twist) + self.connection.camera_info.transport = camera_info_transport = core.LCMTransport( + "/go2/camera_info", CameraInfo + ) + self.connection.camera_pose.transport = camera_pose_transport = core.LCMTransport( + "/go2/camera_pose", PoseStamped + ) + self._transports.extend( + [ + lidar_transport, + odom_transport, + gps_location_transport, + color_image_transport, + cmd_vel_transport, + camera_info_transport, + camera_pose_transport, + ] + ) def _deploy_mapping(self) -> None: """Deploy and configure the mapping module.""" @@ -447,9 +488,18 @@ def _deploy_mapping(self) -> None: Map, voxel_size=0.5, global_publish_interval=2.5, min_height=min_height ) - self.mapper.global_map.transport = core.LCMTransport("/global_map", LidarMessage) - self.mapper.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) - self.mapper.local_costmap.transport = core.LCMTransport("/local_costmap", OccupancyGrid) + self.mapper.global_map.transport = global_map_transport = core.LCMTransport( + "/global_map", LidarMessage + ) + self.mapper.global_costmap.transport = global_costmap_transport = core.LCMTransport( + "/global_costmap", OccupancyGrid + ) + self.mapper.local_costmap.transport = local_costmap_transport = core.LCMTransport( + "/local_costmap", OccupancyGrid + ) + self._transports.extend( + [global_map_transport, global_costmap_transport, local_costmap_transport] + ) self.mapper.lidar.connect(self.connection.lidar) @@ -464,22 +514,53 @@ def _deploy_navigation(self) -> None: ) self.frontier_explorer = self._dimos.deploy(WavefrontFrontierExplorer) - self.navigator.target.transport = core.LCMTransport("/navigation_goal", PoseStamped) - self.navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) - self.navigator.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) - self.navigator.navigation_state.transport = core.LCMTransport("/navigation_state", String) - self.navigator.global_costmap.transport = core.LCMTransport( - "/global_costmap", OccupancyGrid + self.navigator.target.transport = nav_target_transport = core.LCMTransport( + "/navigation_goal", PoseStamped ) - self.global_planner.path.transport = core.LCMTransport("/global_path", Path) - self.local_planner.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - self.frontier_explorer.goal_request.transport = core.LCMTransport( + self.navigator.goal_request.transport = nav_goal_request_transport = core.LCMTransport( "/goal_request", PoseStamped ) - self.frontier_explorer.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) - self.frontier_explorer.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) - self.frontier_explorer.stop_explore_cmd.transport = core.LCMTransport( - "/stop_explore_cmd", Bool + self.navigator.goal_reached.transport = nav_goal_reached_transport = core.LCMTransport( + "/goal_reached", Bool + ) + self.navigator.navigation_state.transport = nav_state_transport = core.LCMTransport( + "/navigation_state", String + ) + self.navigator.global_costmap.transport = nav_global_costmap_transport = core.LCMTransport( + "/global_costmap", OccupancyGrid + ) + self.global_planner.path.transport = global_path_transport = core.LCMTransport( + "/global_path", Path + ) + self.local_planner.cmd_vel.transport = local_cmd_vel_transport = core.LCMTransport( + "/cmd_vel", Twist + ) + self.frontier_explorer.goal_request.transport = fe_goal_request_transport = ( + core.LCMTransport("/goal_request", PoseStamped) + ) + self.frontier_explorer.goal_reached.transport = fe_goal_reached_transport = ( + core.LCMTransport("/goal_reached", Bool) + ) + self.frontier_explorer.explore_cmd.transport = fe_explore_cmd_transport = core.LCMTransport( + "/explore_cmd", Bool + ) + self.frontier_explorer.stop_explore_cmd.transport = fe_stop_explore_cmd_transport = ( + core.LCMTransport("/stop_explore_cmd", Bool) + ) + self._transports.extend( + [ + nav_target_transport, + nav_goal_request_transport, + nav_goal_reached_transport, + nav_state_transport, + nav_global_costmap_transport, + global_path_transport, + local_cmd_vel_transport, + fe_goal_request_transport, + fe_goal_reached_transport, + fe_explore_cmd_transport, + fe_stop_explore_cmd_transport, + ] ) self.global_planner.target.connect(self.navigator.target) @@ -501,11 +582,30 @@ def _deploy_navigation(self) -> None: def _deploy_visualization(self) -> None: """Deploy and configure visualization modules.""" self.websocket_vis = self._dimos.deploy(WebsocketVisModule, port=self.websocket_port) - self.websocket_vis.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) - self.websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") - self.websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) - self.websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) - self.websocket_vis.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + self.websocket_vis.goal_request.transport = vis_goal_request_transport = core.LCMTransport( + "/goal_request", PoseStamped + ) + self.websocket_vis.gps_goal.transport = vis_gps_goal_transport = core.pLCMTransport( + "/gps_goal" + ) + self.websocket_vis.explore_cmd.transport = vis_explore_cmd_transport = core.LCMTransport( + "/explore_cmd", Bool + ) + self.websocket_vis.stop_explore_cmd.transport = vis_stop_explore_cmd_transport = ( + core.LCMTransport("/stop_explore_cmd", Bool) + ) + self.websocket_vis.cmd_vel.transport = vis_cmd_vel_transport = core.LCMTransport( + "/cmd_vel", Twist + ) + self._transports.extend( + [ + vis_goal_request_transport, + vis_gps_goal_transport, + vis_explore_cmd_transport, + vis_stop_explore_cmd_transport, + vis_cmd_vel_transport, + ] + ) self.websocket_vis.odom.connect(self.connection.odom) self.websocket_vis.gps_location.connect(self.connection.gps_location) @@ -535,9 +635,6 @@ def _deploy_perception(self) -> None: self.spatial_memory_module.color_image.transport = core.pSHMTransport( "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE ) - self.spatial_memory_module.odom.transport = core.LCMTransport( - "/go2/camera_pose", PoseStamped - ) logger.info("Spatial memory module deployed and connected") @@ -553,15 +650,19 @@ def _deploy_perception(self) -> None: self.utilization_module = self._dimos.deploy(UtilizationModule) # Set up transports for object tracker - self.object_tracker.detection2darray.transport = core.LCMTransport( + self.object_tracker.detection2darray.transport = ot_detection_transport = core.LCMTransport( "/go2/detection2d", Detection2DArray ) - self.object_tracker.tracked_overlay.transport = core.pSHMTransport( + self.object_tracker.tracked_overlay.transport = ot_overlay_transport = core.pSHMTransport( "/go2/tracked_overlay", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE ) + self._transports.extend([ot_detection_transport, ot_overlay_transport]) # Set up transports for bbox navigator - self.bbox_navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) + self.bbox_navigator.goal_request.transport = bbox_goal_request_transport = ( + core.LCMTransport("/goal_request", PoseStamped) + ) + self._transports.append(bbox_goal_request_transport) logger.info("Object tracker and bbox navigator modules deployed") @@ -624,7 +725,7 @@ def navigate_to(self, pose: PoseStamped, blocking: bool = True) -> bool: time.sleep(1.0) if blocking: - while self.navigator.get_state() == NavigatorState.FOLLOWING_PATH: + while self.navigator.get_state() == NavigationState.FOLLOWING_PATH: time.sleep(0.25) time.sleep(1.0) @@ -681,11 +782,18 @@ def get_odom(self) -> PoseStamped: def main() -> None: """Main entry point.""" + # Clean up dask scratch space to avoid permission errors from previous runs + import shutil + import tempfile + + dask_scratch = os.path.join(tempfile.gettempdir(), "dask-scratch-space") + if os.path.exists(dask_scratch): + logger.info(f"Removing stale dask scratch space: {dask_scratch}") + shutil.rmtree(dask_scratch, ignore_errors=True) + ip = os.getenv("ROBOT_IP") connection_type = os.getenv("CONNECTION_TYPE", "webrtc") - pubsub.lcm.autoconf() - robot = UnitreeGo2(ip=ip, websocket_port=7779, connection_type=connection_type) robot.start() From e1a74b51ca19fc1db20d21a34b68053c1cf7b9fe Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 14 Nov 2025 16:00:25 -0600 Subject: [PATCH 10/45] clean up --- dimos/protocol/pubsub/shm/ipc_factory.py | 6 ------ flake.nix | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/dimos/protocol/pubsub/shm/ipc_factory.py b/dimos/protocol/pubsub/shm/ipc_factory.py index 48a6308594..805dad4d9e 100644 --- a/dimos/protocol/pubsub/shm/ipc_factory.py +++ b/dimos/protocol/pubsub/shm/ipc_factory.py @@ -128,12 +128,6 @@ def __init__( self._shape = tuple(shape) self._dtype = np.dtype(dtype) self._nbytes = int(self._dtype.itemsize * np.prod(self._shape)) - # on MacOS the name (only for SharedMemory) can only be 30 chars for some stupid reason, so we truncate - if sys.platform == "darwin": - if data_name: - data_name = data_name[0:30] - if ctrl_name: - ctrl_name = ctrl_name[0:30] def _create_or_open(name: str, size: int): try: diff --git a/flake.nix b/flake.nix index 0b7361e9e6..4064a7262a 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,7 @@ { vals.pkg=pkgs.libsysprof-capture; flags.packageConfGroup=true; onlyIf=pkgs.stdenv.isDarwin; } { vals.pkg=pkgs.git-lfs; flags={}; } { vals.pkg=pkgs.unixtools.ifconfig; flags={}; } + { vals.pkg=pkgs.unixtools.netstat; flags={}; } ### Python + static analysis { vals.pkg=pkgs.python312; flags={}; } From ffe8f1b7bdaea1c2f0a97b6eb070645771075a9d Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 09:09:28 -0600 Subject: [PATCH 11/45] revert some --- dimos/core/transport.py | 77 ++++---- dimos/protocol/service/lcmservice.py | 3 - dimos/robot/unitree_webrtc/unitree_go2.py | 212 ++++++---------------- 3 files changed, 85 insertions(+), 207 deletions(-) diff --git a/dimos/core/transport.py b/dimos/core/transport.py index b5600fdeaa..32f75e6c33 100644 --- a/dimos/core/transport.py +++ b/dimos/core/transport.py @@ -15,7 +15,7 @@ from __future__ import annotations import traceback -from typing import Any, TypeVar +from typing import TypeVar import dimos.core.colors as colors @@ -38,11 +38,10 @@ class PubSubTransport(Transport[T]): - topic: Any + topic: any - def __init__(self, topic: Any) -> None: + def __init__(self, topic: any) -> None: self.topic = topic - self._start_lock = threading.Lock() def __str__(self) -> str: return ( @@ -63,18 +62,16 @@ def __reduce__(self): return (pLCMTransport, (self.topic,)) def broadcast(self, _, msg) -> None: - with self._start_lock: - if not self._started: - self.lcm.start() - self._started = True + if not self._started: + self.lcm.start() + self._started = True self.lcm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - with self._start_lock: - if not self._started: - self.lcm.start() - self._started = True + if not self._started: + self.lcm.start() + self._started = True return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) @@ -90,26 +87,23 @@ def __reduce__(self): return (LCMTransport, (self.topic.topic, self.topic.lcm_type)) def broadcast(self, _, msg) -> None: - with self._start_lock: - if not self._started: - self.lcm.start() - self._started = True + if not self._started: + self.lcm.start() + self._started = True self.lcm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - with self._start_lock: - if not self._started: - self.lcm.start() - self._started = True + if not self._started: + self.lcm.start() + self._started = True return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) class JpegLcmTransport(LCMTransport): - def __init__(self, topic: str, type: type, **kwargs) -> None: + def __init__(self, topic: str, type: type, **kwargs): self.lcm = JpegLCM(**kwargs) super().__init__(topic, type) - self._start_lock = threading.Lock() def __reduce__(self): return (JpegLcmTransport, (self.topic.topic, self.topic.lcm_type)) @@ -126,18 +120,16 @@ def __reduce__(self): return (pSHMTransport, (self.topic,)) def broadcast(self, _, msg) -> None: - with self._start_lock: - if not self._started: - self.shm.start() - self._started = True + if not self._started: + self.shm.start() + self._started = True self.shm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - with self._start_lock: - if not self._started: - self.shm.start() - self._started = True + if not self._started: + self.shm.start() + self._started = True return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) @@ -152,25 +144,23 @@ def __reduce__(self): return (SHMTransport, (self.topic,)) def broadcast(self, _, msg) -> None: - with self._start_lock: - if not self._started: - self.shm.start() - self._started = True + if not self._started: + self.shm.start() + self._started = True self.shm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - with self._start_lock: - if not self._started: - self.shm.start() - self._started = True + if not self._started: + self.shm.start() + self._started = True return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) class JpegShmTransport(PubSubTransport[T]): _started: bool = False - def __init__(self, topic: str, quality: int = 75, **kwargs) -> None: + def __init__(self, topic: str, quality: int = 75, **kwargs): super().__init__(topic) self.shm = JpegSharedMemory(quality=quality, **kwargs) self.quality = quality @@ -178,7 +168,7 @@ def __init__(self, topic: str, quality: int = 75, **kwargs) -> None: def __reduce__(self): return (JpegShmTransport, (self.topic, self.quality)) - def broadcast(self, _, msg) -> None: + def broadcast(self, _, msg): if not self._started: self.shm.start() self._started = True @@ -186,10 +176,9 @@ def broadcast(self, _, msg) -> None: self.shm.publish(self.topic, msg) def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: - with self._start_lock: - if not self._started: - self.shm.start() - self._started = True + if not self._started: + self.shm.start() + self._started = True return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 097ad606ee..5820ad88e4 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -224,9 +224,6 @@ def autoconf() -> None: return system = platform.system() - if system == "Darwin": - logger.info("macOS detected: Skipping automatic system configuration.") - return commands_needed = [] diff --git a/dimos/robot/unitree_webrtc/unitree_go2.py b/dimos/robot/unitree_webrtc/unitree_go2.py index d6ea9c05a5..b91433ead8 100644 --- a/dimos/robot/unitree_webrtc/unitree_go2.py +++ b/dimos/robot/unitree_webrtc/unitree_go2.py @@ -38,9 +38,8 @@ from dimos.msgs.sensor_msgs import Image from dimos.msgs.std_msgs import Header from dimos.msgs.vision_msgs import Detection2DArray -from dimos.navigation.base import NavigationState from dimos.navigation.bbox_navigation import BBoxNavigationModule -from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator +from dimos.navigation.bt_navigator.navigator import BehaviorTreeNavigator, NavigatorState from dimos.navigation.frontier_exploration import WavefrontFrontierExplorer from dimos.navigation.global_planner import AstarPlanner from dimos.navigation.local_planner.holonomic_local_planner import HolonomicLocalPlanner @@ -55,6 +54,7 @@ from dimos.protocol.pubsub.lcmpubsub import LCM from dimos.protocol.tf import TF from dimos.robot.foxglove_bridge import FoxgloveBridge +from dimos.robot.robot import UnitreeRobot from dimos.robot.unitree_webrtc.connection import UnitreeWebRTCConnection from dimos.robot.unitree_webrtc.type.lidar import LidarMessage from dimos.robot.unitree_webrtc.type.map import Map @@ -145,7 +145,6 @@ class ConnectionModule(Module): _odom: PoseStamped = None _lidar: LidarMessage = None _last_image: Image = None - _global_config: GlobalConfig def __init__( self, @@ -156,10 +155,10 @@ def __init__( *args, **kwargs, ) -> None: - self._global_config = global_config or GlobalConfig() - self.ip = ip if ip is not None else self._global_config.robot_ip - self.connection_type = connection_type or self._global_config.unitree_connection_type - self.rectify_image = not self._global_config.simulation + cfg = global_config or GlobalConfig() + self.ip = ip if ip is not None else cfg.robot_ip + self.connection_type = connection_type or cfg.unitree_connection_type + self.rectify_image = not cfg.use_simulation self.tf = TF() self.connection = None @@ -199,14 +198,14 @@ def start(self) -> None: case "mujoco": from dimos.robot.unitree_webrtc.mujoco_connection import MujocoConnection - self.connection = MujocoConnection(self._global_config) + self.connection = MujocoConnection() case _: raise ValueError(f"Unknown connection type: {self.connection_type}") self.connection.start() # Connect sensor streams to outputs - unsub = self.connection.lidar_stream().subscribe(self._on_lidar) + unsub = self.connection.lidar_stream().subscribe(self.lidar.publish) self._disposables.add(unsub) unsub = self.connection.odom_stream().subscribe(self._publish_tf) @@ -228,22 +227,16 @@ def stop(self) -> None: self.connection.stop() super().stop() - def _on_lidar(self, msg: LidarMessage) -> None: - if self.lidar.transport: - self.lidar.publish(msg) - def _on_video(self, msg: Image) -> None: """Handle incoming video frames and publish synchronized camera data.""" # Apply rectification if enabled if self.rectify_image: rectified_msg = rectify_image(msg, self.camera_matrix, self.dist_coeffs) self._last_image = rectified_msg - if self.color_image.transport: - self.color_image.publish(rectified_msg) + self.color_image.publish(rectified_msg) else: self._last_image = msg - if self.color_image.transport: - self.color_image.publish(msg) + self.color_image.publish(msg) # Publish camera info and pose synchronized with video timestamp = msg.ts if msg.ts else time.time() @@ -255,11 +248,8 @@ def _publish_gps_location(self, msg: LatLon) -> None: def _publish_tf(self, msg) -> None: self._odom = msg - if self.odom.transport: - self.odom.publish(msg) + self.odom.publish(msg) self.tf.publish(Transform.from_pose("base_link", msg)) - - # Publish camera_link transform camera_link = Transform( translation=Vector3(0.3, 0.0, 0.0), rotation=Quaternion(0.0, 0.0, 0.0, 1.0), @@ -267,22 +257,12 @@ def _publish_tf(self, msg) -> None: child_frame_id="camera_link", ts=time.time(), ) - - map_to_world = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="map", - child_frame_id="world", - ts=time.time(), - ) - - self.tf.publish(camera_link, map_to_world) + self.tf.publish(camera_link) def _publish_camera_info(self, timestamp: float) -> None: header = Header(timestamp, "camera_link") self.lcm_camera_info.header = header - if self.camera_info.transport: - self.camera_info.publish(self.lcm_camera_info) + self.camera_info.publish(self.lcm_camera_info) def _publish_camera_pose(self, timestamp: float) -> None: """Publish camera pose from TF lookup.""" @@ -302,8 +282,7 @@ def _publish_camera_pose(self, timestamp: float) -> None: position=transform.translation, orientation=transform.rotation, ) - if self.camera_pose.transport: - self.camera_pose.publish(pose_msg) + self.camera_pose.publish(pose_msg) else: logger.debug("Could not find transform from world to camera_link") @@ -349,7 +328,7 @@ def publish_request(self, topic: str, data: dict): connection = ConnectionModule.blueprint -class UnitreeGo2(Resource): +class UnitreeGo2(UnitreeRobot, Resource): """Full Unitree Go2 robot with navigation and perception capabilities.""" _dimos: ModuleCoordinator @@ -381,7 +360,6 @@ def __init__( self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") self.websocket_port = websocket_port self.lcm = LCM() - self._transports = [] # Initialize skill library if skill_library is None: @@ -452,34 +430,15 @@ def _deploy_connection(self) -> None: ConnectionModule, self.ip, connection_type=self.connection_type ) - self.connection.lidar.transport = lidar_transport = core.LCMTransport( - "/lidar", LidarMessage - ) - self.connection.odom.transport = odom_transport = core.LCMTransport("/odom", PoseStamped) - self.connection.gps_location.transport = gps_location_transport = core.pLCMTransport( - "/gps_location" - ) - self.connection.color_image.transport = color_image_transport = core.pSHMTransport( + self.connection.lidar.transport = core.LCMTransport("/lidar", LidarMessage) + self.connection.odom.transport = core.LCMTransport("/odom", PoseStamped) + self.connection.gps_location.transport = core.pLCMTransport("/gps_location") + self.connection.color_image.transport = core.pSHMTransport( "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE ) - self.connection.cmd_vel.transport = cmd_vel_transport = core.LCMTransport("/cmd_vel", Twist) - self.connection.camera_info.transport = camera_info_transport = core.LCMTransport( - "/go2/camera_info", CameraInfo - ) - self.connection.camera_pose.transport = camera_pose_transport = core.LCMTransport( - "/go2/camera_pose", PoseStamped - ) - self._transports.extend( - [ - lidar_transport, - odom_transport, - gps_location_transport, - color_image_transport, - cmd_vel_transport, - camera_info_transport, - camera_pose_transport, - ] - ) + self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + self.connection.camera_info.transport = core.LCMTransport("/go2/camera_info", CameraInfo) + self.connection.camera_pose.transport = core.LCMTransport("/go2/camera_pose", PoseStamped) def _deploy_mapping(self) -> None: """Deploy and configure the mapping module.""" @@ -488,18 +447,9 @@ def _deploy_mapping(self) -> None: Map, voxel_size=0.5, global_publish_interval=2.5, min_height=min_height ) - self.mapper.global_map.transport = global_map_transport = core.LCMTransport( - "/global_map", LidarMessage - ) - self.mapper.global_costmap.transport = global_costmap_transport = core.LCMTransport( - "/global_costmap", OccupancyGrid - ) - self.mapper.local_costmap.transport = local_costmap_transport = core.LCMTransport( - "/local_costmap", OccupancyGrid - ) - self._transports.extend( - [global_map_transport, global_costmap_transport, local_costmap_transport] - ) + self.mapper.global_map.transport = core.LCMTransport("/global_map", LidarMessage) + self.mapper.global_costmap.transport = core.LCMTransport("/global_costmap", OccupancyGrid) + self.mapper.local_costmap.transport = core.LCMTransport("/local_costmap", OccupancyGrid) self.mapper.lidar.connect(self.connection.lidar) @@ -514,53 +464,22 @@ def _deploy_navigation(self) -> None: ) self.frontier_explorer = self._dimos.deploy(WavefrontFrontierExplorer) - self.navigator.target.transport = nav_target_transport = core.LCMTransport( - "/navigation_goal", PoseStamped - ) - self.navigator.goal_request.transport = nav_goal_request_transport = core.LCMTransport( - "/goal_request", PoseStamped - ) - self.navigator.goal_reached.transport = nav_goal_reached_transport = core.LCMTransport( - "/goal_reached", Bool - ) - self.navigator.navigation_state.transport = nav_state_transport = core.LCMTransport( - "/navigation_state", String - ) - self.navigator.global_costmap.transport = nav_global_costmap_transport = core.LCMTransport( + self.navigator.target.transport = core.LCMTransport("/navigation_goal", PoseStamped) + self.navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) + self.navigator.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) + self.navigator.navigation_state.transport = core.LCMTransport("/navigation_state", String) + self.navigator.global_costmap.transport = core.LCMTransport( "/global_costmap", OccupancyGrid ) - self.global_planner.path.transport = global_path_transport = core.LCMTransport( - "/global_path", Path - ) - self.local_planner.cmd_vel.transport = local_cmd_vel_transport = core.LCMTransport( - "/cmd_vel", Twist - ) - self.frontier_explorer.goal_request.transport = fe_goal_request_transport = ( - core.LCMTransport("/goal_request", PoseStamped) - ) - self.frontier_explorer.goal_reached.transport = fe_goal_reached_transport = ( - core.LCMTransport("/goal_reached", Bool) - ) - self.frontier_explorer.explore_cmd.transport = fe_explore_cmd_transport = core.LCMTransport( - "/explore_cmd", Bool - ) - self.frontier_explorer.stop_explore_cmd.transport = fe_stop_explore_cmd_transport = ( - core.LCMTransport("/stop_explore_cmd", Bool) + self.global_planner.path.transport = core.LCMTransport("/global_path", Path) + self.local_planner.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + self.frontier_explorer.goal_request.transport = core.LCMTransport( + "/goal_request", PoseStamped ) - self._transports.extend( - [ - nav_target_transport, - nav_goal_request_transport, - nav_goal_reached_transport, - nav_state_transport, - nav_global_costmap_transport, - global_path_transport, - local_cmd_vel_transport, - fe_goal_request_transport, - fe_goal_reached_transport, - fe_explore_cmd_transport, - fe_stop_explore_cmd_transport, - ] + self.frontier_explorer.goal_reached.transport = core.LCMTransport("/goal_reached", Bool) + self.frontier_explorer.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) + self.frontier_explorer.stop_explore_cmd.transport = core.LCMTransport( + "/stop_explore_cmd", Bool ) self.global_planner.target.connect(self.navigator.target) @@ -582,30 +501,11 @@ def _deploy_navigation(self) -> None: def _deploy_visualization(self) -> None: """Deploy and configure visualization modules.""" self.websocket_vis = self._dimos.deploy(WebsocketVisModule, port=self.websocket_port) - self.websocket_vis.goal_request.transport = vis_goal_request_transport = core.LCMTransport( - "/goal_request", PoseStamped - ) - self.websocket_vis.gps_goal.transport = vis_gps_goal_transport = core.pLCMTransport( - "/gps_goal" - ) - self.websocket_vis.explore_cmd.transport = vis_explore_cmd_transport = core.LCMTransport( - "/explore_cmd", Bool - ) - self.websocket_vis.stop_explore_cmd.transport = vis_stop_explore_cmd_transport = ( - core.LCMTransport("/stop_explore_cmd", Bool) - ) - self.websocket_vis.cmd_vel.transport = vis_cmd_vel_transport = core.LCMTransport( - "/cmd_vel", Twist - ) - self._transports.extend( - [ - vis_goal_request_transport, - vis_gps_goal_transport, - vis_explore_cmd_transport, - vis_stop_explore_cmd_transport, - vis_cmd_vel_transport, - ] - ) + self.websocket_vis.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) + self.websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") + self.websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) + self.websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) + self.websocket_vis.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) self.websocket_vis.odom.connect(self.connection.odom) self.websocket_vis.gps_location.connect(self.connection.gps_location) @@ -635,6 +535,9 @@ def _deploy_perception(self) -> None: self.spatial_memory_module.color_image.transport = core.pSHMTransport( "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE ) + self.spatial_memory_module.odom.transport = core.LCMTransport( + "/go2/camera_pose", PoseStamped + ) logger.info("Spatial memory module deployed and connected") @@ -650,19 +553,15 @@ def _deploy_perception(self) -> None: self.utilization_module = self._dimos.deploy(UtilizationModule) # Set up transports for object tracker - self.object_tracker.detection2darray.transport = ot_detection_transport = core.LCMTransport( + self.object_tracker.detection2darray.transport = core.LCMTransport( "/go2/detection2d", Detection2DArray ) - self.object_tracker.tracked_overlay.transport = ot_overlay_transport = core.pSHMTransport( + self.object_tracker.tracked_overlay.transport = core.pSHMTransport( "/go2/tracked_overlay", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE ) - self._transports.extend([ot_detection_transport, ot_overlay_transport]) # Set up transports for bbox navigator - self.bbox_navigator.goal_request.transport = bbox_goal_request_transport = ( - core.LCMTransport("/goal_request", PoseStamped) - ) - self._transports.append(bbox_goal_request_transport) + self.bbox_navigator.goal_request.transport = core.LCMTransport("/goal_request", PoseStamped) logger.info("Object tracker and bbox navigator modules deployed") @@ -725,7 +624,7 @@ def navigate_to(self, pose: PoseStamped, blocking: bool = True) -> bool: time.sleep(1.0) if blocking: - while self.navigator.get_state() == NavigationState.FOLLOWING_PATH: + while self.navigator.get_state() == NavigatorState.FOLLOWING_PATH: time.sleep(0.25) time.sleep(1.0) @@ -782,18 +681,11 @@ def get_odom(self) -> PoseStamped: def main() -> None: """Main entry point.""" - # Clean up dask scratch space to avoid permission errors from previous runs - import shutil - import tempfile - - dask_scratch = os.path.join(tempfile.gettempdir(), "dask-scratch-space") - if os.path.exists(dask_scratch): - logger.info(f"Removing stale dask scratch space: {dask_scratch}") - shutil.rmtree(dask_scratch, ignore_errors=True) - ip = os.getenv("ROBOT_IP") connection_type = os.getenv("CONNECTION_TYPE", "webrtc") + pubsub.lcm.autoconf() + robot = UnitreeGo2(ip=ip, websocket_port=7779, connection_type=connection_type) robot.start() From 58a42004cfa79560a399411d784e9e41fef97d19 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 09:37:15 -0600 Subject: [PATCH 12/45] patch lcm service to get running on macOS --- dimos/protocol/service/lcmservice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 5820ad88e4..cbf6c0b367 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -319,6 +319,8 @@ def __init__(self, **kwargs) -> None: # we support passing an existing LCM instance self.l = self.config.lcm + if self.l is None and sys.platform == "darwin": + self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() self._l_lock = threading.Lock() self._stop_event = threading.Event() From 0c1af71fcf2a992edc5f44a7e1427cae9535b6dd Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 09:39:35 -0600 Subject: [PATCH 13/45] flake update --- flake.lock | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/flake.lock b/flake.lock index 6235e35c44..0fc58d3bd9 100644 --- a/flake.lock +++ b/flake.lock @@ -1,17 +1,5 @@ { "nodes": { - "core": { - "locked": { - "lastModified": 1, - "narHash": "sha256-lWX5DUltOFcS57I8wHH0Sz3J++zMORxHf+CXoZZLQzU=", - "path": "./helpers/builtins", - "type": "path" - }, - "original": { - "path": "./helpers/builtins", - "type": "path" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -32,18 +20,17 @@ }, "lib": { "inputs": { - "core": "core", "flakeUtils": [ "flake-utils" ], "libSource": "libSource" }, "locked": { - "lastModified": 1762804113, - "narHash": "sha256-8JsbXhJY4pREh0MHEQCbpmJAwdLYdQJ0dz5G9izCOaM=", + "lastModified": 1763164848, + "narHash": "sha256-OlnnK3Iepi4As1onBrNfIiiQ0xIGzEWsJ16/TrLFcpY=", "owner": "jeff-hykin", "repo": "quick-nix-toolkits", - "rev": "6c6112ec4aabbc43320c0a25d935404f7bab002e", + "rev": "3c820d33a0c4c8480a771484f99490243b3c6b5f", "type": "github" }, "original": { @@ -54,11 +41,11 @@ }, "libSource": { "locked": { - "lastModified": 1762650614, - "narHash": "sha256-tPIUJjNeNs3LMWH8w2nHLx0trZJdOJ54mN2UQhcjZ9g=", + "lastModified": 1763255503, + "narHash": "sha256-7AL5rgcGVjhYgZFbZQt1IndGcY27h5B5xi9OWtLlm6c=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "91ea24e62ff55f95939f32432fa5def2d6d24d2a", + "rev": "56f74a2d6cd236c0ea3097b3df2e053fbb374b26", "type": "github" }, "original": { @@ -69,16 +56,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1762977756, - "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", + "lastModified": 1748026580, + "narHash": "sha256-rWtXrcIzU5wm/C8F9LWvUfBGu5U5E7cFzPYT1pHIJaQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", + "rev": "11cb3517b3af6af300dd6c055aeda73c9bf52c48", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "25.05", "repo": "nixpkgs", "type": "github" } From 7cb5e223fb10bab524190296530a97d22aa817a9 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 09:44:04 -0600 Subject: [PATCH 14/45] fix lcm macos --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 4064a7262a..6412fe652d 100644 --- a/flake.nix +++ b/flake.nix @@ -106,7 +106,7 @@ in { buildInputs = (old.buildInputs or []) ++ pkgConfPackages; - nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.pkg-config ]; + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.pkg-config pkgs.python312 ]; # 1. fix pkg-config on darwin env.PKG_CONFIG_PATH = packageConfPackagesString; # 2. Fix fsync on darwin From 29e0800fd68a6d689bef355bbd9ac23ea6e871b7 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 10:30:33 -0600 Subject: [PATCH 15/45] use mjpython so works on macos --- dimos/robot/unitree_webrtc/mujoco_connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py index 897914385a..e61b451b44 100644 --- a/dimos/robot/unitree_webrtc/mujoco_connection.py +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -80,8 +80,10 @@ def start(self) -> None: # Launch the subprocess try: + # mjpython must be used macOS (because of launch_passive inside mujoco_process.py) + executable = sys.executable if sys.platform != 'darwin' else 'mjpython' self.process = subprocess.Popen( - [sys.executable, str(LAUNCHER_PATH), config_pickle, shm_names_json], + [executable, str(LAUNCHER_PATH), config_pickle, shm_names_json], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, From bbba92f3128816a0d34f5482379f48eb23efe859 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 10:37:32 -0600 Subject: [PATCH 16/45] on mujoco fail, show stderr --- dimos/robot/unitree_webrtc/mujoco_connection.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py index e61b451b44..de60e1c350 100644 --- a/dimos/robot/unitree_webrtc/mujoco_connection.py +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -93,23 +93,27 @@ def start(self) -> None: except Exception as e: self.shm_data.cleanup() raise RuntimeError(f"Failed to start MuJoCo subprocess: {e}") from e - + + get_stderr = lambda: "\n" + self.process.stderr.read().replace('\n', '\n[mujoco_process.py] ') + "\n" if self.process else "" + # Wait for process to be ready ready_timeout = 10 start_time = time.time() while time.time() - start_time < ready_timeout: if self.process.poll() is not None: exit_code = self.process.returncode + stderr_string = get_stderr() self.stop() - raise RuntimeError(f"MuJoCo process failed to start (exit code {exit_code})") + raise RuntimeError(f"{stderr_string}MuJoCo process failed to start (exit code {exit_code})") if self.shm_data.is_ready(): logger.info("MuJoCo process started successfully") return time.sleep(0.1) # Timeout + stderr_string = get_stderr() self.stop() - raise RuntimeError("MuJoCo process failed to start (timeout)") + raise RuntimeError(f"{stderr_string}MuJoCo process failed to start (timeout)") def stop(self) -> None: if self._is_cleaned_up: From 633c1e52aba05b0a8b138d2fa1e5d5607b45c182 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 11:24:08 -0600 Subject: [PATCH 17/45] standardize platform checks --- dimos/protocol/service/lcmservice.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index cbf6c0b367..51947a16da 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -18,7 +18,6 @@ from dataclasses import dataclass from functools import cache import os -import platform import subprocess import sys import threading @@ -49,10 +48,7 @@ def check_multicast() -> list[str]: sudo = "" if check_root() else "sudo " - system = platform.system() - - if system == "Linux": - # Linux commands + if sys.platform == "linux": loopback_interface = "lo" # Check if loopback interface has multicast enabled try: @@ -78,7 +74,7 @@ def check_multicast() -> list[str]: f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev {loopback_interface}" ) - elif system == "Darwin": # macOS + elif sys.platform == "darwin": # macOS loopback_interface = "lo0" # Check if multicast route exists try: @@ -109,9 +105,8 @@ def check_buffers() -> tuple[list[str], int | None]: current_max = None sudo = "" if check_root() else "sudo " - system = platform.system() - if system == "Linux": + if sys.platform == "linux": # Linux buffer configuration try: result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) @@ -135,7 +130,7 @@ def check_buffers() -> tuple[list[str], int | None]: except: commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") - elif system == "Darwin": # macOS + elif sys.platform == "darwin": # macOS # macOS buffer configuration - check and set UDP buffer related sysctls try: result = subprocess.run( @@ -223,8 +218,6 @@ def autoconf() -> None: logger.info("CI environment detected: Skipping automatic system configuration.") return - system = platform.system() - commands_needed = [] # Check multicast configuration @@ -275,7 +268,7 @@ class LCMConfig: lcm: lcm.LCM | None = None def __post_init__(self): - if self.url is None and platform.system() == "Darwin": + if self.url is None and sys.platform == "darwin": # On macOS, use multicast with TTL=0 to keep traffic local self.url = "udpm://239.255.76.67:7667?ttl=0" From ef10293e49e0dae085b3984a90d7b9399685279b Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 11:27:17 -0600 Subject: [PATCH 18/45] make ruff happy --- .../embedding_models_disabled_tests.py | 2 +- dimos/robot/unitree_webrtc/mujoco_connection.py | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/dimos/models/embedding/embedding_models_disabled_tests.py b/dimos/models/embedding/embedding_models_disabled_tests.py index bb1f038410..c2c84aba83 100644 --- a/dimos/models/embedding/embedding_models_disabled_tests.py +++ b/dimos/models/embedding/embedding_models_disabled_tests.py @@ -298,7 +298,7 @@ def test_gpu_query_performance(embedding_model, test_image) -> None: assert len(results) == 5, "Should return top-5 results" # All should be high similarity (same image, allow some variation for image preprocessing) - for idx, sim in results: + for _, sim in results: assert sim > 0.90, f"Same images should have high similarity, got {sim}" diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py index de60e1c350..1c0420e6b4 100644 --- a/dimos/robot/unitree_webrtc/mujoco_connection.py +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -81,7 +81,7 @@ def start(self) -> None: # Launch the subprocess try: # mjpython must be used macOS (because of launch_passive inside mujoco_process.py) - executable = sys.executable if sys.platform != 'darwin' else 'mjpython' + executable = sys.executable if sys.platform != "darwin" else "mjpython" self.process = subprocess.Popen( [executable, str(LAUNCHER_PATH), config_pickle, shm_names_json], stdout=subprocess.PIPE, @@ -93,9 +93,14 @@ def start(self) -> None: except Exception as e: self.shm_data.cleanup() raise RuntimeError(f"Failed to start MuJoCo subprocess: {e}") from e - - get_stderr = lambda: "\n" + self.process.stderr.read().replace('\n', '\n[mujoco_process.py] ') + "\n" if self.process else "" - + + def get_stderr(): + return ( + "\n" + self.process.stderr.read().replace("\n", "\n[mujoco_process.py] ") + "\n" + if self.process + else "" + ) + # Wait for process to be ready ready_timeout = 10 start_time = time.time() @@ -104,7 +109,9 @@ def start(self) -> None: exit_code = self.process.returncode stderr_string = get_stderr() self.stop() - raise RuntimeError(f"{stderr_string}MuJoCo process failed to start (exit code {exit_code})") + raise RuntimeError( + f"{stderr_string}MuJoCo process failed to start (exit code {exit_code})" + ) if self.shm_data.is_ready(): logger.info("MuJoCo process started successfully") return From c4ac2db3aa853ec210160bd9f708f6a1d829473a Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 11:38:00 -0600 Subject: [PATCH 19/45] revert --- dimos/protocol/pubsub/shm/ipc_factory.py | 1 - dimos/protocol/service/lcmservice.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/dimos/protocol/pubsub/shm/ipc_factory.py b/dimos/protocol/pubsub/shm/ipc_factory.py index 805dad4d9e..9aedbfa1c4 100644 --- a/dimos/protocol/pubsub/shm/ipc_factory.py +++ b/dimos/protocol/pubsub/shm/ipc_factory.py @@ -17,7 +17,6 @@ from abc import ABC, abstractmethod from multiprocessing.shared_memory import SharedMemory import os -import sys import time import numpy as np diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 51947a16da..e6550d3904 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -343,16 +343,16 @@ def __setstate__(self, state) -> None: self._call_thread_pool_lock = threading.RLock() def start(self) -> None: - # Run autoconf before LCM initialization if not already initialized - if self.config.autoconf and self.l is None: - autoconf() - - # Reinitialize LCM if it's None (e.g., after unpickling or deferred init) + # Reinitialize LCM if it's None (e.g., after unpickling) if self.l is None: - self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() + if self.config.lcm: + self.l = self.config.lcm + else: + self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() - # Fallback to check_system if autoconf is disabled - if not self.config.autoconf: + if self.config.autoconf: + autoconf() + else: try: check_system() except Exception as e: From ce48207d9c3510708362736e0400ae295c5f114a Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 11:48:15 -0600 Subject: [PATCH 20/45] revert more --- dimos/protocol/service/lcmservice.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index e6550d3904..5e45cf4f60 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -312,7 +312,9 @@ def __init__(self, **kwargs) -> None: # we support passing an existing LCM instance self.l = self.config.lcm - if self.l is None and sys.platform == "darwin": + if self.config.lcm: + self.l = self.config.lcm + else: self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() self._l_lock = threading.Lock() From 927e7d9e8c1044bea491e9efb474dcbd6933f12e Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 12:45:23 -0600 Subject: [PATCH 21/45] clean up opened file descriptors --- dimos/robot/unitree_webrtc/mujoco_connection.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py index 1c0420e6b4..26d9e1f06b 100644 --- a/dimos/robot/unitree_webrtc/mujoco_connection.py +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -95,11 +95,10 @@ def start(self) -> None: raise RuntimeError(f"Failed to start MuJoCo subprocess: {e}") from e def get_stderr(): - return ( - "\n" + self.process.stderr.read().replace("\n", "\n[mujoco_process.py] ") + "\n" - if self.process - else "" - ) + text = "" + if self.process: + text = "\n" + self.process.stderr.read().replace("\n", "\n[mujoco_process.py] ") + "\n" + return text # Wait for process to be ready ready_timeout = 10 @@ -127,6 +126,10 @@ def stop(self) -> None: return self._is_cleaned_up = True + + # clean up open file descriptors + self.process.stderr.close() + self.process.stdout.close() # Cancel any pending timers if self._stop_timer: From 469b6c496eeca41d34b87c08d8b2343cdc5ff38f Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 12:46:11 -0600 Subject: [PATCH 22/45] fix Cuda warning on macos, default to CoreMLExecutionProvider before falling back on CPU --- dimos/agents/memory/image_embedding.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dimos/agents/memory/image_embedding.py b/dimos/agents/memory/image_embedding.py index 7b6dd88515..7d84eb6544 100644 --- a/dimos/agents/memory/image_embedding.py +++ b/dimos/agents/memory/image_embedding.py @@ -72,6 +72,10 @@ def _initialize_model(self): processor_id = "openai/clip-vit-base-patch32" providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] + if sys.platform == "darwin": + # 2025-11-17 12:36:47.877215 [W:onnxruntime:, helper.cc:82 IsInputSupported] CoreML does not support input dim > 16384. Input:text_model.embeddings.token_embedding.weight, shape: {49408,512} + # 2025-11-17 12:36:47.878496 [W:onnxruntime:, coreml_execution_provider.cc:107 GetCapability] CoreMLExecutionProvider::GetCapability, number of partitions supported by CoreML: 88 number of nodes in the graph: 1504 number of nodes supported by CoreML: 933 + providers = ["CoreMLExecutionProvider"] + [ each for each in providers if each != "CUDAExecutionProvider"] self.model = ort.InferenceSession(str(model_id), providers=providers) From fde904eee4c4d44c7e2319e7346fe5a9e6279c91 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 12:46:21 -0600 Subject: [PATCH 23/45] - --- dimos/agents/memory/image_embedding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dimos/agents/memory/image_embedding.py b/dimos/agents/memory/image_embedding.py index 7d84eb6544..577c128ad8 100644 --- a/dimos/agents/memory/image_embedding.py +++ b/dimos/agents/memory/image_embedding.py @@ -22,6 +22,7 @@ import base64 import io import os +import sys import cv2 import numpy as np From 766aeb3d0359e401b7adca0de2402f38f0aa8121 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Mon, 17 Nov 2025 13:03:14 -0600 Subject: [PATCH 24/45] have pytorch take advantage of MacOS Metal --- dimos/agents/agent_ctransformers_gguf.py | 9 ++++++++- dimos/agents/agent_huggingface_local.py | 9 ++++++++- dimos/agents/memory/chroma_impl.py | 12 ++++++++++-- dimos/models/Detic/tools/dump_clip_features.py | 9 ++++++++- dimos/models/embedding/clip.py | 10 +++++++++- dimos/models/embedding/mobileclip.py | 10 +++++++++- dimos/models/embedding/treid.py | 10 +++++++++- dimos/models/vl/moondream.py | 10 +++++++++- dimos/perception/detection/detectors/yolo.py | 13 +++++++++---- dimos/perception/segmentation/sam_2d_seg.py | 9 ++++++++- 10 files changed, 87 insertions(+), 14 deletions(-) diff --git a/dimos/agents/agent_ctransformers_gguf.py b/dimos/agents/agent_ctransformers_gguf.py index 17d233437d..a4f38151c5 100644 --- a/dimos/agents/agent_ctransformers_gguf.py +++ b/dimos/agents/agent_ctransformers_gguf.py @@ -133,7 +133,14 @@ def __init__( self.model_name = model_name self.device = device if self.device == "auto": - self.device = "cuda" if torch.cuda.is_available() else "cpu" + if torch.cuda.is_available(): + self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + else: + self.device = "cpu" + if self.device == "cuda": print(f"Using GPU: {torch.cuda.get_device_name(0)}") else: diff --git a/dimos/agents/agent_huggingface_local.py b/dimos/agents/agent_huggingface_local.py index 69d02bb1d2..a60d3bf865 100644 --- a/dimos/agents/agent_huggingface_local.py +++ b/dimos/agents/agent_huggingface_local.py @@ -92,7 +92,14 @@ def __init__( self.model_name = model_name self.device = device if self.device == "auto": - self.device = "cuda" if torch.cuda.is_available() else "cpu" + if torch.cuda.is_available(): + self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + else: + self.device = "cpu" + if self.device == "cuda": print(f"Using GPU: {torch.cuda.get_device_name(0)}") else: diff --git a/dimos/agents/memory/chroma_impl.py b/dimos/agents/memory/chroma_impl.py index b238b616d8..8f74ede41d 100644 --- a/dimos/agents/memory/chroma_impl.py +++ b/dimos/agents/memory/chroma_impl.py @@ -145,8 +145,16 @@ def __init__( def create(self) -> None: """Create local embedding model and initialize the ChromaDB client.""" # Load the sentence transformer model - # Use CUDA if available, otherwise fall back to CPU - device = "cuda" if torch.cuda.is_available() else "cpu" + + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): + self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + else: + self.device = "cpu" + print(f"Using device: {device}") self.model = SentenceTransformer(self.model_name, device=device) diff --git a/dimos/models/Detic/tools/dump_clip_features.py b/dimos/models/Detic/tools/dump_clip_features.py index 31be161f6d..f994710057 100644 --- a/dimos/models/Detic/tools/dump_clip_features.py +++ b/dimos/models/Detic/tools/dump_clip_features.py @@ -40,7 +40,14 @@ if args.use_underscore: cat_names = [x.strip().replace("/ ", "/").replace(" ", "_") for x in cat_names] print("cat_names", cat_names) - device = "cuda" if torch.cuda.is_available() else "cpu" + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): + device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + device = "mps" + else: + device = "cpu" if args.prompt == "a": sentences = ["a " + x for x in cat_names] diff --git a/dimos/models/embedding/clip.py b/dimos/models/embedding/clip.py index 23ab5e94f2..91952b237f 100644 --- a/dimos/models/embedding/clip.py +++ b/dimos/models/embedding/clip.py @@ -43,7 +43,15 @@ def __init__( device: Device to run on (cuda/cpu), auto-detects if None normalize: Whether to L2 normalize embeddings """ - self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): + self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + else: + self.device = "cpu" + self.normalize = normalize # Load model and processor diff --git a/dimos/models/embedding/mobileclip.py b/dimos/models/embedding/mobileclip.py index 8ddefd3c87..ce6b6c578b 100644 --- a/dimos/models/embedding/mobileclip.py +++ b/dimos/models/embedding/mobileclip.py @@ -51,7 +51,15 @@ def __init__( "Install it with: pip install open-clip-torch" ) - self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): + self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + else: + self.device = "cpu" + self.normalize = normalize # Load model diff --git a/dimos/models/embedding/treid.py b/dimos/models/embedding/treid.py index b00ad11250..339ddf1e0f 100644 --- a/dimos/models/embedding/treid.py +++ b/dimos/models/embedding/treid.py @@ -51,7 +51,15 @@ def __init__( "torchreid is required for TorchReIDModel. Install it with: pip install torchreid" ) - self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): + self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + else: + self.device = "cpu" + self.normalize = normalize # Load model using torchreid's FeatureExtractor diff --git a/dimos/models/vl/moondream.py b/dimos/models/vl/moondream.py index 781f1adbf1..f0e95cf871 100644 --- a/dimos/models/vl/moondream.py +++ b/dimos/models/vl/moondream.py @@ -23,7 +23,15 @@ def __init__( dtype: torch.dtype = torch.bfloat16, ) -> None: self._model_name = model_name - self._device = device or ("cuda" if torch.cuda.is_available() else "cpu") + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): + self._device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self._device = "mps" + else: + self._device = "cpu" + self._dtype = dtype @cached_property diff --git a/dimos/perception/detection/detectors/yolo.py b/dimos/perception/detection/detectors/yolo.py index 64e56ad456..e7fe8005f7 100644 --- a/dimos/perception/detection/detectors/yolo.py +++ b/dimos/perception/detection/detectors/yolo.py @@ -39,14 +39,19 @@ def __init__( if device: self.device = device return - - if is_cuda_available(): + + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): self.device = "cuda" logger.debug("Using CUDA for YOLO 2d detector") + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + self.device = "mps" + logger.debug("Using Metal for YOLO 2d detector") else: - self.device = "cpu" logger.debug("Using CPU for YOLO 2d detector") - + self.device = "cpu" + def process_image(self, image: Image) -> ImageDetections2D: """ Process an image and return detection results. diff --git a/dimos/perception/segmentation/sam_2d_seg.py b/dimos/perception/segmentation/sam_2d_seg.py index b13ebc4c65..697a2dd029 100644 --- a/dimos/perception/segmentation/sam_2d_seg.py +++ b/dimos/perception/segmentation/sam_2d_seg.py @@ -48,14 +48,21 @@ def __init__( use_rich_labeling: bool = False, use_filtering: bool = True, ) -> None: - if is_cuda_available(): + # Use GPU if available, otherwise fall back to CPU + if torch.cuda.is_available(): logger.info("Using CUDA for SAM 2d segmenter") if hasattr(onnxruntime, "preload_dlls"): # Handles CUDA 11 / onnxruntime-gpu<=1.18 onnxruntime.preload_dlls(cuda=True, cudnn=True) self.device = "cuda" + # MacOS Metal performance shaders + elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): + logger.info("Using Metal for SAM 2d segmenter") + self.device = "mps" else: logger.info("Using CPU for SAM 2d segmenter") self.device = "cpu" + + # Core components self.model = FastSAM(get_data(model_path) / model_name) self.use_tracker = use_tracker From 5e80f7af9dfac276a0047cbf4c2ba6dc3cfda591 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 18:50:09 -0600 Subject: [PATCH 25/45] automate nix develop init --- README.md | 16 ++++++++ flake.lock | 8 ++-- flake.nix | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1db93e9887..24d4d88c6c 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,22 @@ We are shipping a first look at the DIMOS x Unitree Go2 integration, allowing fo - **DimOS Interface / Development Tools** - Local development interface to control your robot, orchestrate agents, visualize camera/lidar streams, and debug your dimensional agentive application. +## MacOS Installation + +```sh +# Install Nix +curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install + +# clone the repository +git clone --branch dev --single-branch https://github.com/dimensionalOS/dimos.git + +# setup the environment (follow the prompts) +cd dimos +nix develop + +# You should be able to follow the instructions below as well for a more manual installation +``` + --- ## Python Installation Tested on Ubuntu 22.04/24.04 diff --git a/flake.lock b/flake.lock index 0fc58d3bd9..707922a9b1 100644 --- a/flake.lock +++ b/flake.lock @@ -56,16 +56,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748026580, - "narHash": "sha256-rWtXrcIzU5wm/C8F9LWvUfBGu5U5E7cFzPYT1pHIJaQ=", + "lastModified": 1748929857, + "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "11cb3517b3af6af300dd6c055aeda73c9bf52c48", + "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", "type": "github" }, "original": { "owner": "NixOS", - "ref": "25.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 6412fe652d..245a920dc6 100644 --- a/flake.nix +++ b/flake.nix @@ -139,15 +139,118 @@ export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath ldLibraryPackages}:$LD_LIBRARY_PATH" export DISPLAY=:0 export GI_TYPELIB_PATH="${giTypelibPackagesString}:$GI_TYPELIB_PATH" - PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") - if [ -f "$PROJECT_ROOT/env/bin/activate" ]; then - . "$PROJECT_ROOT/env/bin/activate" - fi # without this alias, the pytest uses the non-venv python and fails alias pytest="python -m pytest" + + PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") [ -f "$PROJECT_ROOT/motd" ] && cat "$PROJECT_ROOT/motd" [ -f "$PROJECT_ROOT/.pre-commit-config.yaml" ] && pre-commit install --install-hooks + cd "$PROJECT_ROOT" + + # + # python & setup + # + if [ -f "$PROJECT_ROOT/venv/bin/activate" ]; then + . "$PROJECT_ROOT/venv/bin/activate" + else + # + # automate the readme + # + + # helper + confirm_ask() { + echo + question="$1";answer="" + while true; do + echo "$question"; read response + if [ -z "$response" ]; then + echo + return 0 # success + break + fi + case "$response" in + [Yy]* ) answer='yes'; break;; + [Nn]* ) answer='no'; break;; + * ) echo "Please answer yes or no.";; + esac + done + if [ "$answer" = "yes" ] + then + echo + return 0 # success + fi + echo + return 1 # failure + } + + macos_version="$(sw_vers -productVersion)" + macos_major_version="''${macos_version%%.*}" + if confirm_ask "Would you like me to set up the environment for you? [y/n]"; then + echo "Making sure git lfs is installed..." + git lfs install || true + + if confirm_ask "Should I donwload the models and data? (around 17Gb) this will be needed to run the simulation [y/n]"; then + echo "Downloading the models and data..." + git lfs fetch --all + git lfs pull + echo "Done!" + fi + + # check if no .env + if ! [ -f ".env" ] + then + echo "Setting up .env file..." + cp default.env .env + echo + echo "note: you might want to edit the .env file with your own settings" + echo + fi + + echo "Setting up virtualenv..." + python3 -m venv venv + echo "Activating virtualenv..." + . venv/bin/activate + echo "Installing python dependencies..." + pip install -e . + + # if really old MacOS then ignore the lcm dependency (it'll be supplied by nix) + if [ "$macos_major_version" -le 13 ]; then + echo "You're on a really old MacOS version. Ignore the errors above (and probably later below) about LCM" + echo "Got it? (press enter)";read _ + rm -f pyproject.original.toml + cp pyproject.toml pyproject.original.toml + # install dimos-lcm without installing lcm + pip install --no-deps 'git+https://github.com/dimensionalOS/dimos-lcm.git' + # manually install dependencies of dimos-lcm + pip install foxglove-websocket numpy + # remove dimos-lcm from pyproject.toml for a moment + grep -v '^\s*#' pyproject.original.toml | grep -v "dimos-lcm @ .*" > pyproject.toml + pip install -e .[cpu,dev] 2>&1 | grep -v -E "Could not find a version that satisfies the requirement lcm |ERROR: No matching distribution found for lcm" + # restore pyproject.toml + rm -f pyproject.toml + mv pyproject.original.toml pyproject.toml + fi + + # CUDA/CPU dependencies + if ! [ "$(uname)" = "Darwin" ] && confirm_ask "Want me to install the cuda dependencies? [y/n]"; then + pip install -e .[cuda,dev] + else + pip install -e .[cpu,dev] + fi + + # Mujoco/Simulation dependencies + if confirm_ask "Want me to install the optional simulation (mujoco) dependencies? [y/n]"; then + pip install -e .[sim] + fi + + if confirm_ask "Would you like me to run the tests to make sure everything is working? [y/n]"; then + echo "Running tests..." + python -m pytest -s "$PROJECT_ROOT/dimos/" + echo "tests finished" + fi + fi + fi ''; }; From 6de81c4dc953c093335fc301e299f848ab6cf14b Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 18:54:21 -0600 Subject: [PATCH 26/45] - --- dimos/protocol/service/lcmservice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 5e45cf4f60..36cc68d189 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -90,7 +90,7 @@ def check_multicast() -> list[str]: else: # For other systems, skip multicast configuration - logger.warning(f"Multicast configuration not supported on {system}") + logger.warning(f"Multicast configuration not supported on {sys.platform}") return commands_needed @@ -170,7 +170,7 @@ def check_buffers() -> tuple[list[str], int | None]: else: # For other systems, skip buffer configuration - logger.warning(f"Buffer configuration not supported on {system}") + logger.warning(f"Buffer configuration not supported on {sys.platform}") return commands_needed, current_max From 7fcb09ff48a00986d09d3da4c7039b143e374273 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 18:56:22 -0600 Subject: [PATCH 27/45] - --- dimos/protocol/service/test_lcmservice.py | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py index 7b7038e16e..accc90be67 100644 --- a/dimos/protocol/service/test_lcmservice.py +++ b/dimos/protocol/service/test_lcmservice.py @@ -34,7 +34,7 @@ def get_sudo_prefix() -> str: def test_check_multicast_all_configured() -> None: """Test check_multicast when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock successful checks with realistic output format mock_run.side_effect = [ @@ -57,7 +57,7 @@ def test_check_multicast_all_configured() -> None: def test_check_multicast_missing_multicast_flag() -> None: """Test check_multicast when loopback interface lacks multicast.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock interface without MULTICAST flag (realistic current system state) mock_run.side_effect = [ @@ -81,7 +81,7 @@ def test_check_multicast_missing_multicast_flag() -> None: def test_check_multicast_missing_route() -> None: """Test check_multicast when multicast route is missing.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock missing route - interface has multicast but no route mock_run.side_effect = [ @@ -105,7 +105,7 @@ def test_check_multicast_missing_route() -> None: def test_check_multicast_all_missing() -> None: """Test check_multicast when both multicast flag and route are missing (current system state).""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock both missing - matches actual current system state mock_run.side_effect = [ @@ -133,7 +133,7 @@ def test_check_multicast_all_missing() -> None: def test_check_multicast_subprocess_exception() -> None: """Test check_multicast when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock subprocess exceptions mock_run.side_effect = Exception("Command failed") @@ -149,7 +149,7 @@ def test_check_multicast_subprocess_exception() -> None: def test_check_multicast_macos() -> None: """Test check_multicast on macOS when configuration is needed.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock netstat -nr to not contain the multicast route mock_run.side_effect = [ @@ -171,7 +171,7 @@ def test_check_multicast_macos() -> None: def test_check_buffers_all_configured() -> None: """Test check_buffers when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock sufficient buffer sizes mock_run.side_effect = [ @@ -190,7 +190,7 @@ def test_check_buffers_all_configured() -> None: def test_check_buffers_low_max_buffer() -> None: """Test check_buffers when rmem_max is too low.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock low rmem_max mock_run.side_effect = [ @@ -210,7 +210,7 @@ def test_check_buffers_low_max_buffer() -> None: def test_check_buffers_low_default_buffer() -> None: """Test check_buffers when rmem_default is too low.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock low rmem_default mock_run.side_effect = [ @@ -230,7 +230,7 @@ def test_check_buffers_low_default_buffer() -> None: def test_check_buffers_both_low() -> None: """Test check_buffers when both buffer sizes are too low.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock both low mock_run.side_effect = [ @@ -254,7 +254,7 @@ def test_check_buffers_both_low() -> None: def test_check_buffers_subprocess_exception() -> None: """Test check_buffers when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock subprocess exceptions mock_run.side_effect = Exception("Command failed") @@ -271,7 +271,7 @@ def test_check_buffers_subprocess_exception() -> None: def test_check_buffers_parsing_error() -> None: """Test check_buffers when output parsing fails.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock malformed output mock_run.side_effect = [ @@ -291,7 +291,7 @@ def test_check_buffers_parsing_error() -> None: def test_check_buffers_dev_container() -> None: """Test check_buffers in dev container where sysctl fails.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock dev container behavior - sysctl returns non-zero mock_run.side_effect = [ @@ -325,7 +325,7 @@ def test_check_buffers_dev_container() -> None: def test_check_buffers_macos_all_configured() -> None: """Test check_buffers on macOS when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock sufficient buffer sizes for macOS mock_run.side_effect = [ @@ -347,7 +347,7 @@ def test_check_buffers_macos_all_configured() -> None: def test_check_buffers_macos_needs_config() -> None: """Test check_buffers on macOS when configuration is needed.""" - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock low buffer sizes for macOS mock_run.side_effect = [ @@ -377,7 +377,7 @@ def test_autoconf_no_config_needed() -> None: """Test autoconf when no configuration is needed.""" # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock all checks passing mock_run.side_effect = [ @@ -418,7 +418,7 @@ def test_autoconf_with_config_needed_success() -> None: """Test autoconf when configuration is needed and commands succeed.""" # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock checks failing, then mock the execution succeeding mock_run.side_effect = [ @@ -480,7 +480,7 @@ def test_autoconf_with_command_failures() -> None: """Test autoconf when some commands fail.""" # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): + with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock checks failing, then mock some commands failing mock_run.side_effect = [ From 83d0046c3445cf30120c31aa239b8fc1d8c38201 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 18:58:37 -0600 Subject: [PATCH 28/45] fix gpu check mistake --- dimos/perception/detection/detectors/yolo.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/dimos/perception/detection/detectors/yolo.py b/dimos/perception/detection/detectors/yolo.py index e7fe8005f7..64e56ad456 100644 --- a/dimos/perception/detection/detectors/yolo.py +++ b/dimos/perception/detection/detectors/yolo.py @@ -39,19 +39,14 @@ def __init__( if device: self.device = device return - - # Use GPU if available, otherwise fall back to CPU - if torch.cuda.is_available(): + + if is_cuda_available(): self.device = "cuda" logger.debug("Using CUDA for YOLO 2d detector") - # MacOS Metal performance shaders - elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): - self.device = "mps" - logger.debug("Using Metal for YOLO 2d detector") else: - logger.debug("Using CPU for YOLO 2d detector") self.device = "cpu" - + logger.debug("Using CPU for YOLO 2d detector") + def process_image(self, image: Image) -> ImageDetections2D: """ Process an image and return detection results. From cf1fd15b968df29e154c6c2b270c66b4e10e3af0 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 18:58:43 -0600 Subject: [PATCH 29/45] fix tests --- dimos/protocol/service/lcmservice.py | 21 ++++++++----- dimos/protocol/service/test_lcmservice.py | 36 +++++++++++------------ 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 36cc68d189..b3c711d0cd 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -18,6 +18,7 @@ from dataclasses import dataclass from functools import cache import os +import platform import subprocess import sys import threading @@ -48,7 +49,10 @@ def check_multicast() -> list[str]: sudo = "" if check_root() else "sudo " - if sys.platform == "linux": + system = platform.system() + + if system == "Linux": + # Linux commands loopback_interface = "lo" # Check if loopback interface has multicast enabled try: @@ -74,7 +78,7 @@ def check_multicast() -> list[str]: f"{sudo}route add -net 224.0.0.0 netmask 240.0.0.0 dev {loopback_interface}" ) - elif sys.platform == "darwin": # macOS + elif system == "Darwin": # macOS loopback_interface = "lo0" # Check if multicast route exists try: @@ -90,7 +94,7 @@ def check_multicast() -> list[str]: else: # For other systems, skip multicast configuration - logger.warning(f"Multicast configuration not supported on {sys.platform}") + logger.warning(f"Multicast configuration not supported on {system}") return commands_needed @@ -105,8 +109,9 @@ def check_buffers() -> tuple[list[str], int | None]: current_max = None sudo = "" if check_root() else "sudo " + system = platform.system() - if sys.platform == "linux": + if system == "Linux": # Linux buffer configuration try: result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) @@ -130,7 +135,7 @@ def check_buffers() -> tuple[list[str], int | None]: except: commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") - elif sys.platform == "darwin": # macOS + elif system == "Darwin": # macOS # macOS buffer configuration - check and set UDP buffer related sysctls try: result = subprocess.run( @@ -170,7 +175,7 @@ def check_buffers() -> tuple[list[str], int | None]: else: # For other systems, skip buffer configuration - logger.warning(f"Buffer configuration not supported on {sys.platform}") + logger.warning(f"Buffer configuration not supported on {system}") return commands_needed, current_max @@ -218,6 +223,8 @@ def autoconf() -> None: logger.info("CI environment detected: Skipping automatic system configuration.") return + system = platform.system() + commands_needed = [] # Check multicast configuration @@ -268,7 +275,7 @@ class LCMConfig: lcm: lcm.LCM | None = None def __post_init__(self): - if self.url is None and sys.platform == "darwin": + if self.url is None and platform.system() == "Darwin": # On macOS, use multicast with TTL=0 to keep traffic local self.url = "udpm://239.255.76.67:7667?ttl=0" diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py index accc90be67..7b7038e16e 100644 --- a/dimos/protocol/service/test_lcmservice.py +++ b/dimos/protocol/service/test_lcmservice.py @@ -34,7 +34,7 @@ def get_sudo_prefix() -> str: def test_check_multicast_all_configured() -> None: """Test check_multicast when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock successful checks with realistic output format mock_run.side_effect = [ @@ -57,7 +57,7 @@ def test_check_multicast_all_configured() -> None: def test_check_multicast_missing_multicast_flag() -> None: """Test check_multicast when loopback interface lacks multicast.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock interface without MULTICAST flag (realistic current system state) mock_run.side_effect = [ @@ -81,7 +81,7 @@ def test_check_multicast_missing_multicast_flag() -> None: def test_check_multicast_missing_route() -> None: """Test check_multicast when multicast route is missing.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock missing route - interface has multicast but no route mock_run.side_effect = [ @@ -105,7 +105,7 @@ def test_check_multicast_missing_route() -> None: def test_check_multicast_all_missing() -> None: """Test check_multicast when both multicast flag and route are missing (current system state).""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock both missing - matches actual current system state mock_run.side_effect = [ @@ -133,7 +133,7 @@ def test_check_multicast_all_missing() -> None: def test_check_multicast_subprocess_exception() -> None: """Test check_multicast when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock subprocess exceptions mock_run.side_effect = Exception("Command failed") @@ -149,7 +149,7 @@ def test_check_multicast_subprocess_exception() -> None: def test_check_multicast_macos() -> None: """Test check_multicast on macOS when configuration is needed.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="darwin"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock netstat -nr to not contain the multicast route mock_run.side_effect = [ @@ -171,7 +171,7 @@ def test_check_multicast_macos() -> None: def test_check_buffers_all_configured() -> None: """Test check_buffers when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock sufficient buffer sizes mock_run.side_effect = [ @@ -190,7 +190,7 @@ def test_check_buffers_all_configured() -> None: def test_check_buffers_low_max_buffer() -> None: """Test check_buffers when rmem_max is too low.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock low rmem_max mock_run.side_effect = [ @@ -210,7 +210,7 @@ def test_check_buffers_low_max_buffer() -> None: def test_check_buffers_low_default_buffer() -> None: """Test check_buffers when rmem_default is too low.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock low rmem_default mock_run.side_effect = [ @@ -230,7 +230,7 @@ def test_check_buffers_low_default_buffer() -> None: def test_check_buffers_both_low() -> None: """Test check_buffers when both buffer sizes are too low.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock both low mock_run.side_effect = [ @@ -254,7 +254,7 @@ def test_check_buffers_both_low() -> None: def test_check_buffers_subprocess_exception() -> None: """Test check_buffers when subprocess calls fail.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock subprocess exceptions mock_run.side_effect = Exception("Command failed") @@ -271,7 +271,7 @@ def test_check_buffers_subprocess_exception() -> None: def test_check_buffers_parsing_error() -> None: """Test check_buffers when output parsing fails.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock malformed output mock_run.side_effect = [ @@ -291,7 +291,7 @@ def test_check_buffers_parsing_error() -> None: def test_check_buffers_dev_container() -> None: """Test check_buffers in dev container where sysctl fails.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock dev container behavior - sysctl returns non-zero mock_run.side_effect = [ @@ -325,7 +325,7 @@ def test_check_buffers_dev_container() -> None: def test_check_buffers_macos_all_configured() -> None: """Test check_buffers on macOS when system is properly configured.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="darwin"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock sufficient buffer sizes for macOS mock_run.side_effect = [ @@ -347,7 +347,7 @@ def test_check_buffers_macos_all_configured() -> None: def test_check_buffers_macos_needs_config() -> None: """Test check_buffers on macOS when configuration is needed.""" - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="darwin"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock low buffer sizes for macOS mock_run.side_effect = [ @@ -377,7 +377,7 @@ def test_autoconf_no_config_needed() -> None: """Test autoconf when no configuration is needed.""" # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock all checks passing mock_run.side_effect = [ @@ -418,7 +418,7 @@ def test_autoconf_with_config_needed_success() -> None: """Test autoconf when configuration is needed and commands succeed.""" # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock checks failing, then mock the execution succeeding mock_run.side_effect = [ @@ -480,7 +480,7 @@ def test_autoconf_with_command_failures() -> None: """Test autoconf when some commands fail.""" # Clear CI environment variable for this test with patch.dict(os.environ, {"CI": ""}, clear=False): - with patch("dimos.protocol.service.lcmservice.sys.platform", return_value="linux"): + with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): with patch("dimos.protocol.service.lcmservice.subprocess.run") as mock_run: # Mock checks failing, then mock some commands failing mock_run.side_effect = [ From c02755418dd9e8208fe5e75a1624e58b2e3a78cb Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 19:12:48 -0600 Subject: [PATCH 30/45] add example command --- flake.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 245a920dc6..b5e890b942 100644 --- a/flake.nix +++ b/flake.nix @@ -226,7 +226,7 @@ pip install foxglove-websocket numpy # remove dimos-lcm from pyproject.toml for a moment grep -v '^\s*#' pyproject.original.toml | grep -v "dimos-lcm @ .*" > pyproject.toml - pip install -e .[cpu,dev] 2>&1 | grep -v -E "Could not find a version that satisfies the requirement lcm |ERROR: No matching distribution found for lcm" + pip install -e .[cpu,dev,sim] 2>&1 | grep -v -E "Could not find a version that satisfies the requirement lcm |ERROR: No matching distribution found for lcm" # restore pyproject.toml rm -f pyproject.toml mv pyproject.original.toml pyproject.toml @@ -249,6 +249,9 @@ python -m pytest -s "$PROJECT_ROOT/dimos/" echo "tests finished" fi + + echo "here's the main command to run:" + echo CONNECTION_TYPE=replay python dimos/robot/unitree_webrtc/unitree_go2.py fi fi ''; From 5d56bb672a228cd9be1d269ed9062e7b58d3ab86 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 19:25:15 -0600 Subject: [PATCH 31/45] add saftey check --- flake.nix | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/flake.nix b/flake.nix index b5e890b942..43464d7a7c 100644 --- a/flake.nix +++ b/flake.nix @@ -152,7 +152,26 @@ # python & setup # if [ -f "$PROJECT_ROOT/venv/bin/activate" ]; then + # if there is a venv, load it + _nix_python_path="$(realpath "$(which python)")" . "$PROJECT_ROOT/venv/bin/activate" + # check the venv to make sure it wasn't created with a different (non nix) python + if [ "$_nix_python_path" != "$(realpath "$(which python)")" ] + then + echo + echo + echo "WARNING:" + echo " Your venv was created with something other than the current nix python" + echo " This could happen if you made the venv before doing `nix develop`" + echo " It could also happen if the nix-python was updated but the venv wasn't" + echo " WHAT YOU NEED TO DO:" + echo " - If you're about to make/test a PR, delete/rename your venv and run `nix develop` again" + echo " - If you're just trying to get the code working, you can continue but you might get bugs FYI" + echo + echo + echo "Got it? (press enter)"; read _ + echo + fi else # # automate the readme From cf2c844d8cd22eb2a52c3da96cbba3bde3ad506b Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Wed, 19 Nov 2025 19:26:42 -0600 Subject: [PATCH 32/45] - --- README.md | 2 +- flake.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 24d4d88c6c..9c4d7bf732 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix # clone the repository git clone --branch dev --single-branch https://github.com/dimensionalOS/dimos.git -# setup the environment (follow the prompts) +# setup the environment (follow the prompts after nix develop) cd dimos nix develop diff --git a/flake.nix b/flake.nix index 43464d7a7c..296eb5468a 100644 --- a/flake.nix +++ b/flake.nix @@ -97,7 +97,7 @@ let # 1. fix pkg-config on darwin pkgConfPackages = aggregation.getAll { hasAllFlags=[ "packageConfGroup" ]; attrPath=[ "pkg" ]; }; - packageConfPackagesString = lib.print { prefix="packageConfPackagesString"; } (aggregation.getAll { + packageConfPackagesString = (aggregation.getAll { hasAllFlags=[ "packageConfGroup" ]; attrPath=[ "pkg" ]; strAppend="/lib/pkgconfig"; From bd86cb0cd6628e9b76422a41804f4c2f6aebc216 Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Tue, 25 Nov 2025 06:44:50 +0200 Subject: [PATCH 33/45] allow for 224.0.0/4 --- dimos/protocol/service/lcmservice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index b3c711d0cd..2d921f7edc 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -83,7 +83,8 @@ def check_multicast() -> list[str]: # Check if multicast route exists try: result = subprocess.run(["netstat", "-nr"], capture_output=True, text=True) - if "224.0.0.0/4" not in result.stdout: + route_exists = "224.0.0.0/4" in result.stdout or "224.0.0/4" in result.stdout + if not route_exists: commands_needed.append( f"{sudo}route add -net 224.0.0.0/4 -interface {loopback_interface}" ) From d86feb70fb9cca8569071dc811fa1dd1c9ac5745 Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Wed, 26 Nov 2025 05:56:00 +0200 Subject: [PATCH 34/45] always use shm for large things --- .../unitree_webrtc/unitree_go2_blueprints.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py index 8973f6cd68..2e8d309f49 100644 --- a/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py +++ b/dimos/robot/unitree_webrtc/unitree_go2_blueprints.py @@ -52,14 +52,20 @@ behavior_tree_navigator(), wavefront_frontier_explorer(), websocket_vis(), - foxglove_bridge(), + foxglove_bridge( + shm_channels=[ + "/go2/color_image#sensor_msgs.Image", + ] + ), ) .global_config(n_dask_workers=4, robot_model="unitree_go2") .transports( # These are kept the same so that we don't have to change foxglove configs. # Although we probably should. { - ("color_image", Image): LCMTransport("/go2/color_image", Image), + ("color_image", Image): pSHMTransport( + "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ), ("camera_pose", PoseStamped): LCMTransport("/go2/camera_pose", PoseStamped), ("camera_info", CameraInfo): LCMTransport("/go2/camera_info", CameraInfo), } @@ -73,21 +79,6 @@ utilization(), ).global_config(n_dask_workers=8) -standard_with_shm = autoconnect( - standard.transports( - { - ("color_image", Image): pSHMTransport( - "/go2/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ), - } - ), - foxglove_bridge( - shm_channels=[ - "/go2/color_image#sensor_msgs.Image", - ] - ), -) - standard_with_jpeglcm = standard.transports( { ("color_image", Image): JpegLcmTransport("/go2/color_image", Image), From 43e37aac28fd4e3660629b5c1cbe9ebfca0b821f Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Sat, 29 Nov 2025 02:51:15 +0200 Subject: [PATCH 35/45] dry --- dimos/protocol/service/lcmservice.py | 76 +++++++--------------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index bfeea5f06b..1cfdb933b3 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -100,13 +100,24 @@ def check_multicast() -> list[str]: return commands_needed +def _set_net_value(commands_needed: list[str], sudo: str, name: str, value: int) -> None: + try: + result = subprocess.run(["sysctl", name], capture_output=True, text=True) + current_max = ( + int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None + ) + if not current_max or current_max < value: + commands_needed.append(f"{sudo}sysctl -w {name}={value}") + except: + commands_needed.append(f"{sudo}sysctl -w {name}={value}") + def check_buffers() -> tuple[list[str], int | None]: """Check if buffer configuration is needed and return required commands and current size. Returns: Tuple of (commands_needed, current_max_buffer_size) """ - commands_needed = [] + commands_needed: list[str] = [] current_max = None sudo = "" if check_root() else "sudo " @@ -114,66 +125,13 @@ def check_buffers() -> tuple[list[str], int | None]: if system == "Linux": # Linux buffer configuration - try: - result = subprocess.run(["sysctl", "net.core.rmem_max"], capture_output=True, text=True) - current_max = ( - int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None - ) - if not current_max or current_max < 2097152: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") - except: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_max=2097152") - - try: - result = subprocess.run( - ["sysctl", "net.core.rmem_default"], capture_output=True, text=True - ) - current_default = ( - int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None - ) - if not current_default or current_default < 2097152: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") - except: - commands_needed.append(f"{sudo}sysctl -w net.core.rmem_default=2097152") - + _set_net_value(commands_needed, sudo, "net.core.rmem_max", 2097152) + _set_net_value(commands_needed, sudo, "net.core.rmem_default", 2097152) elif system == "Darwin": # macOS # macOS buffer configuration - check and set UDP buffer related sysctls - try: - result = subprocess.run( - ["sysctl", "kern.ipc.maxsockbuf"], capture_output=True, text=True - ) - current_max = ( - int(result.stdout.split(":")[1].strip()) if result.returncode == 0 else None - ) - if not current_max or current_max < 8388608: - commands_needed.append(f"{sudo}sysctl -w kern.ipc.maxsockbuf=8388608") - except: - commands_needed.append(f"{sudo}sysctl -w kern.ipc.maxsockbuf=8388608") - - try: - result = subprocess.run( - ["sysctl", "net.inet.udp.recvspace"], capture_output=True, text=True - ) - current_recvspace = ( - int(result.stdout.split(":")[1].strip()) if result.returncode == 0 else None - ) - if not current_recvspace or current_recvspace < 2097152: - commands_needed.append(f"{sudo}sysctl -w net.inet.udp.recvspace=2097152") - except: - commands_needed.append(f"{sudo}sysctl -w net.inet.udp.recvspace=2097152") - - try: - result = subprocess.run( - ["sysctl", "net.inet.udp.maxdgram"], capture_output=True, text=True - ) - current_maxdgram = ( - int(result.stdout.split(":")[1].strip()) if result.returncode == 0 else None - ) - if not current_maxdgram or current_maxdgram < 65535: - commands_needed.append(f"{sudo}sysctl -w net.inet.udp.maxdgram=65535") - except: - commands_needed.append(f"{sudo}sysctl -w net.inet.udp.maxdgram=65535") - + _set_net_value(commands_needed, sudo, "kern.ipc.maxsockbuf", 8388608) + _set_net_value(commands_needed, sudo, "net.inet.udp.recvspace", 2097152) + _set_net_value(commands_needed, sudo, "net.inet.udp.maxdgram", 65535) else: # For other systems, skip buffer configuration logger.warning(f"Buffer configuration not supported on {system}") From 993d98af3b3d7031c6008e06871d2894b5180664 Mon Sep 17 00:00:00 2001 From: paul-nechifor <1262969+paul-nechifor@users.noreply.github.com> Date: Sat, 29 Nov 2025 00:52:02 +0000 Subject: [PATCH 36/45] CI code cleanup --- dimos/protocol/service/lcmservice.py | 5 ++--- dimos/robot/unitree_webrtc/mujoco_connection.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 1cfdb933b3..36c21d20d6 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -103,14 +103,13 @@ def check_multicast() -> list[str]: def _set_net_value(commands_needed: list[str], sudo: str, name: str, value: int) -> None: try: result = subprocess.run(["sysctl", name], capture_output=True, text=True) - current_max = ( - int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None - ) + current_max = int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None if not current_max or current_max < value: commands_needed.append(f"{sudo}sysctl -w {name}={value}") except: commands_needed.append(f"{sudo}sysctl -w {name}={value}") + def check_buffers() -> tuple[list[str], int | None]: """Check if buffer configuration is needed and return required commands and current size. diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py index 26d9e1f06b..dd8c47ca59 100644 --- a/dimos/robot/unitree_webrtc/mujoco_connection.py +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -97,7 +97,9 @@ def start(self) -> None: def get_stderr(): text = "" if self.process: - text = "\n" + self.process.stderr.read().replace("\n", "\n[mujoco_process.py] ") + "\n" + text = ( + "\n" + self.process.stderr.read().replace("\n", "\n[mujoco_process.py] ") + "\n" + ) return text # Wait for process to be ready @@ -126,7 +128,7 @@ def stop(self) -> None: return self._is_cleaned_up = True - + # clean up open file descriptors self.process.stderr.close() self.process.stdout.close() From 3e4d8d0f5c89c563868d35617b4e28167c020a94 Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Sat, 29 Nov 2025 03:00:19 +0200 Subject: [PATCH 37/45] Update dimos/agents/memory/chroma_impl.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- dimos/agents/memory/chroma_impl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dimos/agents/memory/chroma_impl.py b/dimos/agents/memory/chroma_impl.py index 74a98dc17a..96cc32aa54 100644 --- a/dimos/agents/memory/chroma_impl.py +++ b/dimos/agents/memory/chroma_impl.py @@ -155,8 +155,8 @@ def create(self) -> None: else: self.device = "cpu" - print(f"Using device: {device}") - self.model = SentenceTransformer(self.model_name, device=device) # type: ignore[name-defined] + print(f"Using device: {self.device}") + self.model = SentenceTransformer(self.model_name, device=self.device) # type: ignore[name-defined] # Create a custom embedding class that implements the embed_query method class SentenceTransformerEmbeddings: From b7fc9a9ca6af64fadef994f8bb7d2e9ba604cc4e Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Sat, 29 Nov 2025 02:56:02 +0200 Subject: [PATCH 38/45] types --- dimos/perception/segmentation/sam_2d_seg.py | 1 - dimos/protocol/service/lcmservice.py | 2 +- dimos/protocol/service/test_lcmservice.py | 1 - dimos/robot/unitree_webrtc/mujoco_connection.py | 11 +++++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dimos/perception/segmentation/sam_2d_seg.py b/dimos/perception/segmentation/sam_2d_seg.py index 51819f8d21..d613bb43d6 100644 --- a/dimos/perception/segmentation/sam_2d_seg.py +++ b/dimos/perception/segmentation/sam_2d_seg.py @@ -31,7 +31,6 @@ plot_results, ) from dimos.utils.data import get_data -from dimos.utils.gpu_utils import is_cuda_available from dimos.utils.logging_config import setup_logger logger = setup_logger("dimos.perception.segmentation.sam_2d_seg") diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 36c21d20d6..35ed89a527 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -232,7 +232,7 @@ class LCMConfig: autoconf: bool = True lcm: lcm.LCM | None = None - def __post_init__(self): + def __post_init__(self) -> None: if self.url is None and platform.system() == "Darwin": # On macOS, use multicast with TTL=0 to keep traffic local self.url = "udpm://239.255.76.67:7667?ttl=0" diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py index 7b7038e16e..f5704c1c73 100644 --- a/dimos/protocol/service/test_lcmservice.py +++ b/dimos/protocol/service/test_lcmservice.py @@ -14,7 +14,6 @@ import os import subprocess -import sys from unittest.mock import patch import pytest diff --git a/dimos/robot/unitree_webrtc/mujoco_connection.py b/dimos/robot/unitree_webrtc/mujoco_connection.py index dd8c47ca59..0bf42bb54b 100644 --- a/dimos/robot/unitree_webrtc/mujoco_connection.py +++ b/dimos/robot/unitree_webrtc/mujoco_connection.py @@ -94,9 +94,9 @@ def start(self) -> None: self.shm_data.cleanup() raise RuntimeError(f"Failed to start MuJoCo subprocess: {e}") from e - def get_stderr(): + def get_stderr() -> str: text = "" - if self.process: + if self.process and self.process.stderr: text = ( "\n" + self.process.stderr.read().replace("\n", "\n[mujoco_process.py] ") + "\n" ) @@ -130,8 +130,11 @@ def stop(self) -> None: self._is_cleaned_up = True # clean up open file descriptors - self.process.stderr.close() - self.process.stdout.close() + if self.process: + if self.process.stderr: + self.process.stderr.close() + if self.process.stdout: + self.process.stdout.close() # Cancel any pending timers if self._stop_timer: From 970a31d2f91f19ff4b68df0c7b493792ddda0854 Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Sat, 29 Nov 2025 03:38:26 +0200 Subject: [PATCH 39/45] fix tests --- dimos/protocol/service/lcmservice.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index 35ed89a527..90fe75cfca 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -100,14 +100,19 @@ def check_multicast() -> list[str]: return commands_needed -def _set_net_value(commands_needed: list[str], sudo: str, name: str, value: int) -> None: +def _set_net_value(commands_needed: list[str], sudo: str, name: str, value: int) -> int | None: try: result = subprocess.run(["sysctl", name], capture_output=True, text=True) - current_max = int(result.stdout.split("=")[1].strip()) if result.returncode == 0 else None - if not current_max or current_max < value: + if result.returncode == 0: + current = int(result.stdout.replace(":", "=").split("=")[1].strip()) + else: + current = None + if not current or current < value: commands_needed.append(f"{sudo}sysctl -w {name}={value}") + return current except: commands_needed.append(f"{sudo}sysctl -w {name}={value}") + return None def check_buffers() -> tuple[list[str], int | None]: @@ -124,11 +129,11 @@ def check_buffers() -> tuple[list[str], int | None]: if system == "Linux": # Linux buffer configuration - _set_net_value(commands_needed, sudo, "net.core.rmem_max", 2097152) + current_max = _set_net_value(commands_needed, sudo, "net.core.rmem_max", 2097152) _set_net_value(commands_needed, sudo, "net.core.rmem_default", 2097152) elif system == "Darwin": # macOS # macOS buffer configuration - check and set UDP buffer related sysctls - _set_net_value(commands_needed, sudo, "kern.ipc.maxsockbuf", 8388608) + current_max = _set_net_value(commands_needed, sudo, "kern.ipc.maxsockbuf", 8388608) _set_net_value(commands_needed, sudo, "net.inet.udp.recvspace", 2097152) _set_net_value(commands_needed, sudo, "net.inet.udp.maxdgram", 65535) else: From de5f0a646622899ec83cafacf493379681d44aa1 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 29 Nov 2025 11:41:58 -0600 Subject: [PATCH 40/45] fix "device not defined" --- dimos/agents/memory/chroma_impl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dimos/agents/memory/chroma_impl.py b/dimos/agents/memory/chroma_impl.py index 8f74ede41d..02cc8e0876 100644 --- a/dimos/agents/memory/chroma_impl.py +++ b/dimos/agents/memory/chroma_impl.py @@ -148,12 +148,12 @@ def create(self) -> None: # Use GPU if available, otherwise fall back to CPU if torch.cuda.is_available(): - self.device = "cuda" + device = "cuda" # MacOS Metal performance shaders elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): - self.device = "mps" + device = "mps" else: - self.device = "cpu" + device = "cpu" print(f"Using device: {device}") self.model = SentenceTransformer(self.model_name, device=device) From a6edb70ed865432abd5a9c8fea2addf0dd1c4d84 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 29 Nov 2025 11:42:56 -0600 Subject: [PATCH 41/45] cleanup logic --- dimos/protocol/service/lcmservice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py index b3c711d0cd..2f88288cfc 100644 --- a/dimos/protocol/service/lcmservice.py +++ b/dimos/protocol/service/lcmservice.py @@ -318,7 +318,6 @@ def __init__(self, **kwargs) -> None: super().__init__(**kwargs) # we support passing an existing LCM instance - self.l = self.config.lcm if self.config.lcm: self.l = self.config.lcm else: From efb1db9ba92c9da3110209e266a31a4f7f54607b Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Thu, 4 Dec 2025 02:53:54 +0200 Subject: [PATCH 42/45] disable bitsandbytes on macos --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4b80d6c6f6..9273a4837b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ "langchain-text-splitters>=0.3.11,<1", "langchain-huggingface>=0.3.1,<1", "langchain-ollama>=0.3.10,<1", - "bitsandbytes>=0.48.2,<1.0", + "bitsandbytes>=0.48.2,<1.0; sys_platform == 'linux'", "ollama>=0.6.0", # Class Extraction From 040a7062e60ffc62fc751a000ad2d76b9d576931 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Tue, 9 Dec 2025 12:33:46 -0600 Subject: [PATCH 43/45] fixup readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index deb708bdf2..e859c9924c 100644 --- a/README.md +++ b/README.md @@ -115,27 +115,27 @@ pytest -s dimos/ #### Test Dimensional with a replay UnitreeGo2 stream (no robot required) ```bash -CONNECTION_TYPE=replay python dimos/robot/unitree_webrtc/unitree_go2.py +dimos --replay run unitree-go2 ``` #### Test Dimensional with a simulated UnitreeGo2 in MuJoCo (no robot required) ```bash pip install -e '.[sim]' export DISPLAY=:1 # Or DISPLAY=:0 if getting GLFW/OpenGL X11 errors -CONNECTION_TYPE=mujoco python dimos/robot/unitree_webrtc/unitree_go2.py +dimos --simulation run unitree-go2 ``` #### Test Dimensional with a real UnitreeGo2 over WebRTC ```bash export ROBOT_IP=192.168.X.XXX # Add the robot IP address -python dimos/robot/unitree_webrtc/unitree_go2.py +dimos run unitree-go2 ``` #### Test Dimensional with a real UnitreeGo2 running Agents *OpenAI / Alibaba keys required* ```bash export ROBOT_IP=192.168.X.XXX # Add the robot IP address -python dimos/robot/unitree_webrtc/run_agents2.py +dimos run unitree-go2-agentic ``` --- From f1862e85b7564eb932dcbddffe542770413fc404 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Tue, 9 Dec 2025 16:44:01 -0600 Subject: [PATCH 44/45] update xome to support --ignore-environment --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 32fdb4f182..1934527896 100644 --- a/flake.lock +++ b/flake.lock @@ -141,11 +141,11 @@ ] }, "locked": { - "lastModified": 1764452841, - "narHash": "sha256-uSMtf+3G904j4FGYUlTgPNPboh62SO44kSIxQGEWofA=", + "lastModified": 1765320026, + "narHash": "sha256-yPvFElT1PG4ENioIn+ctf682E8y6jPodeosM8One680=", "owner": "jeff-hykin", "repo": "xome", - "rev": "390c8f2060e385c75cd1ea1dcb8950379a1ab1fa", + "rev": "bae4f441d3e1ebe3b2da51a3ecdbdccddda67444", "type": "github" }, "original": { From 73b98878b8759e2d5264b8e6f52f0a464bc5aae0 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Tue, 9 Dec 2025 16:50:22 -0600 Subject: [PATCH 45/45] fix macos old version check on linux --- flake.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index ce070a8340..78c21c0710 100644 --- a/flake.nix +++ b/flake.nix @@ -160,7 +160,7 @@ devShell = (xome.simpleMakeHomeFor { inherit pkgs; pure = true; - commandPassthrough = [ "sudo" "nvim" "code" "sysctl" "sw_vers" ]; # e.g. use external nvim instead of nix's + commandPassthrough = [ "sudo" "nvim" "code" "sysctl" "sw_vers" "git" "vim" "emacs" ]; # e.g. use external nvim instead of nix's # commonly needed for MacOS: [ "osascript" "otool" "hidutil" "logger" "codesign" ] homeSubpathPassthrough = [ "cache/nix/" ]; # share nix cache between projects homeModule = { @@ -257,7 +257,7 @@ return 1 # failure } - macos_version="$(sw_vers -productVersion)" + macos_version="$(sw_vers -productVersion 2>/dev/null || echo "0.0")" macos_major_version="''${macos_version%%.*}" if confirm_ask "Would you like me to set up the environment for you? [y/n]"; then echo "Making sure git lfs is installed..." @@ -288,7 +288,7 @@ pip install -e . # if really old MacOS then ignore the lcm dependency (it'll be supplied by nix) - if [ "$macos_major_version" -le 13 ]; then + if [ "$(uname)" = "Darwin" ] && [ "$macos_major_version" -le 13 ]; then echo "You're on a really old MacOS version. Ignore the errors above (and probably later below) about LCM" echo "Got it? (press enter)";read _ rm -f pyproject.original.toml