ROS 2 토픽과 Arduino Uno 시리얼 통신을 이용한 3색 SMD LED 제어

1. 전체 시스템 구조

이번 실습에서 중요한 점은 Arduino Uno를 ROS 2 노드처럼 직접 사용하지 않는다는 것입니다. Arduino Uno는 단순히 시리얼 명령을 받아 LED를 제어하는 하드웨어 제어기 역할만 합니다.

ROS 2는 Raspberry Pi 4에서 실행합니다. Raspberry Pi 4의 ROS 2 노드는 /rgb_led/cmd 토픽을 구독하고, 토픽으로 들어온 RGB 값을 Arduino Uno로 시리얼 전송합니다. Arduino Uno는 받은 문자열 명령을 해석하여 RGB LED의 빨간색, 초록색, 파란색 밝기를 PWM으로 제어합니다.

이 구조는 실제 로봇 시스템에서 많이 사용하는 방식입니다. Raspberry Pi, Jetson, 산업용 PC 같은 상위 컴퓨터는 ROS 2를 실행하고, Arduino, STM32 같은 MCU는 센서나 LED, 모터, 그리퍼 같은 하드웨어를 직접 제어합니다.

2. 실습 순서

이번 실습은 반드시 다음 순서로 진행합니다.

1. Windows PC에서 Arduino firmware 작성
2. Arduino Uno에 firmware 업로드
3. Windows 시리얼 통신 프로그램으로 Arduino 단독 테스트
4. LED 색상 제어가 정상인지 확인
5. TurtleBot3 Raspberry Pi 4에 Arduino Uno 연결
6. Raspberry Pi 4에서 ROS 2 패키지 작성
7. ROS 2 토픽으로 RGB LED 제어

이 순서가 중요합니다.

Arduino firmware가 정상인지 확인하지 않고 바로 ROS 2부터 실행하면 문제가 발생했을 때 원인을 찾기 어렵습니다. LED 배선 문제인지, Arduino 코드 문제인지, 시리얼 통신 문제인지, ROS 2 노드 문제인지 구분하기 힘들어집니다.

따라서 먼저 Windows에서 Arduino 단독 테스트를 완료한 뒤, 그 다음 TurtleBot3 Raspberry Pi 4에서 ROS 2 프로그램을 실행하는 방식으로 진행합니다.

3. 하드웨어 연결

37종 실습키트에 있는 3색 SMD LED 모듈은 보통 다음 형태입니다.

R
G
B
-

또는

R
G
B
GND

Arduino Uno와 3색 SMD RGB LED 모듈은 다음과 같이 연결합니다.

RGB LED 모듈 R  → Arduino Uno D9
RGB LED 모듈 G  → Arduino Uno D10
RGB LED 모듈 B  → Arduino Uno D11
RGB LED 모듈 -  → Arduino Uno GND

표로 정리하면 다음과 같습니다.

RGB LED 모듈 핀Arduino Uno 핀설명
RD9Red PWM 제어
GD10Green PWM 제어
BD11Blue PWM 제어
– 또는 GNDGND공통 접지

Arduino Uno에서 PWM 출력으로 자주 사용하는 핀은 3, 5, 6, 9, 10, 11입니다. 이번 실습에서는 D9, D10, D11을 사용합니다., 5, 6, 9, 10, 11입니다. 여기서는 배선이 깔끔하고 강의하기 쉬운 9, 10, 11번 핀을 사용합니다.

주의할 점은 RGB LED 모듈이 공통 캐소드(Common Cathode)인지 공통 애노드(Common Anode)인지입니다.

각 색상별 LED 밝기는 analogWrite() 함수로 제어합니다. analogWrite() 값은 0~255 범위를 사용합니다.

0   → 꺼짐
255 → 최대 밝기

단, LED 모듈이 공통 애노드 방식이면 동작이 반대로 됩니다.

공통 캐소드 방식:
analogWrite(pin, 0)   → 꺼짐
analogWrite(pin, 255) → 최대 밝기

공통 애노드 방식:
analogWrite(pin, 255) → 꺼짐
analogWrite(pin, 0)   → 최대 밝기

이번 firmware에는 COMMON_ANODE 설정값을 넣어 두었기 때문에 모듈 방식에 따라 쉽게 변경할 수 있습니다.

4. 시리얼 명령 프로토콜

Raspberry Pi 4에서 Arduino Uno로 보내는 명령은 문자열 형식입니다.

명령 형식은 다음과 같습니다.

RGB,R값,G값,B값

예를 들어 빨간색을 켜려면 다음 명령을 보냅니다.

RGB,255,0,0

초록색은 다음과 같습니다.

RGB,0,255,0

파란색은 다음과 같습니다.

RGB,0,0,255

흰색은 다음과 같습니다.

RGB,255,255,255

LED를 끄려면 다음과 같이 보냅니다.

