Ubuntu 22.04 Server TurtleBot3 Burger를 tmux로 한 번에 실행하기 #1

1. 구성 목표

Ubuntu 22.04 Server와 ROS 2 Humble을 설치한 TurtleBot3 Burger를 SSH로 운용할 때는 일반적으로 여러 터미널이 필요합니다.

이 글에서는 TurtleBot3에 SSH로 접속한 후 하나의 스크립트만 실행하여 다음 네 개의 tmux window를 자동으로 구성합니다.

  1. bringup: TurtleBot3 본체, OpenCR, 모터, LiDAR 실행
  2. camera: Pi Camera 2 영상 노드 실행
  3. audio: 오디오 출력 노드 실행
  4. debug: 노드, 토픽, LiDAR 주기 및 수동 디버깅 환경

사용하는 주요 환경은 다음과 같습니다.

운영체제: Ubuntu 22.04 Server
ROS 버전: ROS 2 Humble
로봇: TurtleBot3 Burger
LiDAR: LDS-03
카메라: Raspberry Pi Camera 2
DDS: Cyclone DDS
ROS_DOMAIN_ID: 200
원격 접속: SSH

2. tmux란 무엇인가

tmux는 하나의 터미널 안에서 여러 터미널 작업 공간을 운영할 수 있게 해주는 터미널 멀티플렉서입니다.

TurtleBot3와 같이 SSH로 원격 운용하는 시스템에서는 다음 장점이 있습니다.

  1. 하나의 SSH 연결에서 여러 ROS 2 노드를 실행할 수 있습니다.
  2. SSH 연결이 끊어져도 tmux 안의 ROS 2 노드는 계속 실행됩니다.
  3. 다시 SSH로 접속하여 기존 화면과 로그를 그대로 확인할 수 있습니다.
  4. 노드별로 window를 분리하여 로그를 관리할 수 있습니다.
  5. 한 window를 여러 pane으로 나누어 디버깅 정보를 동시에 볼 수 있습니다.

tmux의 구조는 다음과 같습니다.

Session
 ├── Window 1
 │    ├── Pane 1
 │    └── Pane 2
 ├── Window 2
 └── Window 3
  1. Session은 전체 작업 묶음입니다.
  2. Window는 일반 터미널 프로그램의 탭과 비슷합니다.
  3. Pane은 하나의 window를 분할한 작은 터미널입니다.

이번에는 turtlebot3이라는 session 안에 네 개의 window를 만듭니다.

3. tmux 설치

TurtleBot3에 SSH로 접속합니다.

ssh sjyong@192.168.200.28

패키지 정보를 갱신합니다.

sudo apt update

tmux를 설치합니다.

sudo apt install -y tmux

설치된 버전을 확인합니다.

tmux -V

다음과 같이 버전이 출력되면 정상입니다.

tmux 3.2a

버전 번호는 설치된 Ubuntu 패키지에 따라 다를 수 있습니다.

4. tmux 간단 사용법

테스트용 tmux session을 생성합니다.

tmux new-session -s test

tmux 내부 명령은 기본적으로 Ctrl+b를 누른 후 다음 키를 누르는 방식입니다.

  1. Ctrl+b, d: 현재 session에서 빠져나오기
  2. Ctrl+b, c: 새 window 생성
  3. Ctrl+b, n: 다음 window로 이동
  4. Ctrl+b, p: 이전 window로 이동
  5. Ctrl+b, 숫자: 해당 번호의 window로 이동
  6. Ctrl+b, w: 전체 window 목록 표시
  7. Ctrl+b, %: 현재 pane을 좌우로 분할
  8. Ctrl+b, ": 현재 pane을 상하로 분할
  9. Ctrl+b, 방향키: pane 이동
  10. Ctrl+b, [: 스크롤 모드 진입
  11. 스크롤 모드에서 q: 스크롤 모드 종료

현재 tmux에서 빠져나올 때는 다음 키를 순서대로 누릅니다.

Ctrl+b
d

실행 중인 tmux session을 확인합니다.

tmux ls

기존 session에 다시 들어갑니다.

tmux attach-session -t test

테스트 session을 종료합니다.

tmux kill-session -t test

5. ROS 2 실행 환경 확인

TurtleBot3에서 사용하는 작업공간은 다음과 같습니다.

source /opt/ros/humble/setup.bash
source ~/turtlebot3_ws/install/setup.bash
source ~/rgb_led_ws/install/setup.bash

환경 변수는 다음과 같습니다.

export ROS_DOMAIN_ID=200
export LDS_MODEL=LDS-03
export TURTLEBOT3_MODEL=burger
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

rgb_led_ws의 환경은 오디오 노드와 해당 작업공간에 포함된 패키지를 찾기 위해 반드시 ROS 2 명령 실행 전에 적용되어야 합니다.

작업공간의 올바른 source 순서는 다음과 같습니다.

ROS 2 Humble 기본 환경
→ TurtleBot3 작업공간
→ RGB LED 및 오디오 작업공간
→ ROS 2 노드 실행

따라서 각 tmux window에서 ROS 2 명령을 실행하기 전에 다음 세 개의 setup 파일을 모두 source하도록 구성합니다.

6. Cyclone DDS 환경 변수 이름 주의

Cyclone DDS 설정에 사용하는 올바른 환경 변수 이름은 다음과 같습니다.

CYCLONEDDS_URI

다음 이름은 잘못된 이름입니다.

CYCLONE_DDS_URI

중간에 밑줄이 들어간 CYCLONE_DDS_URI가 아니라, 밑줄 없이 연결된 CYCLONEDDS_URI를 사용해야 합니다.

네트워크 인터페이스 이름도 실제 TurtleBot3의 인터페이스 이름을 사용해야 합니다.

인터페이스를 확인합니다.

ip address

기본 통신에 사용되는 인터페이스를 확인합니다.

ip route show default

예를 들어 다음과 같이 출력될 수 있습니다.

default via 192.168.0.1 dev wlan0 proto dhcp

이 경우 네트워크 인터페이스 이름은 wlan0입니다.

Ubuntu 환경에 따라 다음과 같은 다른 이름이 사용될 수도 있습니다.

wlan0
wlp2s0
wlx001122334455
eth0
enp3s0

아래에서 작성할 스크립트는 기본 라우트에 사용되는 인터페이스를 자동으로 감지합니다.

자동 감지가 잘못된 경우 실행 전에 직접 지정할 수 있습니다.

export ROS_NETWORK_INTERFACE=wlan0

7. TurtleBot3 tmux 스크립트 생성

홈 디렉터리에 스크립트를 저장할 디렉터리를 만듭니다.

mkdir -p ~/turtlebot3_tmux

스크립트 파일을 생성합니다.

nano ~/turtlebot3_tmux/turtlebot3_tmux.sh

다음 전체 내용을 입력합니다.

#!/usr/bin/env bash
set -Eeuo pipefail

SESSION_NAME="turtlebot3"

ROS_SETUP="/opt/ros/humble/setup.bash"
TB3_SETUP="${HOME}/turtlebot3_ws/install/setup.bash"
RGB_LED_SETUP="${HOME}/rgb_led_ws/install/setup.bash"

CYCLONEDDS_FILE="/tmp/${SESSION_NAME}_cyclonedds_${USER}.xml"

fail()
{
  echo "[ERROR] $*" >&2
  exit 1
}

check_file()
{
  [[ -f "$1" ]] || fail "setup 파일을 찾을 수 없습니다: $1"
}

detect_network_interface()
{
  if [[ -n "${ROS_NETWORK_INTERFACE:-}" ]]; then
    printf '%s\n' "${ROS_NETWORK_INTERFACE}"
    return 0
  fi

  local interface

  interface="$(
    ip route show default 2>/dev/null |
      awk '/default/ {
        for (i = 1; i <= NF; i++) {
          if ($i == "dev" && (i + 1) <= NF) {
            print $(i + 1)
            exit
          }
        }
      }'
  )"

  [[ -n "${interface}" ]] ||
    fail "기본 네트워크 인터페이스를 찾을 수 없습니다. ROS_NETWORK_INTERFACE를 직접 지정하십시오."

  printf '%s\n' "${interface}"
}

