From d7b80dd598c37e44b98d7a9c99a2c4338d5727e6 Mon Sep 17 00:00:00 2001 From: Hector Cruz <37127013+hect95@users.noreply.github.com> Date: Thu, 2 May 2024 19:39:20 +0100 Subject: [PATCH] Initial configuration for bag recording and foxglove bridge (#1) This PR provides an initial configuration/setup for the ros2 bag recording and foxglove bridge from a docker container. - Two types of scripts directories are introduced `host_tools` and `container_tools`. As it names implies `host_tools`provides scripts that are intended for host usage, i.e outside docker container. Moreover `container_tools`, offer scripts that are needed inside the docker container. The summary of the scripts states as follows: - `host_tools`: - `upload_docker_image_to_vehicle.sh`: Receive a docker image as input, to save it with `docker save` and send it over the vehicle uisng `scp`. Docker cmd are used for bash autocomplate, thus after doing "tab tab" method, the only docker images available in your system will be displayed - `install_tool.sh`: Install the upload docker script and autocompletion. - `container_tools`: - `foxglove_bridge.sh`: Runs foxglove_bridge launch file with some arguments - `record_rosbag.sh`: Runs `ros2 bag record` cmd to allow `mcap` bag format. Optionally a `.txt` file can be passed as argument to define record-only topics. - Docker: The docker build implements dev and runtime build stages. Majority of the install relies on `base` stage. The default command for both sages are `bash` and `record_rosbag` respectively - `dev.sh` and `runtime.sh` scripts are provided to ease development and testing. By default BOTH scripts create a `rosbag` dir if it does not exists and mount it to the dockercontainer --- .github/workflows/docker.yml | 52 ++++++ .gitignore | 2 + .pre-commit-config.yaml | 170 ++++++++++++++++++ Dockerfile | 72 ++++++++ config/sensor_topics.txt | 67 +++++++ dev.sh | 23 +++ runtime.sh | 29 +++ scripts/container_tools/foxglove_bridge.sh | 4 + scripts/container_tools/record_rosbag.sh | 44 +++++ scripts/host_tools/install_tools.sh | 45 +++++ .../upload_docker_image_to_vehicle.sh | 72 ++++++++ 11 files changed, 580 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 Dockerfile create mode 100644 config/sensor_topics.txt create mode 100755 dev.sh create mode 100755 runtime.sh create mode 100755 scripts/container_tools/foxglove_bridge.sh create mode 100755 scripts/container_tools/record_rosbag.sh create mode 100755 scripts/host_tools/install_tools.sh create mode 100755 scripts/host_tools/upload_docker_image_to_vehicle.sh diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..b4591ad --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,52 @@ +name: Create and publish a Docker image + +on: + push: + tags: ["*"] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + ROS_DISTRO: humble + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: Dockerfile + target: runtime + build-args: ROS_DISTRO=${{ env.ROS_DISTRO }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a460de7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore ROS bag dir +rosbags/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3358cc8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,170 @@ + +# To use: +# +# pre-commit run -a +# +# Or: +# +# pre-commit install # (runs every time you commit in git) +# +# To update this file: +# +# pre-commit autoupdate +# +# See https://github.com/pre-commit/pre-commit + +repos: + # Standard hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-docstring-first + - id: check-merge-conflict + - id: check-symlinks + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + exclude_types: [rst] + - id: fix-byte-order-marker + + + # Python hooks + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.2 + hooks: + - id: pyupgrade + args: [--py36-plus] + + # PyDocStyle + - repo: https://github.com/PyCQA/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + args: ["--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D203,D212,D404"] + + - repo: https://github.com/psf/black + rev: 24.4.0 + hooks: + - id: black + args: ["--line-length=99"] + + - repo: https://github.com/pycqa/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + args: ["--extend-ignore=E501"] + + # CPP hooks + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.3 + hooks: + - id: clang-format + args: ['-fallback-style=none', '-i'] + + - repo: local + hooks: + - id: ament_cppcheck + name: ament_cppcheck + description: Static code analysis of C/C++ files. + entry: env AMENT_CPPCHECK_ALLOW_SLOW_VERSIONS=1 ament_cppcheck + language: system + files: \.(h\+\+|h|hh|hxx|hpp|cuh|c|cc|cpp|cu|c\+\+|cxx|tpp|txx)$ + + - repo: local + hooks: + - id: ament_cpplint + name: ament_cpplint + description: Static code analysis of C/C++ files. + entry: ament_cpplint + language: system + files: \.(h\+\+|h|hh|hxx|hpp|cuh|c|cc|cpp|cu|c\+\+|cxx|tpp|txx)$ + args: ["--linelength=100", "--filter=-whitespace/newline"] + + # Docs - RestructuredText hooks + - repo: https://github.com/PyCQA/doc8 + rev: v1.1.1 + hooks: + - id: doc8 + args: ['--max-line-length=100', '--ignore=D001'] + exclude: CHANGELOG\.rst$ + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks + exclude: CHANGELOG\.rst$ + - id: rst-directive-colons + - id: rst-inline-touching-normal + + # Spellcheck in comments and docs + # skipping of *.svg files is not working... + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: ['--write-changes', '--uri-ignore-words-list=ist', '-L manuel,ned'] + exclude: CHANGELOG\.rst|\.(svg|pyc|drawio|dae)$ + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.28.2 + hooks: + - id: check-github-workflows + args: ["--verbose"] + - id: check-github-actions + args: ["--verbose"] + - id: check-dependabot + args: ["--verbose"] + + # Bash prettify + - repo: https://github.com/lovesegfault/beautysh + rev: v6.2.1 + hooks: + - id: beautysh + + # ROS checks + - repo: https://github.com/tier4/pre-commit-hooks-ros + rev: v0.8.0 + hooks: + - id: flake8-ros + - id: prettier-xacro + - id: prettier-launch-xml + - id: prettier-package-xml + - id: ros-include-guard + - id: sort-package-xml + + - repo: https://github.com/AleksaC/hadolint-py + rev: v2.12.1-beta + hooks: + - id: hadolint + args: ['--ignore=DL3008'] + + - repo: https://github.com/cheshirekow/cmake-format-precommit + rev: v0.6.13 + hooks: + - id: cmake-format + + - repo: https://github.com/Takishima/cmake-pre-commit-hooks + rev: v1.9.5 + hooks: + - id: clang-format + - id: clang-tidy + args: [--checks=readability-magic-numbers,--warnings-as-errors=*] + - id: cppcheck + - id: include-what-you-use + +ci: + autofix_commit_msg: | + [pre-commit.ci] auto fixes from pre-commit.com hooks + + for more information, see https://pre-commit.ci + autofix_prs: false + autoupdate_branch: '' + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: weekly + submodules: false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8b7770a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +FROM ros:humble-ros-base-jammy AS base + +# Install key dependencies +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive \ + apt-get -y --quiet --no-install-recommends install \ + ros-"$ROS_DISTRO"-rosbag2-storage-mcap \ + ros-"$ROS_DISTRO"-mcap-vendor \ + ros-"$ROS_DISTRO"-foxglove-bridge \ + ros-"$ROS_DISTRO"-rmw-cyclonedds-cpp \ + ros-"$ROS_DISTRO"-can-msgs \ + ros-"$ROS_DISTRO"-dataspeed-ulc-msgs \ + ros-"$ROS_DISTRO"-dbw-ford-msgs \ + ros-"$ROS_DISTRO"-gps-msgs \ + ros-"$ROS_DISTRO"-ouster-msgs \ + ros-"$ROS_DISTRO"-velodyne-msgs \ + ros-"$ROS_DISTRO"-flir-camera-msgs \ + ros-"$ROS_DISTRO"-microstrain-inertial-msgs \ + ros-"$ROS_DISTRO"-novatel-gps-msgs \ + ros-"$ROS_DISTRO"-radar-msgs \ + && rm -rf /var/lib/apt/lists/* + +# Setup ROS workspace folder +ENV ROS_WS /opt/ros_ws +WORKDIR $ROS_WS + +# Copy tools scripts and config +COPY scripts/container_tools $ROS_WS/container_tools +COPY config $ROS_WS/config + +# Add tools to PATH +RUN echo "export PATH=$ROS_WS/container_tools:$PATH " >> /root/.bashrc &&\ + # Add sourcing local workspace command to bashrc for + # convenience when running interactively + echo "source /opt/ros/$ROS_DISTRO/setup.bash" >> /root/.bashrc + +# ----------------------------------------------------------------------- + +FROM base AS prebuilt + +# Nothing to build from source + +# ----------------------------------------------------------------------- + +FROM base AS dev + +# Install basic dev tools (And clean apt cache afterwards) +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive \ + apt-get -y --quiet --no-install-recommends install \ + # Command-line editor + nano \ + # Ping network tools + inetutils-ping \ + # Bash auto-completion for convenience + bash-completion \ + && rm -rf /var/lib/apt/lists/* + +# Add colcon build alias for convenience +RUN echo 'alias colcon_build="colcon build --symlink-install \ + --cmake-args -DCMAKE_BUILD_TYPE=Release && \ + source install/setup.bash"' >> /root/.bashrc + +# Enter bash for clvelopment +CMD ["bash"] + +# ----------------------------------------------------------------------- + +FROM base as runtime + +# Start recording a rosbag by default +CMD ["/opt/ros_ws/container_tools/record_rosbag.sh"] diff --git a/config/sensor_topics.txt b/config/sensor_topics.txt new file mode 100644 index 0000000..9fbe777 --- /dev/null +++ b/config/sensor_topics.txt @@ -0,0 +1,67 @@ +/cameras/fsp_l/camera_info +/cameras/fsp_l/control +/cameras/fsp_l/image_raw +/cameras/fsp_l/meta +/cameras/lspf_r/camera_info +/cameras/lspf_r/control +/cameras/lspf_r/image_raw +/cameras/lspf_r/meta +/cameras/lspr_l/camera_info +/cameras/lspr_l/control +/cameras/lspr_l/image_raw +/cameras/lspr_l/meta +/cameras/rsp_l/camera_info +/cameras/rsp_l/control +/cameras/rsp_l/image_raw +/cameras/rsp_l/meta +/cameras/rspf_l/camera_info +/cameras/rspf_l/control +/cameras/rspf_l/image_raw +/cameras/rspf_l/meta +/cameras/rspr_r/camera_info +/cameras/rspr_r/control +/cameras/rspr_r/image_raw +/cameras/rspr_r/meta +/diagnostics +/ekf/status +/imu/data +/joint_states +/output/radar/bumper_front_centre/far/image +/output/radar/bumper_front_centre/near/image +/output/radar/bumper_front_left/far/image +/output/radar/bumper_front_left/near/image +/output/radar/bumper_front_right/far/image +/output/radar/bumper_front_right/near/image +/output/radar/bumper_rear_centre/far/image +/output/radar/bumper_rear_centre/near/image +/output/radar/roof_left_front/far/image +/output/radar/roof_left_front/near/image +/output/radar/roof_left_rear/far/image +/output/radar/roof_left_rear/near/image +/output/radar/roof_right_front/far/image +/output/radar/roof_right_front/near/image +/output/radar/roof_right_rear/far/image +/output/radar/roof_right_rear/near/image +/parameter_events +/robot_description +/rosout +/sensor/gps/rear/bestpos +/sensor/gps/rear/bestvel +/sensor/gps/rear/corrimudata +/sensor/gps/rear/gps_fix +/sensor/gps/rear/gps_sync +/sensor/gps/rear/imu +/sensor/gps/rear/inscov +/sensor/gps/rear/inspva +/sensor/gps/rear/inspvax +/sensor/gps/rear/insstdev +/sensor/gps/rear/nav_sat_fix +/sensor/gps/rear/psrdop2 +/sensor/lidar/left/points +/sensor/lidar/left/velodyne_packets +/sensor/lidar/right/points +/sensor/lidar/right/velodyne_packets +/sensor/lidar/top/ouster_driver/transition_event +/sensor/lidar/top/points +/tf +/tf_static diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..7a9a2c6 --- /dev/null +++ b/dev.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# ---------------------------------------------------------------- +# Build docker dev stage and add local code for live development +# ---------------------------------------------------------------- + +# Build docker image up to dev stage +DOCKER_BUILDKIT=1 docker build \ + -t av_tools_humble:latest \ + -f Dockerfile --target dev . + +# Get the absolute path of the script +SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") + +# Create a dir to store rosbags +mkdir -p rosbags + +# Run docker image with local code volumes for development +docker run -it --rm --net host --privileged \ + -v /dev/shm:/dev/shm \ + -v $SCRIPT_DIR/rosbags:/opt/ros_ws/rosbags \ + -v $SCRIPT_DIR/scripts/container_tools:/opt/ros_ws/container_tools \ + -v $SCRIPT_DIR/config:/opt/ros_ws/config \ + av_tools_humble:latest diff --git a/runtime.sh b/runtime.sh new file mode 100755 index 0000000..4934378 --- /dev/null +++ b/runtime.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# --------------------------------------------------------------------------- +# Build docker image and run ROS code for runtime or interactively with bash +# --------------------------------------------------------------------------- + +# Initialise CMD as empty +CMD="" + +# If an arg is defined, start container with bash +if [ -n "$1" ]; then + CMD="bash" +fi + +# Build docker image only up to base stage +DOCKER_BUILDKIT=1 docker build \ + -t av_tools_humble:latest \ + -f Dockerfile --target runtime . + +# Get the absolute path of the script +SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") + +# Create a dir to store rosbags +mkdir -p $SCRIPT_DIR/rosbags + +# Run docker image +docker run -it --rm --net host --privileged \ + -v /dev/shm:/dev/shm \ + -v $SCRIPT_DIR/rosbags:/opt/ros_ws/rosbags \ + av_tools_humble:latest $CMD diff --git a/scripts/container_tools/foxglove_bridge.sh b/scripts/container_tools/foxglove_bridge.sh new file mode 100755 index 0000000..6c2ed0c --- /dev/null +++ b/scripts/container_tools/foxglove_bridge.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +ros2 launch foxglove_bridge foxglove_bridge_launch.xml \ + address:=172.31.0.1 send_buffer_limit:=1000000000 num_threads:=4 diff --git a/scripts/container_tools/record_rosbag.sh b/scripts/container_tools/record_rosbag.sh new file mode 100755 index 0000000..946bb74 --- /dev/null +++ b/scripts/container_tools/record_rosbag.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Log colors +CYAN="\033[0;36m" +MAGENTA="\033[0;35m" +NO_COLOR="\033[0m" + +# Define the output directory +OUTPUT_DIR="$ROS_WS/rosbags" + +# Generate a date and time prefix in the format YYYY_MM_DD-HH_MM_SS +DATE_PREFIX=$(date "+%Y_%m_%d-%H_%M_%S") + +# Define default behavior if no file is provided +if [ "$#" -eq 0 ]; then + echo -e "No topics list file provided. ${CYAN}Recording all topics.${NO_COLOR}" + ros2 bag record -s mcap --all -o "$OUTPUT_DIR/${DATE_PREFIX}_sensor_recording" + exit 0 +elif [ "$#" -ne 1 ]; then + echo "Usage: $0 [path-to-topics-list-file]" + exit 1 +fi + +# Path to the topics list file +TOPICS_LIST_FILE="$1" + +# Check if the topics list file exists +if [ ! -f "$TOPICS_LIST_FILE" ]; then + echo "Error: File '$TOPICS_LIST_FILE' not found!" + exit 1 +fi + +# Read topics into an array +readarray -t TOPICS < "$TOPICS_LIST_FILE" + +# Start recording the topics +if [ ${#TOPICS[@]} -eq 0 ]; then + echo "No topics found in the file. Stopping recording." + exit 1 +else + echo -e "Recording topcis from ${MAGENTA}$TOPICS_LIST_FILE${NO_COLOR}" + ros2 bag record -s mcap --max-cache-size 1048576000 --ignore-leaf-topics \ + -o "$OUTPUT_DIR/${DATE_PREFIX}_sensor_recording" "${TOPICS[@]}" +fi diff --git a/scripts/host_tools/install_tools.sh b/scripts/host_tools/install_tools.sh new file mode 100755 index 0000000..d17bdc6 --- /dev/null +++ b/scripts/host_tools/install_tools.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Path to the autocompletion script +COMPLETION_SCRIPT="$HOME/.upload_docker_image_to_vehicle_completion.sh" + +# Create the autocompletion script if it doesn't exist +if [ ! -f "$COMPLETION_SCRIPT" ]; then + cat <<'EOF' > "$COMPLETION_SCRIPT" +# Autocompletion function for uploading Docker images +_upload_docker_image_to_vehicle_completion() { + local cur=${COMP_WORDS[COMP_CWORD]} + local imgs=$(docker images --format "{{.Repository}}:{{.Tag}}" | tr '\n' ' ') + COMPREPLY=($(compgen -W "${imgs}" -- $cur)) +} + +complete -F _upload_docker_image_to_vehicle_completion upload_docker_image_to_vehicle.sh +EOF + echo "Autocompletion script created at $COMPLETION_SCRIPT" +else + echo "Autocompletion script already exists at $COMPLETION_SCRIPT" +fi + +# Check if .bashrc already sources the autocompletion script +if ! grep -q "source $COMPLETION_SCRIPT" "$HOME/.bashrc"; then + echo "source $COMPLETION_SCRIPT" >> "$HOME/.bashrc" + echo "Autocompletion script sourced in .bashrc" +else + echo "Autocompletion script already sourced in .bashrc" +fi + +# Get the absolute directory name of the script +SCRIPT_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")") + +# Check if .bashrc already has the updated PATH +# Use grep with the pattern properly quoted +if ! grep -q "export PATH=.*$SCRIPT_DIR.*" "$HOME/.bashrc"; then + echo "export PATH=\"$SCRIPT_DIR:\$PATH\"" >> ~/.bashrc + echo "Docker upload script path added to .bashrc" +else + echo "Docker upload script path already in .bashrc" +fi + +echo "Vehicle upload script installed successfully." +echo "Please source your .bashrc or restart your terminal to apply changes" +echo " source ~/.bashrc" diff --git a/scripts/host_tools/upload_docker_image_to_vehicle.sh b/scripts/host_tools/upload_docker_image_to_vehicle.sh new file mode 100755 index 0000000..47cf0eb --- /dev/null +++ b/scripts/host_tools/upload_docker_image_to_vehicle.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# Configuration variables +VEHICLE_USER="seb_adm" +VEHICLE_HOST="core5.vp.five.ai" +REMOTE_DIR="/data_ssd/rad/tartan_docker_images/" + +# Directory to save tar files locally +LOCAL_SAVE_DIR="$HOME/tartan_docker_images" + +# ANSI colors +GREEN="\033[0;32m" +CYAN="\033[0;36m" +MAGENTA="\033[0;35m" +BMAGENTA="\033[1;35m" +YELLOW="\033[1;33m" +WHITE="\033[1;37m" +RED="\033[0;31m" +BLUE="\033[0;34m" +NO_COLOR="\033[0m" + +# Check if the image name was provided as an argument +if [ $# -ne 1 ]; then + echo -e "${YELLOW}Usage: $0 ${NO_COLOR}" + exit 1 +fi + +IMAGE_NAME=$1 +TAR_FILE="${IMAGE_NAME//[:\/]/_}.tar" # Create a filename from image name by replacing potential problematic characters + +# Ensure the local directory for saving Docker tar files exists +if [ ! -d "$LOCAL_SAVE_DIR" ]; then + echo -e "${CYAN}Creating directory $LOCAL_SAVE_DIR...${NO_COLOR}" + mkdir -p "$LOCAL_SAVE_DIR" +fi + +# Full path for the tar file +FULL_TAR_PATH="$LOCAL_SAVE_DIR/$TAR_FILE" + +# Check if the tar file already exists +if [ -f "$FULL_TAR_PATH" ]; then + echo -e "${WHITE}File ${CYAN}$FULL_TAR_PATH ${WHITE}already exists. Do you want to overwrite?${NO_COLOR}" + read -p "(y/N): " response + case "$response" in + [yY][eE][sS]|[yY]) + echo -e "${YELLOW}Overwriting the file...${NO_COLOR}" + echo -e "${WHITE}Saving Docker image ${MAGENTA}$IMAGE_NAME ${WHITE}to a tar file at ${CYAN}$FULL_TAR_PATH...${NO_COLOR}" + docker save $IMAGE_NAME > "$FULL_TAR_PATH" + ;; + *) + echo -e "${YELLOW}Not overwriting the file. Proceeding with upload...${NO_COLOR}" + ;; + esac +else + echo -e "${CYAN}Saving Docker image $IMAGE_NAME to a tar file at $FULL_TAR_PATH...${NO_COLOR}" + docker save $IMAGE_NAME > "$FULL_TAR_PATH" +fi + +# Use rsync to transfer the tar file to the remote directory +echo -e "${WHITE}Transferring ${CYAN}$FULL_TAR_PATH ${WHITE}to ${BLUE}$VEHICLE_USER@$VEHICLE_HOST:$REMOTE_DIR${NO_COLOR}" +rsync "$FULL_TAR_PATH" "${VEHICLE_USER}@${VEHICLE_HOST}:${REMOTE_DIR}" + +# Check if rsync succeeded +if [ $? -ne 0 ]; then + echo -e "${RED}Failed to transfer file. Unable to reach host or other transfer error.${NO_COLOR}" + # Handle error as needed + exit 1 +else + echo -e "${WHITE}Image ${MAGENTA}$IMAGE_NAME ${WHITE}uploaded successfully to ${BLUE}$VEHICLE_HOST.${NO_COLOR}" + echo -e "Run \`docker load -i ${REMOTE_DIR}\` on remote" + +fi