RGB,0,0,0

각 값은 0~255 범위를 사용합니다. 이 방식은 사람이 직접 읽고 입력하기 쉬워서 Windows의 Serial Monitor, PuTTY, Tera Term 같은 통신 프로그램으로 테스트하기 좋습니다.

5. Windows에서 Arduino firmware 작성

먼저 Windows PC에서 Arduino IDE를 실행합니다.

Arduino IDE에서 보드와 포트를 선택합니다.

보드: Arduino Uno
포트: COM 포트 선택

예를 들어 장치 관리자에서 Arduino Uno가 COM5로 잡혔다면 Arduino IDE에서도 COM5를 선택합니다.

다음 firmware를 Arduino Uno에 업로드합니다.

/*
  Arduino Uno RGB SMD LED Serial Firmware

  Serial command format:
    RGB,255,0,0
    RGB,0,255,0
    RGB,0,0,255
    RGB,255,255,255
    RGB,0,0,0

  Pin connection:
    RGB LED R  -> Arduino D9
    RGB LED G  -> Arduino D10
    RGB LED B  -> Arduino D11
    RGB LED -  -> Arduino GND
*/

#define RED_PIN    9
#define GREEN_PIN 10
#define BLUE_PIN  11

// 일반적인 37종 키트 RGB SMD LED 모듈은 공통 캐소드 방식이 많습니다.
// LED가 반대로 동작하면 true로 변경합니다.
#define COMMON_ANODE false

String inputLine = "";

int clampValue(int value) {
  if (value < 0) {
    return 0;
  }

  if (value > 255) {
    return 255;
  }

  return value;
}

int convertPwmValue(int value) {
  value = clampValue(value);

  if (COMMON_ANODE) {
    return 255 - value;
  }

  return value;
}

void setRgbLed(int r, int g, int b) {
  analogWrite(RED_PIN, convertPwmValue(r));
  analogWrite(GREEN_PIN, convertPwmValue(g));
  analogWrite(BLUE_PIN, convertPwmValue(b));
}

bool parseRgbCommand(String line, int &r, int &g, int &b) {
  line.trim();

  if (!line.startsWith("RGB,")) {
    return false;
  }

  int firstComma = line.indexOf(',');
  int secondComma = line.indexOf(',', firstComma + 1);
  int thirdComma = line.indexOf(',', secondComma + 1);

  if (firstComma < 0 || secondComma < 0 || thirdComma < 0) {
    return false;
  }

  String rString = line.substring(firstComma + 1, secondComma);
  String gString = line.substring(secondComma + 1, thirdComma);
  String bString = line.substring(thirdComma + 1);

  r = clampValue(rString.toInt());
  g = clampValue(gString.toInt());
  b = clampValue(bString.toInt());

  return true;
}