write_cyclonedds_config()
{
  local interface="$1"

  cat > "${CYCLONEDDS_FILE}" <<XML
<CycloneDDS>
  <Domain id="any">
    <General>
      <NetworkInterfaceAddress>${interface}</NetworkInterfaceAddress>
      <AllowMulticast>default</AllowMulticast>
    </General>
  </Domain>
</CycloneDDS>
XML
}

ros_shell()
{
  local ros_command="$1"
  local payload

  printf -v payload \
    'source %q && source %q && source %q && export ROS_DOMAIN_ID=200 LDS_MODEL=LDS-03 TURTLEBOT3_MODEL=burger RMW_IMPLEMENTATION=rmw_cyclonedds_cpp ROS_NETWORK_INTERFACE=%q CYCLONEDDS_URI=%q && %s' \
    "${ROS_SETUP}" \
    "${TB3_SETUP}" \
    "${RGB_LED_SETUP}" \
    "${ROS_NETWORK_INTERFACE}" \
    "file://${CYCLONEDDS_FILE}" \
    "${ros_command}"

  printf '/bin/bash -lc %q' "${payload}"
}

attach_session()
{
  if [[ -n "${TMUX:-}" ]]; then
    tmux switch-client -t "${SESSION_NAME}"
  else
    exec tmux attach-session -t "${SESSION_NAME}"
  fi
}

create_session()
{
  check_file "${ROS_SETUP}"
  check_file "${TB3_SETUP}"
  check_file "${RGB_LED_SETUP}"

  export ROS_NETWORK_INTERFACE
  ROS_NETWORK_INTERFACE="$(detect_network_interface)"

  write_cyclonedds_config "${ROS_NETWORK_INTERFACE}"

  tmux new-session -d \
    -s "${SESSION_NAME}" \
    -n bringup \
    "$(ros_shell 'exec ros2 launch turtlebot3_bringup robot.launch.py')"

  tmux set-option -t "${SESSION_NAME}" history-limit 50000
  tmux set-option -t "${SESSION_NAME}" mouse on

  tmux new-window \
    -t "${SESSION_NAME}" \
    -n camera \
    "$(ros_shell 'exec ros2 launch turtlebot3_bringup camera.launch.py format:=BGR888 width:=320 height:=240')"

  tmux new-window \
    -t "${SESSION_NAME}" \
    -n audio \
    "$(ros_shell 'exec ros2 run robot_audio_output audio_output_node')"

  tmux new-window \
    -t "${SESSION_NAME}" \
    -n debug \
    "$(ros_shell "watch -n 2 -t 'ros2 node list 2>/dev/null'")"

  tmux split-window \
    -t "${SESSION_NAME}:debug" \
    "$(ros_shell "watch -n 2 -t 'ros2 topic list 2>/dev/null'")"

  tmux split-window \
    -t "${SESSION_NAME}:debug" \
    "$(ros_shell 'ros2 topic hz /scan; exec bash -i')"

  tmux split-window \
    -t "${SESSION_NAME}:debug" \
    "$(ros_shell "printf '\nTurtleBot3 debug shell\nROS_DOMAIN_ID=%s\nTURTLEBOT3_MODEL=%s\nLDS_MODEL=%s\nRMW_IMPLEMENTATION=%s\nROS_NETWORK_INTERFACE=%s\nCYCLONEDDS_URI=%s\n\n' \"\${ROS_DOMAIN_ID}\" \"\${TURTLEBOT3_MODEL}\" \"\${LDS_MODEL}\" \"\${RMW_IMPLEMENTATION}\" \"\${ROS_NETWORK_INTERFACE}\" \"\${CYCLONEDDS_URI}\"; exec bash -i")"

  for window_name in bringup camera audio debug; do
    tmux set-window-option \
      -t "${SESSION_NAME}:${window_name}" \
      remain-on-exit on
  done

  tmux select-layout -t "${SESSION_NAME}:debug" tiled
  tmux select-window -t "${SESSION_NAME}:bringup"

  echo "[OK] tmux session: ${SESSION_NAME}"
  echo "[OK] network interface: ${ROS_NETWORK_INTERFACE}"
  echo "[OK] Cyclone DDS config: ${CYCLONEDDS_FILE}"
}

show_status()
{
  if tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
    tmux list-windows \
      -t "${SESSION_NAME}" \
      -F '#{window_index}:#{window_name} panes=#{window_panes} active=#{window_active}'
  else
    echo "실행 중인 ${SESSION_NAME} 세션이 없습니다."
  fi
}

COMMAND="${1:-start}"

command -v tmux >/dev/null 2>&1 ||
  fail "tmux가 설치되어 있지 않습니다: sudo apt install -y tmux"

case "${COMMAND}" in
  start)
    if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
      create_session
    fi

    attach_session
    ;;

  attach)
    tmux has-session -t "${SESSION_NAME}" 2>/dev/null ||
      fail "실행 중인 ${SESSION_NAME} 세션이 없습니다."

    attach_session
    ;;

  stop)
    if tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
      tmux kill-session -t "${SESSION_NAME}"
      echo "${SESSION_NAME} 세션을 종료했습니다."
    else
      echo "실행 중인 ${SESSION_NAME} 세션이 없습니다."
    fi
    ;;

  restart)
    tmux kill-session -t "${SESSION_NAME}" 2>/dev/null || true

    create_session
    attach_session
    ;;

  status)
    show_status
    ;;

  *)
    echo "사용법: $0 {start|attach|stop|restart|status}" >&2
    exit 2
    ;;
esac

nano에서 저장합니다.

Ctrl+O
Enter
Ctrl+X

8. 실행 권한 설정

스크립트에 실행 권한을 추가합니다.

chmod +x ~/turtlebot3_tmux/turtlebot3_tmux.sh

Bash 문법을 검사합니다.

bash -n ~/turtlebot3_tmux/turtlebot3_tmux.sh

아무 메시지도 출력되지 않으면 기본 Bash 문법에는 문제가 없는 것입니다.

스크립트 상태 명령을 실행합니다.

~/turtlebot3_tmux/turtlebot3_tmux.sh status

아직 실행 전이라면 다음과 같이 출력됩니다.

실행 중인 turtlebot3 세션이 없습니다.

9. 소스 설명

1) Bash 인터프리터 지정

#!/usr/bin/env bash

이 줄을 shebang이라고 합니다.

스크립트를 실행할 때 어떤 프로그램으로 해석할 것인지를 운영체제에 알려줍니다.

