#!/bin/bash SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORKSPACE_ROOT="${SCRIPT_DIR}" DEBUG_LOG_FILE="${WORKSPACE_ROOT}/trae-debug-log-runros-shell-loop.ndjson" ROS_LOG_ROOT="${WORKSPACE_ROOT}/log/ros2" DEFAULT_LAUNCH_PACKAGE="odin_ros_driver" DEFAULT_LAUNCH_FILE="odin1_ros2.launch.py" # Function: print_info # Input: # - $1: message text. # Output: # - Prints an informational message to stdout. # Description: # - Standardizes normal runtime messages for this script. # References: # - None. print_info() { echo "[INFO] $1" } # Function: print_warn # Input: # - $1: warning text. # Output: # - Prints a warning message to stdout. # Description: # - Standardizes warning messages for this script. # References: # - None. print_warn() { echo "[WARN] $1" } # Function: print_error # Input: # - $1: error text. # Output: # - Prints an error message to stderr. # Description: # - Standardizes error messages for this script. # References: # - None. print_error() { echo "[ERROR] $1" >&2 } # Function: write_debug_log # Input: # - $1: event name. # - $2: event detail text. # Output: # - Appends one NDJSON debug record to the local debug log file. # Description: # - Records runtime evidence for script invocation mode, environment loading, # and interactive shell handoff without changing business behavior. # References: # - Uses variables `DEBUG_LOG_FILE`, `WORKSPACE_ROOT`, and `ROS_DISTRO` # defined in this file. # - Called by `load_ros2_environment()`, `load_workspace_environment()`, # `start_interactive_shell()`, and `main()` in this file. write_debug_log() { local event_name="$1" local event_detail="$2" local invoke_mode="executed" if is_script_sourced; then invoke_mode="sourced" fi printf '{"ts":"%s","event":"%s","detail":"%s","pid":"%s","ppid":"%s","mode":"%s","shell":"%s","workspace":"%s","ros_distro":"%s"}\n' \ "$(date '+%Y-%m-%dT%H:%M:%S%z')" \ "${event_name}" \ "${event_detail}" \ "$$" \ "$PPID" \ "${invoke_mode}" \ "${SHELL:-/bin/bash}" \ "${WORKSPACE_ROOT}" \ "${ROS_DISTRO:-unset}" >> "${DEBUG_LOG_FILE}" } # Function: is_script_sourced # Input: # - None. # Output: # - Returns 0 when the script is sourced. # - Returns 1 when the script is executed directly. # Description: # - Detects whether the current script is loaded into the caller shell or # started as a standalone process. # References: # - Uses bash built-in variables `${BASH_SOURCE[0]}` and `${0}`. is_script_sourced() { [[ "${BASH_SOURCE[0]}" != "${0}" ]] } # Function: find_ros2_setup # Input: # - None. # Output: # - Prints the absolute path of the detected ROS 2 `setup.bash`. # - Returns 1 if no ROS 2 installation is found. # Description: # - Prefers the current `ROS_DISTRO` when available, otherwise probes common # ROS 2 distributions from newer to older. # References: # - Uses environment variable `ROS_DISTRO`. # - Searches under `/opt/ros//setup.bash`. find_ros2_setup() { local ros2_setup="" local distros=("jazzy" "iron" "humble" "galactic" "foxy" "rolling") local distro="" if [ -n "${ROS_DISTRO}" ] && [ -f "/opt/ros/${ROS_DISTRO}/setup.bash" ]; then echo "/opt/ros/${ROS_DISTRO}/setup.bash" return 0 fi for distro in "${distros[@]}"; do if [ -f "/opt/ros/${distro}/setup.bash" ]; then ros2_setup="/opt/ros/${distro}/setup.bash" echo "${ros2_setup}" return 0 fi done return 1 } # Function: load_ros2_environment # Input: # - None. # Output: # - Returns 0 when the ROS 2 environment is sourced successfully. # - Returns 1 when no valid ROS 2 environment is found. # Description: # - Locates and loads the base ROS 2 environment required by the workspace. # References: # - Calls `find_ros2_setup()` in this file. # - Sources `/opt/ros//setup.bash`. load_ros2_environment() { local ros2_setup="" ros2_setup="$(find_ros2_setup)" || { write_debug_log "load_ros2_environment_failed" "no_ros2_setup_found" print_error "No ROS 2 installation was found under /opt/ros." return 1 } # shellcheck disable=SC1090 source "${ros2_setup}" write_debug_log "load_ros2_environment" "${ros2_setup}" print_info "Loaded ROS 2 environment: ${ros2_setup}" return 0 } # Function: load_workspace_environment # Input: # - None. # Output: # - Returns 0 after attempting to load the workspace environment. # Description: # - Loads the current workspace overlay when `install/setup.bash` exists. # When the workspace has not been built yet, it keeps only the base ROS 2 # environment and prints the recommended next step. # References: # - Uses variable `WORKSPACE_ROOT` defined in this file. # - Sources `${WORKSPACE_ROOT}/install/setup.bash`. load_workspace_environment() { local workspace_setup="${WORKSPACE_ROOT}/install/setup.bash" cd "${WORKSPACE_ROOT}" || return 1 if [ -f "${workspace_setup}" ]; then # shellcheck disable=SC1090 source "${workspace_setup}" write_debug_log "load_workspace_environment" "${workspace_setup}" print_info "Loaded workspace environment: ${workspace_setup}" else write_debug_log "load_workspace_environment_missing" "${workspace_setup}" print_warn "Workspace overlay not found: ${workspace_setup}" print_warn "Run 'colcon build' first if you need package overlays." fi export ROS_WORKSPACE="${WORKSPACE_ROOT}" print_info "Workspace root: ${WORKSPACE_ROOT}" return 0 } # Function: ensure_ros_log_directory # Input: # - None. # Output: # - Returns 0 when the ROS 2 log directory is ready for use. # - Returns 1 when the log directory cannot be created. # Description: # - Ensures ROS 2 launch logs are written into the workspace-local log # directory instead of relying on the user's home directory. # References: # - Uses variables `ROS_LOG_ROOT` and `WORKSPACE_ROOT` defined in this file. # - Called by `main()` in this file. ensure_ros_log_directory() { mkdir -p "${ROS_LOG_ROOT}" || { write_debug_log "ensure_ros_log_directory_failed" "${ROS_LOG_ROOT}" print_error "Failed to create ROS log directory: ${ROS_LOG_ROOT}" return 1 } export ROS_LOG_DIR="${ROS_LOG_ROOT}" write_debug_log "ensure_ros_log_directory" "${ROS_LOG_DIR}" print_info "ROS log directory: ${ROS_LOG_DIR}" return 0 } # Function: run_command_with_environment # Input: # - $@: command and arguments to execute. # Output: # - Replaces the current process with the provided command. # Description: # - Executes a user-specified command after the ROS 2 and workspace # environment have been loaded successfully. # References: # - Called by `main()` in this file. run_command_with_environment() { write_debug_log "run_command_with_environment" "$*" print_info "Running command with ROS 2 environment loaded: $*" exec "$@" } # Function: launch_default_ros2_stack # Input: # - None. # Output: # - Replaces the current process with the default ROS 2 launch command. # Description: # - Starts the default `odin_ros_driver` ROS 2 launch file after the # workspace environment has been loaded successfully. # References: # - Uses variables `DEFAULT_LAUNCH_PACKAGE` and `DEFAULT_LAUNCH_FILE` # defined in this file. # - Calls `run_command_with_environment()` in this file. launch_default_ros2_stack() { write_debug_log "launch_default_ros2_stack" "${DEFAULT_LAUNCH_PACKAGE} ${DEFAULT_LAUNCH_FILE}" print_info "Starting default ROS 2 launch: ${DEFAULT_LAUNCH_PACKAGE} ${DEFAULT_LAUNCH_FILE}" run_command_with_environment ros2 launch "${DEFAULT_LAUNCH_PACKAGE}" "${DEFAULT_LAUNCH_FILE}" } # Function: start_interactive_shell # Input: # - None. # Output: # - Replaces the current process with a clean interactive bash shell. # Description: # - Keeps the loaded ROS 2 and workspace environment in a new interactive # shell when the script is executed directly, while avoiding user shell # rc files that may recursively modify configuration. # References: # - Uses `/bin/bash --noprofile --norc -i`. start_interactive_shell() { write_debug_log "start_interactive_shell" "exec_clean_interactive_bash" print_info "Starting a clean interactive bash shell with ROS 2 environment loaded." exec /bin/bash --noprofile --norc -i } # Function: print_usage # Input: # - None. # Output: # - Prints the script usage text to stdout. # Description: # - Documents the default one-click launch behavior, the custom command mode, # and the explicit shell mode for this script. # References: # - Uses variables `DEFAULT_LAUNCH_PACKAGE` and `DEFAULT_LAUNCH_FILE` # defined in this file. print_usage() { cat < [args...] Behavior: - source runros.sh Load ROS 2 and workspace environment into the current shell. - runros.sh Launch: ros2 launch ${DEFAULT_LAUNCH_PACKAGE} ${DEFAULT_LAUNCH_FILE} - runros.sh --shell Open a clean interactive bash shell with the environment loaded. - runros.sh [args...] Run the provided command with the environment loaded. EOF } # Function: main # Input: # - None. # Output: # - Returns 0 on success. # - Returns 1 when required environment loading fails. # Description: # - Coordinates ROS 2 base environment loading, workspace overlay loading, # and chooses behavior for sourced vs executed usage. # References: # - Calls `is_script_sourced()` in this file. # - Calls `load_ros2_environment()` in this file. # - Calls `load_workspace_environment()` in this file. # - Calls `ensure_ros_log_directory()` in this file. # - Calls `launch_default_ros2_stack()` in this file. # - Calls `run_command_with_environment()` in this file. # - Calls `start_interactive_shell()` in this file. # - Calls `print_usage()` in this file. main() { write_debug_log "main_enter" "argv:$*" if ! is_script_sourced; then case "$1" in -h|--help) print_usage return 0 ;; esac fi load_ros2_environment || return 1 load_workspace_environment || return 1 ensure_ros_log_directory || return 1 if is_script_sourced; then if [ "$#" -gt 0 ]; then write_debug_log "main_warn" "arguments_ignored_when_sourced" print_warn "Arguments are ignored when the script is sourced." fi write_debug_log "main_exit" "current_shell_ready" print_info "Environment is ready in the current shell." return 0 fi if [ "$#" -eq 0 ]; then write_debug_log "main_handoff" "default_launch_requested" launch_default_ros2_stack fi if [ "$1" = "--shell" ]; then write_debug_log "main_handoff" "interactive_shell_requested" start_interactive_shell fi if [ "$#" -gt 0 ]; then write_debug_log "main_handoff" "command_execution_requested" run_command_with_environment "$@" fi } main "$@"