void setup() {
  pinMode(RED_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(BLUE_PIN, OUTPUT);

  setRgbLed(0, 0, 0);

  Serial.begin(115200);
  inputLine.reserve(40);

  Serial.println("Arduino RGB LED firmware ready");
}

void loop() {
  while (Serial.available() > 0) {
    char receivedChar = (char)Serial.read();

    if (receivedChar == '\n') {
      int r = 0;
      int g = 0;
      int b = 0;

      if (parseRgbCommand(inputLine, r, g, b)) {
        setRgbLed(r, g, b);

        Serial.print("OK RGB ");
        Serial.print(r);
        Serial.print(" ");
        Serial.print(g);
        Serial.print(" ");
        Serial.println(b);
      } else {
        Serial.print("ERR ");
        Serial.println(inputLine);
      }

      inputLine = "";
    } else if (receivedChar != '\r') {
      inputLine += receivedChar;

      if (inputLine.length() > 40) {
        inputLine = "";
        Serial.println("ERR command too long");
      }
    }
  }
}

6. Arduino firmware 설명

이 firmware의 핵심은 세 부분입니다.

첫 번째는 PWM 핀 설정입니다.

#define RED_PIN    9
#define GREEN_PIN 10
#define BLUE_PIN  11

RGB LED의 R, G, B 핀을 Arduino Uno의 PWM 핀에 연결합니다. Arduino는 각 핀에 analogWrite()를 사용하여 LED 밝기를 조절합니다.

두 번째는 공통 애노드와 공통 캐소드 처리입니다.

#define COMMON_ANODE false

일반적인 공통 캐소드 LED는 false로 사용합니다. 이 경우 255가 최대 밝기이고 0이 꺼짐입니다.

만약 명령을 보냈을 때 LED 밝기가 반대로 동작하면 다음처럼 바꿉니다.

#define COMMON_ANODE true

이 경우 내부적으로 PWM 값을 반전합니다.

if (COMMON_ANODE) {
  return 255 - value;
}

세 번째는 시리얼 명령 해석입니다.

RGB,255,0,0

이런 문자열이 들어오면 parseRgbCommand() 함수가 콤마 위치를 찾아서 R, G, B 값을 분리합니다.

int firstComma = line.indexOf(',');
int secondComma = line.indexOf(',', firstComma + 1);
int thirdComma = line.indexOf(',', secondComma + 1);

그 다음 각각의 문자열을 숫자로 바꿉니다.

r = clampValue(rString.toInt());
g = clampValue(gString.toInt());
b = clampValue(bString.toInt());

마지막으로 setRgbLed() 함수가 실제 PWM 출력을 변경합니다.

analogWrite(RED_PIN, convertPwmValue(r));
analogWrite(GREEN_PIN, convertPwmValue(g));
analogWrite(BLUE_PIN, convertPwmValue(b));

7. Windows에서 시리얼 통신 프로그램으로 Arduino 단독 테스트

Arduino firmware를 업로드한 뒤에는 바로 ROS 2로 넘어가지 말고 Windows에서 먼저 단독 테스트를 합니다.

사용할 수 있는 프로그램은 다음 중 하나입니다.

Arduino IDE Serial Monitor
Tera Term
PuTTY
CoolTerm

가장 간단한 방법은 Arduino IDE의 Serial Monitor를 사용하는 것입니다.

Arduino IDE에서 Serial Monitor를 열고 다음 설정을 맞춥니다.

Baudrate: 115200
Line ending: Newline 또는 Both NL & CR

명령 입력창에서 하나씩 입력합니다.

위드로봇에서 공개한 시리얼 통신 프로그램을 추천합니다.

아래의 사이트에서 다운로드 하시고 설치합니다.

시리얼통신프로그램 다운로드 사이트 : https://withrobot.com/download.cm?tk=YToyOntpOjA7czoxMzoiNDczMTBiMTMyZjc2NSI7aToxO3M6MjI6ImYyMDI1MDQwMjVlYTExODgxNGIwYTkiO30=

명령 입력창에 다음을 하나씩 입력합니다.

RGB,255,0,0

정상이라면 빨간색 LED가 켜집니다.

RGB,0,255,0

정상이라면 초록색 LED가 켜집니다.

RGB,0,0,255

정상이라면 파란색 LED가 켜집니다.

RGB,255,255,255

정상이라면 흰색에 가까운 색이 켜집니다.

RGB,255,255,0

정상이라면 노란색이 켜집니다.

RGB,0,255,255

정상이라면 보라색이 켜집니다.

RGB,255,0,255

정상이라면 청록색이 켜집니다.

RGB,0,0,0

LED가 꺼집니다.

Arduino가 명령을 정상적으로 처리하면 Serial Monitor에 다음과 같은 응답이 나옵니다.

OK RGB 255 0 0

잘못된 명령을 입력하면 다음처럼 나옵니다.

ERR TEST

이 단계에서 반드시 확인해야 할 것은 다음입니다.

1. Arduino Uno에 firmware가 정상 업로드되었는가?
2. Windows에서 COM 포트가 정상적으로 열리는가?
3. Baudrate가 115200으로 맞춰져 있는가?
4. RGB 명령을 입력했을 때 LED 색상이 바뀌는가?
5. R, G, B 색상이 서로 바뀌어 연결되지 않았는가?
6. LED가 반대로 동작하지 않는가?

만약 빨간색 명령을 보냈는데 초록색이 켜지면 R, G 배선이 바뀐 것입니다. 이 경우 배선을 수정하거나 Arduino 코드의 핀 번호를 수정하면 됩니다.

만약 RGB,255,0,0을 보냈는데 빨간색이 꺼지고, RGB,0,0,0을 보냈을 때 켜지는 식으로 반대로 동작하면 공통 애노드 모듈일 가능성이 큽니다. 이 경우 firmware에서 다음 값을 수정합니다.

#define COMMON_ANODE true

Windows 테스트가 성공하면 Arduino firmware와 LED 회로는 정상입니다. 이제 TurtleBot3 Raspberry Pi 4에서 ROS 2 프로그램을 실행하면 됩니다.

8. TurtleBot3 Raspberry Pi 4에 Arduino 연결

Windows 테스트가 끝났으면 Arduino Uno를 Windows PC에서 분리하고 TurtleBot3 Burger의 Raspberry Pi 4 USB 포트에 연결합니다.

Raspberry Pi 4에서 다음 명령으로 장치가 인식되었는지 확인합니다.

ls /dev/ttyACM*

또는

ls /dev/ttyUSB*

Arduino Uno는 보통 다음 중 하나로 잡힙니다.

/dev/ttyACM0
/dev/ttyUSB0

대부분의 Arduino Uno는 /dev/ttyACM0로 인식됩니다. 호환 보드나 CH340 USB 시리얼 칩을 사용하는 보드는 /dev/ttyUSB0로 인식될 수 있습니다.

시리얼 포트 권한 문제가 발생하면 다음 명령을 실행합니다.

sudo usermod -aG dialout $USER
sudo reboot

임시로 테스트할 때는 다음 명령을 사용할 수도 있습니다.

sudo chmod 666 /dev/ttyACM0

하지만 강의나 실제 사용에서는 dialout 그룹에 사용자를 추가하는 방법이 맞습니다.

9. ROS 2 워크스페이스 생성

Raspberry Pi 4에서 ROS 2 워크스페이스를 생성합니다.

mkdir -p ~/rgb_led_ws/src
cd ~/rgb_led_ws/src

이번 실습에서는 두 개의 ROS 2 패키지를 만듭니다.

rgb_led_interfaces
rgb_led_serial

rgb_led_interfaces는 사용자 정의 메시지를 담는 패키지입니다.

rgb_led_serial은 ROS 2 토픽을 구독하고 Arduino Uno로 시리얼 명령을 보내는 패키지입니다.

10. ROS 2 토픽 인터페이스 정의

먼저 메시지 인터페이스 패키지를 만듭니다.

cd ~/rgb_led_ws/src
ros2 pkg create rgb_led_interfaces --build-type ament_cmake

메시지 폴더를 생성합니다.

cd ~/rgb_led_ws/src/rgb_led_interfaces
mkdir msg

메시지 파일을 작성합니다.

nano msg/RgbLed.msg

내용은 다음과 같습니다.

uint8 r
uint8 g
uint8 b
bool enable

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

필드자료형설명
ruint8Red 밝기, 0~255
guint8Green 밝기, 0~255
buint8Blue 밝기, 0~255
enablebooltrue면 LED 사용, false면 LED 끔

이 메시지는 /rgb_led/cmd 토픽에서 사용됩니다.

예를 들어 다음 메시지는 빨간색 LED를 켜는 명령입니다.

r: 255
g: 0
b: 0
enable: true

다음 메시지는 LED를 끄는 명령입니다.

r: 0
g: 0
b: 0
enable: false

11. rgb_led_interfaces package.xml

파일을 수정합니다.

nano ~/rgb_led_ws/src/rgb_led_interfaces/package.xml

내용은 다음과 같습니다.

<?xml version="1.0"?>
<package format="3">
  <name>rgb_led_interfaces</name>
  <version>0.0.1</version>
  <description>Custom RGB LED message interface for ROS 2</description>
  <maintainer email="user@example.com">user</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>

  <member_of_group>rosidl_interface_packages</member_of_group>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

여기서 중요한 부분은 다음입니다.

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>

rosidl_default_generators.msg 파일을 ROS 2에서 사용할 수 있는 코드로 변환해 줍니다.

rosidl_default_runtime은 실행 시 메시지 타입을 사용할 수 있게 해 줍니다.

다음 설정도 중요합니다.

<member_of_group>rosidl_interface_packages</member_of_group>

이 패키지가 ROS 2 인터페이스 패키지라는 것을 알려줍니다.

또한 다음 export 설정도 넣어야 합니다.

<export>
  <build_type>ament_cmake</build_type>
</export>

package.xml에서 <build_type>ament_cmake</build_type>가 빠지면 패키지 인식이나 빌드 과정에서 문제가 날 수 있으므로 반드시 넣어 둡니다.

12. rgb_led_interfaces CMakeLists.txt

파일을 수정합니다.

nano ~/rgb_led_ws/src/rgb_led_interfaces/CMakeLists.txt

내용은 다음과 같습니다.

cmake_minimum_required(VERSION 3.8)
project(rgb_led_interfaces)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/RgbLed.msg"
)