/usr/bin/env bash

는 현재 환경의 PATH에서 Bash 실행 파일을 찾아서 사용한다는 뜻입니다.

다음과 같이 직접 경로를 지정할 수도 있습니다.

#!/bin/bash

그러나 /usr/bin/env bash 방식은 Bash 설치 위치가 시스템마다 다를 가능성을 고려한 형태입니다.

이 스크립트에서는 다음과 같은 Bash 전용 문법을 사용하므로 Bash로 실행해야 합니다.

[[ ... ]]
local
printf -v
${변수:-기본값}

따라서 다음과 같이 sh로 실행하면 안 됩니다.

sh turtlebot3_tmux.sh

다음과 같이 직접 실행하거나 Bash로 실행해야 합니다.

./turtlebot3_tmux.sh

또는 다음과 같이 실행합니다.

bash turtlebot3_tmux.sh

2) Bash 엄격 실행 설정

set -Eeuo pipefail

이 설정은 스크립트에서 오류를 가능한 한 빨리 발견하기 위해 사용합니다.

각 옵션은 다음 의미입니다.

set -E

함수나 명령 치환 내부에서 발생한 오류에도 ERR trap이 상속되도록 합니다.

현재 코드에는 trap ERR이 없지만, 나중에 다음과 같은 오류 추적 기능을 추가할 때 유용합니다.

trap 'echo "오류 발생: ${LINENO}번 줄"' ERR

set -e

명령이 실패하면 스크립트를 중단합니다.

예를 들어 tmux session 생성이 실패하거나 ROS 2 setup 파일을 읽지 못하면 이후 명령을 계속 실행하지 않습니다.

set -u

정의되지 않은 변수를 사용하면 오류로 처리합니다.

다음 코드에서 ROBOT_NAME이 정의되지 않았다면 스크립트가 종료됩니다.

echo "${ROBOT_NAME}"

정의되지 않을 가능성이 있는 변수는 다음처럼 작성해야 합니다.

echo "${ROBOT_NAME:-burger}"

set -o pipefail

파이프라인 중간에 있는 명령이 실패한 경우에도 전체 파이프라인을 실패로 처리합니다.

예를 들어 다음 명령에서는 ip routeawk가 파이프로 연결되어 있습니다.

ip route show default | awk '...'

기본 Bash 동작에서는 마지막 명령인 awk의 종료 상태만 확인할 수 있습니다.

pipefail을 사용하면 앞쪽의 ip route가 실패한 경우도 감지할 수 있습니다.

3) tmux session 이름 지정

SESSION_NAME="turtlebot3"

생성할 tmux session의 이름을 지정합니다.

실제로 생성되는 session 이름은 다음과 같습니다.

turtlebot3

이 session은 다음 명령으로 확인할 수 있습니다.

tmux ls

출력 예시는 다음과 같습니다.

turtlebot3: 4 windows

session 이름을 변수로 관리하면 코드 전체에서 동일한 이름을 반복해서 직접 작성할 필요가 없습니다.

예를 들어 다음 코드는:

tmux new-session -s "${SESSION_NAME}"

실제로는 다음과 같은 의미입니다.

tmux new-session -s turtlebot3

나중에 session 이름을 변경하려면 다음 한 줄만 수정하면 됩니다.

SESSION_NAME="tb3_robot"

4) ROS 2 setup 파일 경로 설정

ROS_SETUP="/opt/ros/humble/setup.bash"
TB3_SETUP="${HOME}/turtlebot3_ws/install/setup.bash"
RGB_LED_SETUP="${HOME}/rgb_led_ws/install/setup.bash"

각 ROS 2 환경 설정 파일의 경로를 변수로 저장합니다.

ROS 2 Humble 기본 환경
ROS_SETUP="/opt/ros/humble/setup.bash"

ROS 2 Humble이 설치된 기본 환경입니다.

다음 파일을 source하면 ros2 명령과 Humble에 설치된 기본 패키지를 사용할 수 있습니다.

source /opt/ros/humble/setup.bash
TurtleBot3 작업공간
TB3_SETUP="${HOME}/turtlebot3_ws/install/setup.bash"

직접 컴파일한 TurtleBot3 패키지를 사용하기 위한 작업공간입니다.

${HOME}은 현재 사용자의 홈 디렉터리를 의미합니다.

사용자 이름이 ubuntu라면 다음 경로가 됩니다.

/home/ubuntu/turtlebot3_ws/install/setup.bash
RGB LED 및 오디오 작업공간
RGB_LED_SETUP="${HOME}/rgb_led_ws/install/setup.bash"

robot_audio_output 패키지와 RGB LED 관련 패키지가 포함된 작업공간입니다.

이 환경을 source하지 않으면 다음 명령에서 패키지를 찾지 못할 수 있습니다.

ros2 run robot_audio_output audio_output_node
source 순서

스크립트가 사용하는 source 순서는 다음과 같습니다.

ROS 2 Humble 기본 환경
→ TurtleBot3 작업공간
→ RGB LED 및 오디오 작업공간

실제 명령으로 표현하면 다음과 같습니다.

source /opt/ros/humble/setup.bash
source ~/turtlebot3_ws/install/setup.bash
source ~/rgb_led_ws/install/setup.bash

뒤에서 source한 작업공간이 앞쪽 환경 위에 overlay되는 구조입니다.

5) Cyclone DDS 설정 파일 경로

CYCLONEDDS_FILE="/tmp/${SESSION_NAME}_cyclonedds_${USER}.xml"

Cyclone DDS가 사용할 XML 설정 파일 경로를 생성합니다.

예를 들어 다음 조건이라면:

SESSION_NAME=turtlebot3
USER=sjyong

파일 경로는 다음과 같습니다.

/tmp/turtlebot3_cyclonedds_sjyong.xml

/tmp 디렉터리는 임시 파일을 저장하는 표준 디렉터리입니다.

스크립트를 실행할 때마다 이 파일의 내용이 새로 작성됩니다.

Cyclone DDS는 나중에 다음 환경 변수로 이 파일을 사용합니다.

CYCLONEDDS_URI=file:///tmp/turtlebot3_cyclonedds_sjyong.xml

여기서 사용하는 환경 변수 이름은 다음과 같습니다.

CYCLONEDDS_URI

CYCLONE_DDS_URI가 아니라 CYCLONEDDS_URI입니다.

6) 오류 처리 함수

fail()
{
  echo "[ERROR] $*" >&2
  exit 1
}

fail() 함수는 오류 메시지를 출력하고 스크립트를 종료합니다.

함수 선언
fail()
{

Bash에서 함수를 정의하는 문법입니다.

다음 두 방식은 같은 의미입니다.

fail()
{
  ...
}
function fail
{
  ...
}
전달된 인자 출력
echo "[ERROR] $*" >&2

$*는 함수에 전달된 모든 인자를 하나의 문자열처럼 나타냅니다.

다음과 같이 호출하면:

fail "setup 파일이 없습니다."

다음과 같이 출력됩니다.

[ERROR] setup 파일이 없습니다.
표준 오류 출력
>&2

메시지를 표준 출력이 아니라 표준 오류로 보냅니다.

리눅스의 주요 출력 번호는 다음과 같습니다.

0: 표준 입력
1: 표준 출력
2: 표준 오류

오류 메시지를 표준 오류로 보내면 일반 출력과 오류 출력을 분리할 수 있습니다.

예를 들어 다음과 같이 실행할 수 있습니다.

./turtlebot3_tmux.sh 2> error.log

이 경우 오류 메시지만 error.log에 저장됩니다.

비정상 종료
exit 1

스크립트를 종료 코드 1로 종료합니다.

일반적으로 종료 코드의 의미는 다음과 같습니다.

0: 정상 종료
0 이외의 값: 오류 종료

7) 파일 존재 여부 확인 함수

