1. 로봇에 스크립트 폴더 만들기
먼저 원격 PC에서 TurtleBot3 Burger에 SSH로 접속합니다.
ssh sjyong@192.168.200.28
여기서 192.168.200.28은 예시 IP입니다.
수업 환경에서는 각자의 TurtleBot3 IP 주소로 변경하셔야 합니다.
로봇에 접속한 후 스크립트를 저장할 폴더를 만듭니다.
mkdir -p ~/tb3_scripts

이 폴더 안에 bringup 실행, 정지, 상태 확인 스크립트를 만들겠습니다.
2. bringup 시작 스크립트 만들기
다음 명령으로 시작 스크립트를 만듭니다.
nano ~/tb3_scripts/start_bringup.sh
아래 내용을 입력합니다.
#!/bin/bash
PID_FILE="$HOME/tb3_bringup.pid"
LOG_FILE="$HOME/tb3_bringup.log"
if [ -f "$PID_FILE" ]; then
OLD_PID=$(cat "$PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "ALREADY_RUNNING"
exit 0
else
rm -f "$PID_FILE"
fi
fi
setsid bash -lc '
source /opt/ros/humble/setup.bash
source ~/turtlebot3_ws/install/setup.bash
export TURTLEBOT3_MODEL=burger
export ROS_DOMAIN_ID=200
export LDS_MODEL=LDS-03
ros2 launch turtlebot3_bringup robot.launch.py
' > "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
sleep 2
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "STARTED"
else
echo "FAILED"
tail -n 20 "$LOG_FILE"
exit 1
fi

저장 후 실행 권한을 부여합니다.
chmod +x ~/tb3_scripts/start_bringup.sh

3. start_bringup.sh 소스 설명
이 스크립트는 TurtleBot3 bringup을 백그라운드에서 실행하는 역할을 합니다.
1) PID 파일 설정
PID_FILE="$HOME/tb3_bringup.pid"
LOG_FILE="$HOME/tb3_bringup.log"
PID_FILE은 실행 중인 bringup 프로세스의 번호를 저장하는 파일입니다.
예를 들어 bringup이 실행되면 다음과 같은 파일이 만들어집니다.
/home/ubuntu/tb3_bringup.pid
이 파일 안에는 실행 중인 프로세스 ID가 저장됩니다.
LOG_FILE은 bringup 실행 로그를 저장하는 파일입니다.
/home/ubuntu/tb3_bringup.log
bringup 실행 중 발생하는 메시지나 에러는 이 파일에서 확인할 수 있습니다.
2) 이미 실행 중인지 확인
if [ -f "$PID_FILE" ]; then
OLD_PID=$(cat "$PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "ALREADY_RUNNING"
exit 0
else
rm -f "$PID_FILE"
fi
fi
이 부분은 bringup이 이미 실행 중인지 확인합니다.
tb3_bringup.pid 파일이 있으면 이전에 실행된 bringup이 있다고 판단합니다.
kill -0 "$OLD_PID"
이 명령은 실제로 프로세스를 죽이는 명령이 아닙니다.
해당 PID의 프로세스가 살아 있는지만 확인하는 용도입니다.
이미 실행 중이면 다음 메시지를 출력합니다.
ALREADY_RUNNING
이렇게 하면 PyQt 버튼을 여러 번 눌러도 bringup이 중복 실행되지 않습니다.
3) ROS 2 환경 설정
SSH 접속 종료
↓
bringup도 같이 종료됨
setsid를 쓰면 이렇게 됩니다.
SSH 접속 종료
↓
bringup은 로봇에서 계속 실행됨
source /opt/ros/humble/setup.bash
source ~/turtlebot3_ws/install/setup.bash
첫 번째 줄은 ROS 2 Humble 기본 환경을 불러옵니다.
source /opt/ros/humble/setup.bash
두 번째 줄은 직접 빌드한 TurtleBot3 workspace 환경을 불러옵니다.
source ~/turtlebot3_ws/install/setup.bash
수강생분들의 workspace 이름이 다를 수 있습니다.
예를 들어 workspace 이름이 ros2_ws라면 다음처럼 바꾸셔야 합니다.
source ~/ros2_ws/install/setup.bash
4) TurtleBot3 모델 설정
export TURTLEBOT3_MODEL=burger
TurtleBot3는 Burger, Waffle, Waffle Pi 모델이 있습니다.
이번 글에서는 TurtleBot3 Burger를 사용하므로 다음처럼 설정합니다.
export TURTLEBOT3_MODEL=burger
이 설정이 없으면 TurtleBot3 bringup 실행 시 모델 정보를 찾지 못해 문제가 생길 수 있습니다.
5) ROS_DOMAIN_ID 설정
export ROS_DOMAIN_ID=200
ROS_DOMAIN_ID는 팀의 고유 번호를 사용하세요.
6) bringup 실행
ros2 launch turtlebot3_bringup robot.launch.py
이 명령이 실제 TurtleBot3 bringup을 실행하는 명령입니다.
이 명령이 실행되면 OpenCR, 모터, 센서, odometry, tf, scan 등의 TurtleBot3 관련 노드가 실행됩니다.
7) setsid를 사용하는 이유
setsid bash -lc ' ... ' > "$LOG_FILE" 2>&1 &
setsid는 SSH 세션과 bringup 프로세스를 분리하기 위해 사용합니다.
그냥 SSH로 ros2 launch를 실행하면 SSH 연결이 끊길 때 bringup도 같이 종료될 수 있습니다.
하지만 setsid를 사용하면 SSH 명령이 끝나도 bringup이 로봇에서 계속 실행됩니다.
마지막의 &는 명령을 백그라운드로 실행하라는 의미입니다.
> "$LOG_FILE" 2>&1
이 부분은 실행 결과와 에러 메시지를 모두 로그 파일에 저장하라는 의미입니다.
4. bringup 정지 스크립트 만들기
다음 명령으로 정지 스크립트를 만듭니다.
nano ~/tb3_scripts/stop_bringup.sh
아래 내용을 입력합니다.
#!/bin/bash
PID_FILE="$HOME/tb3_bringup.pid"
if [ ! -f "$PID_FILE" ]; then
echo "NOT_RUNNING"
exit 0
fi
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
kill -INT -"$PID" 2>/dev/null
sleep 2
if kill -0 "$PID" 2>/dev/null; then
kill -TERM -"$PID" 2>/dev/null
sleep 1
fi
if kill -0 "$PID" 2>/dev/null; then
kill -KILL -"$PID" 2>/dev/null
fi
rm -f "$PID_FILE"
echo "STOPPED"
else
rm -f "$PID_FILE"
echo "NOT_RUNNING"
fi