ament_export_dependencies(rosidl_default_runtime)

ament_package()

핵심 부분은 다음입니다.

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/RgbLed.msg"
)

이 명령이 RgbLed.msg를 실제 ROS 2 메시지 인터페이스로 생성합니다.

13. ROS 2 시리얼 제어 패키지 생성

이제 Arduino Uno와 시리얼 통신하는 ROS 2 Python 패키지를 만듭니다.

cd ~/rgb_led_ws/src
ros2 pkg create rgb_led_serial --build-type ament_python --dependencies rclpy rgb_led_interfaces

Python에서 시리얼 통신을 하기 위해 pyserial을 설치합니다. 설치가 완료된 경우 아래의 과정을 생략합니다.

sudo apt update
sudo apt install -y python3-pip python3-serial
pip3 install pyserial

14. ROS 2 시리얼 노드 작성

파일을 생성합니다.

nano ~/rgb_led_ws/src/rgb_led_serial/rgb_led_serial/serial_led_node.py

내용은 다음과 같습니다.

#!/usr/bin/env python3

import time

import rclpy
from rclpy.node import Node

from rgb_led_interfaces.msg import RgbLed

try:
    import serial
except ImportError:
    serial = None


class RgbLedSerialNode(Node):
    def __init__(self):
        super().__init__('rgb_led_serial_node')

        self.declare_parameter('port', '/dev/ttyACM0')
        self.declare_parameter('baudrate', 115200)
        self.declare_parameter('topic_name', '/rgb_led/cmd')
        self.declare_parameter('serial_timeout', 1.0)

        self.port = self.get_parameter('port').value
        self.baudrate = int(self.get_parameter('baudrate').value)
        self.topic_name = self.get_parameter('topic_name').value
        self.serial_timeout = float(self.get_parameter('serial_timeout').value)

        self.serial_port = None

        self.open_serial()

        self.subscription = self.create_subscription(
            RgbLed,
            self.topic_name,
            self.led_callback,
            10
        )

        self.get_logger().info('RGB LED Serial Node started')
        self.get_logger().info(f'Subscribed topic: {self.topic_name}')
        self.get_logger().info(f'Serial port: {self.port}, baudrate: {self.baudrate}')

    def open_serial(self):
        if serial is None:
            self.get_logger().error('pyserial is not installed. Run: pip3 install pyserial')
            return

        try:
            self.serial_port = serial.Serial(
                port=self.port,
                baudrate=self.baudrate,
                timeout=self.serial_timeout
            )

            time.sleep(2.0)

            self.get_logger().info('Serial connection opened')

        except serial.SerialException as e:
            self.serial_port = None
            self.get_logger().error(f'Failed to open serial port: {e}')

    def clamp_u8(self, value):
        if value < 0:
            return 0

        if value > 255:
            return 255

        return int(value)

    def led_callback(self, msg):
        if self.serial_port is None or not self.serial_port.is_open:
            self.get_logger().warn('Serial port is not open')
            return

        if msg.enable:
            r = self.clamp_u8(msg.r)
            g = self.clamp_u8(msg.g)
            b = self.clamp_u8(msg.b)
        else:
            r = 0
            g = 0
            b = 0

        command = f'RGB,{r},{g},{b}\n'

        try:
            self.serial_port.write(command.encode('utf-8'))
            self.serial_port.flush()

            self.get_logger().info(f'Sent to Arduino: {command.strip()}')

        except serial.SerialException as e:
            self.get_logger().error(f'Serial write failed: {e}')

    def destroy_node(self):
        if self.serial_port is not None and self.serial_port.is_open:
            try:
                self.serial_port.write(b'RGB,0,0,0\n')
                self.serial_port.close()
            except Exception:
                pass

        super().destroy_node()