check_file()
{
  [[ -f "$1" ]] || fail "setup 파일을 찾을 수 없습니다: $1"
}

이 함수는 전달받은 파일이 실제로 존재하는지 확인합니다.

첫 번째 함수 인자
$1

함수에 전달된 첫 번째 인자입니다.

다음과 같이 호출하면:

check_file "${ROS_SETUP}"

$1에는 다음 값이 들어갑니다.

/opt/ros/humble/setup.bash
일반 파일 검사
[[ -f "$1" ]]

-f는 대상 경로가 일반 파일인지 확인합니다.

파일이 존재하면 참을 반환하고, 존재하지 않으면 거짓을 반환합니다.

|| 연산자
명령1 || 명령2

앞 명령이 실패할 때만 뒤 명령을 실행합니다.

따라서 다음 코드는:

[[ -f "$1" ]] || fail "setup 파일을 찾을 수 없습니다: $1"

다음 의미입니다.

파일이 존재하면 계속 진행
파일이 없으면 fail 함수 호출

일반적인 if 문으로 바꾸면 다음과 같습니다.

if [[ ! -f "$1" ]]; then
  fail "setup 파일을 찾을 수 없습니다: $1"
fi

8) 네트워크 인터페이스 자동 감지 함수

detect_network_interface()
{

이 함수는 ROS 2 Cyclone DDS 통신에 사용할 네트워크 인터페이스를 결정합니다.

우선 사용자가 직접 지정한 값이 있는지 확인하고, 없으면 기본 라우트에서 자동으로 감지합니다.

사용자가 지정한 네트워크 인터페이스 확인
if [[ -n "${ROS_NETWORK_INTERFACE:-}" ]]; then
  printf '%s\n' "${ROS_NETWORK_INTERFACE}"
  return 0
fi
미정의 변수 안전 처리
"${ROS_NETWORK_INTERFACE:-}"

스크립트 상단에 set -u가 있으므로 정의되지 않은 변수를 그대로 읽으면 오류가 발생합니다.

다음 코드는 위험합니다.

"${ROS_NETWORK_INTERFACE}"

변수가 존재하지 않으면 set -u에 의해 스크립트가 종료될 수 있습니다.

다음처럼 :-를 사용하면 변수가 없을 때 빈 문자열을 사용합니다.

"${ROS_NETWORK_INTERFACE:-}"
문자열 길이 검사
[[ -n "${ROS_NETWORK_INTERFACE:-}" ]]

-n은 문자열 길이가 0보다 큰지 검사합니다.

즉, 사용자가 다음과 같이 미리 지정했다면:

export ROS_NETWORK_INTERFACE=wlan0

자동 감지하지 않고 wlan0을 그대로 사용합니다.

인터페이스 이름 출력
printf '%s\n' "${ROS_NETWORK_INTERFACE}"

함수의 결과를 표준 출력으로 보냅니다.

뒤에서 이 함수는 다음과 같이 사용됩니다.

ROS_NETWORK_INTERFACE="$(detect_network_interface)"

따라서 함수가 출력한 문자열이 변수에 저장됩니다.

함수 정상 종료
return 0

함수를 성공 상태로 종료합니다.

이 줄이 실행되면 아래쪽의 자동 감지 코드는 실행되지 않습니다.

지역 변수 선언
local interface

interface 변수를 현재 함수 내부에서만 사용할 수 있도록 선언합니다.

local을 사용하지 않으면 함수에서 만든 변수가 스크립트 전체에 영향을 줄 수 있습니다.

다음과 같이 사용하는 것이 안전합니다.

detect_network_interface()
{
  local interface
}

함수 실행이 끝나면 지역 변수의 역할도 끝납니다.

기본 네트워크 인터페이스 검색

interface="$(
  ip route show default 2>/dev/null |
    awk '/default/ {
      for (i = 1; i <= NF; i++) {
        if ($i == "dev" && (i + 1) <= NF) {
          print $(i + 1)
          exit
        }
      }
    }'
)"

이 부분은 기본 라우트에서 네트워크 장치 이름을 찾아 interface 변수에 저장합니다.

명령 치환
interface="$(명령)"

$(...) 안의 명령을 실행하고 출력 결과를 변수에 저장합니다.

예를 들어 다음 명령 결과가:

ip route show default

다음과 같다면:

default via 192.168.0.1 dev wlan0 proto dhcp metric 600

awkwlan0만 출력하고 최종적으로 다음과 같이 저장됩니다.

interface=wlan0
기본 라우트 확인
ip route show default

시스템의 기본 네트워크 경로를 표시합니다.

일반적인 출력은 다음과 같습니다.

default via 192.168.0.1 dev wlan0 proto dhcp metric 600

여기서 dev 다음에 나오는 값이 네트워크 인터페이스입니다.

dev wlan0

유선 연결이라면 다음처럼 표시될 수 있습니다.

default via 192.168.0.1 dev eth0

Ubuntu의 예측 가능한 네트워크 이름을 사용하면 다음과 같을 수도 있습니다.

default via 192.168.0.1 dev wlp2s0
오류 메시지 제거
2>/dev/null

ip route 명령에서 발생하는 표준 오류를 화면에 표시하지 않습니다.

2는 표준 오류이고 /dev/null은 데이터를 버리는 장치입니다.

awk를 이용한 인터페이스 추출

/default/ {
  for (i = 1; i <= NF; i++) {
    if ($i == "dev" && (i + 1) <= NF) {
      print $(i + 1)
      exit
    }
  }
}
default 행 선택
/default/

default라는 문자열이 포함된 행만 처리합니다.

필드 순회
for (i = 1; i <= NF; i++)

현재 행의 모든 필드를 처음부터 끝까지 확인합니다.

awk에서 NF는 현재 행의 필드 개수를 의미합니다.

다음 행을 예로 들면:

default via 192.168.0.1 dev wlan0 proto dhcp

각 필드는 다음과 같습니다.

$1 = default
$2 = via
$3 = 192.168.0.1
$4 = dev
$5 = wlan0
$6 = proto
$7 = dhcp
dev 필드 검색
if ($i == "dev" && (i + 1) <= NF)

현재 필드가 dev인지 확인합니다.

동시에 다음 필드가 실제로 존재하는지도 확인합니다.

(i + 1) <= NF

장치 이름 출력

print $(i + 1)

dev 다음 필드를 출력합니다.

dev wlan0

에서는 wlan0이 출력됩니다.

검색 종료
exit

첫 번째 인터페이스를 찾으면 awk 실행을 끝냅니다.

기본 라우트가 여러 개일 때 첫 번째 결과만 선택합니다.

자동 감지 결과 검증

[[ -n "${interface}" ]] ||
  fail "기본 네트워크 인터페이스를 찾을 수 없습니다. ROS_NETWORK_INTERFACE를 직접 지정하십시오."