저장 후 실행 권한을 부여합니다.
chmod +x ~/tb3_scripts/stop_bringup.sh

5. stop_bringup.sh 소스 설명
이 스크립트는 실행 중인 bringup을 종료하는 역할을 합니다.
1) PID 파일 확인
PID_FILE="$HOME/tb3_bringup.pid"
시작 스크립트에서 저장한 PID 파일을 사용합니다.
if [ ! -f "$PID_FILE" ]; then
echo "NOT_RUNNING"
exit 0
fi
PID 파일이 없으면 bringup이 실행 중이 아니라고 판단합니다.
이 경우 다음 메시지를 출력합니다.
NOT_RUNNING
2) 프로세스 ID 읽기
PID=$(cat "$PID_FILE")
PID 파일에 저장된 프로세스 번호를 읽습니다.
이 번호를 이용해 실행 중인 bringup 프로세스를 종료합니다.
3) 프로세스 종료
kill -INT -"$PID" 2>/dev/null
이 명령은 bringup 프로세스 그룹에 Ctrl + C와 비슷한 종료 신호를 보냅니다.
여기서 중요한 부분은 -"$PID"입니다.
-"$PID"
앞에 -를 붙이면 해당 PID 하나만 종료하는 것이 아니라, 같은 프로세스 그룹 전체에 신호를 보냅니다.
ROS 2 launch는 내부적으로 여러 개의 노드를 실행할 수 있기 때문에, 하나의 프로세스만 종료하면 자식 프로세스가 남을 수 있습니다.
그래서 프로세스 그룹 전체에 종료 신호를 보내는 방식이 더 안전합니다.
4) 단계적 종료
스크립트는 다음 순서로 프로세스를 종료합니다.
kill -INT
kill -TERM
kill -KILL
첫 번째는 부드러운 종료입니다.
kill -INT
두 번째는 일반적인 종료 요청입니다.
kill -TERM
마지막은 강제 종료입니다.
kill -KILL
처음부터 강제 종료하지 않고, 정상 종료를 먼저 시도하는 이유는 ROS 2 노드가 종료 작업을 처리할 시간을 주기 위해서입니다.
5) PID 파일 삭제
rm -f "$PID_FILE"
bringup이 종료되면 PID 파일을 삭제합니다.
그래야 다음에 다시 start_bringup.sh를 실행할 수 있습니다.
6. bringup 상태 확인 스크립트 만들기
다음 명령으로 상태 확인 스크립트를 만듭니다.
nano ~/tb3_scripts/status_bringup.sh
아래 내용을 입력합니다.
#!/bin/bash
PID_FILE="$HOME/tb3_bringup.pid"
if [ ! -f "$PID_FILE" ]; then
echo "STOPPED"
exit 0
fi
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "RUNNING"
else
rm -f "$PID_FILE"
echo "STOPPED"
fi