def main(args=None):
    rclpy.init(args=args)

    node = RgbLedSerialNode()

    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

실행 권한을 부여합니다.

chmod +x ~/rgb_led_ws/src/rgb_led_serial/rgb_led_serial/serial_led_node.py

15. ROS 2 시리얼 노드 설명

이 노드는 /rgb_led/cmd 토픽을 구독하고 Arduino Uno로 문자열 명령을 보냅니다.

먼저 필요한 라이브러리를 불러옵니다.

import rclpy
from rclpy.node import Node
from rgb_led_interfaces.msg import RgbLed

rclpy는 ROS 2 Python 클라이언트 라이브러리입니다. RgbLed는 앞에서 직접 만든 커스텀 메시지입니다.

시리얼 통신을 위해 serial 모듈을 사용합니다.

import serial

이 모듈은 pyserial 패키지에서 제공합니다.

노드 이름은 다음과 같이 설정했습니다.

super().__init__('rgb_led_serial_node')

ROS 2에서 확인하면 이 노드는 rgb_led_serial_node라는 이름으로 표시됩니다.

다음 부분은 파라미터 선언입니다.

self.declare_parameter('port', '/dev/ttyACM1')
self.declare_parameter('baudrate', 115200)
self.declare_parameter('topic_name', '/rgb_led/cmd')
self.declare_parameter('serial_timeout', 1.0)

이렇게 파라미터로 만들어 두면 실행할 때 쉽게 변경할 수 있습니다.

예를 들어 Arduino가 /dev/ttyUSB1로 잡혔다면 다음처럼 실행할 수 있습니다.

ros2 run rgb_led_serial serial_led_node --ros-args -p port:=/dev/ttyUSB1

시리얼 포트를 여는 부분은 다음입니다.

self.serial_port = serial.Serial(
    port=self.port,
    baudrate=self.baudrate,
    timeout=self.serial_timeout
)

baudrate는 Arduino firmware와 동일하게 115200으로 맞춰야 합니다.

중요한 부분은 다음입니다.

time.sleep(2.0)

Arduino Uno는 USB 시리얼 포트가 열릴 때 자동으로 리셋되는 경우가 많습니다. 그래서 포트를 열고 바로 명령을 보내면 Arduino가 아직 준비되지 않았을 수 있습니다. 이를 방지하기 위해 2초 정도 기다립니다.