자동 감지한 인터페이스가 빈 문자열인지 확인합니다.

정상적으로 감지되었다면 다음과 같은 값이 들어 있습니다.

wlan0

감지하지 못했다면 fail() 함수가 실행됩니다.

[ERROR] 기본 네트워크 인터페이스를 찾을 수 없습니다. ROS_NETWORK_INTERFACE를 직접 지정하십시오.

이 경우 스크립트 실행 전에 직접 지정할 수 있습니다.

export ROS_NETWORK_INTERFACE=wlan0

그 다음 다시 실행합니다.

./turtlebot3_tmux.sh start
감지한 인터페이스 반환
printf '%s\n' "${interface}"

감지한 네트워크 인터페이스를 출력합니다.

이 출력은 뒤에서 다음 명령에 의해 변수로 저장됩니다.

ROS_NETWORK_INTERFACE="$(detect_network_interface)"

함수 자체에 문자열 반환 기능이 없으므로, Bash에서는 일반적으로 표준 출력과 명령 치환을 사용하여 문자열 결과를 전달합니다.

9) Cyclone DDS XML 생성 함수

write_cyclonedds_config()
{
  local interface="$1"

이 함수는 전달받은 네트워크 인터페이스를 사용하여 Cyclone DDS XML 파일을 생성합니다.

다음처럼 호출됩니다.

write_cyclonedds_config "${ROS_NETWORK_INTERFACE}"

ROS_NETWORK_INTERFACEwlan0이면 함수 내부에서는 다음과 같습니다.

interface=wlan0

10) Here Document를 이용한 XML 작성

cat > "${CYCLONEDDS_FILE}" <<XML
<CycloneDDS>
  <Domain id="any">
    <General>
      <NetworkInterfaceAddress>${interface}</NetworkInterfaceAddress>
      <AllowMulticast>default</AllowMulticast>
    </General>
  </Domain>
</CycloneDDS>
XML

<<XML은 Bash의 Here Document 문법입니다.

여러 줄의 텍스트를 파일에 기록할 때 사용합니다.

기존 파일 덮어쓰기
cat > "${CYCLONEDDS_FILE}"

>는 파일을 새로 작성합니다.

같은 이름의 파일이 이미 있다면 기존 내용을 지우고 새 내용으로 덮어씁니다.

변수 치환

Here Document 시작 부분이 다음과 같습니다.

<<XML

구분자인 XML에 작은따옴표가 없으므로 내부 변수는 실제 값으로 치환됩니다.

다음 코드에서:

<NetworkInterfaceAddress>${interface}</NetworkInterfaceAddress>

interfacewlan0이면 다음 내용으로 저장됩니다.

<NetworkInterfaceAddress>wlan0</NetworkInterfaceAddress>
생성되는 실제 파일

최종 파일은 대략 다음과 같습니다.

<CycloneDDS>
  <Domain id="any">
    <General>
      <NetworkInterfaceAddress>wlan0</NetworkInterfaceAddress>
      <AllowMulticast>default</AllowMulticast>
    </General>
  </Domain>
</CycloneDDS>
Domain 설정
<Domain id="any">

모든 DDS Domain에 이 설정을 적용한다는 의미입니다.

ROS 2 Domain ID 자체는 별도로 다음 환경 변수에서 지정합니다.

ROS_DOMAIN_ID=200
네트워크 인터페이스 지정
<NetworkInterfaceAddress>wlan0</NetworkInterfaceAddress>

Cyclone DDS가 어떤 네트워크 장치를 사용하여 통신할지 지정합니다.

TurtleBot3가 유선과 무선 네트워크를 동시에 사용하거나 Docker, VPN 인터페이스가 존재하는 경우 잘못된 장치를 선택하는 문제를 줄일 수 있습니다.

Multicast 설정
<AllowMulticast>default</AllowMulticast>

Cyclone DDS의 기본 multicast 정책을 사용합니다.

ROS 2 노드 검색은 일반적으로 multicast 환경의 영향을 받습니다.

같은 공유기에 연결되어 있어도 공유기의 AP isolation 또는 client isolation 기능이 활성화되어 있으면 원격 PC에서 TurtleBot3를 찾지 못할 수 있습니다.

11) ROS 2 실행 명령 생성 함수