저장 후 실행 권한을 부여합니다.
chmod +x ~/tb3_scripts/status_bringup.sh

7. status_bringup.sh 소스 설명
이 스크립트는 bringup이 실행 중인지 확인합니다.
1) PID 파일이 없는 경우
if [ ! -f "$PID_FILE" ]; then
echo "STOPPED"
exit 0
fi
PID 파일이 없으면 bringup이 실행 중이 아니라고 판단합니다.
출력은 다음과 같습니다.
STOPPED
2) PID 프로세스가 살아 있는지 확인
if kill -0 "$PID" 2>/dev/null; then
echo "RUNNING"
else
rm -f "$PID_FILE"
echo "STOPPED"
fi
PID 파일이 있더라도 실제 프로세스가 이미 종료되었을 수 있습니다.
그래서 kill -0 명령으로 해당 프로세스가 살아 있는지 확인합니다.
살아 있으면 다음을 출력합니다.
RUNNING
죽어 있으면 PID 파일을 삭제하고 다음을 출력합니다.
STOPPED
8. 로봇에서 직접 테스트하기
PyQt GUI를 만들기 전에 반드시 로봇에서 스크립트가 정상 동작하는지 먼저 확인하셔야 합니다.
먼저 bringup을 시작합니다.
~/tb3_scripts/start_bringup.sh

정상이라면 다음과 같이 출력됩니다.
STARTED
상태를 확인합니다.
~/tb3_scripts/status_bringup.sh

정상적으로 실행 중이면 다음과 같이 출력됩니다.
RUNNING
로그를 확인합니다.
tail -n 50 ~/tb3_bringup.log

bringup을 정지합니다.
~/tb3_scripts/stop_bringup.sh

정상적으로 정지되면 다음과 같이 출력됩니다.
STOPPED
9. 원격 PC에서 SSH로 테스트하기
로봇에서 직접 테스트가 끝났다면 원격 PC에서 SSH로 실행해 봅니다.
bringup 시작:
ssh sjyong@192.168.200.28 '~/tb3_scripts/start_bringup.sh'

bringup 상태 확인:
ssh sjyong@192.168.200.28 '~/tb3_scripts/status_bringup.sh'

bringup 정지:
ssh sjyong@192.168.200.28 '~/tb3_scripts/stop_bringup.sh'