토픽 구독은 다음 부분입니다.

self.subscription = self.create_subscription(
    RgbLed,
    self.topic_name,
    self.led_callback,
    10
)

/rgb_led/cmd 토픽으로 RgbLed 메시지가 들어오면 led_callback() 함수가 실행됩니다.

콜백 함수에서는 먼저 enable 값을 확인합니다.

if msg.enable:
    r = self.clamp_u8(msg.r)
    g = self.clamp_u8(msg.g)
    b = self.clamp_u8(msg.b)
else:
    r = 0
    g = 0
    b = 0

enabletrue이면 메시지에 들어온 RGB 값을 그대로 사용합니다. enablefalse이면 RGB 값을 모두 0으로 만들어 LED를 끕니다.

그 다음 Arduino로 보낼 문자열 명령을 만듭니다.

command = f'RGB,{r},{g},{b}\n'

예를 들어 ROS 2 토픽으로 다음 값이 들어오면,

r: 255
g: 0
b: 0
enable: true

Arduino로 전송되는 문자열은 다음과 같습니다.

RGB,255,0,0

마지막의 \n은 매우 중요합니다. Arduino firmware는 줄바꿈 문자가 들어왔을 때 하나의 명령이 끝났다고 판단합니다.

시리얼 전송은 다음 코드가 담당합니다.

self.serial_port.write(command.encode('utf-8'))
self.serial_port.flush()

encode('utf-8')은 Python 문자열을 바이트 데이터로 바꾸는 역할을 합니다. 시리얼 통신은 문자열 그대로 보내는 것이 아니라 바이트 단위로 전송됩니다.

self.serial_port.flush()는 시리얼 포트에 쓰기(write)한 데이터가 실제로 전송될 때까지 기다리는 함수입니다.

노드가 종료될 때는 LED를 끄도록 했습니다.

self.serial_port.write(b'RGB,0,0,0\n')
self.serial_port.close()

이렇게 하면 ROS 2 노드를 종료했을 때 LED가 계속 켜져 있는 상황을 방지할 수 있습니다.

16. rgb_led_serial setup.py 수정

파일을 수정합니다.

nano ~/rgb_led_ws/src/rgb_led_serial/setup.py

내용은 다음과 같습니다.

from setuptools import setup
from glob import glob
import os

package_name = 'rgb_led_serial'

setup(
    name=package_name,
    version='0.0.1',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
    ],
    install_requires=['setuptools', 'pyserial'],
    zip_safe=True,
    maintainer='user',
    maintainer_email='user@example.com',
    description='ROS 2 serial bridge node for Arduino RGB LED control',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'serial_led_node = rgb_led_serial.serial_led_node:main',
        ],
    },
)

중요한 부분은 다음입니다.

'serial_led_node = rgb_led_serial.serial_led_node:main'

이 설정이 있어야 다음 명령으로 노드를 실행할 수 있습니다.

ros2 run rgb_led_serial serial_led_node

17. rgb_led_serial package.xml 수정

파일을 수정합니다.

nano ~/rgb_led_ws/src/rgb_led_serial/package.xml

내용은 다음과 같습니다.

<?xml version="1.0"?>
<package format="3">
  <name>rgb_led_serial</name>
  <version>0.0.1</version>
  <description>ROS 2 serial bridge node for Arduino RGB LED control</description>
  <maintainer email="user@example.com">user</maintainer>
  <license>Apache-2.0</license>

  <depend>rclpy</depend>
  <depend>rgb_led_interfaces</depend>

  <exec_depend>python3-serial</exec_depend>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

여기에서도 다음 부분이 중요합니다.

<export>
  <build_type>ament_python</build_type>
</export>

Python 패키지이므로 ament_python으로 설정합니다.

18. Launch 파일 작성

매번 긴 명령어를 입력하지 않기 위해 launch 파일을 만듭니다.

mkdir -p ~/rgb_led_ws/src/rgb_led_serial/launch
nano ~/rgb_led_ws/src/rgb_led_serial/launch/rgb_led_serial.launch.py

내용은 다음과 같습니다.

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='rgb_led_serial',
            executable='serial_led_node',
            name='rgb_led_serial_node',
            output='screen',
            parameters=[
                {'port': '/dev/ttyACM0'},
                {'baudrate': 115200},
                {'topic_name': '/rgb_led/cmd'},
                {'serial_timeout': 1.0},
            ]
        )
    ])

Arduino가 /dev/ttyUSB0로 잡히는 경우에는 다음 부분을 수정합니다.

{'port': '/dev/ttyUSB0'}

19. rgb_led_serial setup.py 수정

파일을 수정합니다.

nano ~/rgb_led_ws/src/rgb_led_serial/setup.py

내용은 다음과 같습니다.

from setuptools import setup
from glob import glob
import os

package_name = 'rgb_led_serial'

