1. 환경 설정

sudo apt update && sudo apt upgrade

sudo apt install ros-humble-gazebo-*
sudo apt install ros-humble-cartographer
sudo apt install ros-humble-cartographer-ros
sudo apt install ros-humble-navigation2
sudo apt install ros-humble-nav2-bringup
sudo apt install ros-humble-dynamixel-sdk

sudo apt remove ros-humble-turtlebot3*

cd ~/robot_ws/src
#이건 이미 다른 패키지를 설치하면서 만들어 둔 것을 사용하면 된다.

git clone -b humble <https://github.com/ROBOTIS-GIT/turtlebot3.git>
git clone -b humble <https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git>
git clone -b humble <https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git>

**robot@robot:~/robot_ws$ colcon build --symlink-install --packages-select turtlebot3 turtlebot3_msgs turtlebot3_simulations**
[0.504s] WARNING:colcon.colcon_core.package_selection:Some selected packages are already built in one or more underlay workspaces:
	'turtlebot3_msgs' is in: /home/robot/robot_ws/install/turtlebot3_msgs
If a package in a merged underlay workspace is overridden and it installs headers, then all packages in the overlay must sort their include directories by workspace order. Failure to do so may result in build failures or undefined behavior at run time.
If the overridden package is used by another package in any underlay, then the overriding package in the overlay must be API and ABI compatible or undefined behavior at run time may occur.

If you understand the risks and want to override a package anyways, add the following to the command line:
	--allow-overriding turtlebot3_msgs

This may be promoted to an error in a future release of colcon-override-check.
Starting >>> turtlebot3_msgs
Finished <<< turtlebot3_msgs [0.35s]                     
Starting >>> turtlebot3_simulations
Starting >>> turtlebot3
Finished <<< turtlebot3_simulations [0.05s]                                  
Finished <<< turtlebot3 [0.06s]

Summary: 3 packages finished [0.83s]
robot@robot:~/robot_ws$ 

source install/setup.bash

<aside> 💡

.bashrc 파일에 추가될 것들


source /usr/share/gazebo/setup.sh
source ~/turtlebot3_ws/install/local_setup.bash
export TURTLEBOT3_MODEL=burger

</aside>

<aside> 💡

.bashrc 최종


# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

# don't put duplicate lines or lines starting with space in the history.
# See bash(1) for more options
HISTCONTROL=ignoreboth

# append to the history file, don't overwrite it
shopt -s histappend

# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
HISTSIZE=1000
HISTFILESIZE=2000

# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize

# If set, the pattern "**" used in a pathname expansion context will
# match all files and zero or more directories and subdirectories.
#shopt -s globstar

# make less more friendly for non-text input files, see lesspipe(1)
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"

# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
    debian_chroot=$(cat /etc/debian_chroot)
fi

# set a fancy prompt (non-color, unless we know we "want" color)
case "$TERM" in
    xterm-color|*-256color) color_prompt=yes;;
esac

# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
#force_color_prompt=yes

if [ -n "$force_color_prompt" ]; then
    if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
	# We have color support; assume it's compliant with Ecma-48
	# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
	# a case would tend to support setf rather than setaf.)
	color_prompt=yes
    else
	color_prompt=
    fi
fi

if [ "$color_prompt" = yes ]; then
    PS1='${debian_chroot:+($debian_chroot)}\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ '
else
    PS1='${debian_chroot:+($debian_chroot)}\\u@\\h:\\w\\$ '
fi
unset color_prompt force_color_prompt

# If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*)
    PS1="\\[\\e]0;${debian_chroot:+($debian_chroot)}\\u@\\h: \\w\\a\\]$PS1"
    ;;
*)
    ;;
esac

# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
    test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
    alias ls='ls --color=auto'
    #alias dir='dir --color=auto'
    #alias vdir='vdir --color=auto'

    alias grep='grep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias egrep='egrep --color=auto'
fi

# colored GCC warnings and errors
#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'

# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