로그 확인:
ssh sjyong@192.168.200.28 'tail -n 50 ~/tb3_bringup.log'

여기까지 정상 동작하면 PyQt 버튼에서는 이 SSH 명령만 실행하면 됩니다.
10. SSH 비밀번호 입력 없애기
PyQt 버튼을 누를 때마다 비밀번호를 입력하면 불편합니다.
수업에서는 SSH key를 미리 등록해 두는 것이 좋습니다.
원격 PC에서 다음 명령을 실행합니다.
ssh-keygen
특별한 설정이 필요 없으면 계속 Enter를 누르시면 됩니다.

그다음 TurtleBot3로 공개키를 복사합니다.
ssh-copy-id sjyong@192.168.200.28

이후 다시 접속해 봅니다.
ssh sjyong@192.168.200.28

비밀번호 없이 접속되면 성공입니다.
11. 원격 PC PyQt GUI 만들기
원격 PC에 다음 파일을 만듭니다.
cd ~/pyqt_ws/src/tb3_nav2_pyqt_gui/tb3_nav2_pyqt_gui/
touch tb3_gui.py
아래 소스를 입력합니다.
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QTextEdit, QVBoxLayout, QLabel
from PyQt5.QtCore import QProcess
ROBOT_USER = "sjyong"
ROBOT_IP = "192.168.200.28"
ROBOT = f"{ROBOT_USER}@{ROBOT_IP}"
class TurtleBot3GUI(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("TurtleBot3 Bringup GUI")
self.resize(500, 350)
self.status_label = QLabel("Status: Unknown")
self.log_box = QTextEdit()
self.log_box.setReadOnly(True)
self.btn_start = QPushButton("Run Bringup")
self.btn_stop = QPushButton("Stop Bringup")
self.btn_status = QPushButton("Check Status")
self.btn_log = QPushButton("Show Log")
layout = QVBoxLayout()
layout.addWidget(self.status_label)
layout.addWidget(self.btn_start)
layout.addWidget(self.btn_stop)
layout.addWidget(self.btn_status)
layout.addWidget(self.btn_log)
layout.addWidget(self.log_box)
self.setLayout(layout)
self.btn_start.clicked.connect(self.start_bringup)
self.btn_stop.clicked.connect(self.stop_bringup)
self.btn_status.clicked.connect(self.check_status)
self.btn_log.clicked.connect(self.show_log)
self.process = None
def run_ssh(self, command):
self.process = QProcess(self)
ssh_command = [
ROBOT,
command
]
self.process.readyReadStandardOutput.connect(self.read_stdout)
self.process.readyReadStandardError.connect(self.read_stderr)
self.process.start("ssh", ssh_command)
def read_stdout(self):
data = self.process.readAllStandardOutput().data().decode()
data = data.strip()
if data:
self.log_box.append(data)
if "STARTED" in data or "ALREADY_RUNNING" in data or "RUNNING" in data:
self.status_label.setText("Status: RUNNING")
elif "STOPPED" in data or "NOT_RUNNING" in data:
self.status_label.setText("Status: STOPPED")
elif "FAILED" in data:
self.status_label.setText("Status: FAILED")
def read_stderr(self):
data = self.process.readAllStandardError().data().decode()
data = data.strip()
if data:
self.log_box.append("[ERROR] " + data)
def start_bringup(self):
self.log_box.append("Run bringup button clicked")
self.run_ssh("~/tb3_scripts/start_bringup.sh")
def stop_bringup(self):
self.log_box.append("Stop bringup button clicked")
self.run_ssh("~/tb3_scripts/stop_bringup.sh")
def check_status(self):
self.log_box.append("Check status button clicked")
self.run_ssh("~/tb3_scripts/status_bringup.sh")
def show_log(self):
self.log_box.append("Show log button clicked")
self.run_ssh("tail -n 50 ~/tb3_bringup.log")
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = TurtleBot3GUI()
gui.show()
sys.exit(app.exec_())

12. PyQt 소스 설명
1) 필요한 모듈 불러오기
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QTextEdit, QVBoxLayout, QLabel
from PyQt5.QtCore import QProcess
sys는 PyQt 프로그램 실행에 필요합니다.
QApplication은 PyQt 프로그램 전체를 관리합니다.
QWidget은 GUI 창을 만드는 기본 클래스입니다.
QPushButton은 버튼입니다.
QTextEdit은 로그 메시지를 표시하는 창입니다.
QVBoxLayout은 위에서 아래 방향으로 위젯을 배치합니다.
QLabel은 상태 표시 문구를 출력합니다.
QProcess는 외부 명령을 실행할 때 사용합니다.
여기서는 ssh 명령을 실행하는 데 사용합니다.
2) 로봇 접속 정보
ROBOT_USER = "sjyong"
ROBOT_IP = "192.168.200.28"
ROBOT = f"{ROBOT_USER}@{ROBOT_IP}"
이 부분은 TurtleBot3의 SSH 접속 정보를 설정합니다.
수업 환경에 맞게 IP를 바꾸셔야 합니다.
예를 들어 로봇 IP가 192.168.1.23이면 다음처럼 수정합니다.
ROBOT_IP = "192.168.1.23"
3) GUI 클래스
class TurtleBot3GUI(QWidget):
TurtleBot3GUI 클래스는 전체 GUI 창을 담당합니다.
이 클래스 안에 버튼, 로그창, 상태 표시창, SSH 실행 함수가 모두 들어 있습니다.
4) 창 제목과 크기 설정
self.setWindowTitle("TurtleBot3 Bringup GUI")
self.resize(500, 350)
첫 번째 줄은 창 제목을 설정합니다.
두 번째 줄은 창 크기를 설정합니다.
5) 상태 표시 라벨
self.status_label = QLabel("Status: Unknown")
현재 bringup 상태를 표시하는 부분입니다.
처음 실행하면 상태를 모르기 때문에 Unknown으로 표시합니다.
이후 bringup이 실행되면 RUNNING, 정지되면 STOPPED로 바뀝니다.
6) 로그 출력창
self.log_box = QTextEdit()
self.log_box.setReadOnly(True)
QTextEdit은 여러 줄의 텍스트를 표시할 수 있는 위젯입니다.
setReadOnly(True)를 사용하면 사용자가 직접 내용을 수정하지 못하고, 프로그램이 출력하는 로그만 볼 수 있습니다.
7) 버튼 생성
self.btn_start = QPushButton("Run Bringup")
self.btn_stop = QPushButton("Stop Bringup")
self.btn_status = QPushButton("Check Status")
self.btn_log = QPushButton("Show Log")
총 4개의 버튼을 만듭니다.
Run Bringup → bringup 시작
Stop Bringup → bringup 정지
Check Status → 상태 확인
Show Log → 로그 확인
8) 화면 배치
layout = QVBoxLayout()
layout.addWidget(self.status_label)
layout.addWidget(self.btn_start)
layout.addWidget(self.btn_stop)
layout.addWidget(self.btn_status)
layout.addWidget(self.btn_log)
layout.addWidget(self.log_box)
self.setLayout(layout)
QVBoxLayout은 위에서 아래로 위젯을 배치합니다.
GUI 화면은 다음 순서로 구성됩니다.
상태 표시
Run Bringup 버튼
Stop Bringup 버튼
Check Status 버튼
Show Log 버튼
로그창
9) 버튼과 함수 연결
self.btn_start.clicked.connect(self.start_bringup)
self.btn_stop.clicked.connect(self.stop_bringup)
self.btn_status.clicked.connect(self.check_status)
self.btn_log.clicked.connect(self.show_log)
각 버튼을 클릭했을 때 실행할 함수를 연결합니다.
예를 들어 Run Bringup 버튼을 누르면 start_bringup() 함수가 실행됩니다.
10) SSH 명령 실행 함수
def run_ssh(self, command):
self.process = QProcess(self)
ssh_command = [
ROBOT,
command
]
self.process.readyReadStandardOutput.connect(self.read_stdout)
self.process.readyReadStandardError.connect(self.read_stderr)
self.process.start("ssh", ssh_command)
이 함수가 핵심입니다.
QProcess를 사용해 원격 PC에서 ssh 명령을 실행합니다.
예를 들어 다음 코드가 실행되면
self.run_ssh("~/tb3_scripts/start_bringup.sh")
실제로는 아래 명령과 같은 동작을 합니다.
ssh sjyong@192.168.200.28 '~/tb3_scripts/start_bringup.sh'
즉, PyQt 버튼은 직접 bringup을 실행하는 것이 아니라 로봇에 SSH로 접속해서 로봇 안의 스크립트를 실행합니다.
11) 표준 출력 읽기
def read_stdout(self):
data = self.process.readAllStandardOutput().data().decode()
data = data.strip()
if data:
self.log_box.append(data)
SSH 명령 실행 결과를 읽어와서 GUI 로그창에 출력합니다.
예를 들어 로봇의 스크립트가 다음을 출력하면
STARTED
GUI 로그창에도 STARTED가 표시됩니다.
12) 상태 표시 업데이트
if "STARTED" in data or "ALREADY_RUNNING" in data or "RUNNING" in data:
self.status_label.setText("Status: RUNNING")
elif "STOPPED" in data or "NOT_RUNNING" in data:
self.status_label.setText("Status: STOPPED")
elif "FAILED" in data:
self.status_label.setText("Status: FAILED")
스크립트 출력값에 따라 GUI 상태 표시를 바꿉니다.
STARTED 또는 RUNNING → Status: RUNNING
STOPPED 또는 NOT_RUNNING → Status: STOPPED
FAILED → Status: FAILED
이 구조는 초보자에게 설명하기 좋습니다.
왜냐하면 로봇 스크립트의 출력 메시지가 그대로 GUI 상태 표시와 연결되기 때문입니다.
13) 에러 출력 읽기
def read_stderr(self):
data = self.process.readAllStandardError().data().decode()
data = data.strip()
if data:
self.log_box.append("[ERROR] " + data)
SSH 실행 중 에러가 발생하면 GUI 로그창에 [ERROR]를 붙여 출력합니다.
예를 들어 SSH 접속이 실패하면 에러 메시지가 이곳에 표시됩니다.
14) Run Bringup 버튼 함수
def start_bringup(self):
self.log_box.append("Run bringup button clicked")
self.run_ssh("~/tb3_scripts/start_bringup.sh")
Run Bringup 버튼을 누르면 실행됩니다.
로봇의 start_bringup.sh 스크립트를 SSH로 실행합니다.
15) Stop Bringup 버튼 함수
def stop_bringup(self):
self.log_box.append("Stop bringup button clicked")
self.run_ssh("~/tb3_scripts/stop_bringup.sh")
Stop Bringup 버튼을 누르면 실행됩니다.
로봇의 stop_bringup.sh 스크립트를 SSH로 실행합니다.
16) Check Status 버튼 함수
def check_status(self):
self.log_box.append("Check status button clicked")
self.run_ssh("~/tb3_scripts/status_bringup.sh")
Check Status 버튼을 누르면 실행됩니다.
로봇의 status_bringup.sh 스크립트를 실행해서 현재 bringup 상태를 확인합니다.
17) Show Log 버튼 함수
def show_log(self):
self.log_box.append("Show log button clicked")
self.run_ssh("tail -n 50 ~/tb3_bringup.log")
Show Log 버튼을 누르면 로봇의 bringup 로그 마지막 50줄을 가져옵니다.
실제로 실행되는 명령은 다음과 같습니다.
ssh sjyong@192.168.200.23 'tail -n 50 ~/tb3_bringup.log'
bringup이 실패했을 때 이 버튼을 누르면 원인을 확인하기 좋습니다.
13. 실행
실행합니다.
python3 tb3_gui.py