setup(
    name=package_name,
    version='0.0.1',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
    ],
    install_requires=['setuptools', 'pyserial'],
    zip_safe=True,
    maintainer='user',
    maintainer_email='user@example.com',
    description='ROS 2 serial bridge node for Arduino RGB LED control',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'serial_led_node= rgb_led_serial.serial_led_node:main',
        ],
    },
)

아래의 부분을 추가합니다.

from glob import glob
import os



 (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),

20. ROS 2 패키지 빌드

워크스페이스 루트로 이동합니다.

cd ~/rgb_led_ws

빌드합니다.

colcon build

환경 설정을 적용합니다.

source install/setup.bash

매번 입력하기 싫으면 ~/.bashrc에 추가할 수 있습니다.

echo "source ~/rgb_led_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrc

메시지 인터페이스가 정상 생성되었는지 확인합니다.

ros2 interface show rgb_led_interfaces/msg/RgbLed

정상이라면 다음처럼 출력됩니다.

uint8 r
uint8 g
uint8 b
bool enable

21. ROS 2 노드 실행

Arduino Uno가 Raspberry Pi 4에 연결되어 있고 /dev/ttyACM0로 잡힌 경우 다음 명령으로 실행합니다.

ros2 run rgb_led_serial serial_led_node

포트가 /dev/ttyUSB0이면 다음처럼 실행합니다.

ros2 run rgb_led_serial serial_led_node --ros-args -p port:=/dev/ttyUSB0

launch 파일을 사용할 경우 다음 명령을 실행합니다.

ros2 launch rgb_led_serial rgb_led_serial.launch.py

정상 실행되면 다음과 비슷한 로그가 나옵니다.

RGB LED Serial Node started
Subscribed topic: /rgb_led/cmd
Serial port: /dev/ttyACM1, baudrate: 115200
Serial connection opened

22. 원격 PC에 LED 제어 인터페이스 복사 및 컴파일

먼저 메시지 인터페이스 패키지를 만듭니다.

cd ~/turtlebot3_ws/src
ros2 pkg create rgb_led_interfaces --build-type ament_cmake

아래의 명령어를 실행하여 RgbLed.msg 파일을 원격 PC의 turtlebot3_mags.msg에 복사합니다.

scp -r /home/sjyong/rgb_led_ws/src/rgb_led_interfaces/  sjyong@192.168.200.25:/home/sjyong/turtlebot3_ws/src/

파일을 수정합니다.

nano ~/turtlebot3_ws/src/rgb_led_interfaces/package.xml

내용은 다음과 같습니다.

<?xml version="1.0"?>
<package format="3">
<name>rgb_led_interfaces</name>
<version>0.0.1</version>
<description>Custom RGB LED message interface for ROS 2</description>
<maintainer email="user@example.com">user</maintainer>
<license>Apache-2.0</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>

<member_of_group>rosidl_interface_packages</member_of_group>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>

파일을 수정합니다.

nano ~/turtlebot3_ws/src/rgb_led_interfaces/CMakeLists.txt

내용은 다음과 같습니다.

cmake_minimum_required(VERSION 3.8)
project(rgb_led_interfaces)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/RgbLed.msg"
)

ament_export_dependencies(rosidl_default_runtime)

ament_package()

핵심 부분은 다음입니다.

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/RgbLed.msg"
)

이 명령이 RgbLed.msg를 실제 ROS 2 메시지 인터페이스로 생성합니다.

빌드합니다.

cd ~/turtlebot3_ws
colcon build --packages-select rgb_led_interfaces
source install/setup.bash
ros2 interface show rgb_led_interfaces/msg/RgbLed

23. ROS 2 토픽으로 LED 제어 테스트

로터틀봇3에서 launch 파일을 사용할 경우 다음 명령을 실행합니다.

ros2 launch rgb_led_serial rgb_led_serial.launch.py

정상 실행되면 다음과 비슷한 로그가 나옵니다.

RGB LED Serial Node started
Subscribed topic: /rgb_led/cmd
Serial port: /dev/ttyACM1, baudrate: 115200
Serial connection opened

이제 다른 터미널을 열고 다음을 실행합니다.

cd ~/rgb_led_ws
source install/setup.bash

빨간색 LED를 켭니다. 만약 LED가 켜지지 않으면 /dev/ttyACM1 번호를 확인하시고 launch/rgb_led_serial.launch.py에서 /dev/ttyACM0이나 다른 번호로 수정하시고 다시 컴파일하여 노드를 실행하시기 바랍니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 255, g: 0, b: 0, enable: true}"

초록색 LED를 켭니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 0, g: 255, b: 0, enable: true}"

파란색 LED를 켭니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 0, g: 0, b: 255, enable: true}"

흰색 LED를 켭니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 255, g: 255, b: 255, enable: true}"

노란색 LED를 켭니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 255, g: 255, b: 0, enable: true}"

