From 31d82d86160d98eb9a2b2d310574d751d73769ab Mon Sep 17 00:00:00 2001 From: ishakumbam Date: Tue, 19 Aug 2025 06:43:59 -0500 Subject: [PATCH] ROS2 Humble turtlesim task: Dockerized sim+follower, custom msg, launch files, README --- Dockerfile | 313 +++--------------- Dockerfile.bak | 56 ++++ docker-compose.yml | 150 ++------- overlay_src/my_interfaces/CMakeLists.txt | 7 + overlay_src/my_interfaces/msg/ChaseMode.msg | 1 + overlay_src/my_interfaces/package.xml | 12 + .../launch/follower_bringup.launch.py | 8 + .../launch/sim_bringup.launch.py | 10 + overlay_src/turtlesim_tasks/package.xml | 23 ++ .../turtlesim_tasks/resource/turtlesim_tasks | 1 + overlay_src/turtlesim_tasks/setup.cfg | 4 + overlay_src/turtlesim_tasks/setup.py | 30 ++ .../turtlesim_tasks/__init__.py | 1 + .../turtlesim_tasks/follower_node.py | 64 ++++ .../turtlesim_tasks/mode_toggle.py | 30 ++ .../turtlesim_tasks/mouse_goal_publisher.py | 46 +++ .../turtlesim_tasks/spawn_second.py | 36 ++ .../turtlesim_tasks/turtle1_controller.py | 51 +++ ros_entrypoint.sh | 8 + 19 files changed, 445 insertions(+), 406 deletions(-) mode change 100755 => 100644 Dockerfile create mode 100644 Dockerfile.bak mode change 100755 => 100644 docker-compose.yml create mode 100644 overlay_src/my_interfaces/CMakeLists.txt create mode 100644 overlay_src/my_interfaces/msg/ChaseMode.msg create mode 100644 overlay_src/my_interfaces/package.xml create mode 100644 overlay_src/turtlesim_tasks/launch/follower_bringup.launch.py create mode 100644 overlay_src/turtlesim_tasks/launch/sim_bringup.launch.py create mode 100644 overlay_src/turtlesim_tasks/package.xml create mode 100644 overlay_src/turtlesim_tasks/resource/turtlesim_tasks create mode 100644 overlay_src/turtlesim_tasks/setup.cfg create mode 100644 overlay_src/turtlesim_tasks/setup.py create mode 100644 overlay_src/turtlesim_tasks/turtlesim_tasks/__init__.py create mode 100644 overlay_src/turtlesim_tasks/turtlesim_tasks/follower_node.py create mode 100644 overlay_src/turtlesim_tasks/turtlesim_tasks/mode_toggle.py create mode 100644 overlay_src/turtlesim_tasks/turtlesim_tasks/mouse_goal_publisher.py create mode 100644 overlay_src/turtlesim_tasks/turtlesim_tasks/spawn_second.py create mode 100644 overlay_src/turtlesim_tasks/turtlesim_tasks/turtle1_controller.py create mode 100755 ros_entrypoint.sh diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 index de2bf8c79..1d16f9648 --- a/Dockerfile +++ b/Dockerfile @@ -1,275 +1,38 @@ -# multi-stage for caching -FROM ros:humble - -# This variable tells future scripts that user input isn't available during the Docker build. -ENV DEBIAN_FRONTEND noninteractive - -LABEL maintainer="Nova" - -# Get wget first so we can get the NVIDIA keys -#RUN apt-get install -y wget - -# To fix GPG key error when running apt-get update -# https://developer.nvidia.com/blog/updating-the-cuda-linux-gpg-repository-key/ -###RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/3bf863cc.pub -###RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu2204/x86_64/7fa2af80.pub -#RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb && dpkg -i cuda-keyring_1.0-1_all.deb - -# Install torch -#RUN apt install -y nvidia-cuda-toolkit -#RUN pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu101 - -### REMOVE?? ### -### RUN pip3 install -U openmim && mim install mmcv-full && git clone https://github.com/open-mmlab/mmsegmentation.git /mmsegmentation -### WORKDIR /mmsegmentation -### RUN pip install -v -e . && mim download mmsegmentation --config pspnet_r50-d8_512x1024_40k_cityscapes --dest . - -# Install MMCV https://github.com/open-mmlab/mmcv (requires PyTorch) -#RUN pip3 install -U openmim && mim install mmcv-full - -# apt-get & apt installs -# Also need to upgrade the apt packages, otherwise there are incompatibilities running ros2/rviz2 -RUN apt-get update && apt update && apt upgrade -y && \ - # ================================= - ## apt-get installs =============== - apt-get install -y \ - # important linux tool - wget \ - # install python libraries - python3-pip \ - # be able to use git inside container - git \ - # - fprintd \ - # - libfprint-2-2:amd64 \ - # - libgl1-mesa-glx \ - # - libglib2.0-0 \ - # - libpam-fprintd:amd64 \ - # - libsm6 \ - # - libxrender-dev \ - # - libxext6 \ - # - ninja-build \ - # - ros-humble-diagnostic-updater \ - # ================================= - # apt installs ==================== - && apt install -y \ - # - alsa-base \ - # - alsa-utils \ - # - byobu \ - # - espeak \ - # - ffmpeg \ - # - libespeak1 \ - # - libsndfile1-dev \ - # - minicom \ - # - python3-matplotlib \ - # - python3-nose \ - # - python3-scipy \ - # - ros-humble-ament-cmake* \ - # needed for ros2_numpy package (maybe others) - ros-humble-ament-cmake-nose \ - # - ros-humble-async-web-server-cpp \ - # - ros-humble-cv-bridge \ - # - ros-humble-image-transport \ - # - ros-humble-joy-linux \ - # - ros-humble-lgsvl-msgs \ - # Nav2 stack - used for path planning - ros-humble-navigation2 \ - # - ros-humble-pcl-conversions \ - # - ros-humble-pcl-ros \ - # - ros-humble-rmw-fastrtps-cpp \ - # - ros-humble-robot-localization \ - # - ros-humble-rosbridge-server \ - # - ros-humble-ros-testing \ - # rqt provides oscilliscope-style plotting of topics, etc - ros-humble-rqt* \ - # - ros-humble-rviz2 \ - # - ros-humble-tf-transformations \ - # ROS Support for Velodyne LiDAR - ros-humble-velodyne-driver \ - ros-humble-velodyne-laserscan \ - ros-humble-velodyne-msgs \ - ros-humble-velodyne-pointcloud \ - # - ros-humble-vision-opencv \ - # - software-properties-common \ - # cleanup to make image smaller - && apt clean && rm -rf /var/lib/apt/lists/* - -# install cmake 3.24+ -RUN wget https://github.com/Kitware/CMake/releases/download/v3.24.4/cmake-3.24.4-linux-x86_64.tar.gz -RUN tar -zxvf cmake-3.24.4-linux-x86_64.tar.gz -RUN mv cmake-3.24.4-linux-x86_64 /opt/cmake -RUN ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake - -# pip3 installs -RUN pip3 install --ignore-installed \ - # - autopep8==2.3.1 \ - # - cmake_format==0.6.11 \ - # - dictor==0.1.12 \ - # - ephem==4.1.5 \ - # Used for the road signs classifier API call to the model. - inference-sdk==0.26.0 \ - # - matplotlib \ - # (possibly needs to be version 2.2) - networkx==3.3 \ - # Scientific Computing - used widely - numpy==1.26.4 \ - # - open3d==0.18.0 \ - # - opencv-python==4.10.0.84 \ - # - pathfinding==1.0.11 \ - # - pep8==1.7.1 \ - # - pexpect==4.9.0 \ - # - psutil==5.9.0 \ - # - pygame==2.6.0 \ - # - pylint==3.2.7 \ - # - pymap3d==2.9.1 \ - # - pynmea2==1.19.0 \ - # - pyproj==3.6.1 \ - # - pyserial==3.5 \ - # - python-can==4.4.2 \ - # - py-trees==0.8.3 \ - # - requests==2.28.2 \ - # - scikit-image==0.24.0 \ - # Scientific Computing - used widely - scipy==1.14.1 \ - # - shapely==2.0.2 \ - # - simple-pid==2.0.1 \ - # - simple-watchdog-timer==0.1.1 \ - # - six==1.16.0 \ - # - tabulate==0.9.0 \ - # - transforms3d==0.4.2 \ - # - xmlschema==1.0.18 \ - # distro \ - torch \ - torchvision \ - # openmim \ - # mmcv-full==1.6.0 \ - # packaging \ - # prettytable \ - # python-dateutil>=2.1 \ - # kiwisolver>=1.0.1 \ - # pyparsing>=2.0.1 \ - # cycler>=0.10 \ - # wcwidth \ - # six>=1.5 \ - # mmcls \ - easydict==1.13 \ - kiss-icp==1.0.0 \ - g2o-python==0.0.12 \ - rosbags==0.10.4 \ - ultralytics - -# Install SAM2 seperately to avoid storage space errors on GitHub Actions -RUN pip3 install sam2 - -# Install Black for Python code formatting. -RUN pip3 install black==24.10.0 - -# install mmdetection3d for 3d object detection -RUN pip3 install -U openmim -RUN mim install mmengine -RUN mim install 'mmcv>=2.0.0rc4' -RUN mim install 'mmdet>=3.0.0' -RUN mim install "mmdet3d>=1.1.0" -RUN mim install "mmpose>=1.1.0" - - -# install loop closure package "MapClosures" -# this issue was addressed here: https://github.com/abetlen/llama-cpp-python/issues/707 -# RUN python3 -m pip uninstall -y exceptiongroup -# RUN sudo apt remove python3-pathspec -y -# RUN pip3 install --no-cache-dir pathspec yamllint "exceptiongroup<=1.2.0" -# RUN git clone https://github.com/PRBonn/MapClosures.git -# WORKDIR MapClosures -# RUN cmake -B build -S cpp -# RUN cmake --build build -j8 -# RUN make - -WORKDIR / - -# https://stackoverflow.com/questions/66669735/ubuntu-20-04-cant-find-pcl-because-of-incorrect-include-directory-after-install -RUN mkdir /lib/x86_64-linux-gnu/cmake/pcl/include && ln -s /usr/include/pcl-1.10/pcl /lib/x86_64-linux-gnu/cmake/pcl/include/pcl - -# Lemon -# Library for Efficient Modeling and Optimization in Networks. -# It is a C++ template library providing efficient implementations of common data structures and algorithms -# with focus on combinatorial optimization tasks connected mainly with graphs and networks. -RUN git clone https://github.com/The-OpenROAD-Project/lemon-graph.git && cd lemon-graph \ - # This specific commit is for Lemon-1.3.1. See https://github.com/The-OpenROAD-Project/lemon-graph/commits/master. - && git checkout 62ac753 \ - && mkdir build && cd build && cmake .. && make && make install - -# RUN useradd -ms /bin/bash docker -RUN usermod -a -G dialout root -RUN usermod -a -G tty root -# USER docker - -ENV ROS_VERSION 2 - -WORKDIR /navigator -COPY ./docker/entrypoint.sh /opt/entrypoint.sh - -ENTRYPOINT [ "/opt/entrypoint.sh" ] +# Base ROS 2 Humble image with desktop + turtlesim +FROM osrf/ros:humble-desktop + +SHELL ["/bin/bash", "-lc"] + +# System deps (+ CycloneDDS RMW) +RUN apt-get update && apt-get install -y \ + python3-pip python3-tk python3-colcon-common-extensions \ + python3-ament-package \ + ros-humble-turtlesim x11-apps \ + ros-humble-rmw-cyclonedds-cpp \ + && rm -rf /var/lib/apt/lists/* + +# Pin setuptools compatible with Humble, plus wheel +RUN python3 -m pip install --no-cache-dir -U pip && \ + pip3 install --no-cache-dir 'setuptools==65.5.1' 'setuptools_scm<7' wheel + +# Overlay workspace (your packages) +WORKDIR /workspaces/overlay +COPY overlay_src/ ./src/ + +# Build overlay +RUN source /opt/ros/humble/setup.bash && \ + colcon build --merge-install + +# Entrypoint +COPY ros_entrypoint.sh /ros_entrypoint.sh +RUN chmod +x /ros_entrypoint.sh + +# DDS + X11 defaults +ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp +ENV ROS_DOMAIN_ID=23 +ENV ROS_LOCALHOST_ONLY=0 +ENV QT_X11_NO_MITSHM=1 +ENV LIBGL_ALWAYS_SOFTWARE=1 + +ENTRYPOINT ["/ros_entrypoint.sh"] +CMD ["bash"] diff --git a/Dockerfile.bak b/Dockerfile.bak new file mode 100644 index 000000000..4a9401762 --- /dev/null +++ b/Dockerfile.bak @@ -0,0 +1,56 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y locales && locale-gen en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 + +# Base deps; colcon/vcstool/rosdep via pip +RUN apt-get update && apt-get install -y \ + build-essential cmake git wget curl gnupg2 lsb-release \ + python3-pip python3-venv python3-empy python3-numpy python3-tk \ + libasio-dev libtinyxml2-dev libeigen3-dev libyaml-cpp-dev \ + qtbase5-dev libqt5widgets5 \ + libx11-6 x11-apps \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m pip install --no-cache-dir -U pip \ + && pip3 install --no-cache-dir colcon-common-extensions vcstool rosdep + +# rosdep init/update +RUN rosdep init || true && rosdep update || true + +# === Build ROS 2 Humble from source === +WORKDIR /opt/ros_ws +RUN mkdir -p src +RUN wget https://raw.githubusercontent.com/ros2/ros2/humble/ros2.repos +RUN vcs import src < ros2.repos + +# IMPORTANT: skip the ROS binary key "fastcdr" so the vendor pulls source +RUN apt-get update && \ + rosdep install --from-paths src --ignore-src -y --rosdistro humble --skip-keys "fastcdr" && \ + rm -rf /var/lib/apt/lists/* + +RUN colcon build --merge-install --cmake-args -DCMAKE_BUILD_TYPE=Release + +# === Overlay workspace (your packages) === +WORKDIR /workspaces/overlay +RUN mkdir -p src +COPY overlay_src/ src/ +RUN . /opt/ros_ws/install/setup.sh && \ + rosdep install --from-paths src --ignore-src -y --rosdistro humble || true && \ + colcon build --merge-install --cmake-args -DCMAKE_BUILD_TYPE=Release || true + +# Entrypoint +COPY ros_entrypoint.sh /ros_entrypoint.sh +RUN chmod +x /ros_entrypoint.sh + +# Env +ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp +ENV ROS_DOMAIN_ID=23 +ENV ROS_LOCALHOST_ONLY=0 +ENV QT_X11_NO_MITSHM=1 +ENV LIBGL_ALWAYS_SOFTWARE=1 + +ENTRYPOINT ["/ros_entrypoint.sh"] +CMD ["bash"] diff --git a/docker-compose.yml b/docker-compose.yml old mode 100755 new mode 100644 index 9c8c88123..9e8635def --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,140 +1,28 @@ -version: "2.3" -name: navigator services: - navigator: - image: navigator - container_name: navigator_${USER} + sim: build: context: . dockerfile: Dockerfile - - navigator_carla: - privileged: true - stdin_open: true - tty: true - runtime: nvidia - - image: navigator - container_name: navigator_carla_${USER} - - volumes: - - type: bind - source: $PWD - target: /navigator - - type: bind - source: /home/share/carla - target: /carla - - type: bind - source: /storage/bags - target: /bags - - type: bind - source: /storage/navigator_binaries - target: /navigator_binaries - - type: bind - source: ${HOME}/.Xauthority - target: /root/.Xauthority - - type: bind - source: /tmp/.X11-unix - target: /tmp/.X11-unix - # need to enable shared memory for ros to communicate between docker containers - # see: https://answers.ros.org/question/370595/ros2-foxy-nodes-cant-communicate-through-docker-container-border/ - - type: bind - source: /dev - target: /dev - #- type: bind - # source: /dev/shm - # target: /dev/shm - + platforms: ["linux/arm64"] environment: - - DISPLAY=${DISPLAY} - - ROS_DOMAIN_ID=${ROS_DOMAIN_ID} - - NVIDIA_VISIBLE_DEVICES=all - # Specify the ROS Middleware to be consistent across containers: - # see: https://discourse.ros.org/t/ros-cross-distribution-communication/27335 - - RMW_IMPLEMENTATION=rmw_fastrtps_cpp - - # deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: all - # capabilities: [gpu] - + - DISPLAY=host.docker.internal:0 + - QT_X11_NO_MITSHM=1 + - LIBGL_ALWAYS_SOFTWARE=1 + - ROS_DOMAIN_ID=23 + - ROS_LOCALHOST_ONLY=0 + command: > + bash -lc "ros2 launch turtlesim_tasks sim_bringup.launch.py" + + follower: build: context: . dockerfile: Dockerfile - - network_mode: "host" - - # On Board Computer (OBC) does not have Carla or Bags directory - # runtime nvidia has yet to be set up on the OBC - navigator_obc: - privileged: true - stdin_open: true - tty: true - - image: navigator - container_name: navigator_obc_${USER} - - volumes: - - type: bind - source: $PWD - target: /navigator - - type: bind - source: ${HOME}/.Xauthority - target: /root/.Xauthority - - type: bind - source: /tmp/.X11-unix - target: /tmp/.X11-unix - # need to enable shared memory for ros to communicate between docker containers - # see: https://answers.ros.org/question/370595/ros2-foxy-nodes-cant-communicate-through-docker-container-border/ - - type: bind - source: /dev - target: /dev - #- type: bind - # source: /dev/shm - # target: /dev/shm - + platforms: ["linux/arm64"] environment: - - DISPLAY=${DISPLAY} - - ROS_DOMAIN_ID=${ROS_DOMAIN_ID} - - NVIDIA_VISIBLE_DEVICES=all - # Specify the ROS Middleware to be consistent across containers: - # see: https://discourse.ros.org/t/ros-cross-distribution-communication/27335 - - RMW_IMPLEMENTATION=rmw_fastrtps_cpp - - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: all - capabilities: [gpu] - - build: - context: . - dockerfile: Dockerfile - - network_mode: "host" - - zenith: - extends: - file: zenith/docker-compose.yaml - service: zenith - - zenith_backend: - extends: - file: zenith/docker-compose.yaml - service: zenith_backend - - zenith_dev: - extends: - file: zenith/docker-compose.yaml - service: zenith_dev - - zenith_backend_dev: - extends: - file: zenith/docker-compose.yaml - service: zenith_backend_dev - + - DISPLAY=host.docker.internal:0 + - QT_X11_NO_MITSHM=1 + - LIBGL_ALWAYS_SOFTWARE=1 + - ROS_DOMAIN_ID=23 + - ROS_LOCALHOST_ONLY=0 + command: > + bash -lc "ros2 launch turtlesim_tasks follower_bringup.launch.py" diff --git a/overlay_src/my_interfaces/CMakeLists.txt b/overlay_src/my_interfaces/CMakeLists.txt new file mode 100644 index 000000000..18d54763d --- /dev/null +++ b/overlay_src/my_interfaces/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.5) +project(my_interfaces) +find_package(rosidl_default_generators REQUIRED) +rosidl_generate_interfaces(${PROJECT_NAME} + "msg/ChaseMode.msg" +) +ament_package() diff --git a/overlay_src/my_interfaces/msg/ChaseMode.msg b/overlay_src/my_interfaces/msg/ChaseMode.msg new file mode 100644 index 000000000..cc1bb6e08 --- /dev/null +++ b/overlay_src/my_interfaces/msg/ChaseMode.msg @@ -0,0 +1 @@ +bool chase diff --git a/overlay_src/my_interfaces/package.xml b/overlay_src/my_interfaces/package.xml new file mode 100644 index 000000000..0dc5cfad6 --- /dev/null +++ b/overlay_src/my_interfaces/package.xml @@ -0,0 +1,12 @@ + + + my_interfaces + 0.0.1 + Custom interfaces for turtlesim task. + You + Apache-2.0 + ament_cmake + rosidl_default_generators + rosidl_default_runtime + rosidl_interface_packages + diff --git a/overlay_src/turtlesim_tasks/launch/follower_bringup.launch.py b/overlay_src/turtlesim_tasks/launch/follower_bringup.launch.py new file mode 100644 index 000000000..16611b385 --- /dev/null +++ b/overlay_src/turtlesim_tasks/launch/follower_bringup.launch.py @@ -0,0 +1,8 @@ +from launch import LaunchDescription +from launch_ros.actions import Node + +def generate_launch_description(): + return LaunchDescription([ + Node(package='turtlesim_tasks', executable='mode_toggle', name='mode_toggle', output='screen'), + Node(package='turtlesim_tasks', executable='follower_node', name='follower', output='screen'), + ]) diff --git a/overlay_src/turtlesim_tasks/launch/sim_bringup.launch.py b/overlay_src/turtlesim_tasks/launch/sim_bringup.launch.py new file mode 100644 index 000000000..14a015e3e --- /dev/null +++ b/overlay_src/turtlesim_tasks/launch/sim_bringup.launch.py @@ -0,0 +1,10 @@ +from launch import LaunchDescription +from launch_ros.actions import Node + +def generate_launch_description(): + return LaunchDescription([ + Node(package='turtlesim', executable='turtlesim_node', name='turtlesim'), + Node(package='turtlesim_tasks', executable='spawn_second', name='spawn_second', output='screen'), + Node(package='turtlesim_tasks', executable='mouse_goal_publisher', name='mouse_goal', output='screen'), + Node(package='turtlesim_tasks', executable='turtle1_controller', name='turtle1_controller', output='screen'), + ]) diff --git a/overlay_src/turtlesim_tasks/package.xml b/overlay_src/turtlesim_tasks/package.xml new file mode 100644 index 000000000..ae9a20511 --- /dev/null +++ b/overlay_src/turtlesim_tasks/package.xml @@ -0,0 +1,23 @@ + + + turtlesim_tasks + 0.0.1 + Custom nodes and launches for the turtlesim task. + You + Apache-2.0 + + + ament_python + + + rclpy + turtlesim + geometry_msgs + std_msgs + my_interfaces + python3-tk + + + ament_python + + diff --git a/overlay_src/turtlesim_tasks/resource/turtlesim_tasks b/overlay_src/turtlesim_tasks/resource/turtlesim_tasks new file mode 100644 index 000000000..b0c7601c5 --- /dev/null +++ b/overlay_src/turtlesim_tasks/resource/turtlesim_tasks @@ -0,0 +1 @@ +turtlesim_tasks diff --git a/overlay_src/turtlesim_tasks/setup.cfg b/overlay_src/turtlesim_tasks/setup.cfg new file mode 100644 index 000000000..99324e812 --- /dev/null +++ b/overlay_src/turtlesim_tasks/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script-dir=$base/lib/turtlesim_tasks +[install] +install-scripts=$base/lib/turtlesim_tasks diff --git a/overlay_src/turtlesim_tasks/setup.py b/overlay_src/turtlesim_tasks/setup.py new file mode 100644 index 000000000..528a60178 --- /dev/null +++ b/overlay_src/turtlesim_tasks/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup +package_name = 'turtlesim_tasks' +setup( + name=package_name, + version='0.0.1', + packages=[package_name], + data_files=[ + ('share/ament_index/resource_index/packages', ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ('share/' + package_name + '/launch', [ + 'launch/sim_bringup.launch.py', + 'launch/follower_bringup.launch.py' + ]), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='You', + maintainer_email='you@example.com', + description='Turtlesim task nodes', + license='Apache-2.0', + entry_points={ + 'console_scripts': [ + 'mouse_goal_publisher = turtlesim_tasks.mouse_goal_publisher:main', + 'turtle1_controller = turtlesim_tasks.turtle1_controller:main', + 'spawn_second = turtlesim_tasks.spawn_second:main', + 'follower_node = turtlesim_tasks.follower_node:main', + 'mode_toggle = turtlesim_tasks.mode_toggle:main', + ], + }, +) diff --git a/overlay_src/turtlesim_tasks/turtlesim_tasks/__init__.py b/overlay_src/turtlesim_tasks/turtlesim_tasks/__init__.py new file mode 100644 index 000000000..1bb8bf6d7 --- /dev/null +++ b/overlay_src/turtlesim_tasks/turtlesim_tasks/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/overlay_src/turtlesim_tasks/turtlesim_tasks/follower_node.py b/overlay_src/turtlesim_tasks/turtlesim_tasks/follower_node.py new file mode 100644 index 000000000..f342e295d --- /dev/null +++ b/overlay_src/turtlesim_tasks/turtlesim_tasks/follower_node.py @@ -0,0 +1,64 @@ +import math +import rclpy +from rclpy.node import Node +from geometry_msgs.msg import Twist +from turtlesim.msg import Pose +from my_interfaces.msg import ChaseMode + +def norm_angle(a): + while a > math.pi: a -= 2*math.pi + while a < -math.pi: a += 2*math.pi + return a + +class Follower(Node): + def __init__(self): + super().__init__('follower_node') + self.pose1 = None + self.pose2 = None + self.chase = True + self.pub = self.create_publisher(Twist, '/turtle2/cmd_vel', 10) + self.sub1 = self.create_subscription(Pose, '/turtle1/pose', self.on_pose1, 10) + self.sub2 = self.create_subscription(Pose, '/turtle2/pose', self.on_pose2, 10) + self.subm = self.create_subscription(ChaseMode, '/chase_mode', self.on_mode, 10) + self.timer = self.create_timer(0.03, self.loop) + self.k_lin = 2.0 + self.k_ang = 6.0 + self.close_thresh = 0.5 + self.far_thresh = 6.0 + + def on_pose1(self, msg): self.pose1 = msg + def on_pose2(self, msg): self.pose2 = msg + def on_mode(self, msg): + self.chase = bool(msg.chase) + self.get_logger().info(f"Follower mode -> {'CHASE' if self.chase else 'RUN AWAY'}") + + def loop(self): + cmd = Twist() + if self.pose1 is None or self.pose2 is None: + self.pub.publish(cmd); return + dx = self.pose1.x - self.pose2.x + dy = self.pose1.y - self.pose2.y + dist = math.hypot(dx, dy) + target_theta = math.atan2(dy, dx) + if self.chase: + if dist > self.close_thresh: + ang_err = norm_angle(target_theta - self.pose2.theta) + cmd.linear.x = max(min(self.k_lin * dist, 2.0), -2.0) + cmd.angular.z = max(min(self.k_ang * ang_err, 4.0), -4.0) + else: + if dist < self.far_thresh: + away_theta = norm_angle(target_theta + math.pi) + ang_err = norm_angle(away_theta - self.pose2.theta) + cmd.linear.x = 2.0 + cmd.angular.z = max(min(self.k_ang * ang_err, 4.0), -4.0) + self.pub.publish(cmd) + +def main(): + rclpy.init() + node = Follower() + rclpy.spin(node) + node.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/overlay_src/turtlesim_tasks/turtlesim_tasks/mode_toggle.py b/overlay_src/turtlesim_tasks/turtlesim_tasks/mode_toggle.py new file mode 100644 index 000000000..4bada5c00 --- /dev/null +++ b/overlay_src/turtlesim_tasks/turtlesim_tasks/mode_toggle.py @@ -0,0 +1,30 @@ +import rclpy +from rclpy.node import Node +from my_interfaces.msg import ChaseMode + +class ModeToggle(Node): + def __init__(self): + super().__init__('mode_toggle') + self.pub = self.create_publisher(ChaseMode, '/chase_mode', 10) + self.mode = True + self.timer = self.create_timer(10.0, self.flip) + self.publish_now() + + def publish_now(self): + msg = ChaseMode(chase=self.mode) + self.pub.publish(msg) + self.get_logger().info(f"Mode: {'CHASE' if self.mode else 'RUN AWAY'}") + + def flip(self): + self.mode = not self.mode + self.publish_now() + +def main(): + rclpy.init() + node = ModeToggle() + rclpy.spin(node) + node.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/overlay_src/turtlesim_tasks/turtlesim_tasks/mouse_goal_publisher.py b/overlay_src/turtlesim_tasks/turtlesim_tasks/mouse_goal_publisher.py new file mode 100644 index 000000000..83a15ba9a --- /dev/null +++ b/overlay_src/turtlesim_tasks/turtlesim_tasks/mouse_goal_publisher.py @@ -0,0 +1,46 @@ +import rclpy +from rclpy.node import Node +from geometry_msgs.msg import Point +import tkinter as tk + +WORLD = 11.0 +PIXELS = 550 +SCALE = WORLD / PIXELS + +class MouseGoalPublisher(Node): + def __init__(self): + super().__init__('mouse_goal_publisher') + self.pub = self.create_publisher(Point, '/mouse_goal', 10) + self.get_logger().info('Mouse window: click to set a target for turtle1') + self.root = tk.Tk() + self.root.title("Click-to-move (maps to turtlesim 11x11)") + self.canvas = tk.Canvas(self.root, width=PIXELS, height=PIXELS, bg="white") + self.canvas.pack() + self.canvas.bind("", self.on_click) + self.timer = self.create_timer(0.02, self._tk_poll) + + def _tk_poll(self): + self.root.update_idletasks() + self.root.update() + + def on_click(self, event): + x = event.x * SCALE + y = (PIXELS - event.y) * SCALE + msg = Point(x=x, y=y, z=0.0) + self.pub.publish(msg) + self.get_logger().info(f"Clicked -> goal ({x:.2f}, {y:.2f})") + r = 4 + self.canvas.create_oval(event.x-r, event.y-r, event.x+r, event.y+r) + +def main(): + rclpy.init() + node = MouseGoalPublisher() + try: + rclpy.spin(node) + except tk.TclError: + pass + node.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/overlay_src/turtlesim_tasks/turtlesim_tasks/spawn_second.py b/overlay_src/turtlesim_tasks/turtlesim_tasks/spawn_second.py new file mode 100644 index 000000000..566bce14f --- /dev/null +++ b/overlay_src/turtlesim_tasks/turtlesim_tasks/spawn_second.py @@ -0,0 +1,36 @@ +import rclpy +from rclpy.node import Node +from turtlesim.srv import Spawn + +class Spawner(Node): + def __init__(self): + super().__init__('spawn_second') + self.cli = self.create_client(Spawn, '/spawn') + self.timer = self.create_timer(0.5, self.try_spawn) + + def try_spawn(self): + if not self.cli.wait_for_service(timeout_sec=0.1): + self.get_logger().info('Waiting for /spawn...') + return + req = Spawn.Request() + req.x = 8.0; req.y = 8.0; req.theta = 0.0; req.name = 'turtle2' + fut = self.cli.call_async(req) + fut.add_done_callback(self._done) + self.timer.cancel() + + def _done(self, fut): + try: + res = fut.result() + self.get_logger().info(f"Spawned: {res.name}") + except Exception as e: + self.get_logger().error(f"Spawn failed: {e}") + +def main(): + rclpy.init() + node = Spawner() + rclpy.spin(node) + node.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/overlay_src/turtlesim_tasks/turtlesim_tasks/turtle1_controller.py b/overlay_src/turtlesim_tasks/turtlesim_tasks/turtle1_controller.py new file mode 100644 index 000000000..2eca66e55 --- /dev/null +++ b/overlay_src/turtlesim_tasks/turtlesim_tasks/turtle1_controller.py @@ -0,0 +1,51 @@ +import math +import rclpy +from rclpy.node import Node +from geometry_msgs.msg import Twist, Point +from turtlesim.msg import Pose + +def norm_angle(a): + while a > math.pi: a -= 2*math.pi + while a < -math.pi: a += 2*math.pi + return a + +class Turtle1Controller(Node): + def __init__(self): + super().__init__('turtle1_controller') + self.goal = None + self.pose = None + self.pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10) + self.sub_pose = self.create_subscription(Pose, '/turtle1/pose', self.on_pose, 10) + self.sub_goal = self.create_subscription(Point, '/mouse_goal', self.on_goal, 10) + self.timer = self.create_timer(0.03, self.loop) + self.k_lin = 2.0 + self.k_ang = 6.0 + self.stop_dist = 0.15 + + def on_pose(self, msg): self.pose = msg + def on_goal(self, msg): self.goal = (msg.x, msg.y) + + def loop(self): + if self.pose is None or self.goal is None: + self.pub.publish(Twist()) + return + dx = self.goal[0] - self.pose.x + dy = self.goal[1] - self.pose.y + dist = math.hypot(dx, dy) + target_theta = math.atan2(dy, dx) + ang_err = norm_angle(target_theta - self.pose.theta) + cmd = Twist() + if dist > self.stop_dist: + cmd.linear.x = max(min(self.k_lin * dist, 2.0), -2.0) + cmd.angular.z = max(min(self.k_ang * ang_err, 4.0), -4.0) + self.pub.publish(cmd) + +def main(): + rclpy.init() + node = Turtle1Controller() + rclpy.spin(node) + node.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/ros_entrypoint.sh b/ros_entrypoint.sh new file mode 100755 index 000000000..b46608a90 --- /dev/null +++ b/ros_entrypoint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e +source /opt/ros/humble/setup.bash +if [ -f /workspaces/overlay/install/setup.bash ]; then + source /workspaces/overlay/install/setup.bash +fi +export DISPLAY="${DISPLAY:-host.docker.internal:0}" +exec "$@"