# Add an "alert" alias for long running commands.  Use like so:
#   sleep 10; alert
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\\''s/^\\s*[0-9]\\+\\s*//;s/[;&|]\\s*alert$//'\\'')"'

# Alias definitions.
# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.

if [ -f ~/.bash_aliases ]; then
    . ~/.bash_aliases
fi

# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
  if [ -f /usr/share/bash-completion/bash_completion ]; then
    . /usr/share/bash-completion/bash_completion
  elif [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
  fi
fi

**# ==========================================
# 워크스페이스 환경 자동화 및 네트워크 설정
# ==========================================**

# [Shell 환경 설정]
# ROS 2 기본 환경 로드
**source /opt/ros/humble/setup.bash**
# 현재 워크스페이스의 빌드 결과물(패키지)을 환경 변수에 추가 (Overlay)
**source ~/robot_ws/install/local_setup.bash**
# Gazebo 설정 로드 (사용자 요청)
**source /usr/share/gazebo/setup.sh**
# colcon 명령어 자동 완성 기능 활성화
source /usr/share/colcon_argcomplete/hook/colcon-argcomplete.bash
# vcs(vcstool) 명령어 자동 완성 활성화
source /usr/share/vcstool-completion/vcs.bash
# colcon_cd 명령어로 패키지 디렉토리 간 빠른 이동 지원
source /usr/share/colcon_cd/function/colcon_cd.sh

# colcon_cd의 루트 경로 지정
export _colcon_cd_root=~/robot_ws

# [네트워크 및 도메인 설정]
# 동일 네트워크에서 같은 ID를 가진 기기끼리만 통신하도록 분리 (0~101 권장)
export ROS_DOMAIN_ID=88
# 통신 범위를 로컬 호스트(내 컴퓨터 내부)로 제한 (외부 통신 시 주석 유지, 로컬만 사용 시 주석 해제)
#export ROS_LOCALHOST_ONLY=1

# 로봇모델 선언
export TURTLEBOT3_MODEL=burger

unset LIBGL_ALWAYS_SOFTWARE

# export QT_X11_NO_MITSHM=1       # (선택) X11 포워딩 문제시 사용
export OGRE_RTShader_Write_GLSL_Enabled=false

# [로깅(Logging) 포맷 설정]
# 콘솔 출력 포맷 지정: [노드이름] [심각도] [시간]: 메시지 형태로 출력
export RCUTILS_CONSOLE_OUTPUT_FORMAT='[{name}] [{severity}] [{time}]: {message}'
# 로그 출력 시 가독성을 위한 색상 활성화
export RCUTILS_COLORIZED_OUTPUT=1
# 로깅 시 표준 에러(stderr) 대신 표준 출력(stdout) 사용 설정 (0: stdout)
export RCUTILS_LOGGING_USE_STDOUT=0
# 로그 스트림 버퍼링 활성화 (성능 최적화)
export RCUTILS_LOGGING_BUFFERED_STREAM=1

# [개발 편의를 위한 Alias 설정]
# 설정 및 소스
alias nr='nano ~/.bashrc'
alias sr='source ~/.bashrc'

# 디렉토리 이동
alias cw='cd ~/robot_ws'
alias cs='cd ~/robot_ws/src'

# 워크스페이스 정리 및 빌드
alias dw='cd ~/robot_ws && rm -rf build/ install/ log/'
alias cb='cd ~/robot_ws && colcon build --symlink-install --continue-on-error'
alias cbs='colcon build --symlink-install'
alias cbp='colcon build --symlink-install --packages-select'
alias cbu='colcon build --symlink-install --packages-up-to'

# 테스트 관련
alias ct='cd ~/robot_ws && mkdir -p test_result && colcon test --test-result-base ./test_result/'
alias ctp='colcon test --packages-select'
alias ctr='colcon test-result'

# ROS 2 모니터링 단축어
alias rt='ros2 topic list'
alias re='ros2 topic echo'
alias rn='ros2 node list'

# 기타 유틸리티
# 1. Gazebo 시뮬레이터 강제 종료
#    Gazebo 프로세스뿐만 아니라, 이를 실행시킨 ros2 launch 프로세스도 함께 종료합니다.
alias killgazebo='killall -9 gazebo gzserver gzclient; pkill -9 -f "ros2 launch.*gazebo"'

# 2. 통합 코드 스타일 및 정적 분석(Linting) 실행
#    ROS 2 공식 가이드에서 권장하는 다양한 린트 도구들을 한 번에 실행하여 코드 품질을 관리한다.
#    - uncrustify: 소스 코드의 인덴트, 괄호 위치 등 전반적인 포맷 수정
#    - cpplint: Google C++ 스타일 가이드 준수 여부 체크
#    - cppcheck: C++ 정적 분석 도구 (버그, 메모리 누수 가능성 등 탐색)
#    - flake8: Python 코드 스타일 및 문법 체크
#    - pep257: Python Docstring(문서화) 스타일 체크
#    - lint_cmake: CMakeLists.txt 스타일 체크
#    - xmllint: package.xml 등 XML 파일 문법 검사
#    - copyright: 모든 소스 파일에 라이선스/저작권 문구가 포함되었는지 확인
alias roslint='ament_uncrustify && ament_cpplint && ament_cppcheck && ament_flake8 && ament_pep257 && ament_lint_cmake && ament_xmllint && ament_copyright'

</aside>

2. 시뮬레이션 - 엠티 월드 노드

2.1 turtlebot3_gazebo 패키지의 특정 노드 launch로 열기

**robot@robot:~/robot_ws$ ros2 launch turtlebot3_gazebo empty_world.launch.py** 
[INFO] [launch]: All log files can be found below /home/robot/.ros/log/2026-02-11-14-08-05-955912-robot-9817
[INFO] [launch]: Default logging verbosity is set to INFO
urdf_file_name : turtlebot3_burger.urdf
urdf_file_name : turtlebot3_burger.urdf
[INFO] [gzserver-1]: process started with pid [9818]
[INFO] [gzclient-2]: process started with pid [9820]
[INFO] [robot_state_publisher-3]: process started with pid [9822]
[INFO] [spawn_entity.py-4]: process started with pid [9824]
[robot_state_publisher-3] [robot_state_publisher] [INFO] [1770786486.738379230]: got segment base_footprint
[robot_state_publisher-3] [robot_state_publisher] [INFO] [1770786486.738442642]: got segment base_link
[robot_state_publisher-3] [robot_state_publisher] [INFO] [1770786486.738447739]: got segment base_scan
[robot_state_publisher-3] [robot_state_publisher] [INFO] [1770786486.738450811]: got segment caster_back_link
[robot_state_publisher-3] [robot_state_publisher] [INFO] [1770786486.738453839]: got segment imu_link
[robot_state_publisher-3] [robot_state_publisher] [INFO] [1770786486.738456712]: got segment wheel_left_link
[robot_state_publisher-3] [robot_state_publisher] [INFO] [1770786486.738459617]: got segment wheel_right_link
[spawn_entity.py-4] [spawn_entity] [INFO] [1770786486.995522275]: Spawn Entity started
[spawn_entity.py-4] [spawn_entity] [INFO] [1770786486.996013693]: Loading entity XML from file /home/robot/robot_ws/install/turtlebot3_gazebo/share/turtlebot3_gazebo/models/turtlebot3_burger/model.sdf
[spawn_entity.py-4] [spawn_entity] [INFO] [1770786486.996669473]: Waiting for service /spawn_entity, timeout = 30
[spawn_entity.py-4] [spawn_entity] [INFO] [1770786486.996970178]: Waiting for service /spawn_entity
[spawn_entity.py-4] [spawn_entity] [INFO] [1770786487.500842230]: Calling service /spawn_entity
[gzserver-1] [turtlebot3_imu] [INFO] [1770786487.738349326]: <initial_orientation_as_reference> is unset, using default value of false to comply with REP 145 (world as orientation reference)
[spawn_entity.py-4] [spawn_entity] [INFO] [1770786487.869774773]: Spawn status: SpawnEntity: Successfully spawned entity [burger]
[gzserver-1] [turtlebot3_diff_drive] [INFO] [1770786487.890227104]: Wheel pair 1 separation set to [0.160000m]
[gzserver-1] [turtlebot3_diff_drive] [INFO] [1770786487.890264111]: Wheel pair 1 diameter set to [0.066000m]
[gzserver-1] [turtlebot3_diff_drive] [INFO] [1770786487.890936297]: Subscribed to [/cmd_vel]
[gzserver-1] [turtlebot3_diff_drive] [INFO] [1770786487.892449363]: Advertise odometry on [/odom]
[gzserver-1] [turtlebot3_diff_drive] [INFO] [1770786487.894601443]: Publishing odom transforms between [odom] and [base_footprint]
[gzserver-1] [turtlebot3_joint_state] [INFO] [1770786487.904003199]: Going to publish joint [wheel_left_joint]
[gzserver-1] [turtlebot3_joint_state] [INFO] [1770786487.904038075]: Going to publish joint [wheel_right_joint]
[INFO] [spawn_entity.py-4]: process has finished cleanly [pid 9824]

2.2 gazebo로 보기

image.png

2.3 rqt_graph로 토픽과 노드보그

image.png

2.4 cli로 노드 및 토픽 확인하기

<aside> 💡

노드 확인


rqt_graph의 타원에 정의되어있다. 활성 노드들이다.

**robot@robot:~/robot_ws$ ros2 node list**
/gazebo
/robot_state_publisher
~~/rqt_gui_py_node_10345~~
/turtlebot3_diff_drive
/turtlebot3_imu
/turtlebot3_joint_state
/turtlebot3_laserscan

</aside>

<aside> 💡

토픽 확인


토픽은 사각형 안에 나타난다.

**robot@robot:~/robot_ws$ ros2 topic list**
~~/clock~~
/cmd_vel
/imu
/joint_states
/odom
/parameter_events
/performance_metrics
/robot_description
~~/rosout~~
/scan
/tf
~~/tf_static~~

</aside>

3. 터틀 움직이기 - /cmd_vel 토픽

3.1 코드

<aside> 💡

키보드로 입력을 준 후 엔터를 쳐야 동작하는 코드


# ~/robot_ws/src/my_turtlebot_pkg/my_turtlebot_pkg/move_turtle_pub.py

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from rclpy.qos import QoSProfile
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan

class MoveTurtle(Node):
  def __init__(self):
    super().__init__('move_turtle_pub') # Changed node name to 'move_turtle_pub'
    self.qos_profile = QoSProfile(depth = 10)
    self.move_turtle = self.create_publisher(Twist, '/cmd_vel', self.qos_profile)
    self.velocity = 0.0
    self.angular = 0.0
    # self.timer = self.create_timer(1, self.turtle_move)
    # self.turtle_key_move()

  def turtle_move(self):
    msg = Twist()
    msg.linear.x = self.velocity
    msg.linear.y = 0.0
    msg.linear.z = 0.0

    msg.angular.x = 0.0
    msg.angular.y = 0.0
    msg.angular.z = 1.0
    self.move_turtle.publish(msg)
    self.get_logger().info(f'Published mesage: {msg.linear}, {msg.angular}')
    self.velocity += 0.08
    if self.velocity > 2:
      self.velocity = 0.0

  def turtle_key_move(self):
    while True:
      # input keyboard to control trutle ('w','a','s','d','x')
      key = input("Enter command: ")
      if key == 'w' or key == 'W':
        self.velocity += 0.01
      elif key == 'a' or key == 'A':
        self.angular += 0.1
      elif key == 's' or key == 'S':
        self.velocity = 0.0
        self.angular = 0.0
      elif key == 'd' or key == 'D':
        self.angular -= 0.1
      elif key == 'x' or key == 'X':
        self.velocity -= 0.01

      msg = Twist()
      msg.linear.x = self.velocity
      msg.linear.y = 0.0
      msg.linear.z = 0.0
      msg.angular.x = 0.0
      msg.angular.y = 0.0
      msg.angular.z = self.angular
      self.move_turtle.publish(msg)
      self.get_logger().info(f'Pub: v={msg.linear.x:.2f}, w={msg.angular.z:.2f}')

def main(args=None):
  rclpy.init(args=args)
  node = MoveTurtle()
  try:
    # rclpy.spin(node) # Use spin if using timer or callbacks
    node.turtle_key_move() # Call function directly for blocking input
  except KeyboardInterrupt:
    node.get_logger().info('Keyboard interrupt!!!!')
  finally:
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
  main()

</aside>

<aside> 💡

키보드를 누르면 작동되도록 하는 코드


# ~/robot_ws/src/my_turtlebot_pkg/my_turtlebot_pkg/move_turtle_pub_adv.py

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from rclpy.qos import QoSProfile
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan

class MoveTurtle(Node):
  def __init__(self):
    super().__init__('move_turtle_pub_adv') # Changed node name to 'move_turtle_pub_adv'
    self.qos_profile = QoSProfile(depth = 10)
    self.move_turtle = self.create_publisher(Twist, '/cmd_vel', self.qos_profile)
    self.velocity = 0.0
    self.angular = 0.0
    # self.timer = self.create_timer(1, self.turtle_move)
    # self.turtle_key_move()

  def turtle_move(self):
    msg = Twist()
    msg.linear.x = self.velocity
    msg.linear.y = 0.0
    msg.linear.z = 0.0

    msg.angular.x = 0.0
    msg.angular.y = 0.0
    msg.angular.z = 1.0
    self.move_turtle.publish(msg)
    self.get_logger().info(f'Published mesage: {msg.linear}, {msg.angular}')
    self.velocity += 0.08
    if self.velocity > 2:
      self.velocity = 0.0

  def turtle_key_move(self):
    while True:
      # input keyboard to control trutle ('w','a','s','d','x')
      key = input("Enter command: ")
      if key == 'w' or key == 'W':
        self.velocity += 0.01
      elif key == 'a' or key == 'A':
        self.angular += 0.1
      elif key == 's' or key == 'S':
        self.velocity = 0.0
        self.angular = 0.0
      elif key == 'd' or key == 'D':
        self.angular -= 0.1
      elif key == 'x' or key == 'X':
        self.velocity -= 0.01

      msg = Twist()
      msg.linear.x = self.velocity
      msg.linear.y = 0.0
      msg.linear.z = 0.0
      msg.angular.x = 0.0
      msg.angular.y = 0.0
      msg.angular.z = self.angular
      self.move_turtle.publish(msg)
      self.get_logger().info(f'Pub: v={msg.linear.x:.2f}, w={msg.angular.z:.2f}')

def main(args=None):
  rclpy.init(args=args)
  node = MoveTurtle()
  try:
    # rclpy.spin(node) # Use spin if using timer or callbacks
    node.turtle_key_move() # Call function directly for blocking input
  except KeyboardInterrupt:
    node.get_logger().info('Keyboard interrupt!!!!')
  finally:
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
  main()

</aside>

<aside> 💡

setup.py


from setuptools import find_packages, setup

package_name = 'my_turtlebot_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='robot',
    maintainer_email='jalanwang@gmail.com',
    description='TODO: Package description',
    license='TODO: License declaration',
    extras_require={
        'test': [
            'pytest',
        ],
    },
    entry_points={
        'console_scripts': [
          'move_turtle_pub = my_turtlebot_pkg.move_turtle_pub:main',
          'move_turtle_pub_adv = my_turtlebot_pkg.move_turtle_pub_adv:main',

        ],
    },
)

</aside>