보라색 LED를 켭니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 255, g: 0, b: 255, enable: true}"

청록색 LED를 켭니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 0, g: 255, b: 255, enable: true}"

LED를 끕니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 0, g: 0, b: 0, enable: false}"

24. 전체 프로그램 동작 과정

전체 동작은 다음 순서로 진행됩니다.

1. 사용자가 ROS 2 토픽 발행 명령 실행
2. /rgb_led/cmd 토픽에 RgbLed 메시지 발행
3. rgb_led_serial_node가 메시지 수신
4. enable 값 확인
5. r, g, b 값을 0~255 범위로 제한
6. RGB,r,g,b 문자열 생성
7. USB 시리얼로 Arduino Uno에 전송
8. Arduino Uno가 문자열 수신
9. Arduino Uno가 콤마 기준으로 문자열 파싱
10. R, G, B 값을 PWM 출력으로 변환
11. 3색 SMD LED 색상 변경

예를 들어 다음 명령을 실행했다고 가정합니다.

ros2 topic pub --once /rgb_led/cmd rgb_led_interfaces/msg/RgbLed "{r: 255, g: 0, b: 0, enable: true}"

그러면 ROS 2 메시지는 다음 값을 가집니다.

r = 255
g = 0
b = 0
enable = true

rgb_led_serial_node는 이 값을 다음 문자열로 바꿉니다.

RGB,255,0,0

Arduino Uno는 이 문자열을 받아서 다음처럼 동작합니다.

D9  PWM 255 출력
D10 PWM 0 출력
D11 PWM 0 출력

결과적으로 빨간색 LED가 켜집니다.

25. Windows 테스트와 ROS 2 테스트의 차이

Windows 테스트와 ROS 2 테스트는 목적이 다릅니다.

Windows 테스트는 Arduino firmware와 LED 회로가 정상인지 확인하는 과정입니다.

Windows PC
    ↓
Serial Monitor 또는 Tera Term
    ↓
Arduino Uno
    ↓
RGB LED

이 단계에서는 ROS 2가 전혀 필요 없습니다.

반면 ROS 2 테스트는 TurtleBot3 Raspberry Pi 4에서 토픽 기반 제어가 정상인지 확인하는 과정입니다.

ROS 2 Topic
    ↓
Raspberry Pi 4 ROS 2 Node
    ↓
Serial
    ↓
Arduino Uno
    ↓
RGB LED

Windows 테스트가 실패하면 ROS 2를 실행해도 정상 동작하지 않습니다. 따라서 반드시 Windows에서 Arduino 단독 테스트를 먼저 끝내야 합니다.

26. 실습 확장 아이디어

이번 실습은 RGB LED 하나를 제어하는 간단한 예제이지만, 구조는 실제 로봇 시스템에 그대로 확장할 수 있습니다.

예를 들어 TurtleBot3의 상태에 따라 LED 색상을 자동으로 바꿀 수 있습니다.

로봇 상태LED 색상
대기 상태파란색
주행 중초록색
장애물 감지노란색
오류 발생빨간색
미션 완료흰색
긴급 정지빨간색 점멸

추가로 다음 토픽과 연동할 수 있습니다.

/cmd_vel
/odom
/battery_state
/diagnostics

예를 들어 /cmd_vel 토픽을 구독해서 로봇이 움직이면 초록색 LED를 켜고, 멈춰 있으면 파란색 LED를 켜는 방식으로 확장할 수 있습니다.

또한 Arduino 쪽에서 OK RGB 255 0 0 같은 응답을 보내고, ROS 2 노드가 이를 읽어서 /rgb_led/state 토픽으로 발행하도록 만들 수도 있습니다.

27. 정리

이번 실습의 핵심은 다음 구조입니다.

Windows PC에서 Arduino firmware 먼저 테스트
    ↓
Arduino firmware와 LED 회로 정상 확인
    ↓
TurtleBot3 Raspberry Pi 4에 Arduino 연결
    ↓
ROS 2 토픽으로 LED 제어

기술적으로는 다음 구조입니다.

ROS 2 Topic
    ↓
Raspberry Pi 4 Python Node
    ↓
USB Serial
    ↓
Arduino Uno Firmware
    ↓
PWM Output
    ↓
RGB SMD LED Module

이 방식은 로봇 개발에서 매우 실용적인 구조입니다. ROS 2는 판단, 명령, 토픽 통신을 담당하고 Arduino Uno는 단순하고 안정적인 하드웨어 제어를 담당합니다.

이번 RGB LED 예제는 작지만 중요한 실습입니다. LED 대신 부저, 릴레이, 서보모터, 그리퍼, 상태 표시등, 전원 제어 모듈을 연결하면 실제 로봇 시스템의 하드웨어 제어 구조로 바로 확장할 수 있습니다.

Leave a Comment