ros_shell()
{
  local ros_command="$1"
  local payload

ros_shell() 함수는 tmux window 안에서 실행할 완전한 Bash 명령 문자열을 생성합니다.

단순히 다음 명령만 실행하는 것이 아닙니다.

ros2 launch ...

각 tmux window에서 다음 작업을 모두 수행하는 문자열을 만듭니다.

  1. ROS 2 Humble 환경 source
  2. TurtleBot3 작업공간 source
  3. RGB LED 작업공간 source
  4. ROS Domain ID 설정
  5. TurtleBot3 모델 설정
  6. LiDAR 모델 설정
  7. Cyclone DDS RMW 설정
  8. 네트워크 인터페이스 설정
  9. Cyclone DDS XML 경로 설정
  10. 실제 ROS 2 명령 실행

tmux의 각 window는 별도의 셸 프로세스로 실행되므로 모든 window에 환경을 개별적으로 적용해야 합니다.

실행할 ROS 명령 전달
local ros_command="$1"

함수에 전달된 첫 번째 인자를 ros_command라는 지역 변수에 저장합니다.

예를 들어 다음과 같이 호출하면:

ros_shell 'exec ros2 launch turtlebot3_bringup robot.launch.py'

ros_command에는 다음 문자열이 저장됩니다.

exec ros2 launch turtlebot3_bringup robot.launch.py
payload 문자열 생성
printf -v payload \
  'source %q && source %q && source %q && export ROS_DOMAIN_ID=200 LDS_MODEL=LDS-03 TURTLEBOT3_MODEL=burger RMW_IMPLEMENTATION=rmw_cyclonedds_cpp ROS_NETWORK_INTERFACE=%q CYCLONEDDS_URI=%q && %s' \
  "${ROS_SETUP}" \
  "${TB3_SETUP}" \
  "${RGB_LED_SETUP}" \
  "${ROS_NETWORK_INTERFACE}" \
  "file://${CYCLONEDDS_FILE}" \
  "${ros_command}"

이 부분은 스크립트에서 가장 복잡하지만 가장 중요한 부분입니다.

printf -v의 의미
printf -v payload

일반 printf는 결과를 화면에 출력합니다.

printf '%s\n' "hello"

-v를 사용하면 화면에 출력하지 않고 지정한 변수에 저장합니다.

printf -v payload '형식' 값

결과적으로 완성된 명령 문자열이 payload 변수에 저장됩니다.

%q의 의미
%q

Bash의 printf에서 %q는 문자열을 셸에서 안전하게 다시 사용할 수 있도록 escape합니다.

예를 들어 경로에 공백이나 특수문자가 있더라도 하나의 안전한 인자로 전달할 수 있습니다.

다음 변수들이 %q로 처리됩니다.

"${ROS_SETUP}"
"${TB3_SETUP}"
"${RGB_LED_SETUP}"
"${ROS_NETWORK_INTERFACE}"
"file://${CYCLONEDDS_FILE}"

일반적인 환경에서는 경로에 공백이 없지만, 자동 생성되는 셸 명령 문자열에서는 %q를 사용하는 것이 더 안전합니다.

&&로 명령 연결

완성되는 payload는 대략 다음 구조입니다.

source ROS_SETUP &&
source TB3_SETUP &&
source RGB_LED_SETUP &&
export 환경변수들 &&
실제 ROS 2 명령

&&는 앞 명령이 성공했을 때만 다음 명령을 실행합니다.

예를 들어 TurtleBot3 setup 파일 source에 실패하면 카메라나 bringup 명령은 실행되지 않습니다.

이는 오류 상태에서 불완전한 환경으로 ROS 2 노드를 실행하는 것을 방지합니다.

ROS 2 환경 변수 설정
export ROS_DOMAIN_ID=200

ROS 2 통신 Domain을 200으로 지정합니다.

원격 PC도 같은 값을 사용해야 합니다.

export ROS_DOMAIN_ID=200

다음 설정은 LiDAR 모델을 LDS-03으로 지정합니다.

export LDS_MODEL=LDS-03

TurtleBot3 모델은 Burger로 설정합니다.

export TURTLEBOT3_MODEL=burger

RMW 구현체는 Cyclone DDS를 사용합니다.

export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

자동 감지한 인터페이스도 환경 변수로 전달합니다.

export ROS_NETWORK_INTERFACE=wlan0

Cyclone DDS 설정 파일은 URI 형태로 전달합니다.

export CYCLONEDDS_URI=file:///tmp/turtlebot3_cyclonedds_sjyong.xml

로컬 파일 URI는 file:// 뒤에 절대 경로가 붙으므로 /tmp 경로에서는 슬래시가 세 개가 됩니다.

file:///tmp/파일.xml
%s로 실제 명령 삽입
&& %s

마지막 %s 위치에 ros_command가 그대로 들어갑니다.

bringup 명령을 전달했다면 최종 payload 끝부분은 다음과 같습니다.

&& exec ros2 launch turtlebot3_bringup robot.launch.py
/bin/bash -lc 명령 생성
printf '/bin/bash -lc %q' "${payload}"

이 줄은 최종적으로 tmux가 실행할 명령을 출력합니다.

결과는 대략 다음 형태가 됩니다.

/bin/bash -lc 'source ... && export ... && ros2 launch ...'
/bin/bash

tmux window 안에서 Bash를 실행합니다.

-l

login shell로 실행합니다.

로그인 셸의 환경 초기화 규칙을 적용할 수 있습니다.

-c

뒤에 전달된 문자열을 명령으로 실행합니다.

bash -c "명령 문자열"
최종 출력

ros_shell() 함수는 직접 ROS 2 명령을 실행하지 않습니다.

tmux에 전달할 완성된 명령 문자열을 출력합니다.

뒤에서는 다음과 같이 명령 치환으로 사용됩니다.

"$(ros_shell 'exec ros2 launch ...')"
exec ros2를 사용하는 이유

bringup, camera, audio 명령 앞에는 exec가 들어 있습니다.

exec ros2 launch turtlebot3_bringup robot.launch.py

exec는 현재 Bash 프로세스를 ROS 2 프로세스로 교체합니다.

일반 실행 구조는 다음과 같습니다.

tmux
└── bash
    └── ros2

exec를 사용하면 다음처럼 됩니다.

tmux
└── ros2

중간 Bash 프로세스가 없어지므로 다음 장점이 있습니다.

  1. tmux에서 종료 신호가 ROS 2 프로세스에 직접 전달됩니다.
  2. 불필요한 중간 셸 프로세스가 남지 않습니다.
  3. pane 종료와 ROS 2 프로세스 종료 관계가 명확해집니다.

12) 기존 tmux session 접속 함수

attach_session()
{
  if [[ -n "${TMUX:-}" ]]; then
    tmux switch-client -t "${SESSION_NAME}"
  else
    exec tmux attach-session -t "${SESSION_NAME}"
  fi
}

이 함수는 현재 사용자가 이미 tmux 안에 있는지 확인한 후 적절한 접속 명령을 선택합니다.

TMUX 환경 변수 확인
[[ -n "${TMUX:-}" ]]

tmux 안에서 실행 중이면 보통 TMUX 환경 변수가 설정되어 있습니다.

다음 명령으로 확인할 수 있습니다.

echo "$TMUX"

tmux 내부에서는 대략 다음과 같은 값이 표시될 수 있습니다.

/tmp/tmux-1000/default,1234,0

tmux 외부에서는 일반적으로 빈 값입니다.

${TMUX:-}를 사용한 이유는 set -u 상태에서도 안전하게 확인하기 위해서입니다.

tmux 내부에서 session 변경
tmux switch-client -t "${SESSION_NAME}"

이미 tmux 안에 있는 상태에서 다른 session으로 이동할 때 사용합니다.

tmux 내부에서 다시 attach-session을 실행하면 중첩 tmux 문제가 발생할 수 있습니다.

따라서 tmux 안에서는 switch-client를 사용합니다.

실제 대상은 다음과 같습니다.

tmux switch-client -t turtlebot3
tmux 외부에서 session 접속
exec tmux attach-session -t "${SESSION_NAME}"

현재 일반 SSH 터미널이라면 기존 tmux session에 접속합니다.

실제 명령은 다음과 같습니다.

tmux attach-session -t turtlebot3

앞의 exec는 현재 스크립트 프로세스를 tmux attach 프로세스로 교체합니다.

사용자가 tmux에서 빠져나오면 불필요한 Bash 스크립트 프로세스가 남지 않습니다.

13) tmux session 생성 함수

