Ubuntu 22.04 Server와 ROS 2 Humble을 사용하는 TurtleBot3 Burger는 일반적인 데스크톱 PC와 운영 방식이 다릅니다.
로봇에 모니터와 키보드를 연결하지 않고 원격 PC에서 SSH로 접속해 사용한다면 다음 기능이 필요합니다.
- 로봇 전원이 켜지면 ROS 2 노드가 자동 실행되어야 합니다.
- SSH 연결이 끊어져도 ROS 2 노드는 계속 실행되어야 합니다.
- 실행 중인 노드의 터미널 화면을 다시 확인할 수 있어야 합니다.
- 특정 노드가 종료되면 전체 로봇 시스템을 자동으로 복구해야 합니다.
- 명령어 하나로 로봇을 시작하거나 중지할 수 있어야 합니다.
tmux는 SSH 연결과 관계없이 터미널 세션을 유지하는 데 적합합니다. PM2는 프로세스 상태 확인, 자동 재시작, 로그 관리, 시스템 부팅 시 자동 실행을 담당합니다.
이 글에서는 다음 구조로 TurtleBot3 자동 실행 환경을 구성합니다.
Ubuntu systemd
└── PM2
└── 감시용 Bash 스크립트
└── tmux 세션
├── TurtleBot3 Bringup
├── Raspberry Pi Camera
├── Audio Node
└── ROS 2 Debug Window
1. PM2란 무엇인가
PM2는 백그라운드 프로세스를 실행하고 감시하는 프로세스 관리자입니다.
본래 Node.js 애플리케이션을 운영하기 위해 만들어졌지만 Bash 스크립트, Python 프로그램, 실행 파일도 관리할 수 있습니다. PM2 공식 문서에서도 Bash 스크립트를 직접 실행할 수 있다고 설명합니다.
PM2의 주요 기능은 다음과 같습니다.
- 프로그램 백그라운드 실행
- 프로그램 비정상 종료 감지
- 비정상 종료 시 자동 재실행
- 표준 출력과 오류 로그 저장
- CPU 및 메모리 상태 확인
- 여러 프로그램을 하나의 설정 파일로 관리
- Ubuntu 부팅 시 프로그램 자동 실행
- SSH 연결 종료 후에도 프로그램 실행 유지
PM2는 내부적으로 데몬 프로세스를 실행합니다. 사용자가 SSH 접속을 종료해도 PM2 데몬과 PM2가 관리하는 프로그램은 계속 동작합니다.
PM2에서 자주 사용하는 명령어는 다음과 같습니다.
pm2 status
pm2 list
pm2 start 프로그램
pm2 stop 프로그램이름
pm2 restart 프로그램이름
pm2 delete 프로그램이름
pm2 logs 프로그램이름
pm2 monit
pm2 status 또는 pm2 list는 등록된 프로그램의 상태를 표시합니다.
pm2 status
pm2 logs는 프로그램의 표준 출력과 오류 출력을 확인합니다.
pm2 logs
특정 프로그램 로그만 확인하려면 프로그램 이름을 지정합니다.
pm2 logs turtlebot3-robot
PM2 로그는 기본적으로 사용자 홈 디렉터리의 다음 위치에 저장됩니다.
~/.pm2/logs
PM2는 pm2 logs, pm2 monit 등의 명령을 통해 로그와 프로세스 상태를 확인할 수 있습니다.
2. tmux와 PM2의 역할 차이
tmux와 PM2는 비슷해 보이지만 담당하는 역할이 다릅니다.
tmux는 여러 터미널을 하나의 세션으로 묶고, SSH 연결이 종료된 뒤에도 터미널을 유지합니다.
PM2는 프로그램이 실행 중인지 감시하고, 프로그램이 종료되면 다시 실행합니다.
따라서 다음과 같이 역할을 분리하는 것이 좋습니다.
tmux: ROS 2 노드의 터미널 화면 관리
PM2: tmux 실행 스크립트의 상태 관리
systemd: Ubuntu 부팅 시 PM2 실행
이 구조를 사용하면 SSH로 접속한 뒤 다음 명령으로 ROS 2 노드 화면을 확인할 수 있습니다.
tmux attach -t turtlebot3
동시에 PM2는 감시 스크립트가 종료되거나 tmux 세션이 비정상적으로 사라졌을 때 시스템을 다시 실행합니다.
3. PM2를 직접 ROS 2 노드마다 사용하지 않는 이유
PM2를 이용해 ROS 2 노드를 각각 실행하는 것도 가능합니다.
예를 들면 다음과 같습니다.
pm2 start "ros2 launch turtlebot3_bringup robot.launch.py"
pm2 start "ros2 launch turtlebot3_bringup camera.launch.py"
pm2 start "ros2 run robot_audio_output audio_output_node"
그러나 이 방식에는 몇 가지 문제가 있습니다.
- 각 프로세스마다 ROS 2 환경을 별도로 설정해야 합니다.
/opt/ros/humble/setup.bash와 워크스페이스 설정 파일을 정확히 불러와야 합니다.- 각 프로세스의
ROS_DOMAIN_ID, DDS 설정, 로봇 모델 환경 변수를 동일하게 맞춰야 합니다. - 실행 중인 ROS 2 터미널 화면에 직접 들어가기 어렵습니다.
- 여러 노드를 한 화면에서 확인하기 어렵습니다.
ROS 2 명령과 패키지를 사용하려면 새로운 셸마다 ROS 2 및 워크스페이스의 setup 파일을 불러와야 합니다.
이미 tmux를 이용해 Bringup, 카메라, 오디오, 디버그 노드를 관리하고 있다면 PM2가 tmux 시스템 전체를 감시하도록 구성하는 편이 관리하기 쉽습니다.
4. 기존 tmux 스크립트를 PM2에서 바로 실행하면 안 되는 이유
기존 스크립트의 start 명령은 세션을 생성한 다음 attach_session 함수를 실행합니다.
start)
if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
create_session
fi
attach_session
;;
PM2는 일반적인 SSH 터미널이 아닌 백그라운드 환경에서 스크립트를 실행합니다.
이 상태에서 다음 명령이 실행되면 문제가 발생할 수 있습니다.
tmux attach-session -t turtlebot3
터미널이 없는 PM2 환경에서 tmux 세션에 접속하려 하면 다음과 비슷한 오류가 발생할 수 있습니다.
open terminal failed: not a terminal
또한 스크립트가 tmux 세션을 생성한 뒤 정상 종료되면 PM2는 프로그램이 종료된 것으로 판단합니다. PM2는 기본적으로 종료된 프로그램을 다시 실행하므로 불필요한 재시작 반복이 발생할 수 있습니다. PM2는 기본적으로 프로그램 종료나 충돌을 감지하면 자동으로 다시 실행합니다.
이를 해결하려면 다음 두 가지가 필요합니다.
- tmux 세션만 생성하고 접속하지 않는
boot명령 - tmux 세션과 중요 창을 계속 감시하는 supervisor 스크립트
5. 실행 파일을 저장할 디렉터리 생성
먼저 로봇 실행 파일과 로그를 저장할 디렉터리를 생성합니다.
mkdir -p ~/robot_scripts
mkdir -p ~/robot_logs
기존 tmux 스크립트를 다음 위치에 저장한다고 가정합니다.
~/robot_scripts/turtlebot3_tmux.sh
실행 권한을 추가합니다.
chmod +x ~/robot_scripts/turtlebot3_tmux.sh
6. 기존 tmux 스크립트의 사용자 변수 보강
기존 스크립트에는 다음 코드가 있습니다.
CYCLONEDDS_FILE="/tmp/${SESSION_NAME}_cyclonedds_${USER}.xml"
SSH 로그인 셸에서는 일반적으로 USER 변수가 존재합니다. 그러나 systemd와 PM2로 실행하는 환경에서는 USER 환경 변수가 항상 전달된다고 가정하지 않는 것이 안전합니다.
스크립트 상단을 다음과 같이 변경합니다.
SESSION_NAME="turtlebot3"
RUN_USER="${USER:-$(id -un)}"
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_${RUN_USER}.xml"
${USER:-$(id -un)}은 USER 변수가 없을 경우 현재 실행 사용자의 이름을 id -un 명령으로 가져옵니다.
set -u가 설정된 스크립트에서 정의되지 않은 환경 변수를 직접 사용하면 스크립트가 즉시 종료될 수 있으므로 이 처리가 필요합니다.
7. tmux 스크립트에 boot 명령 추가
기존 스크립트의 case 문에 boot 명령을 추가합니다.
boot 명령은 tmux 세션을 생성하지만 세션에 접속하지 않습니다.
case "${COMMAND}" in
start)
if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
create_session
fi
attach_session
;;
boot)
if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
create_session
else
echo "${SESSION_NAME} 세션이 이미 실행 중입니다."
fi
;;
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|boot|attach|stop|restart|status}" >&2
exit 2
;;
esac
수정 후 다음 명령으로 동작을 확인합니다.
~/robot_scripts/turtlebot3_tmux.sh boot
tmux 세션이 생성됐는지 확인합니다.
tmux ls
정상적으로 실행되었다면 다음과 비슷하게 표시됩니다.
turtlebot3: 4 windows
세션에 접속합니다.
~/robot_scripts/turtlebot3_tmux.sh attach
테스트가 끝나면 세션을 종료합니다.
~/robot_scripts/turtlebot3_tmux.sh stop
8. PM2용 supervisor 스크립트 작성
PM2가 기존 tmux 스크립트를 직접 실행하게 만들지 않고, 별도의 supervisor 스크립트를 실행하도록 구성합니다.
다음 파일을 생성합니다.
nano ~/robot_scripts/turtlebot3_pm2_supervisor.sh
파일 내용은 다음과 같습니다.
#!/usr/bin/env bash
set -Eeuo pipefail
SESSION_NAME="turtlebot3"
ROBOT_CONTROL_SCRIPT="${
ROBOT_CONTROL_SCRIPT:-${HOME}/robot_scripts/turtlebot3_tmux.sh
}"
NETWORK_WAIT_COUNT=60
NETWORK_WAIT_INTERVAL=2
fail()
{
echo "[ERROR] $*" >&2
exit 1
}
shutdown_robot()
{
trap - INT TERM HUP
echo "[INFO] PM2 종료 신호를 수신했습니다."
"${ROBOT_CONTROL_SCRIPT}" stop || true
exit 0
}
restart_robot()
{
local reason="$1"
echo "[ERROR] ${reason}" >&2
echo "[INFO] tmux 세션을 정리한 후 PM2 재시작을 요청합니다." >&2
"${ROBOT_CONTROL_SCRIPT}" stop || true
exit 1
}
wait_for_network()
{
local count
for ((count = 1; count <= NETWORK_WAIT_COUNT; count++)); do
if ip route show default 2>/dev/null |
grep -qE '^default[[:space:]]'; then
echo "[OK] 기본 네트워크 경로가 준비되었습니다."
return 0
fi
echo "[WAIT] 네트워크 준비 대기 중: ${count}/${NETWORK_WAIT_COUNT}"
sleep "${NETWORK_WAIT_INTERVAL}"
done
fail "기본 네트워크 경로를 찾을 수 없습니다."
}
is_window_alive()
{
local window_name="$1"
local pane_dead
pane_dead="$(
tmux display-message \
-p \
-t "${SESSION_NAME}:${window_name}" \
'#{pane_dead}' \
2>/dev/null || true
)"
[[ "${pane_dead}" == "0" ]]
}
command -v tmux >/dev/null 2>&1 ||
fail "tmux가 설치되어 있지 않습니다."
[[ -x "${ROBOT_CONTROL_SCRIPT}" ]] ||
fail "로봇 실행 스크립트를 실행할 수 없습니다: ${ROBOT_CONTROL_SCRIPT}"
trap shutdown_robot INT TERM HUP
wait_for_network
# 이전 실행에서 남은 불완전한 세션이 있으면 정리합니다.
"${ROBOT_CONTROL_SCRIPT}" stop >/dev/null 2>&1 || true
echo "[INFO] TurtleBot3 tmux 세션을 시작합니다."
"${ROBOT_CONTROL_SCRIPT}" boot
while true; do
if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
restart_robot "tmux 세션이 종료되었습니다."
fi
for window_name in bringup camera audio; do
if ! is_window_alive "${window_name}"; then
restart_robot \
"중요 tmux 창이 종료되었습니다: ${window_name}"
fi
done
sleep 5
done
1) tmux 세션 이름 설정
SESSION_NAME="turtlebot3"
감시 대상이 되는 tmux 세션 이름을 turtlebot3으로 지정합니다.
이 값은 다음 명령에서 사용됩니다.
tmux has-session -t "${SESSION_NAME}"
실제로 turtlebot3_tmux.sh가 생성하는 세션 이름도 반드시 turtlebot3이어야 합니다.
예를 들어 실행 스크립트에서 다음과 같이 세션을 생성해야 합니다.
tmux new-session -d -s turtlebot3
세션 이름이 서로 다르면 로봇 프로세스는 실행되고 있어도 감시 스크립트는 세션이 종료된 것으로 판단합니다.
2) 로봇 제어 스크립트 경로 설정
ROBOT_CONTROL_SCRIPT="${
ROBOT_CONTROL_SCRIPT:-${HOME}/robot_scripts/turtlebot3_tmux.sh
}"
이 코드는 로봇 실행과 종료를 담당하는 외부 스크립트 경로를 결정합니다.
기본 경로는 다음과 같습니다.
${HOME}/robot_scripts/turtlebot3_tmux.sh
사용자 홈 디렉터리가 /home/sjyong이라면 실제 경로는 다음과 같습니다.
/home/sjyong/robot_scripts/turtlebot3_tmux.sh
:- 문법은 환경 변수가 정의되어 있지 않거나 값이 비어 있을 때 기본값을 사용한다는 의미입니다.
환경 변수 ROBOT_CONTROL_SCRIPT가 설정되어 있다면 해당 값을 우선 사용합니다.
예를 들어 다음과 같이 실행할 수 있습니다.
ROBOT_CONTROL_SCRIPT=/opt/robot/bin/turtlebot3_tmux.sh \
pm2 start turtlebot3_watchdog.sh
이 구조를 사용하면 스크립트 소스를 수정하지 않고도 개발 환경과 운영 환경에서 서로 다른 실행 스크립트를 지정할 수 있습니다.
3) 네트워크 대기 설정
NETWORK_WAIT_COUNT=60
NETWORK_WAIT_INTERVAL=2
네트워크 준비 상태를 확인하기 위한 반복 횟수와 대기 시간을 설정합니다.
각 설정의 의미는 다음과 같습니다.
- 최대 확인 횟수: 60회
- 확인 간격: 2초
- 최대 대기 시간: 120초
최대 대기 시간은 다음과 같이 계산됩니다.
60회 × 2초 = 120초
부팅 직후 DHCP 주소 할당이나 Wi-Fi 연결에 시간이 걸리는 환경을 고려한 설정입니다.
단순히 IP 주소가 할당되었는지를 검사하는 것이 아니라 기본 라우팅 경로가 존재하는지를 검사합니다.
4) 공통 오류 종료 함수
fail()
{
echo "[ERROR] $*" >&2
exit 1
}
fail 함수는 오류 메시지를 출력하고 스크립트를 비정상 종료하는 공통 함수입니다.
$*의 의미
$*
함수에 전달된 모든 인수를 하나의 문자열로 출력합니다.
예를 들어 다음과 같이 호출하면
fail "tmux가 설치되어 있지 않습니다."
다음 메시지가 출력됩니다.
[ERROR] tmux가 설치되어 있지 않습니다.
>&2의 의미
>&2
메시지를 표준 출력이 아닌 표준 오류로 전송합니다.
리눅스의 기본 파일 디스크립터는 다음과 같습니다.
0: 표준 입력1: 표준 출력2: 표준 오류
오류 메시지를 표준 오류로 분리하면 PM2 로그, systemd 로그, 파일 리다이렉션 환경에서 정상 메시지와 오류 메시지를 구분하기 쉽습니다.
exit 1의 의미
exit 1
스크립트를 실패 상태로 종료합니다.
PM2가 자동 재시작하도록 설정되어 있다면 종료 코드 1을 감지한 뒤 감시 스크립트를 다시 실행합니다.
즉, 이 스크립트에서 exit 1은 단순한 종료가 아니라 PM2에 재시작을 요청하는 신호로 사용됩니다.
5) PM2 종료 신호 처리
shutdown_robot()
{
trap - INT TERM HUP
echo "[INFO] PM2 종료 신호를 수신했습니다."
"${ROBOT_CONTROL_SCRIPT}" stop || true
exit 0
}
이 함수는 PM2 또는 운영체제로부터 정상 종료 신호를 받았을 때 실행됩니다.
기존 트랩 해제
trap - INT TERM HUP
INT, TERM, HUP 신호에 등록되어 있던 트랩을 해제합니다.
이 처리가 필요한 이유는 종료 작업 중 같은 신호가 다시 들어왔을 때 shutdown_robot 함수가 중복 실행되는 것을 방지하기 위해서입니다.
종료 메시지 출력
echo "[INFO] PM2 종료 신호를 수신했습니다."
PM2가 프로세스를 중지하거나 재시작할 때 종료 요청을 받았다는 내용을 로그로 남깁니다.
로봇 프로세스 정리
"${ROBOT_CONTROL_SCRIPT}" stop || true
외부 제어 스크립트의 stop 명령을 호출합니다.
일반적으로 turtlebot3_tmux.sh stop은 다음 작업을 수행해야 합니다.
turtlebot3tmux 세션 종료- tmux 내부의 ROS 2 노드 종료
- 카메라 프로세스 종료
- 오디오 프로세스 종료
- 관련 백그라운드 프로세스 정리
뒤에 붙은 || true는 종료 작업이 실패하더라도 현재 감시 스크립트의 종료 절차를 계속 진행하기 위한 처리입니다.
예를 들어 tmux 세션이 이미 종료된 상태에서는 stop 명령이 실패할 수 있습니다.
하지만 종료 과정에서는 이 상태가 치명적인 오류가 아니므로 무시합니다.
정상 종료 코드 반환
exit 0
PM2의 명시적인 종료 요청에 따라 정상적으로 종료되었음을 나타냅니다.
이 부분은 장애 감지 함수의 exit 1과 목적이 다릅니다.
exit 0: 사용자의 중지 명령이나 PM2 종료 요청exit 1: 장애 발생으로 인한 PM2 재시작 요청
6) 장애 발생 시 재시작 요청
restart_robot()
{
local reason="$1"
echo "[ERROR] ${reason}" >&2
echo "[INFO] tmux 세션을 정리한 후 PM2 재시작을 요청합니다." >&2
"${ROBOT_CONTROL_SCRIPT}" stop || true
exit 1
}
restart_robot 함수는 감시 중 장애가 발견되었을 때 호출됩니다.
장애 원인 저장
local reason="$1"
함수의 첫 번째 인수를 지역 변수 reason에 저장합니다.
예를 들어 다음과 같이 호출됩니다.
restart_robot "tmux 세션이 종료되었습니다."
또는 다음과 같이 호출됩니다.
restart_robot "중요 tmux 창이 종료되었습니다: camera"
local을 사용했기 때문에 reason 변수는 함수 내부에서만 유효합니다.
오류 원인 기록
echo "[ERROR] ${reason}" >&2
장애가 발생한 원인을 PM2 오류 로그에 기록합니다.
이를 통해 다음과 같은 상태를 구분할 수 있습니다.
- 전체 tmux 세션 종료
- bringup 창 종료
- camera 창 종료
- audio 창 종료
기존 세션 정리
"${ROBOT_CONTROL_SCRIPT}" stop || true
일부 창만 종료된 경우에도 전체 tmux 세션을 정리합니다.
예를 들어 camera 창만 비정상 종료되었을 때 카메라 창만 다시 만드는 대신, 전체 로봇 시스템을 정리한 뒤 PM2를 통해 다시 시작합니다.
이 방식은 복구 단위가 크지만 상태 일관성을 유지하기 쉽다는 장점이 있습니다.
ROS 2 노드 간 의존 관계가 복잡할 때는 일부 노드만 재시작하는 것보다 전체 프로세스를 다시 시작하는 편이 더 안정적일 수 있습니다.
PM2 재시작 유도
exit 1
감시 스크립트를 비정상 종료합니다.
PM2가 자동 재시작 옵션으로 관리하고 있다면 이 종료를 장애로 판단하고 스크립트를 다시 실행합니다.
전체 복구 순서는 다음과 같습니다.
- tmux 세션 또는 중요 창의 장애 감지
restart_robot함수 호출- 기존 tmux 세션 정리
- 감시 스크립트 종료 코드 1 반환
- PM2가 감시 스크립트 재실행
- 네트워크 상태 재확인
- 기존 세션 재정리
- TurtleBot3 tmux 세션 재생성
- 감시 루프 재진입
7) 네트워크 준비 대기 함수
wait_for_network()
{
local count
for ((count = 1; count <= NETWORK_WAIT_COUNT; count++)); do
if ip route show default 2>/dev/null |
grep -qE '^default[[:space:]]'; then
echo "[OK] 기본 네트워크 경로가 준비되었습니다."
return 0
fi
echo "[WAIT] 네트워크 준비 대기 중: ${count}/${NETWORK_WAIT_COUNT}"
sleep "${NETWORK_WAIT_INTERVAL}"
done
fail "기본 네트워크 경로를 찾을 수 없습니다."
}
이 함수는 기본 네트워크 경로가 생성될 때까지 대기합니다.
반복 변수 선언
local count
반복 횟수를 저장할 지역 변수입니다.
최대 60회 반복
for ((count = 1; count <= NETWORK_WAIT_COUNT; count++)); do
count 값을 1부터 시작해 NETWORK_WAIT_COUNT 값인 60까지 증가시킵니다.
기본 경로 확인
ip route show default
시스템의 기본 라우팅 경로를 출력합니다.
정상적인 환경에서는 다음과 비슷한 결과가 출력됩니다.
default via 192.168.0.1 dev wlan0 proto dhcp metric 600
유선 네트워크 환경에서는 다음과 같이 나타날 수 있습니다.
default via 192.168.0.1 dev eth0 proto dhcp metric 100
오류 메시지 숨김
2>/dev/null
ip route 명령에서 발생하는 표준 오류를 /dev/null로 보냅니다.
네트워크 인터페이스가 준비되지 않은 초기 부팅 단계에서 불필요한 오류 메시지가 반복 출력되는 것을 방지합니다.
기본 경로 문자열 검사
grep -qE '^default[[:space:]]'
출력 내용이 default로 시작하고 그 뒤에 공백 문자가 있는지 검사합니다.
각 옵션의 의미는 다음과 같습니다.
-q: 결과를 화면에 출력하지 않고 성공 여부만 반환-E: 확장 정규 표현식 사용^default: 줄의 시작이default[[:space:]]: 공백 문자
기본 경로가 존재하면 grep은 종료 코드 0을 반환합니다.
네트워크 준비 완료
echo "[OK] 기본 네트워크 경로가 준비되었습니다."
return 0
기본 경로를 찾으면 성공 메시지를 출력하고 함수 실행을 종료합니다.
return 0은 현재 함수만 종료하며 전체 스크립트는 계속 실행됩니다.
네트워크 대기 메시지
echo "[WAIT] 네트워크 준비 대기 중: ${count}/${NETWORK_WAIT_COUNT}"
sleep "${NETWORK_WAIT_INTERVAL}"
네트워크가 준비되지 않았다면 현재 반복 횟수를 출력하고 2초 동안 대기합니다.
예상 로그는 다음과 같습니다.
[WAIT] 네트워크 준비 대기 중: 1/60
[WAIT] 네트워크 준비 대기 중: 2/60
[WAIT] 네트워크 준비 대기 중: 3/60
[OK] 기본 네트워크 경로가 준비되었습니다.
최대 대기 시간 초과
fail "기본 네트워크 경로를 찾을 수 없습니다."
120초 동안 기본 경로가 생성되지 않으면 스크립트를 종료 코드 1로 종료합니다.
PM2가 자동 재시작하도록 설정되어 있다면 일정 시간 후 다시 네트워크 확인을 시작하게 됩니다.
8) tmux 창 상태 확인 함수
is_window_alive()
{
local window_name="$1"
local pane_dead
pane_dead="$(
tmux display-message \
-p \
-t "${SESSION_NAME}:${window_name}" \
'#{pane_dead}' \
2>/dev/null || true
)"
[[ "${pane_dead}" == "0" ]]
}
이 함수는 지정한 tmux 창의 현재 pane이 살아 있는지 검사합니다.
창 이름 전달
local window_name="$1"
함수를 호출할 때 전달된 첫 번째 값을 창 이름으로 사용합니다.
다음과 같이 호출할 수 있습니다.
is_window_alive "bringup"
is_window_alive "camera"
is_window_alive "audio"
pane 상태 저장 변수
local pane_dead
tmux의 pane_dead 값을 저장할 지역 변수입니다.
tmux 상태 조회
tmux display-message \
-p \
-t "${SESSION_NAME}:${window_name}" \
'#{pane_dead}'
각 옵션의 의미는 다음과 같습니다.
display-message: tmux 내부 상태 정보를 조회-p: 결과를 표준 출력으로 반환-t: 검사할 세션과 창 지정#{pane_dead}: pane 종료 상태 조회
예를 들어 다음과 같은 대상이 만들어집니다.
turtlebot3:bringup
turtlebot3:camera
turtlebot3:audio
pane_dead 값은 일반적으로 다음 의미를 가집니다.
0: pane에서 실행 중인 프로세스가 살아 있음1: pane의 프로세스가 종료됨
명령 치환
pane_dead="$( ... )"
tmux 명령의 출력 결과를 pane_dead 변수에 저장합니다.
조회 오류 무시
2>/dev/null || true
창이 존재하지 않거나 세션이 사라진 경우 tmux display-message가 실패할 수 있습니다.
set -e가 활성화된 상태이므로 단순히 명령이 실패하면 전체 스크립트가 즉시 종료될 수 있습니다.
이를 방지하기 위해 || true를 사용합니다.
명령이 실패하면 pane_dead에는 빈 문자열이 저장됩니다.
창 생존 여부 반환
[[ "${pane_dead}" == "0" ]]
pane_dead 값이 정확히 0인지 검사합니다.
결과는 함수의 종료 코드로 사용됩니다.
- 값이
0이면 함수 성공 - 값이
1또는 빈 문자열이면 함수 실패
따라서 다음 코드에서 창의 상태를 조건문으로 검사할 수 있습니다.
if ! is_window_alive "${window_name}"; then
restart_robot "중요 tmux 창이 종료되었습니다: ${window_name}"
fi
9) tmux 설치 여부 검사
command -v tmux >/dev/null 2>&1 ||
fail "tmux가 설치되어 있지 않습니다."
command -v는 지정한 명령의 실행 가능 여부를 검사합니다.
tmux가 설치되어 있다면 실행 파일 경로가 반환됩니다.
/usr/bin/tmux
출력은 다음 코드로 숨깁니다.
>/dev/null 2>&1
tmux를 찾지 못하면 command -v가 실패하고 || 뒤의 fail 함수가 실행됩니다.
Ubuntu에서는 다음 명령으로 tmux를 설치할 수 있습니다.
sudo apt update
sudo apt install tmux
10) 로봇 제어 스크립트 실행 권한 검사
[[ -x "${ROBOT_CONTROL_SCRIPT}" ]] ||
fail "로봇 실행 스크립트를 실행할 수 없습니다: ${ROBOT_CONTROL_SCRIPT}"
-x 조건은 해당 파일이 존재하고 실행 가능한지를 검사합니다.
다음과 같은 경우 검사에 실패할 수 있습니다.
- 파일이 존재하지 않음
- 경로가 잘못됨
- 실행 권한이 없음
- 대상이 일반 파일이 아님
- 현재 사용자가 실행할 권한이 없음
실행 권한이 없다면 다음 명령으로 권한을 부여할 수 있습니다.
chmod +x ~/robot_scripts/turtlebot3_tmux.sh
이 검사는 로봇 실행을 시작하기 전에 수행되므로, 잘못된 경로나 권한 문제를 초기에 발견할 수 있습니다.
11) 종료 신호 트랩 등록
trap shutdown_robot INT TERM HUP
현재 프로세스가 다음 신호를 받으면 shutdown_robot 함수를 실행하도록 등록합니다.
INT
키보드에서 Ctrl+C를 누를 때 발생하는 인터럽트 신호입니다.
터미널에서 스크립트를 직접 실행한 뒤 Ctrl+C를 누르면 tmux 세션을 정리하고 종료합니다.
TERM
프로세스의 정상 종료를 요청하는 대표적인 신호입니다.
PM2의 stop, restart, delete 명령은 일반적으로 관리 중인 프로세스에 종료 신호를 전달합니다.
HUP
터미널 연결 종료나 세션 변경과 관련된 신호입니다.
환경에 따라 프로세스 재시작이나 연결 종료 상황에서 전달될 수 있습니다.
트랩을 등록하지 않으면 감시 스크립트만 종료되고 tmux 세션이 백그라운드에 남을 가능성이 있습니다.
트랩을 통해 외부 종료 요청과 내부 장애 종료를 구분할 수 있습니다.
12) 네트워크 준비 확인
wait_for_network
필수 프로그램과 실행 스크립트를 검사한 뒤 네트워크 기본 경로가 준비될 때까지 대기합니다.
ROS 2 시스템은 네트워크 인터페이스와 DDS 통신 환경에 영향을 받기 때문에 부팅 직후 너무 빠르게 노드를 실행하면 다음과 같은 문제가 발생할 수 있습니다.
- ROS 2 노드 검색 실패
- DDS 인터페이스 선택 오류
- 카메라 스트리밍 연결 실패
- 원격 PC와의 통신 실패
- 시간 동기화 이전의 잘못된 타임스탬프
- Wi-Fi 연결 이전의 서비스 초기화 실패
다만 기본 경로가 존재한다고 해서 외부 인터넷이나 원격 제어 PC까지 연결 가능하다는 뜻은 아닙니다.
이 함수는 네트워크 인터페이스에 기본 라우팅이 설정되었는지만 확인합니다.
13) 기존 tmux 세션 정리
"${ROBOT_CONTROL_SCRIPT}" stop >/dev/null 2>&1 || true
새로운 tmux 세션을 시작하기 전에 이전 실행에서 남은 세션을 정리합니다.
PM2나 시스템이 비정상 종료된 경우 다음 상태가 발생할 수 있습니다.
- PM2 프로세스는 종료되었지만 tmux 세션은 남아 있음
- 일부 tmux 창만 살아 있음
- 기존 ROS 2 노드가 백그라운드에서 계속 실행 중
- 카메라 장치가 이전 프로세스에 의해 점유됨
- 오디오 장치가 이전 프로세스에 의해 점유됨
- 같은 이름의 tmux 세션이 이미 존재함
이전 세션을 정리하지 않고 새 세션을 생성하면 다음과 같은 오류가 발생할 수 있습니다.
duplicate session: turtlebot3
또는 동일한 ROS 2 노드가 중복 실행될 수 있습니다.
출력은 다음 코드로 모두 숨깁니다.
>/dev/null 2>&1
초기 정리 작업은 정상적인 실행 과정이므로 세션이 없다는 오류를 로그에 계속 표시하지 않도록 처리한 것입니다.
14) TurtleBot3 tmux 세션 시작
echo "[INFO] TurtleBot3 tmux 세션을 시작합니다."
"${ROBOT_CONTROL_SCRIPT}" boot
네트워크가 준비되고 기존 세션 정리가 완료되면 외부 로봇 제어 스크립트에 boot 인수를 전달합니다.
외부 스크립트는 다음과 비슷한 구조로 작성될 수 있습니다.
case "${1:-}" in
boot)
start_robot
;;
stop)
stop_robot
;;
*)
echo "사용법: $0 {boot|stop}" >&2
exit 1
;;
esac
boot 명령은 일반적으로 다음 작업을 수행합니다.
turtlebot3tmux 세션 생성bringup창 생성camera창 생성audio창 생성- 각 창에서 필요한 ROS 2 명령 실행
세션은 구성하지만 연결은 하지 않습니다.
boot 명령이 실패하면 set -e 설정에 의해 감시 스크립트가 종료 코드 1로 종료됩니다.
PM2가 자동 재시작하도록 설정되어 있다면 다시 실행을 시도합니다.
15) 무한 감시 루프
while true; do
로봇 시스템이 시작된 이후 무한 반복하면서 tmux 상태를 검사합니다.
반복문은 정상 상태에서는 종료되지 않습니다.
종료되는 경우는 다음과 같습니다.
- PM2 또는 사용자가 종료 신호를 보냄
- tmux 세션이 종료됨
- 중요 tmux 창이 종료됨
- 예기치 않은 셸 오류 발생
- 시스템 종료 또는 재부팅
16) 전체 tmux 세션 검사
if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
restart_robot "tmux 세션이 종료되었습니다."
fi
tmux has-session 명령은 지정한 이름의 tmux 세션이 존재하는지 확인합니다.
검사 대상은 다음과 같습니다.
turtlebot3
세션이 존재하면 명령은 성공 상태를 반환합니다.
세션이 존재하지 않으면 실패 상태를 반환합니다.
앞에 붙은 !는 결과를 반전합니다.
따라서 세션이 존재하지 않을 때 restart_robot 함수가 호출됩니다.
오류 출력은 다음 코드로 숨깁니다.
2>/dev/null
세션이 사라진 상황은 이미 스크립트에서 직접 처리하고 있으므로 tmux 자체 오류 메시지를 중복 표시하지 않습니다.
전체 세션이 종료되는 원인은 다음과 같을 수 있습니다.
- 사용자가
tmux kill-session실행 - 외부 스크립트가 세션 종료
- tmux 서버 비정상 종료
- 시스템 자원 부족
- tmux 내부의 마지막 창 종료
- 로봇 실행 스크립트 오류
17) 중요 tmux 창 검사
for window_name in bringup camera audio; do
if ! is_window_alive "${window_name}"; then
restart_robot \
"중요 tmux 창이 종료되었습니다: ${window_name}"
fi
done
전체 tmux 세션이 존재하더라도 일부 창의 프로세스만 종료될 수 있습니다.
이를 감지하기 위해 다음 3개의 창을 순서대로 검사합니다.
bringupcameraaudio
bringup 창
bringup 창은 일반적으로 로봇 기본 구동에 필요한 노드를 실행합니다.
예를 들면 다음 기능이 포함될 수 있습니다.
- 모터 제어
- 라이다
- 조인트 상태
- TF
- 로봇 상태 발행
- 센서 드라이버
bringup 창이 종료되면 로봇의 핵심 기능이 중단될 가능성이 높기 때문에 전체 시스템을 재시작합니다.
camera 창
카메라 드라이버나 영상 처리 노드를 실행하는 창입니다.
카메라 프로세스가 장치 오류, USB 연결 문제, 프레임 수신 오류 등으로 종료되면 감시 스크립트가 이를 감지합니다.
audio 창
마이크, 스피커, 음성 인식 또는 오디오 처리 관련 프로세스를 실행하는 창입니다.
오디오 기능이 로봇의 필수 기능이라면 중요 창에 포함하는 것이 적절합니다.
창 이름 일치 조건
외부 로봇 제어 스크립트에서 생성한 tmux 창 이름이 정확히 다음과 같아야 합니다.
bringup
camera
audio
예를 들어 실제 창 이름이 cam인데 감시 스크립트에서는 camera를 검사하면 창이 존재하지 않는 것으로 판단하고 계속 재시작할 수 있습니다.
18) 감시 주기
sleep 5
한 번의 상태 검사가 완료되면 5초 동안 대기합니다.
즉, 장애가 발생한 뒤 최대 약 5초 이내에 상태를 감지합니다.
감시 주기를 너무 짧게 설정하면 다음 문제가 발생할 수 있습니다.
- 불필요한 CPU 사용
- tmux 서버에 과도한 상태 조회 요청
- 로그 증가
- 일시적인 상태 변화를 장애로 오인
반대로 감시 주기를 너무 길게 설정하면 장애 복구가 늦어집니다.
일반적인 로봇 시스템에서는 3초에서 10초 정도가 현실적인 범위입니다.
현재 값인 5초는 반응성과 시스템 부하 사이에서 무난한 설정입니다.
19) PM2와의 동작 관계
이 스크립트는 자체적으로 프로세스를 다시 실행하지 않습니다.
장애가 발생하면 다음 코드로 종료합니다.
exit 1
실제 재실행은 PM2가 담당합니다.
PM2에는 다음과 같이 등록할 수 있습니다.
pm2 start ~/robot_scripts/turtlebot3_watchdog.sh \
--name turtlebot3-watchdog \
--interpreter bash \
--restart-delay 5000
각 옵션의 의미는 다음과 같습니다.
--name turtlebot3-watchdog: PM2 프로세스 이름 지정--interpreter bash: bash 스크립트로 실행--restart-delay 5000: 실패 후 5초 뒤 재시작
현재 상태는 다음 명령으로 확인할 수 있습니다.
pm2 status
로그는 다음 명령으로 확인할 수 있습니다.
pm2 logs turtlebot3-watchdog
수동 재시작은 다음과 같이 수행할 수 있습니다.
pm2 restart turtlebot3-watchdog
정상 종료는 다음과 같이 수행할 수 있습니다.
pm2 stop turtlebot3-watchdog
PM2 프로세스 목록에서 제거하려면 다음 명령을 사용합니다.
pm2 delete turtlebot3-watchdog
재부팅 후 자동 실행을 위해서는 PM2 startup과 save 설정이 필요합니다.
pm2 startup
pm2 save
pm2 startup 실행 결과로 출력되는 관리자 권한 명령도 추가로 실행해야 합니다.
20) 정상 종료와 장애 종료의 차이
이 스크립트에서 종료 코드는 명확한 의미를 가집니다.
정상 종료
exit 0
다음 상황에서 사용됩니다.
- PM2가 정상 종료를 요청한 경우
- 사용자가
Ctrl+C를 입력한 경우 - 시스템 종료 과정에서 종료 신호를 받은 경우
정상 종료 전에는 turtlebot3_tmux.sh stop을 호출해 로봇 프로세스를 정리합니다.
장애 종료
exit 1
다음 상황에서 사용됩니다.
- tmux 세션이 사라진 경우
- bringup 창이 종료된 경우
- camera 창이 종료된 경우
- audio 창이 종료된 경우
- 네트워크가 제한 시간 안에 준비되지 않은 경우
- 필수 명령이나 파일 조건이 충족되지 않은 경우
- boot 명령 실행이 실패한 경우
PM2는 장애 종료를 감지한 뒤 스크립트를 다시 시작할 수 있습니다.
21) 이 구조의 장점
이 스크립트 구조의 주요 장점은 다음과 같습니다.
- ROS 2 노드를 기능별 tmux 창으로 분리할 수 있습니다.
- SSH 연결이 종료되어도 로봇 프로세스가 유지됩니다.
- tmux 세션 전체 상태를 감시할 수 있습니다.
- 특정 중요 창의 종료 상태를 감지할 수 있습니다.
- 장애 발생 시 전체 시스템을 초기 상태에서 재시작할 수 있습니다.
- PM2를 이용해 프로세스 재시작 횟수와 로그를 관리할 수 있습니다.
- 시스템 재부팅 후 자동 실행 구성이 가능합니다.
- 정상 종료 시 tmux 세션을 정리할 수 있습니다.
- 개발 환경과 운영 환경에서 제어 스크립트 경로를 다르게 지정할 수 있습니다.
- 네트워크 준비 이전에 ROS 2 노드가 실행되는 문제를 줄일 수 있습니다.
22) 주의해야 할 부분
pane이 종료되지 않는 형태의 오류
현재 감시 방식은 pane_dead 값만 확인합니다.
하지만 tmux 창 안에서 실행한 ROS 2 명령이 종료된 후 자동으로 셸 프롬프트로 돌아오는 구조라면 pane 자체는 살아 있을 수 있습니다.
예를 들어 다음 명령이 종료되어도
ros2 launch turtlebot3_bringup robot.launch.py
tmux pane 내부의 bash 셸이 계속 실행 중이라면 pane_dead 값은 0일 수 있습니다.
이 경우 ROS 2 프로세스는 종료되었지만 감시 스크립트는 정상으로 판단할 가능성이 있습니다.
이를 방지하려면 tmux 창에서 셸 대신 ROS 2 명령을 직접 실행하거나, 명령 종료 시 pane도 종료되도록 구성해야 합니다.
예를 들어 다음과 같이 실행할 수 있습니다.
tmux new-window \
-t turtlebot3 \
-n bringup \
"bash -lc 'source /opt/ros/humble/setup.bash &&
source ~/robot_ws/install/setup.bash &&
exec ros2 launch turtlebot3_bringup robot.launch.py'"
exec를 사용하면 bash 프로세스가 ROS 2 실행 프로세스로 대체됩니다.
ROS 2 프로세스가 종료되면 pane도 종료되므로 pane_dead 감지가 정확해집니다.
remain-on-exit 설정
tmux에서 종료된 pane 상태를 확인하려면 remain-on-exit 설정이 필요할 수 있습니다.
tmux set-option -t turtlebot3 remain-on-exit on
이 설정이 없으면 pane이 종료되는 즉시 창 자체가 사라질 수 있습니다.
창이 사라져도 현재 함수는 빈 문자열을 반환해 장애로 판단하므로 기본 감시는 가능하지만, 종료 코드와 종료 원인을 확인하려면 remain-on-exit 설정이 유용합니다.
기본 경로만으로는 통신 성공을 보장하지 않음
현재 네트워크 검사는 다음 조건만 확인합니다.
ip route show default
이 검사는 기본 게이트웨이가 설정되었는지만 확인합니다.
다음 항목은 확인하지 않습니다.
- 원격 PC 연결 가능 여부
- ROS 2 DDS 참여자 검색 가능 여부
- 인터넷 연결 가능 여부
- DNS 정상 동작 여부
- 특정 서버 연결 가능 여부
- Wi-Fi 신호 품질
원격 제어 PC 연결까지 확인해야 한다면 ping 또는 TCP 포트 검사 기능을 추가하는 것이 좋습니다.
예를 들면 다음과 같습니다.
ping -c 1 -W 1 192.168.0.10 >/dev/null 2>&1
다만 원격 PC가 꺼져 있어도 로봇 자체는 동작해야 하는 구조라면 이러한 검사를 필수 조건으로 설정하면 안 됩니다.
PM2 무한 재시작 가능성
다음과 같은 설정 오류가 있으면 PM2가 계속 재시작할 수 있습니다.
- tmux 세션 이름 불일치
- 창 이름 불일치
- 제어 스크립트 경로 오류
- boot 명령 실패
- ROS 2 환경 설정 누락
- 네트워크 기본 경로 미생성
- 실행 권한 문제
- 카메라 장치 연결 실패
PM2의 재시작 횟수는 다음 명령으로 확인할 수 있습니다.
pm2 status
재시작 반복이 발생하면 다음 로그를 확인해야 합니다.
pm2 logs turtlebot3-watchdog --lines 200
ROS 2 환경 변수 설정
PM2는 사용자가 터미널에 로그인했을 때와 다른 환경 변수 상태로 실행될 수 있습니다.
따라서 외부 로봇 제어 스크립트에서 ROS 2 환경을 명시적으로 불러오는 것이 안전합니다.
source /opt/ros/humble/setup.bash
source "${HOME}/robot_ws/install/setup.bash"
필요하다면 다음 환경 변수도 직접 설정해야 합니다.
export ROS_DOMAIN_ID=30
export TURTLEBOT3_MODEL=burger
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
.bashrc에만 의존하면 PM2나 비대화형 셸 환경에서 설정이 적용되지 않을 수 있습니다.
실행 권한을 추가합니다.
chmod +x ~/robot_scripts/turtlebot3_pm2_supervisor.sh
이 스크립트는 다음 작업을 수행합니다.
- Ubuntu의 기본 네트워크 경로가 생성될 때까지 기다립니다.
- 기존에 남아 있는 tmux 세션을 정리합니다.
turtlebot3_tmux.sh boot명령으로 로봇을 실행합니다.- tmux 세션 존재 여부를 5초마다 확인합니다.
bringup,camera,audio창의 프로세스가 살아 있는지 확인합니다.- 중요 창이 종료되면 tmux 세션 전체를 정리하고 오류 코드로 종료합니다.
- PM2가 supervisor 종료를 감지하고 전체 로봇 시스템을 다시 실행합니다.
- PM2로부터 종료 신호를 받으면 tmux 세션도 함께 종료합니다.
9. tmux의 remain-on-exit와 상태 감시
기존 스크립트에는 다음 설정이 있습니다.
tmux set-window-option \
-t "${SESSION_NAME}:${window_name}" \
remain-on-exit on
remain-on-exit on을 설정하면 ROS 2 프로세스가 종료되어도 tmux 창이 사라지지 않습니다.
이 설정은 오류 메시지를 확인할 수 있다는 장점이 있습니다. 하지만 tmux 세션 자체는 계속 존재하기 때문에 단순히 다음 명령만 확인해서는 ROS 2 프로세스 종료를 감지할 수 없습니다.
tmux has-session -t turtlebot3
따라서 supervisor 스크립트에서는 다음 tmux 변수를 확인합니다.
#{pane_dead}
값이 0이면 프로세스가 실행 중이고, 값이 1이면 해당 pane의 프로세스가 종료된 상태입니다.
이 방식으로 Bringup, 카메라 또는 오디오 프로세스가 종료됐을 때 전체 tmux 세션을 다시 시작할 수 있습니다.
10. PM2 설치 전 Node.js 버전 확인
현재 PM2 공식 저장소는 Node.js 18 이상을 지원 대상으로 안내하고 있습니다.
먼저 현재 Node.js 버전을 확인합니다.
node --version
npm --version
Node.js가 설치되어 있지 않거나 버전이 18보다 낮다면 NVM을 이용해 최신 LTS 버전을 설치하는 방법을 권장합니다.
NVM이 이미 설치된 경우 다음 명령을 실행합니다.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc
nvm --version
nvm install --lts
nvm use --lts
nvm alias default 'lts/*'
버전을 다시 확인합니다.
node --version
npm --version
PM2를 설치합니다.
npm install pm2@latest -g
PM2 공식 문서에서도 최신 PM2를 전역 npm 패키지로 설치하는 방식을 안내합니다.
설치 결과를 확인합니다.
pm2 --version
which pm2
which node
NVM을 이용한 경우 which pm2 결과는 다음과 비슷한 형태가 됩니다.
/home/sjyong/.nvm/versions/node/vXX.XX.X/bin/pm2
PM2를 root 계정으로 실행하지 말고 TurtleBot3 워크스페이스를 소유한 일반 사용자 계정으로 실행해야 합니다.
예를 들어 ROS 2 워크스페이스가 다음 위치에 있다면 해당 홈 디렉터리의 사용자가 PM2를 실행해야 합니다.
/home/ubuntu/turtlebot3_ws
/home/ubuntu/rgb_led_ws