diff --git a/data/.lfs/office_building_1.tar.gz b/data/.lfs/office_building_1.tar.gz new file mode 100644 index 0000000000..0dc013bd94 --- /dev/null +++ b/data/.lfs/office_building_1.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70aac31ca76597b3eee1ddfcbe2ba71d432fd427176f66d8281d75da76641f49 +size 1061581652 diff --git a/dimos/navigation/demo_ros_navigation.py b/dimos/navigation/demo_ros_navigation.py new file mode 100644 index 0000000000..fac19e1b24 --- /dev/null +++ b/dimos/navigation/demo_ros_navigation.py @@ -0,0 +1,72 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import rclpy + +from dimos import core +from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Twist, Vector3 +from dimos.msgs.nav_msgs import Path +from dimos.msgs.sensor_msgs import PointCloud2 +from dimos.navigation.rosnav import ROSNav +from dimos.protocol import pubsub +from dimos.utils.logging_config import setup_logger + +logger = setup_logger(__file__) + + +def main() -> None: + pubsub.lcm.autoconf() + dimos = core.start(2) + + ros_nav = dimos.deploy(ROSNav) + + ros_nav.goal_req.transport = core.LCMTransport("/goal", PoseStamped) + ros_nav.pointcloud.transport = core.LCMTransport("/pointcloud_map", PointCloud2) + ros_nav.global_pointcloud.transport = core.LCMTransport("/global_pointcloud", PointCloud2) + ros_nav.goal_active.transport = core.LCMTransport("/goal_active", PoseStamped) + ros_nav.path_active.transport = core.LCMTransport("/path_active", Path) + ros_nav.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) + + ros_nav.start() + + logger.info("\nTesting navigation in 2 seconds...") + time.sleep(2) + + test_pose = PoseStamped( + ts=time.time(), + frame_id="map", + position=Vector3(2.0, 2.0, 0.0), + orientation=Quaternion(0.0, 0.0, 0.0, 1.0), + ) + + logger.info("Sending navigation goal to: (2.0, 2.0, 0.0)") + success = ros_nav.navigate_to(test_pose, timeout=30.0) + logger.info(f"Navigated successfully: {success}") + + try: + logger.info("\nNavBot running. Press Ctrl+C to stop.") + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("\nShutting down...") + ros_nav.stop() + + if rclpy.ok(): + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/dimos/navigation/rosnav.py b/dimos/navigation/rosnav.py index f0d04926d3..34ed27fb6e 100644 --- a/dimos/navigation/rosnav.py +++ b/dimos/navigation/rosnav.py @@ -19,7 +19,7 @@ """ from collections.abc import Generator -from dataclasses import dataclass +from dataclasses import dataclass, field import logging import threading import time @@ -64,8 +64,8 @@ class Config(ModuleConfig): local_pointcloud_freq: float = 2.0 global_pointcloud_freq: float = 1.0 - sensor_to_base_link_transform: Transform = Transform( - frame_id="sensor", child_frame_id="base_link" + sensor_to_base_link_transform: Transform = field( + default_factory=lambda: Transform(frame_id="sensor", child_frame_id="base_link") ) diff --git a/dimos/protocol/rpc/spec.py b/dimos/protocol/rpc/spec.py index 283b84f1dd..ee9f99e16b 100644 --- a/dimos/protocol/rpc/spec.py +++ b/dimos/protocol/rpc/spec.py @@ -44,7 +44,7 @@ def call(self, name: str, arguments: Args, cb: Callable | None) -> Callable[[], # we expect to crash if we don't get a return value after 10 seconds # but callers can override this timeout for extra long functions def call_sync( - self, name: str, arguments: Args, rpc_timeout: float | None = 30.0 + self, name: str, arguments: Args, rpc_timeout: float | None = 120.0 ) -> tuple[Any, Callable[[], None]]: event = threading.Event() diff --git a/docker/navigation/.env.hardware b/docker/navigation/.env.hardware new file mode 100644 index 0000000000..2d8866b179 --- /dev/null +++ b/docker/navigation/.env.hardware @@ -0,0 +1,59 @@ +# Hardware Configuration Environment Variables +# Copy this file to .env and customize for your hardware setup + +# ============================================ +# NVIDIA GPU Support +# ============================================ +# Set the Docker runtime to nvidia for GPU support (it's runc by default) +#DOCKER_RUNTIME=nvidia + +# ============================================ +# ROS Configuration +# ============================================ +# ROS domain ID for multi-robot setups +ROS_DOMAIN_ID=42 + +# Robot configuration ('mechanum_drive', 'unitree/unitree_g1', 'unitree/unitree_g1', etc) +ROBOT_CONFIG_PATH=mechanum_drive + +# ============================================ +# Mid-360 Lidar Configuration +# ============================================ +# Network interface connected to the lidar (e.g., eth0, enp0s3) +# Find with: ip addr show +LIDAR_INTERFACE=eth0 + +# Processing computer IP address on the lidar subnet +# Must be on the same subnet as the lidar (e.g., 192.168.1.5) +# LIDAR_COMPUTER_IP=192.168.123.5 # FOR UNITREE G1 EDU +LIDAR_COMPUTER_IP=192.168.1.5 + +# Gateway IP address for the lidar subnet +# LIDAR_GATEWAY=192.168.123.1 # FOR UNITREE G1 EDU +LIDAR_GATEWAY=192.168.1.1 + +# Full IP address of your Mid-360 lidar +# This should match the IP configured on your lidar device +# Common patterns: 192.168.1.1XX or 192.168.123.1XX +# LIDAR_IP=192.168.123.120 # FOR UNITREE G1 EDU +LIDAR_IP=192.168.1.116 + +# ============================================ +# Motor Controller Configuration +# ============================================ +# Serial device for motor controller +# Check with: ls /dev/ttyACM* or ls /dev/ttyUSB* +MOTOR_SERIAL_DEVICE=/dev/ttyACM0 + +# ============================================ +# Network Communication (for base station) +# ============================================ +# Enable WiFi buffer optimization for data transmission +# Set to true if using wireless base station +ENABLE_WIFI_BUFFER=false + +# ============================================ +# Display Configuration +# ============================================ +# X11 display (usually auto-detected) +# DISPLAY=:0 diff --git a/docker/navigation/.gitignore b/docker/navigation/.gitignore new file mode 100644 index 0000000000..434f1e37bc --- /dev/null +++ b/docker/navigation/.gitignore @@ -0,0 +1,20 @@ +# Cloned repository +ros-navigation-autonomy-stack/ + +# Unity models (large binary files) +unity_models/ + +# ROS bag files +bagfiles/ + +# Config files (may contain local settings) +config/ + +# Docker volumes +.docker/ + +# Temporary files +*.tmp +*.log +*.swp +*~ \ No newline at end of file diff --git a/docker/navigation/Dockerfile b/docker/navigation/Dockerfile new file mode 100644 index 0000000000..aceb3f1726 --- /dev/null +++ b/docker/navigation/Dockerfile @@ -0,0 +1,223 @@ +# Base image with ROS Jazzy desktop full +FROM osrf/ros:jazzy-desktop-full + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive +ENV ROS_DISTRO=jazzy +ENV WORKSPACE=/ros2_ws +ENV DIMOS_PATH=/workspace/dimos + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + # ROS packages + ros-jazzy-pcl-ros \ + # Development tools + git \ + git-lfs \ + cmake \ + build-essential \ + python3-colcon-common-extensions \ + # PCL and system libraries + libpcl-dev \ + libgoogle-glog-dev \ + libgflags-dev \ + libatlas-base-dev \ + libeigen3-dev \ + libsuitesparse-dev \ + # X11 and GUI support for RVIZ + x11-apps \ + xorg \ + openbox \ + # Networking tools + iputils-ping \ + net-tools \ + iproute2 \ + ethtool \ + # USB and serial tools (for hardware support) + usbutils \ + udev \ + # Time synchronization (for multi-computer setup) + chrony \ + # Editor (optional but useful) + nano \ + vim \ + # Python tools + python3-pip \ + python3-setuptools \ + python3-venv \ + # Additional dependencies for dimos + ffmpeg \ + portaudio19-dev \ + libsndfile1 \ + # For OpenCV + libgl1 \ + libglib2.0-0 \ + # For Open3D + libgomp1 \ + # For TurboJPEG + libturbojpeg0-dev \ + # Clean up + && rm -rf /var/lib/apt/lists/* + +# Create workspace directory +RUN mkdir -p ${WORKSPACE}/src + +# Copy the autonomy stack repository (should be cloned by build.sh) +COPY docker/navigation/ros-navigation-autonomy-stack ${WORKSPACE}/src/ros-navigation-autonomy-stack + +# Set working directory +WORKDIR ${WORKSPACE} + +# Set up ROS environment +RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> ~/.bashrc + +# Build all hardware dependencies +RUN \ + # Build Livox-SDK2 for Mid-360 lidar + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/Livox-SDK2 && \ + mkdir -p build && cd build && \ + cmake .. && make -j$(nproc) && make install && ldconfig && \ + # Install Sophus + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/Sophus && \ + mkdir -p build && cd build && \ + cmake .. -DBUILD_TESTS=OFF && make -j$(nproc) && make install && \ + # Install Ceres Solver + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/ceres-solver && \ + mkdir -p build && cd build && \ + cmake .. && make -j$(nproc) && make install && \ + # Install GTSAM + cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/gtsam && \ + mkdir -p build && cd build && \ + cmake .. -DGTSAM_USE_SYSTEM_EIGEN=ON -DGTSAM_BUILD_WITH_MARCH_NATIVE=OFF && \ + make -j$(nproc) && make install && ldconfig + +# Build the autonomy stack +RUN /bin/bash -c "source /opt/ros/${ROS_DISTRO}/setup.bash && \ + cd ${WORKSPACE} && \ + colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release" + +# Source the workspace setup +RUN echo "source ${WORKSPACE}/install/setup.bash" >> ~/.bashrc + +# Create directory for Unity environment models +RUN mkdir -p ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity + +# Copy the dimos repository +RUN mkdir -p ${DIMOS_PATH} +COPY . ${DIMOS_PATH}/ + +# Create a virtual environment in /opt (not in /workspace/dimos) +# This ensures the venv won't be overwritten when we mount the host dimos directory +# The container will always use its own dependencies, independent of the host +RUN python3 -m venv /opt/dimos-venv + +# Activate Python virtual environment in interactive shells +RUN echo "source /opt/dimos-venv/bin/activate" >> ~/.bashrc + +# Install Python dependencies for dimos +WORKDIR ${DIMOS_PATH} +RUN /bin/bash -c "source /opt/dimos-venv/bin/activate && \ + pip install --upgrade pip setuptools wheel && \ + pip install -e .[cpu,dev] 'mmengine>=0.10.3' 'mmcv>=2.1.0'" + +# Copy helper scripts +COPY docker/navigation/run_both.sh /usr/local/bin/run_both.sh +COPY docker/navigation/ros_launch_wrapper.py /usr/local/bin/ros_launch_wrapper.py +RUN chmod +x /usr/local/bin/run_both.sh /usr/local/bin/ros_launch_wrapper.py + +# Set up udev rules for USB devices (motor controller) +RUN echo 'SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE="0666", GROUP="dialout"' > /etc/udev/rules.d/99-motor-controller.rules && \ + usermod -a -G dialout root || true + +# Set up entrypoint script +RUN echo '#!/bin/bash\n\ +set -e\n\ +\n\ +git config --global --add safe.directory /workspace/dimos\n\ +\n\ +# Source ROS setup\n\ +source /opt/ros/${ROS_DISTRO}/setup.bash\n\ +source ${WORKSPACE}/install/setup.bash\n\ +\n\ +# Activate Python virtual environment for dimos\n\ +source /opt/dimos-venv/bin/activate\n\ +\n\ +# Export ROBOT_CONFIG_PATH for autonomy stack\n\ +export ROBOT_CONFIG_PATH="${ROBOT_CONFIG_PATH:-mechanum_drive}"\n\ +\n\ +# Hardware-specific configurations\n\ +if [ "${HARDWARE_MODE}" = "true" ]; then\n\ + # Set network buffer sizes for WiFi data transmission (if needed)\n\ + if [ "${ENABLE_WIFI_BUFFER}" = "true" ]; then\n\ + sysctl -w net.core.rmem_max=67108864 net.core.rmem_default=67108864 2>/dev/null || true\n\ + sysctl -w net.core.wmem_max=67108864 net.core.wmem_default=67108864 2>/dev/null || true\n\ + fi\n\ + \n\ + # Configure network interface for Mid-360 lidar if specified\n\ + if [ -n "${LIDAR_INTERFACE}" ] && [ -n "${LIDAR_COMPUTER_IP}" ]; then\n\ + ip addr add ${LIDAR_COMPUTER_IP}/24 dev ${LIDAR_INTERFACE} 2>/dev/null || true\n\ + ip link set ${LIDAR_INTERFACE} up 2>/dev/null || true\n\ + if [ -n "${LIDAR_GATEWAY}" ]; then\n\ + ip route add default via ${LIDAR_GATEWAY} dev ${LIDAR_INTERFACE} 2>/dev/null || true\n\ + fi\n\ + fi\n\ + \n\ + # Generate MID360_config.json if LIDAR_COMPUTER_IP and LIDAR_IP are set\n\ + if [ -n "${LIDAR_COMPUTER_IP}" ] && [ -n "${LIDAR_IP}" ]; then\n\ + cat > ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/config/MID360_config.json < /ros_entrypoint.sh && \ + chmod +x /ros_entrypoint.sh + +# Set the entrypoint +ENTRYPOINT ["/ros_entrypoint.sh"] + +# Default command +CMD ["bash"] diff --git a/docker/navigation/README.md b/docker/navigation/README.md new file mode 100644 index 0000000000..f518cf7674 --- /dev/null +++ b/docker/navigation/README.md @@ -0,0 +1,155 @@ +# ROS Docker Integration for DimOS + +This directory contains Docker configuration files to run DimOS and the ROS autonomy stack in the same container, enabling communication between the two systems. + +## New Ubuntu Installation + +**For fresh Ubuntu systems**, use the automated setup script: + +```bash +curl -fsSL https://raw.githubusercontent.com/dimensionalOS/dimos/dimos-rosnav-docker/docker/navigation/setup.sh | bash +``` + +Or download and run locally: + +```bash +wget https://raw.githubusercontent.com/dimensionalOS/dimos/dimos-rosnav-docker/docker/navigation/setup.sh +chmod +x setup.sh +./setup.sh +``` + +**Installation time:** Approximately 20-30 minutes depending on your internet connection. + +**After installation, start the demo:** +```bash +cd ~/dimos/docker/navigation +./start.sh --all +``` + +**Options:** +```bash +./setup.sh --help # Show all options +./setup.sh --install-dir /opt/dimos # Custom installation directory +./setup.sh --skip-build # Skip Docker image build +``` + +If the automated script encounters issues, follow the manual setup below. + +## Prerequisites + +1. **Install Docker with `docker compose` support**. Follow the [official Docker installation guide](https://docs.docker.com/engine/install/). +2. **Install NVIDIA GPU drivers**. See [NVIDIA driver installation](https://www.nvidia.com/download/index.aspx). +3. **Install NVIDIA Container Toolkit**. Follow the [installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html). + +## Automated Quick Start + +This is an optimistic overview. Use the commands below for an in depth version. + +**Build the Docker image:** + +```bash +cd docker/navigation +./build.sh +``` + +This will: +- Clone the ros-navigation-autonomy-stack repository (jazzy branch) +- Build a Docker image with both ROS and DimOS dependencies +- Set up the environment for both systems + +Note that the build will take over 10 minutes and build an image over 30GiB. + +**Run the simulator to test it's working:** + +```bash +./start.sh --simulation +``` + +## Manual build + +Go to the docker dir and clone the ROS navigation stack. + +```bash +cd docker/navigation +git clone -b jazzy git@github.com:dimensionalOS/ros-navigation-autonomy-stack.git +``` + +Download a [Unity environment model for the Mecanum wheel platform](https://drive.google.com/drive/folders/1G1JYkccvoSlxyySuTlPfvmrWoJUO8oSs?usp=sharing) and unzip the files to `unity_models`. + +Alternativelly, extract `office_building_1` from LFS: + +```bash +tar -xf ../../data/.lfs/office_building_1.tar.gz +mv office_building_1 unity_models +``` + +Then, go back to the root and build the docker image: + +```bash +cd ../.. +docker compose -f docker/navigation/docker-compose.yml build +``` + +## On Real Hardware + +### Configure the WiFi + +[Read this](https://github.com/dimensionalOS/ros-navigation-autonomy-stack/tree/jazzy?tab=readme-ov-file#transmitting-data-over-wifi) to see how to configure the WiFi. + +### Configure the Livox Lidar + +The MID360_config.json file is automatically generated on container startup based on your environment variables (LIDAR_COMPUTER_IP and LIDAR_IP). + +### Copy Environment Template +```bash +cp .env.hardware .env +``` + +### Edit `.env` File + +Key configuration parameters: + +```bash +# Lidar Configuration +LIDAR_INTERFACE=eth0 # Your ethernet interface (find with: ip link show) +LIDAR_COMPUTER_IP=192.168.1.5 # Computer IP on the lidar subnet +LIDAR_GATEWAY=192.168.1.1 # Gateway IP address for the lidar subnet +LIDAR_IP=192.168.1.116 # Full IP address of your Mid-360 lidar + +# Motor Controller +MOTOR_SERIAL_DEVICE=/dev/ttyACM0 # Serial device (check with: ls /dev/ttyACM*) +``` + +### Start the Container + +Start the container and leave it open. + +```bash +./start.sh --hardware +``` + +It doesn't do anything by default. You have to run commands on it by `exec`-ing: + +```bash +docker exec -it dimos_hardware_container bash +``` + +### In the container + +In the container you can run any of the ROS or Python code. + +#### ROS + +```bash +cd /ros2_ws/src/ros-navigation-autonomy-stack +./system_real_robot_with_route_planner.sh +``` + +### Python + +Demo which moves the robot 2 meters forward and 2 meters left. + +```bash +source /opt/dimos-venv/bin/activate +python3 dimos/navigation/demo_ros_navigation.py +``` diff --git a/docker/navigation/build.sh b/docker/navigation/build.sh new file mode 100755 index 0000000000..da0aa2de8c --- /dev/null +++ b/docker/navigation/build.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}================================================${NC}" +echo -e "${GREEN}Building DimOS + ROS Autonomy Stack Docker Image${NC}" +echo -e "${GREEN}================================================${NC}" +echo "" + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +if [ ! -d "ros-navigation-autonomy-stack" ]; then + echo -e "${YELLOW}Cloning ros-navigation-autonomy-stack repository...${NC}" + git clone -b jazzy git@github.com:dimensionalOS/ros-navigation-autonomy-stack.git + echo -e "${GREEN}Repository cloned successfully!${NC}" +fi + +if [ ! -d "unity_models" ]; then + echo -e "${YELLOW}Using office_building_1 as the Unity environment...${NC}" + tar -xf ../../data/.lfs/office_building_1.tar.gz + mv office_building_1 unity_models +fi + +echo "" +echo -e "${YELLOW}Building Docker image with docker compose...${NC}" +echo "This will take a while as it needs to:" +echo " - Download base ROS Jazzy image" +echo " - Install ROS packages and dependencies" +echo " - Build the autonomy stack" +echo " - Build Livox-SDK2 for Mid-360 lidar" +echo " - Build SLAM dependencies (Sophus, Ceres, GTSAM)" +echo " - Install Python dependencies for DimOS" +echo "" + +cd ../.. + +docker compose -f docker/navigation/docker-compose.yml build + +echo "" +echo -e "${GREEN}================================${NC}" +echo -e "${GREEN}Docker image built successfully!${NC}" +echo -e "${GREEN}================================${NC}" +echo "" +echo "To run in SIMULATION mode:" +echo -e "${YELLOW} ./start.sh${NC}" +echo "" +echo "To run in HARDWARE mode:" +echo " 1. Configure your hardware settings in .env file" +echo " (copy from .env.hardware if needed)" +echo " 2. Run the hardware container:" +echo -e "${YELLOW} ./start.sh --hardware${NC}" +echo "" +echo "The script runs in foreground. Press Ctrl+C to stop." +echo "" diff --git a/docker/navigation/docker-compose.yml b/docker/navigation/docker-compose.yml new file mode 100644 index 0000000000..8678297e95 --- /dev/null +++ b/docker/navigation/docker-compose.yml @@ -0,0 +1,150 @@ +services: + # Simulation profile + dimos_simulation: + build: + context: ../.. + dockerfile: docker/navigation/Dockerfile + image: dimos_autonomy_stack:jazzy + container_name: dimos_simulation_container + profiles: ["", "simulation"] # Active by default (empty profile) AND with --profile simulation + + # Enable interactive terminal + stdin_open: true + tty: true + + # Network configuration - required for ROS communication + network_mode: host + + # Use nvidia runtime for GPU acceleration (falls back to runc if not available) + runtime: ${DOCKER_RUNTIME:-nvidia} + + # Environment variables for display and ROS + environment: + - DISPLAY=${DISPLAY} + - QT_X11_NO_MITSHM=1 + - NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:-all} + - NVIDIA_DRIVER_CAPABILITIES=${NVIDIA_DRIVER_CAPABILITIES:-all} + - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-42} + - ROBOT_CONFIG_PATH=${ROBOT_CONFIG_PATH:-mechanum_drive} + - HARDWARE_MODE=false + + # Volume mounts + volumes: + # X11 socket for GUI + - /tmp/.X11-unix:/tmp/.X11-unix:rw + - ${HOME}/.Xauthority:/root/.Xauthority:rw + + # Mount Unity environment models (if available) + - ./unity_models:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity:rw + + # Mount the autonomy stack source for development + - ./ros-navigation-autonomy-stack:/ros2_ws/src/ros-navigation-autonomy-stack:rw + + # Mount entire dimos directory for live development + - ../..:/workspace/dimos:rw + + # Mount bagfiles directory + - ./bagfiles:/ros2_ws/bagfiles:rw + + # Mount config files for easy editing + - ./config:/ros2_ws/config:rw + + # Device access (for joystick controllers) + devices: + - /dev/input:/dev/input + - /dev/dri:/dev/dri + + # Working directory + working_dir: /workspace/dimos + + # Command to run both ROS and DimOS + command: /usr/local/bin/run_both.sh + + # Hardware profile - for real robot + dimos_hardware: + build: + context: ../.. + dockerfile: docker/navigation/Dockerfile + image: dimos_autonomy_stack:jazzy + container_name: dimos_hardware_container + profiles: ["hardware"] + + # Enable interactive terminal + stdin_open: true + tty: true + + # Network configuration - MUST be host for hardware access + network_mode: host + + # Privileged mode REQUIRED for hardware access + privileged: true + + # Override runtime for GPU support + runtime: ${DOCKER_RUNTIME:-runc} + + # Hardware environment variables + environment: + - DISPLAY=${DISPLAY} + - QT_X11_NO_MITSHM=1 + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=all + - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-42} + - ROBOT_CONFIG_PATH=${ROBOT_CONFIG_PATH:-mechanum_drive} + - HARDWARE_MODE=true + # Mid-360 Lidar configuration + - LIDAR_INTERFACE=${LIDAR_INTERFACE:-} + - LIDAR_COMPUTER_IP=${LIDAR_COMPUTER_IP:-192.168.1.5} + - LIDAR_GATEWAY=${LIDAR_GATEWAY:-192.168.1.1} + - LIDAR_IP=${LIDAR_IP:-192.168.1.116} + # Motor controller + - MOTOR_SERIAL_DEVICE=${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0} + # Network optimization + - ENABLE_WIFI_BUFFER=true + + # Volume mounts + volumes: + # X11 socket for GUI + - /tmp/.X11-unix:/tmp/.X11-unix:rw + - ${HOME}/.Xauthority:/root/.Xauthority:rw + # Mount Unity environment models (optional for hardware) + - ./unity_models:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity:rw + # Mount the autonomy stack source + - ./ros-navigation-autonomy-stack:/ros2_ws/src/ros-navigation-autonomy-stack:rw + # Mount entire dimos directory + - ../..:/workspace/dimos:rw + # Mount bagfiles directory + - ./bagfiles:/ros2_ws/bagfiles:rw + # Mount config files for easy editing + - ./config:/ros2_ws/config:rw + # Hardware-specific volumes + - ./logs:/ros2_ws/logs:rw + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - /dev/bus/usb:/dev/bus/usb:rw + - /sys:/sys:ro + + # Device access for hardware + devices: + # Joystick controllers + - /dev/input:/dev/input + # GPU access + - /dev/dri:/dev/dri + # Motor controller serial ports + - ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}:${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0} + # Additional serial ports (can be enabled via environment) + # - /dev/ttyUSB0:/dev/ttyUSB0 + # - /dev/ttyUSB1:/dev/ttyUSB1 + # Cameras (can be enabled via environment) + # - /dev/video0:/dev/video0 + + # Working directory + working_dir: /workspace/dimos + + # Command - for hardware, we run bash as the user will launch specific scripts + command: bash + + # Capabilities for hardware operations + cap_add: + - NET_ADMIN # Network interface configuration + - SYS_ADMIN # System operations + - SYS_TIME # Time synchronization diff --git a/docker/navigation/ros_launch_wrapper.py b/docker/navigation/ros_launch_wrapper.py new file mode 100755 index 0000000000..c6458a99c9 --- /dev/null +++ b/docker/navigation/ros_launch_wrapper.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Wrapper script to properly handle ROS2 launch file shutdown. +This script ensures clean shutdown of all ROS nodes when receiving SIGINT. +""" + +import os +import signal +import subprocess +import sys +import time + + +class ROSLaunchWrapper: + def __init__(self): + self.ros_process = None + self.dimos_process = None + self.shutdown_in_progress = False + + def signal_handler(self, _signum, _frame): + """Handle shutdown signals gracefully""" + if self.shutdown_in_progress: + return + + self.shutdown_in_progress = True + print("\n\nShutdown signal received. Stopping services gracefully...") + + # Stop DimOS first + if self.dimos_process and self.dimos_process.poll() is None: + print("Stopping DimOS...") + self.dimos_process.terminate() + try: + self.dimos_process.wait(timeout=5) + print("DimOS stopped cleanly.") + except subprocess.TimeoutExpired: + print("Force stopping DimOS...") + self.dimos_process.kill() + self.dimos_process.wait() + + # Stop ROS - send SIGINT first for graceful shutdown + if self.ros_process and self.ros_process.poll() is None: + print("Stopping ROS nodes (this may take a moment)...") + + # Send SIGINT to trigger graceful ROS shutdown + self.ros_process.send_signal(signal.SIGINT) + + # Wait for graceful shutdown with timeout + try: + self.ros_process.wait(timeout=15) + print("ROS stopped cleanly.") + except subprocess.TimeoutExpired: + print("ROS is taking too long to stop. Sending SIGTERM...") + self.ros_process.terminate() + try: + self.ros_process.wait(timeout=5) + except subprocess.TimeoutExpired: + print("Force stopping ROS...") + self.ros_process.kill() + self.ros_process.wait() + + # Clean up any remaining processes + print("Cleaning up any remaining processes...") + cleanup_commands = [ + "pkill -f 'ros2' || true", + "pkill -f 'localPlanner' || true", + "pkill -f 'pathFollower' || true", + "pkill -f 'terrainAnalysis' || true", + "pkill -f 'sensorScanGeneration' || true", + "pkill -f 'vehicleSimulator' || true", + "pkill -f 'visualizationTools' || true", + "pkill -f 'far_planner' || true", + "pkill -f 'graph_decoder' || true", + ] + + for cmd in cleanup_commands: + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + print("All services stopped.") + sys.exit(0) + + def run(self): + # Register signal handlers + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + + print("Starting ROS route planner and DimOS...") + + # Change to the ROS workspace directory + os.chdir("/ros2_ws/src/ros-navigation-autonomy-stack") + + # Start ROS route planner + print("Starting ROS route planner...") + self.ros_process = subprocess.Popen( + ["bash", "./system_simulation_with_route_planner.sh"], + preexec_fn=os.setsid, # Create new process group + ) + + print("Waiting for ROS to initialize...") + time.sleep(5) + + print("Starting DimOS navigation bot...") + + nav_bot_path = "/workspace/dimos/dimos/navigation/demo_ros_navigation.py" + venv_python = "/opt/dimos-venv/bin/python" + + if not os.path.exists(nav_bot_path): + print(f"ERROR: demo_ros_navigation.py not found at {nav_bot_path}") + nav_dir = "/workspace/dimos/dimos/navigation/" + if os.path.exists(nav_dir): + print(f"Contents of {nav_dir}:") + for item in os.listdir(nav_dir): + print(f" - {item}") + else: + print(f"Directory not found: {nav_dir}") + return + + if not os.path.exists(venv_python): + print(f"ERROR: venv Python not found at {venv_python}, using system Python") + return + + print(f"Using Python: {venv_python}") + print(f"Starting script: {nav_bot_path}") + + # Use the venv Python explicitly + try: + self.dimos_process = subprocess.Popen( + [venv_python, nav_bot_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + universal_newlines=True, + ) + + # Give it a moment to start and check if it's still running + time.sleep(2) + poll_result = self.dimos_process.poll() + if poll_result is not None: + # Process exited immediately + stdout, stderr = self.dimos_process.communicate(timeout=1) + print(f"ERROR: DimOS failed to start (exit code: {poll_result})") + if stdout: + print(f"STDOUT: {stdout}") + if stderr: + print(f"STDERR: {stderr}") + self.dimos_process = None + else: + print(f"DimOS started successfully (PID: {self.dimos_process.pid})") + + except Exception as e: + print(f"ERROR: Failed to start DimOS: {e}") + self.dimos_process = None + + if self.dimos_process: + print("Both systems are running. Press Ctrl+C to stop.") + else: + print("ROS is running (DimOS failed to start). Press Ctrl+C to stop.") + print("") + + # Wait for processes + try: + # Monitor both processes + while True: + # Check if either process has died + if self.ros_process.poll() is not None: + print("ROS process has stopped unexpectedly.") + self.signal_handler(signal.SIGTERM, None) + break + if self.dimos_process and self.dimos_process.poll() is not None: + print("DimOS process has stopped.") + # DimOS stopping is less critical, but we should still clean up ROS + self.signal_handler(signal.SIGTERM, None) + break + time.sleep(1) + except KeyboardInterrupt: + pass # Signal handler will take care of cleanup + + +if __name__ == "__main__": + wrapper = ROSLaunchWrapper() + wrapper.run() diff --git a/docker/navigation/run_both.sh b/docker/navigation/run_both.sh new file mode 100755 index 0000000000..a433cf5b01 --- /dev/null +++ b/docker/navigation/run_both.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Script to run both ROS route planner and DimOS together + +echo "Starting ROS route planner and DimOS..." + +# Variables for process IDs +ROS_PID="" +DIMOS_PID="" +SHUTDOWN_IN_PROGRESS=false + +# Function to handle cleanup +cleanup() { + if [ "$SHUTDOWN_IN_PROGRESS" = true ]; then + return + fi + SHUTDOWN_IN_PROGRESS=true + + echo "" + echo "Shutdown initiated. Stopping services..." + + # First, try to gracefully stop DimOS + if [ -n "$DIMOS_PID" ] && kill -0 $DIMOS_PID 2>/dev/null; then + echo "Stopping DimOS..." + kill -TERM $DIMOS_PID 2>/dev/null || true + + # Wait up to 5 seconds for DimOS to stop + for i in {1..10}; do + if ! kill -0 $DIMOS_PID 2>/dev/null; then + echo "DimOS stopped cleanly." + break + fi + sleep 0.5 + done + + # Force kill if still running + if kill -0 $DIMOS_PID 2>/dev/null; then + echo "Force stopping DimOS..." + kill -9 $DIMOS_PID 2>/dev/null || true + fi + fi + + # Then handle ROS - send SIGINT to the launch process group + if [ -n "$ROS_PID" ] && kill -0 $ROS_PID 2>/dev/null; then + echo "Stopping ROS nodes (this may take a moment)..." + + # Send SIGINT to the process group to properly trigger ROS shutdown + kill -INT -$ROS_PID 2>/dev/null || kill -INT $ROS_PID 2>/dev/null || true + + # Wait up to 15 seconds for graceful shutdown + for i in {1..30}; do + if ! kill -0 $ROS_PID 2>/dev/null; then + echo "ROS stopped cleanly." + break + fi + sleep 0.5 + done + + # If still running, send SIGTERM + if kill -0 $ROS_PID 2>/dev/null; then + echo "Sending SIGTERM to ROS..." + kill -TERM -$ROS_PID 2>/dev/null || kill -TERM $ROS_PID 2>/dev/null || true + sleep 2 + fi + + # Final resort: SIGKILL + if kill -0 $ROS_PID 2>/dev/null; then + echo "Force stopping ROS..." + kill -9 -$ROS_PID 2>/dev/null || kill -9 $ROS_PID 2>/dev/null || true + fi + fi + + # Clean up any remaining ROS2 processes + echo "Cleaning up any remaining processes..." + pkill -f "ros2" 2>/dev/null || true + pkill -f "localPlanner" 2>/dev/null || true + pkill -f "pathFollower" 2>/dev/null || true + pkill -f "terrainAnalysis" 2>/dev/null || true + pkill -f "sensorScanGeneration" 2>/dev/null || true + pkill -f "vehicleSimulator" 2>/dev/null || true + pkill -f "visualizationTools" 2>/dev/null || true + pkill -f "far_planner" 2>/dev/null || true + pkill -f "graph_decoder" 2>/dev/null || true + + echo "All services stopped." +} + +# Set up trap to call cleanup on exit +trap cleanup EXIT INT TERM + +# Start ROS route planner in background (in new process group) +echo "Starting ROS route planner..." +cd /ros2_ws/src/ros-navigation-autonomy-stack +setsid bash -c './system_simulation_with_route_planner.sh' & +ROS_PID=$! + +# Wait a bit for ROS to initialize +echo "Waiting for ROS to initialize..." +sleep 5 + +# Start DimOS +echo "Starting DimOS navigation bot..." + +# Check if the script exists +if [ ! -f "/workspace/dimos/dimos/navigation/demo_ros_navigation.py" ]; then + echo "ERROR: demo_ros_navigation.py not found at /workspace/dimos/dimos/navigation/demo_ros_navigation.py" + echo "Available files in /workspace/dimos/dimos/navigation/:" + ls -la /workspace/dimos/dimos/navigation/ 2>/dev/null || echo "Directory not found" +else + echo "Found demo_ros_navigation.py, activating virtual environment..." + if [ -f "/opt/dimos-venv/bin/activate" ]; then + source /opt/dimos-venv/bin/activate + echo "Python path: $(which python)" + echo "Python version: $(python --version)" + else + echo "WARNING: Virtual environment not found at /opt/dimos-venv, using system Python" + fi + + echo "Starting demo_ros_navigation.py..." + # Capture any startup errors + python /workspace/dimos/dimos/navigation/demo_ros_navigation.py 2>&1 & + DIMOS_PID=$! + + # Give it a moment to start and check if it's still running + sleep 2 + if kill -0 $DIMOS_PID 2>/dev/null; then + echo "DimOS started successfully with PID: $DIMOS_PID" + else + echo "ERROR: DimOS failed to start (process exited immediately)" + echo "Check the logs above for error messages" + DIMOS_PID="" + fi +fi + +echo "" +if [ -n "$DIMOS_PID" ]; then + echo "Both systems are running. Press Ctrl+C to stop." +else + echo "ROS is running (DimOS failed to start). Press Ctrl+C to stop." +fi +echo "" + +# Wait for processes +if [ -n "$DIMOS_PID" ]; then + wait $ROS_PID $DIMOS_PID 2>/dev/null || true +else + wait $ROS_PID 2>/dev/null || true +fi \ No newline at end of file diff --git a/docker/navigation/setup.sh b/docker/navigation/setup.sh new file mode 100755 index 0000000000..5edf9abfd5 --- /dev/null +++ b/docker/navigation/setup.sh @@ -0,0 +1,706 @@ +#!/bin/bash +set -e +set -o pipefail + +################################################################################ +# DimOS Navigation Setup Script +# +# Usage: ./setup.sh [OPTIONS] +# --install-dir DIR Installation directory (default: ~/dimos) +# --skip-docker Skip Docker installation +# --skip-build Skip building Docker images +# --help Show this help message +# +################################################################################ + +# Color codes for output +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly CYAN='\033[0;36m' +readonly NC='\033[0m' +readonly BOLD='\033[1m' + +# Configuration +INSTALL_DIR="${HOME}/dimos" +SKIP_DOCKER=false +SKIP_BUILD=false +LOG_FILE="${HOME}/dimos-setup.log" +SCRIPT_START_TIME=$(date +%s) + +# Step tracking +CURRENT_STEP=0 +TOTAL_STEPS=8 + +################################################################################ +# Utility Functions +################################################################################ + +log() { + local level="$1" + shift + local message="$*" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[${timestamp}] [${level}] ${message}" >> "${LOG_FILE}" +} + +print_banner() { + echo -e "${CYAN}${BOLD}" + cat << "EOF" + ____ _ __ ___ ____ _____ + / __ \(_) |/ / / __ \/ ___/ + / / / / / /|_/ / / / / /\__ \ + / /_/ / / / / / / /_/ /___/ / +/_____/_/_/ /_/ \____//____/ + + Navigation Setup Script +EOF + echo -e "${NC}" + echo -e "${BLUE}This script will set up your Ubuntu system for DimOS Navigation${NC}" + echo -e "${BLUE}Installation may take 20-30 minutes depending on your connection${NC}" + echo "" +} + +step() { + CURRENT_STEP=$((CURRENT_STEP + 1)) + echo "" + echo -e "${CYAN}${BOLD}[Step ${CURRENT_STEP}/${TOTAL_STEPS}]${NC} ${BOLD}$1${NC}" + log "INFO" "Step ${CURRENT_STEP}/${TOTAL_STEPS}: $1" +} + +info() { + echo -e "${BLUE}ℹ${NC} $1" + log "INFO" "$1" +} + +success() { + echo -e "${GREEN}✓${NC} $1" + log "SUCCESS" "$1" +} + +warning() { + echo -e "${YELLOW}⚠${NC} $1" + log "WARNING" "$1" +} + +error() { + echo -e "${RED}✗${NC} $1" + log "ERROR" "$1" +} + +fatal() { + error "$1" + echo "" + echo -e "${RED}${BOLD}Installation failed.${NC}" + echo -e "Check the log file for details: ${LOG_FILE}" + echo "" + exit 1 +} + +confirm() { + local prompt="$1" + local default="${2:-n}" + local response + + if [[ "${default}" == "y" ]]; then + prompt="${prompt} [Y/n]: " + else + prompt="${prompt} [y/N]: " + fi + + read -r -p "$(echo -e "${YELLOW}${prompt}${NC}")" response + response=${response:-${default}} + + [[ "${response,,}" =~ ^y(es)?$ ]] +} + +check_command() { + command -v "$1" >/dev/null 2>&1 +} + +################################################################################ +# Pre-flight Checks +################################################################################ + +preflight_checks() { + step "Running pre-flight checks" + + if [[ "$(uname -s)" != "Linux" ]]; then + fatal "This script is designed for Linux systems only" + fi + + if ! check_command apt-get; then + fatal "This script requires Ubuntu or Debian-based system" + fi + + if [[ -f /etc/os-release ]]; then + source /etc/os-release + info "Detected: ${PRETTY_NAME}" + + OS_VERSION_CODENAME="${VERSION_CODENAME:-}" + + VERSION_NUM=$(echo "${VERSION_ID:-0}" | cut -d. -f1) + if ! [[ "${VERSION_NUM}" =~ ^[0-9]+$ ]]; then + warning "Unable to determine Ubuntu version number" + VERSION_NUM=0 + fi + + if [[ "${VERSION_NUM}" -ne 0 ]] && [[ "${VERSION_NUM}" -lt 24 ]]; then + warning "Ubuntu 24.04 is required. You have ${VERSION_ID}" + if ! confirm "Continue anyway?"; then + exit 0 + fi + fi + fi + + if [[ $EUID -eq 0 ]]; then + fatal "This script should NOT be run as root. Run as a regular user with sudo access." + fi + + if ! sudo -n true 2>/dev/null; then + info "This script requires sudo access. You may be prompted for your password." + if ! sudo true; then + fatal "Failed to obtain sudo access" + fi + fi + + local target_dir=$(dirname "${INSTALL_DIR}") + mkdir -p "${target_dir}" 2>/dev/null || target_dir="${HOME}" + local available_space=$(df -BG "${target_dir}" 2>/dev/null | awk 'NR==2 {print $4}' | sed 's/G//' || echo "0") + info "Available disk space at ${target_dir}: ${available_space}GB" + if [[ "${available_space}" -lt 50 ]]; then + warning "Low disk space detected. At least 50GB is recommended." + warning "Docker images and builds will require significant space." + if ! confirm "Continue anyway?"; then + exit 0 + fi + fi + + info "Checking internet connectivity..." + if ! ping -c 1 8.8.8.8 >/dev/null 2>&1; then + fatal "No internet connection detected. Please check your network." + fi + + success "Pre-flight checks passed" +} + +################################################################################ +# System Setup +################################################################################ + +update_system() { + step "Updating system packages" + + info "Running apt-get update..." + if sudo apt-get update -y >> "${LOG_FILE}" 2>&1; then + success "Package lists updated" + else + warning "Package update had some warnings (check log)" + fi +} + +install_base_tools() { + step "Installing base tools" + + local packages=( + "git" + "ssh" + "zip" + "curl" + "wget" + "jq" + "nano" + "vim" + "htop" + "ca-certificates" + "gnupg" + ) + + info "Installing: ${packages[*]}" + + if sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}" >> "${LOG_FILE}" 2>&1; then + success "Base tools installed" + else + fatal "Failed to install base tools" + fi + + if check_command ufw; then + info "Configuring firewall (UFW)..." + if sudo ufw status | grep -q "Status: active"; then + info "UFW is active, ensuring SSH is allowed..." + sudo ufw allow 22/tcp >> "${LOG_FILE}" 2>&1 || true + else + info "UFW is inactive, skipping firewall configuration" + fi + fi +} + +################################################################################ +# Docker Installation +################################################################################ + +install_docker() { + if [[ "${SKIP_DOCKER}" == true ]]; then + info "Skipping Docker installation (--skip-docker flag)" + return + fi + + step "Installing Docker" + + if check_command docker; then + local docker_version=$(docker --version 2>/dev/null || echo "unknown") + success "Docker is already installed: ${docker_version}" + + if docker compose version >/dev/null 2>&1; then + success "Docker Compose plugin is available" + else + warning "Docker Compose plugin not found, will attempt to install" + fi + + if ! confirm "Reinstall Docker anyway?" "n"; then + return + fi + fi + + info "Adding Docker's official GPG key..." + sudo install -m 0755 -d /etc/apt/keyrings + + if curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>> "${LOG_FILE}"; then + sudo chmod a+r /etc/apt/keyrings/docker.gpg + success "Docker GPG key added" + else + fatal "Failed to add Docker GPG key" + fi + + info "Adding Docker repository..." + local version_codename="${OS_VERSION_CODENAME}" + if [[ -z "${version_codename}" ]] && [[ -f /etc/os-release ]]; then + version_codename=$(. /etc/os-release && echo "$VERSION_CODENAME") + fi + + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + ${version_codename} stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + + success "Docker repository added" + + info "Updating package lists..." + sudo apt-get update -y >> "${LOG_FILE}" 2>&1 + + info "Installing Docker packages (this may take a few minutes)..." + local docker_packages=( + "docker-ce" + "docker-ce-cli" + "containerd.io" + "docker-buildx-plugin" + "docker-compose-plugin" + ) + + if sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${docker_packages[@]}" >> "${LOG_FILE}" 2>&1; then + success "Docker installed successfully" + else + fatal "Failed to install Docker packages" + fi + + info "Configuring Docker group permissions..." + + if ! getent group docker >/dev/null; then + sudo groupadd docker + fi + + if sudo usermod -aG docker "${USER}"; then + success "User ${USER} added to docker group" + else + warning "Failed to add user to docker group" + fi + + info "Verifying Docker installation..." + if sudo docker run --rm hello-world >> "${LOG_FILE}" 2>&1; then + success "Docker is working correctly" + else + warning "Docker verification failed, but installation may still be successful" + fi + + warning "Docker group changes require logout/login to take effect" + info "For now, we'll use 'sudo docker' commands" +} + +################################################################################ +# Git LFS Setup +################################################################################ + +install_git_lfs() { + step "Installing Git LFS" + + if check_command git-lfs; then + success "Git LFS is already installed" + return + fi + + info "Adding Git LFS repository..." + if curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash >> "${LOG_FILE}" 2>&1; then + success "Git LFS repository added" + else + fatal "Failed to add Git LFS repository" + fi + + info "Installing Git LFS..." + if sudo apt-get install -y git-lfs >> "${LOG_FILE}" 2>&1; then + success "Git LFS installed" + else + fatal "Failed to install Git LFS" + fi + + info "Configuring Git LFS..." + if git lfs install >> "${LOG_FILE}" 2>&1; then + success "Git LFS configured" + else + warning "Git LFS configuration had issues (may already be configured)" + fi +} + +################################################################################ +# SSH Key Configuration +################################################################################ + +setup_ssh_keys() { + step "Configuring GitHub SSH access" + + info "Testing GitHub SSH connection..." + if timeout 10 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -T git@github.com 2>&1 | grep -q "successfully authenticated"; then + success "GitHub SSH access is already configured" + return + fi + + warning "GitHub SSH access is not configured" + echo "" + echo -e "${YELLOW}${BOLD}SSH Key Setup Required${NC}" + echo "" + echo "To clone the private DimOS repository, you need SSH access to GitHub." + echo "" + + if [[ -f "${HOME}/.ssh/id_rsa.pub" ]] || [[ -f "${HOME}/.ssh/id_ed25519.pub" ]]; then + info "Existing SSH key found" + echo "" + + if [[ -f "${HOME}/.ssh/id_ed25519.pub" ]]; then + echo -e "${CYAN}Your public key (id_ed25519.pub):${NC}" + cat "${HOME}/.ssh/id_ed25519.pub" + elif [[ -f "${HOME}/.ssh/id_rsa.pub" ]]; then + echo -e "${CYAN}Your public key (id_rsa.pub):${NC}" + cat "${HOME}/.ssh/id_rsa.pub" + fi + + echo "" + echo -e "${YELLOW}Please add this key to your GitHub account:${NC}" + echo " 1. Go to: https://github.com/settings/keys" + echo " 2. Click 'New SSH key'" + echo " 3. Paste the key above" + echo " 4. Click 'Add SSH key'" + echo "" + else + info "No SSH key found. Let's create one." + echo "" + + if confirm "Generate a new SSH key?" "y"; then + local email + echo -n "Enter your GitHub email address: " + read -r email + + info "Generating SSH key..." + if ssh-keygen -t ed25519 -C "${email}" -f "${HOME}/.ssh/id_ed25519" -N "" >> "${LOG_FILE}" 2>&1; then + success "SSH key generated" + + eval "$(ssh-agent -s)" > /dev/null + if ssh-add "${HOME}/.ssh/id_ed25519" 2>> "${LOG_FILE}"; then + success "SSH key added to agent" + else + warning "Could not add key to ssh-agent (non-critical)" + fi + + echo "" + echo -e "${CYAN}Your new public key:${NC}" + cat "${HOME}/.ssh/id_ed25519.pub" + echo "" + echo -e "${YELLOW}Please add this key to your GitHub account:${NC}" + echo " 1. Go to: https://github.com/settings/keys" + echo " 2. Click 'New SSH key'" + echo " 3. Paste the key above" + echo " 4. Click 'Add SSH key'" + echo "" + else + fatal "Failed to generate SSH key" + fi + else + echo "" + error "SSH key is required to continue" + echo "Please set up SSH access manually and run this script again." + exit 1 + fi + fi + + echo "" + if ! confirm "Have you added the SSH key to GitHub?" "n"; then + echo "" + warning "Setup paused. Please add the SSH key and run this script again." + exit 0 + fi + + info "Testing GitHub SSH connection..." + if timeout 10 ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -T git@github.com 2>&1 | grep -q "successfully authenticated"; then + success "GitHub SSH access verified!" + else + error "GitHub SSH connection failed" + echo "" + echo "Please verify:" + echo " 1. The SSH key was added to GitHub correctly" + echo " 2. You're using the correct GitHub account" + echo " 3. Try: ssh -T git@github.com" + echo "" + if ! confirm "Continue anyway?" "n"; then + exit 1 + fi + fi +} + +################################################################################ +# Repository Setup +################################################################################ + +clone_repository() { + step "Cloning DimOS repository" + + if [[ -d "${INSTALL_DIR}" ]]; then + if [[ -d "${INSTALL_DIR}/.git" ]]; then + success "Repository already exists at ${INSTALL_DIR}" + + local remote_url=$(git -C "${INSTALL_DIR}" remote get-url origin 2>/dev/null || echo "") + if [[ "${remote_url}" =~ "dimos" ]]; then + info "Existing repository verified" + return + else + warning "Directory exists but doesn't appear to be the DimOS repo" + if ! confirm "Remove and re-clone?" "n"; then + fatal "Cannot proceed with existing directory" + fi + rm -rf "${INSTALL_DIR}" + fi + else + warning "Directory ${INSTALL_DIR} exists but is not a git repository" + if ! confirm "Remove and re-clone?" "n"; then + fatal "Cannot proceed with existing directory" + fi + rm -rf "${INSTALL_DIR}" + fi + fi + + info "Cloning to ${INSTALL_DIR}..." + if git clone git@github.com:dimensionalOS/dimos.git "${INSTALL_DIR}" >> "${LOG_FILE}" 2>&1; then + success "Repository cloned successfully" + else + fatal "Failed to clone repository. Check your SSH access." + fi + + info "Pulling Git LFS files (this may take several minutes)..." + if git -C "${INSTALL_DIR}" lfs pull >> "${LOG_FILE}" 2>&1; then + success "LFS files downloaded" + else + warning "Some LFS files may not have downloaded correctly" + fi +} + +################################################################################ +# Build and Launch +################################################################################ + +build_docker_images() { + if [[ "${SKIP_BUILD}" == true ]]; then + info "Skipping Docker build (--skip-build flag)" + return + fi + + step "Building Docker images" + + local build_dir="${INSTALL_DIR}/docker/navigation" + if [[ ! -d "${build_dir}" ]]; then + fatal "Directory not found: ${build_dir}" + fi + + if [[ ! -f "${build_dir}/build.sh" ]]; then + fatal "build.sh not found in ${build_dir}" + fi + + echo "" + warning "Building Docker images will take 10-15 minutes and download ~30GB" + info "This step will:" + echo " • Clone the ROS navigation autonomy stack" + echo " • Build a large Docker image with ROS Jazzy" + echo " • Install all dependencies" + echo "" + + if ! confirm "Start the build now?" "y"; then + warning "Build skipped. You can build later with:" + echo " cd ${build_dir}" + echo " ./build.sh" + return + fi + + info "Starting build process..." + echo "" + + pushd "${build_dir}" >> "${LOG_FILE}" 2>&1 || fatal "Failed to change to ${build_dir}" + + ./build.sh 2>&1 | tee -a "${LOG_FILE}" + local build_status=${PIPESTATUS[0]} + + popd >> "${LOG_FILE}" 2>&1 || true + + if [[ ${build_status} -eq 0 ]]; then + success "Docker images built successfully" + else + fatal "Docker build failed. Check the log for details." + fi +} + +################################################################################ +# Completion +################################################################################ + +print_summary() { + local elapsed=$(($(date +%s) - SCRIPT_START_TIME)) + local minutes=$((elapsed / 60)) + local seconds=$((elapsed % 60)) + + echo "" + echo "" + echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}${BOLD}║ ║${NC}" + echo -e "${GREEN}${BOLD}║ Setup completed successfully! 🎉 ║${NC}" + echo -e "${GREEN}${BOLD}║ ║${NC}" + echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e "${CYAN}Installation time: ${minutes}m ${seconds}s${NC}" + echo -e "${CYAN}Installation directory: ${INSTALL_DIR}${NC}" + echo -e "${CYAN}Log file: ${LOG_FILE}${NC}" + echo "" + echo -e "${BOLD}Next steps:${NC}" + echo "" + echo " 1. If Docker commands failed, log out and back in for group changes" + echo " Or run: newgrp docker" + echo "" + echo " 2. Navigate to the project:" + echo " cd ${INSTALL_DIR}/docker/navigation" + echo "" + echo " 3. Start the demo:" + echo " ./start.sh --all" + echo "" + echo " 4. Or get an interactive shell:" + echo " ./start.sh" + echo "" + echo -e "${CYAN}For more information, see the README.md in docker/navigation/${NC}" + echo "" +} + +################################################################################ +# Argument Parsing +################################################################################ + +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + --install-dir) + if [[ -z "$2" ]] || [[ "$2" == --* ]]; then + error "Error: --install-dir requires a directory path" + echo "Run '$0 --help' for usage information" + exit 1 + fi + INSTALL_DIR="$2" + shift 2 + ;; + --skip-docker) + SKIP_DOCKER=true + shift + ;; + --skip-build) + SKIP_BUILD=true + shift + ;; + --help) + print_banner + cat << EOF +Usage: $0 [OPTIONS] + +Options: + --install-dir DIR Installation directory (default: ~/dimos) + --skip-docker Skip Docker installation + --skip-build Skip building Docker images + --help Show this help message + +Examples: + $0 # Full installation + $0 --install-dir /opt/dimos # Install to custom directory + $0 --skip-docker # Skip Docker installation + $0 --skip-docker --skip-build # Only clone repository + +After installation, navigate to the project and start the demo: + cd ~/dimos/docker/navigation + ./start.sh --all + +For more information, visit: + https://github.com/dimensionalOS/dimos + +EOF + exit 0 + ;; + *) + error "Unknown option: $1" + echo "Run '$0 --help' for usage information" + exit 1 + ;; + esac + done +} + +################################################################################ +# Main +################################################################################ + +main() { + log "INFO" "DimOS Navigation Setup Script started" + log "INFO" "User: ${USER}" + log "INFO" "Install directory: ${INSTALL_DIR}" + + print_banner + + echo -e "${YELLOW}This script will:${NC}" + echo " • Update your system" + echo " • Install Docker and dependencies" + echo " • Configure Git LFS" + echo " • Set up GitHub SSH access" + echo " • Clone the DimOS repository" + echo " • Build Docker images (~30GB, 10-15 minutes)" + echo "" + + if ! confirm "Continue with installation?" "y"; then + echo "Installation cancelled." + exit 0 + fi + + preflight_checks + update_system + install_base_tools + install_docker + install_git_lfs + setup_ssh_keys + clone_repository + build_docker_images + + print_summary + + log "INFO" "Setup completed successfully" +} + +parse_arguments "$@" +main diff --git a/docker/navigation/start.sh b/docker/navigation/start.sh new file mode 100755 index 0000000000..cd05dff69b --- /dev/null +++ b/docker/navigation/start.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# Parse command line arguments +MODE="simulation" +while [[ $# -gt 0 ]]; do + case $1 in + --hardware) + MODE="hardware" + shift + ;; + --simulation) + MODE="simulation" + shift + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --simulation Start simulation container (default)" + echo " --hardware Start hardware container for real robot" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 # Start simulation container" + echo " $0 --hardware # Start hardware container" + echo "" + echo "Press Ctrl+C to stop the container" + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + echo "Run '$0 --help' for usage information" + exit 1 + ;; + esac +done + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +echo -e "${GREEN}================================================${NC}" +echo -e "${GREEN}Starting DimOS Docker Container${NC}" +echo -e "${GREEN}Mode: ${MODE}${NC}" +echo -e "${GREEN}================================================${NC}" +echo "" + +# Hardware-specific checks +if [ "$MODE" = "hardware" ]; then + # Check if .env file exists + if [ ! -f ".env" ]; then + if [ -f ".env.hardware" ]; then + echo -e "${YELLOW}Creating .env from .env.hardware template...${NC}" + cp .env.hardware .env + echo -e "${RED}Please edit .env file with your hardware configuration:${NC}" + echo " - LIDAR_IP: Full IP address of your Mid-360 lidar" + echo " - LIDAR_COMPUTER_IP: IP address of this computer on the lidar subnet" + echo " - LIDAR_INTERFACE: Network interface connected to lidar" + echo " - MOTOR_SERIAL_DEVICE: Serial device for motor controller" + echo "" + echo "After editing, run this script again." + exit 1 + fi + fi + + # Source the environment file + if [ -f ".env" ]; then + set -a + source .env + set +a + + # Check for required environment variables + if [ -z "$LIDAR_IP" ] || [ "$LIDAR_IP" = "192.168.1.116" ]; then + echo -e "${YELLOW}Warning: LIDAR_IP still using default value in .env${NC}" + echo "Set LIDAR_IP to the actual IP address of your Mid-360 lidar" + fi + + if [ -z "$LIDAR_GATEWAY" ]; then + echo -e "${YELLOW}Warning: LIDAR_GATEWAY not configured in .env${NC}" + echo "Set LIDAR_GATEWAY to the gateway IP address for the lidar subnet" + fi + + # Check for serial devices + echo -e "${GREEN}Checking for serial devices...${NC}" + if [ -e "${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}" ]; then + echo -e " Found device at: ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}" + else + echo -e "${YELLOW} Warning: Device not found at ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}${NC}" + echo -e "${YELLOW} Available serial devices:${NC}" + ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || echo " None found" + fi + + # Check network interface for lidar + if [ -n "$LIDAR_INTERFACE" ]; then + echo -e "${GREEN}Checking network interface for lidar...${NC}" + if ip link show "$LIDAR_INTERFACE" &>/dev/null; then + echo -e " Network interface $LIDAR_INTERFACE found" + else + echo -e "${YELLOW} Warning: Interface $LIDAR_INTERFACE not found${NC}" + echo -e "${YELLOW} Available interfaces:${NC}" + ip link show | grep -E "^[0-9]+:" | awk '{print " " $2}' | sed 's/:$//' + fi + fi + fi + +fi + +# Check if unified image exists +if ! docker images | grep -q "dimos_autonomy_stack.*jazzy"; then + echo -e "${YELLOW}Docker image not found. Building...${NC}" + ./build.sh +fi + +# Check for X11 display +if [ -z "$DISPLAY" ]; then + echo -e "${YELLOW}Warning: DISPLAY not set. GUI applications may not work.${NC}" + export DISPLAY=:0 +fi + +# Allow X11 connections from Docker +echo -e "${GREEN}Configuring X11 access...${NC}" +xhost +local:docker 2>/dev/null || true + +cleanup() { + xhost -local:docker 2>/dev/null || true +} + +trap cleanup EXIT + +# Check for NVIDIA runtime +if docker info 2>/dev/null | grep -q nvidia; then + echo -e "${GREEN}NVIDIA Docker runtime detected${NC}" + export DOCKER_RUNTIME=nvidia + if [ "$MODE" = "hardware" ]; then + export NVIDIA_VISIBLE_DEVICES=all + export NVIDIA_DRIVER_CAPABILITIES=all + fi +else + echo -e "${YELLOW}NVIDIA Docker runtime not found. GPU acceleration disabled.${NC}" +fi + +# Set container name for reference +if [ "$MODE" = "hardware" ]; then + CONTAINER_NAME="dimos_hardware_container" +else + CONTAINER_NAME="dimos_simulation_container" +fi + +# Print helpful info before starting +echo "" +if [ "$MODE" = "hardware" ]; then + echo "Hardware mode - Interactive shell" + echo "" + echo -e "${GREEN}=================================================${NC}" + echo -e "${GREEN}The container is running. Exec in to run scripts:${NC}" + echo -e " ${YELLOW}docker exec -it ${CONTAINER_NAME} bash${NC}" + echo -e "${GREEN}=================================================${NC}" +else + echo "Simulation mode - Auto-starting ROS simulation and DimOS" + echo "" + echo "The container will automatically run:" + echo " - ROS navigation stack with route planner" + echo " - DimOS navigation demo" + echo "" + echo "To enter the container from another terminal:" + echo " docker exec -it ${CONTAINER_NAME} bash" +fi + +if [ "$MODE" = "hardware" ]; then + docker compose -f docker-compose.yml --profile hardware up +else + docker compose -f docker-compose.yml up +fi