create_session()
{

이 함수는 TurtleBot3용 tmux session 전체를 생성합니다.

실행 순서는 다음과 같습니다.

→ setup 파일 검사
→ 네트워크 인터페이스 감지
→ Cyclone DDS 설정 파일 생성
→ bringup window 생성
→ camera window 생성
→ audio window 생성
→ debug window 생성
→ debug pane 분할
→ window 유지 옵션 설정
→ debug pane 정렬
→ bringup window 선택
→ 설정 결과 출력
setup 파일 확인
check_file "${ROS_SETUP}"
check_file "${TB3_SETUP}"
check_file "${RGB_LED_SETUP}"

필요한 세 개의 setup 파일이 모두 존재하는지 검사합니다.

검사 대상은 다음과 같습니다.

/opt/ros/humble/setup.bash
~/turtlebot3_ws/install/setup.bash
~/rgb_led_ws/install/setup.bash

하나라도 없으면 session 생성을 중단합니다.

예를 들어 RGB LED 작업공간이 빌드되지 않았다면 다음 오류가 출력됩니다.

[ERROR] setup 파일을 찾을 수 없습니다: /home/ubuntu/rgb_led_ws/install/setup.bash
네트워크 인터페이스 변수 export
export ROS_NETWORK_INTERFACE
ROS_NETWORK_INTERFACE="$(detect_network_interface)"

첫 번째 줄은 ROS_NETWORK_INTERFACE를 환경 변수로 export합니다.

export ROS_NETWORK_INTERFACE

두 번째 줄에서 자동 감지 함수의 결과를 저장합니다.

ROS_NETWORK_INTERFACE="$(detect_network_interface)"

감지 결과가 wlan0이면 다음과 같은 상태가 됩니다.

export ROS_NETWORK_INTERFACE=wlan0

이 변수는 현재 스크립트뿐 아니라 자식 프로세스에서도 사용할 수 있습니다.

Cyclone DDS 설정 파일 생성
write_cyclonedds_config "${ROS_NETWORK_INTERFACE}"

감지된 인터페이스 이름을 전달하여 Cyclone DDS XML 파일을 생성합니다.

예를 들어 인터페이스가 wlan0이라면 XML 내부에 다음 값이 들어갑니다.

<NetworkInterfaceAddress>wlan0</NetworkInterfaceAddress>

bringup session 생성

tmux new-session -d \
  -s "${SESSION_NAME}" \
  -n bringup \
  "$(ros_shell 'exec ros2 launch turtlebot3_bringup robot.launch.py')"

이 명령은 새로운 tmux session과 첫 번째 window를 동시에 생성합니다.

백그라운드 생성
-d

detached 상태로 session을 생성합니다.

즉, session을 만들지만 즉시 화면에 접속하지는 않습니다.

나머지 window와 pane을 모두 구성한 후 별도로 접속합니다.

session 이름
-s "${SESSION_NAME}"

session 이름을 turtlebot3으로 지정합니다.

첫 번째 window 이름
-n bringup

첫 번째 window 이름을 bringup으로 지정합니다.

실행 명령
"$(ros_shell 'exec ros2 launch turtlebot3_bringup robot.launch.py')"

ros_shell() 함수가 ROS 환경 설정이 포함된 완전한 명령을 생성합니다.

bringup window에서 최종적으로 실행되는 핵심 명령은 다음과 같습니다.

ros2 launch turtlebot3_bringup robot.launch.py

이 window에서는 일반적으로 다음 기능이 실행됩니다.

  1. OpenCR 통신
  2. 모터 제어
  3. 엔코더 데이터
  4. Odometry
  5. TF
  6. LDS-03 LiDAR
  7. 배터리 및 센서 상태
tmux 스크롤 기록 크기 설정
tmux set-option -t "${SESSION_NAME}" history-limit 50000

tmux pane에 저장할 과거 출력 줄 수를 50,000줄로 설정합니다.

ROS 2 launch 로그가 길게 출력되더라도 이전 내용을 더 많이 확인할 수 있습니다.

tmux 스크롤 모드는 다음 키로 들어갑니다.

Ctrl+b
[

방향키나 Page Up, Page Down으로 이동할 수 있습니다.

스크롤 모드 종료는 다음 키입니다.

q

tmux 버전이나 설정 범위에 따라 history-limit 관련 오류가 발생한다면 window 옵션으로 명시할 수 있습니다.

tmux set-window-option -t "${SESSION_NAME}" history-limit 50000
마우스 기능 활성화
tmux set-option -t "${SESSION_NAME}" mouse on

tmux에서 마우스를 사용할 수 있도록 설정합니다.

활성화되면 다음 기능을 사용할 수 있습니다.

  1. 마우스로 pane 선택
  2. 마우스 휠로 로그 스크롤
  3. window 선택
  4. pane 경계 크기 조절

SSH 터미널 프로그램의 설정에 따라 마우스 선택 동작이 달라질 수 있습니다.

camera window 생성

tmux new-window \
  -t "${SESSION_NAME}" \
  -n camera \
  "$(ros_shell 'exec ros2 launch turtlebot3_bringup camera.launch.py format:=BGR888 width:=320 height:=240')"

기존 turtlebot3 session에 새로운 window를 추가합니다.

대상 session
-t "${SESSION_NAME}"

새 window를 추가할 session을 지정합니다.

window 이름
-n camera

window 이름을 camera로 지정합니다.

카메라 launch 실행
ros2 launch turtlebot3_bringup camera.launch.py \
  format:=BGR888 \
  width:=320 \
  height:=240

카메라 출력 형식은 BGR888로 설정합니다.

영상 크기는 다음과 같습니다.

가로: 320
세로: 240

낮은 해상도를 사용하면 TurtleBot3의 CPU 사용량과 네트워크 대역폭을 줄일 수 있습니다.

audio window 생성

tmux new-window \
  -t "${SESSION_NAME}" \
  -n audio \
  "$(ros_shell 'exec ros2 run robot_audio_output audio_output_node')"

세 번째 window를 생성하고 오디오 출력 노드를 실행합니다.

실행되는 핵심 명령은 다음과 같습니다.

ros2 run robot_audio_output audio_output_node

이 명령을 실행하기 전에 ros_shell() 함수가 다음 환경을 모두 source합니다.

source /opt/ros/humble/setup.bash
source ~/turtlebot3_ws/install/setup.bash
source ~/rgb_led_ws/install/setup.bash

따라서 robot_audio_output 패키지를 찾기 위해 사용자가 audio window에서 수동으로 source할 필요가 없습니다.

debug window 생성

tmux new-window \
  -t "${SESSION_NAME}" \
  -n debug \
  "$(ros_shell "watch -n 2 -t 'ros2 node list 2>/dev/null'")"

네 번째 window를 만들고 첫 번째 디버깅 pane을 실행합니다.

watch 명령

watch -n 2 -t 'ros2 node list 2>/dev/null'

watch는 지정한 명령을 반복 실행하고 화면을 갱신합니다.

-n 2

2초 간격으로 실행합니다.

-t

watch의 제목 줄을 표시하지 않습니다.

실제로 반복 실행되는 명령은 다음과 같습니다.

ros2 node list

ROS 2 노드 목록을 2초마다 갱신하여 보여줍니다.

2>/dev/null

ROS 2 discovery 과정에서 발생할 수 있는 오류 메시지를 숨깁니다.

다만 오류 원인을 분석할 때는 오류 메시지가 숨겨져 있다는 점을 고려해야 합니다.

debug 두 번째 pane 생성
tmux split-window \
  -t "${SESSION_NAME}:debug" \
  "$(ros_shell "watch -n 2 -t 'ros2 topic list 2>/dev/null'")"

debug window를 분할하여 두 번째 pane을 만듭니다.

대상은 다음과 같습니다.

turtlebot3:debug

두 번째 pane에서 실행되는 명령은 다음과 같습니다.

watch -n 2 -t 'ros2 topic list 2>/dev/null'

ROS 2 토픽 목록을 2초마다 갱신합니다.

TurtleBot3가 정상적으로 실행되면 일반적으로 다음 토픽이 표시됩니다.

/scan
/odom
/tf
/tf_static
/joint_states
/cmd_vel
/battery_state

카메라가 정상이라면 영상 관련 토픽도 표시됩니다.

/camera/image_raw

실제 토픽 이름은 카메라 패키지 설정에 따라 다를 수 있습니다.

debug 세 번째 pane 생성
tmux split-window \
  -t "${SESSION_NAME}:debug" \
  "$(ros_shell 'ros2 topic hz /scan; exec bash -i')"

세 번째 pane에서는 LiDAR의 /scan 토픽 발행 주기를 확인합니다.

ros2 topic hz /scan

정상적으로 LiDAR 데이터가 발행되면 다음과 비슷한 결과가 반복 출력됩니다.

average rate: 10.012
min: 0.098s max: 0.102s std dev: 0.001
세미콜론의 의미
ros2 topic hz /scan; exec bash -i

세미콜론은 앞 명령이 성공하거나 실패하더라도 뒤 명령을 실행합니다.

ros2 topic hz /scan은 일반적으로 사용자가 Ctrl+C를 누를 때까지 계속 실행됩니다.

사용자가 측정을 중단하면 다음 명령이 실행됩니다.

exec bash -i
대화형 Bash 유지
exec bash -i

-i는 interactive shell을 의미합니다.

따라서 /scan 주기 측정을 중단한 뒤 pane이 바로 닫히지 않고 사용자가 다른 ROS 2 명령을 입력할 수 있습니다.

예를 들어 다음 명령을 실행할 수 있습니다.

ros2 topic echo /scan --once
ros2 topic info /scan -v
ros2 topic hz /odom
debug 네 번째 pane 생성
tmux split-window \
  -t "${SESSION_NAME}:debug" \
  "$(ros_shell "printf '\nTurtleBot3 debug shell\nROS_DOMAIN_ID=%s\nTURTLEBOT3_MODEL=%s\nLDS_MODEL=%s\nRMW_IMPLEMENTATION=%s\nROS_NETWORK_INTERFACE=%s\nCYCLONEDDS_URI=%s\n\n' \"\${ROS_DOMAIN_ID}\" \"\${TURTLEBOT3_MODEL}\" \"\${LDS_MODEL}\" \"\${RMW_IMPLEMENTATION}\" \"\${ROS_NETWORK_INTERFACE}\" \"\${CYCLONEDDS_URI}\"; exec bash -i")"

네 번째 pane은 현재 ROS 2 환경 변수 값을 출력한 후 대화형 Bash를 실행합니다.

출력 형식은 다음과 같습니다.

TurtleBot3 debug shell
ROS_DOMAIN_ID=200
TURTLEBOT3_MODEL=burger
LDS_MODEL=LDS-03
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ROS_NETWORK_INTERFACE=wlan0
CYCLONEDDS_URI=file:///tmp/turtlebot3_cyclonedds_ubuntu.xml
변수 앞의 역슬래시

다음 부분에는 변수 앞에 역슬래시가 들어 있습니다.

\"\${ROS_DOMAIN_ID}\"

역슬래시가 없으면 바깥쪽 스크립트가 ros_shell() 함수를 호출할 때 변수가 너무 일찍 확장될 수 있습니다.

\${ROS_DOMAIN_ID}

형태로 작성하면 최종적으로 tmux 내부의 Bash가 실행될 때 변수가 확장됩니다.

즉, 변수 평가 시점을 안쪽 셸로 미루는 역할을 합니다.

이 부분은 여러 단계의 셸 문자열을 생성할 때 중요합니다.

실행 구조는 다음과 같습니다.

현재 Bash 스크립트
→ ros_shell 함수에서 명령 문자열 생성
→ tmux가 문자열 전달
→ /bin/bash -lc가 문자열 실행
→ 내부 환경 변수 평가
디버깅 셸 유지

환경 변수를 출력한 후 다음 명령이 실행됩니다.

exec bash -i

따라서 사용자는 네 번째 pane에서 자유롭게 명령을 입력할 수 있습니다.

대표적인 디버깅 명령은 다음과 같습니다.

ros2 node list
ros2 topic list
ros2 service list
ros2 action list
ros2 topic echo /odom --once
ros2 topic info /scan -v
ros2 topic hz /camera/image_raw
모든 window에 remain-on-exit 적용
for window_name in bringup camera audio debug; do
  tmux set-window-option \
    -t "${SESSION_NAME}:${window_name}" \
    remain-on-exit on
done

for 반복문으로 네 개의 window에 동일한 옵션을 적용합니다.

반복되는 window 이름은 다음과 같습니다.

bringup
camera
audio
debug

실제로는 다음 네 명령과 같습니다.

tmux set-window-option -t turtlebot3:bringup remain-on-exit on
tmux set-window-option -t turtlebot3:camera remain-on-exit on
tmux set-window-option -t turtlebot3:audio remain-on-exit on
tmux set-window-option -t turtlebot3:debug remain-on-exit on

remain-on-exit on은 window 안의 프로그램이 종료되어도 pane을 없애지 않습니다.

카메라 노드가 장치 오류로 종료되었다면 해당 pane이 남아 있기 때문에 마지막 오류 메시지를 확인할 수 있습니다.

이 옵션이 없으면 프로그램 종료와 함께 pane 또는 window가 사라져 문제를 확인하기 어려울 수 있습니다.

debug pane 자동 정렬
tmux select-layout -t "${SESSION_NAME}:debug" tiled

debug window에 생성된 네 개 pane을 균등하게 배치합니다.

최종 형태는 대략 다음과 같습니다.

┌────────┬────────┐
│ ros2 node list    │ ros2 topic list    │
│ 2초마다 갱신     │ 2초마다 갱신     │
├────────┼────────┤
│ ros2 topic hz     │ debug shell       │
│ /scan               │환경 변수 및 명령│
└────────┴────────┘

split-window를 연속해서 실행하면 pane 크기가 불균형할 수 있습니다.

tiled 레이아웃은 전체 pane을 가능한 한 균등한 크기로 재배치합니다.

기본 표시 window 선택
tmux select-window -t "${SESSION_NAME}:bringup"

session 구성 완료 후 bringup window를 현재 활성 window로 선택합니다.

따라서 사용자가 session에 접속하면 가장 먼저 TurtleBot3 본체 실행 로그를 확인할 수 있습니다.

bringup 로그는 다음 문제를 확인하는 데 중요합니다.

  1. OpenCR 연결 실패
  2. LiDAR 연결 실패
  3. 시리얼 포트 권한 문제
  4. 잘못된 TurtleBot3 모델 설정
  5. 센서 초기화 오류
  6. 모터 통신 오류
생성 완료 정보 출력
echo "[OK] tmux session: ${SESSION_NAME}"
echo "[OK] network interface: ${ROS_NETWORK_INTERFACE}"
echo "[OK] Cyclone DDS config: ${CYCLONEDDS_FILE}"

session 생성 후 핵심 설정값을 출력합니다.

출력 예시는 다음과 같습니다.

[OK] tmux session: turtlebot3
[OK] network interface: wlan0
[OK] Cyclone DDS config: /tmp/turtlebot3_cyclonedds_ubuntu.xml

첫 번째 줄은 생성한 tmux session 이름입니다.

두 번째 줄은 Cyclone DDS가 사용할 네트워크 인터페이스입니다.

세 번째 줄은 생성한 Cyclone DDS XML 파일 경로입니다.

이 정보는 원격 PC에서 ROS 2 노드가 검색되지 않을 때 중요한 점검 자료가 됩니다.

14) create_session 함수가 생성하는 최종 구조

create_session() 함수 실행이 완료되면 다음 구조가 만들어집니다.

Session: turtlebot3

Window 0: bringup
└── ros2 launch turtlebot3_bringup robot.launch.py

Window 1: camera
└── ros2 launch turtlebot3_bringup camera.launch.py
    format:=BGR888 width:=320 height:=240

Window 2: audio
└── ros2 run robot_audio_output audio_output_node

Window 3: debug
├── Pane 1: ros2 node list
├── Pane 2: ros2 topic list
├── Pane 3: ros2 topic hz /scan
└── Pane 4: 수동 디버깅 Bash

Leave a Comment