Sun넘었조 - 차량 운전자 눈부심 방지 선바이저 로봇

MIE capstone
이동: 둘러보기, 검색
Anti-Glare Sunvisor Robot
for Vehicle Driver
Sun넘었조
팀로고.png
학교 서울시립대학교
학과 기계정보공학과
학번 및 성명 20204300** 최*현(팀장)
20194300** 이*현
20204300** 오*택
20204300** 조*호
20204300** 조*규

목차

프로젝트 개요

프로젝트 요약

  • 완성된 이미지
  • figure 1. 선바이저 로봇
  • figure 2. 제어함
  • figure 3. 실제 차량 적용 모습

본 프로젝트는 운전 도중 태양, 밝은 조명 등으로 인해 발생할 수 있는 눈부심(일명 Glare)을 자동으로 방지해줄 수 있는 선바이저 로봇을 제작하는 것을 목표로 하였다. 본 프로젝트에서 제안하는 선바이저 로봇은 운전 중에 차량의 앞 유리를 통해 들어오는 강한 Glare를 인식한다. 이후 자동으로 선바이저가 눈부심을 가릴 수 있는 위치로 이동하여, 사용자의 전방 시야를 확보하고 사고를 예방할 수 있다.

프로젝트 배경 및 기대효과

배경

figure 4. 위험 사례 영상

강한 햇빛, 조명 등과 같이 운전 중에 발생하게 되는 강한 눈부심은 운전자의 시야를 일시적으로 방해하게 되는데, 이는 교통사고로 이어질 수 있는 심각한 안전 문제이다. 실제로 운전자가 돌발적인 눈부심을 겪게 되면 순간적으로 전방 차량 및 보행자가 시야에서 사라지거나 신호를 놓치게 되는 등 사고 발생의 위험이 증가한다. 현재로서는 위와 같은 문제 상황에 대해서, 운전자가 눈부심을 인지한 뒤 선바이저를 수동으로 조작하거나, 선글라스를 착용하는 방식으로 대응하고 있으나, 이는 운전 중 손을 핸들에서 떼거나 시선이 흐트러지는 등 주행 안전성을 저해하는 요소로 작용한다. 기존의 선바이저나 고정형 선팅 필름만으로는 해당 문제 상황에 능동적으로 대응하기 어렵고, 빠르게 발전하는 차량 기술에 비해, 눈부심 방지 기술은 여전히 수동적인 방식에 머물러 있는 것이 현실이다. 따라서 본 프로젝트는 운전자의 별도 조작 없이, 실시간으로 Glare를 인식하고, 자동으로 운전자의 시야를 확보할 수 있는 선바이저 로봇을 제안하고자 한다.

기대 효과

자동 선바이저를 사용하면 운전 중 Glare에 의한 눈부심으로 인해 발생할 수 있는 돌발 상황을 예방할 수 있다. 또한 운전자로 하여금 선바이저 작동에 있어 별도의 조작을 필요로 하지 않기 때문에 안전성 측면에서 효과를 기대할 수 있다. 따라서 본 시스템을 사용하게 되면 별도의 운전자의 조작이 필요하지 않다는 점, Glare에 반응하여 상하좌우 모두 유동적으로 시야를 확보할 수 있다. 또한 블랙박스와의 연동 가능성뿐만 아니라, 차량 내부 옵션으로의 발전할 수 있다는 확장성도 가지고 있다.

프로젝트 개발 목표

경제성

  • 재료비
불필요한 고사양 부품 사용을 지양하며, 오픈 소스 하드웨어 및 소프트웨어를 적극적으로 활용하고, 각 부품(모터, 구동장치, 제어장치 등) 선정 시 요구 성능을 만족하면서 전체 시스템 구축 비용을 개발 소요 비용 목표치(500천원) 내에서 해결할 수 있도록 시스템을 구축하였다.

기능성

  • 정확성
본 시스템은 Glare의 존재 유무에 따라 선바이저의 작동 여부가 결정되고, Glare의 위치에 따라 선바이저가 목표 위치로 이동하는 기능을 갖는다. 따라서 Glare의 존재 유무 및 Glare의 위치를 정확하게 특정할 수 있어야 한다.
  • 실시간성
본 시스템은 차량 운전 상황에서 동적으로 변화하는 Glare의 위치에 맞게 신속하게 대응할 수 있어야 한다.

안정성

  • 구조안정성
본 시스템은 차량 주행 중 발생하는 진동 환경에서도 안정적으로 작동할 수 있도록,각 부품의 내구성을 고려하여 설계되어야 한다. 특히 반복적인 선바이저 구동 상황에서도 성능의 저하나 물리적 손상없이 지속적인 작동이 가능하도록 해야한다.
  • 유지보수성
본 시스템은 제어부와 구동부로 핵심 구성 요소들을 독립적인 모듈 단위로 설계함으로써, 특정 부분에 문제가 발생했을 경우 해당 모듈만 쉽게 분리하여 진단 및 교체가 가능할 수 있도록 하여 유지보수의 편의성을 지니게 하고자 한다.
또한, 시스템 전체의 물리적 구조와 배선을 최대한 단순화함으로써, 고장이 발생할 수 있는 잠재적 요소를 줄이며, 문제 발생 시 원인 파악과 해당 부품 접근이 용이하도록 구성하고자 한다.

동작 시나리오

본 프로젝트에서 제안하는 시스템은 다음과 같은 4가지 운전 상황에 대해 선바이저 로봇이 각각의 상황에 맞는 동작을 수행하도록 설계되었다.

  • Case 1. 전방에 Glare가 존재하지 않는 경우
전방에 Glare가 존재하지 않을 경우
→ 선바이저는 접힌 형태를 유지
  • Case 2. 전방에 Glare가 존재하는 경우
선바이저 로봇이 전방의 Glare를 인식하게 될 경우
→ 피에조 부저(Piezo Buzzer)를 통해 운전자에게 경고음 알림 제공
→ 선바이저가 펼쳐짐
→ Glare 위치에 해당하는 Grid 좌표로 선바이저 로봇이 이동하여 Glare를 차단
  • Case 3. Glare의 위치가 이동하는 경우
차량 주행 중 Glare의 위치가 변화하는 경우
→ 선바이저 로봇은 Glare의 움직임을 추적
→ 현재 위치에 해당하는 Grid 좌표로 이동하며 Glare를 차단
  • Case 4. 전방의 Glare가 사라진 경우
전방에 존재하던 Glare가 사라질 경우
→ 피에조 부저를 통해 운전자에게 알림 제공
→ 선바이저가 접힌 상태로 복귀

구현 내용

하드웨어 설계 및 구현

시스템 개략도

figure 5. 시스템 개략도

회로부

회로부는 크게 전원부, 제어부, 출력부로 구성된다. 전원부는 외부 전원 제어 장치(차량용 시거잭 인버터), 내부 전원 제어 장치(SMPS)로 구성되며, 제어부는 라즈베리파이와, 아두이노 메가로 구성되어 Glare Detection 및 좌표 변환을 담당한다. 또한, 출력부는 피에조 부저 및 모터로 구성되며, 제어부의 신호를 바탕으로 선바이저를 구동시키게 된다.

figure 6. 전체 회로 구상도
  • 전원부
  • 외부 전원 제어 장치(차량용 시거잭 인버터)
차량의 12V DC 전원을 AC 전원처럼 사용 할 수 있도록 변환해 주는 장치로, 이를 통해 외부 배터리 없이도 라즈베리파이 및 아두이노 기반 구동부에 안정적인 전원 공급을 가능하게 한다.또한 장시간 운행시에도 지속적으로 시스템이 작동할 수 있도록 하며, 별도의 충전이나 전력 소모 걱정 없이 차량 자체의 전원으로 구동이 가능하다.
  • 내부 전원 제어 장치(SMPS)
콘센트용 케이블을 통해 차량용 시거잭 인버터와 연결 되며,인버터로 받아온 차량의 AC전원을 다시 DC 전원으로 변환해 준다. 사용 예상 전력을 토대로, 프로젝트에서 사용한 SMPS의 사양(12V 10A)을 선택했다. SMPS에서 나온 전력은 전체 내부 시스템(라즈베리파이5, 아두이노 메가, 모터 등)의 전력을 담당한다.
  • 제어부
  • 라즈베리파이 카메라 모듈 V3
차량 내부에 고정되어, 차량 전방의 영상을 라즈베리파이로 전달한다.
  • 라즈베리파이 5
카메라로부터 영상 입력을 수신하여 OpenCV 기반으로 Glare 인식 및 위치를 특정하고, 좌표 변환 모듈을 통해 카메라 내의 Glare 위치 좌표를 선바이저의 그리드 좌표로 변환한다. 이때 Glare 유무, 지속시간 등을 판단하여 최종 제어 명령을 생산하여 아두이노로 전달한다.
  • 아두이노 메가
라즈베리파이와의 시리얼 통신을 통해 제어 명령을 수신하여, 수신된 명령을 해석하고 연결된 모터 드라이버에 정밀한 제어 신호를 출력한다. 즉, 실제 모터를 구동하고 선바이저의 물리적 위치를 제어한다. 라즈베리파이와 아두이노 간의 통신은 UART 기반의 유선 시리얼 통신 방식을 사용한다. 데이터 전송 속도는 시스템의 요구 반응 속도를 고려하여 115200 bps로 설정하였다. 안정성을 고려하여 전송 속도를 늦출 수도 있었으나 안정성 문제는 발견되지 않아 높은 전송 속도를 채택하였다.
  • 모터 드라이버
아두이노 메가로부터 받은 명령을 기반으로 스텝모터를 제어한다. 스텝모터의 방향 제어를 수행하며, 이를 통해 로봇이 레일 위를 정확하게 이동하도록 한다. 프로젝트에서 사용한 드라이버는 마이크로 스테핑 기능을 내장하는데, 이는 분주비를 조절하여 정밀한 제어를 가능하게 하는 방법이다. 소음을 고려하고자 마이크로 스테핑 기능을 사용하고자 했으나, 분주비의 증가로 모터의 발열에 큰 영향을 미치게 되어, 기능 사용을 철회했다.
  • 출력부
  • 피에조 부저
햇빛 인식을 통해 접혀있던 선바이저가 펴지는 상황에서, 사용자에게 동작 여부를 직관적으로 알리기 위해 피에조 부저를 적용하였다. 부저는 아두이노 메가의 제어 신호에 따라 작동하며, 소리를 통해 사용자에게 선바이저 동작 여부를 즉각적으로 전달한다. 이는 운전 중 시각적인 확인이 어려운 상황에서도 청각적 피드백을 통해 사용자의 인지성을 높이기 위함이다.
  • Main 서보모터(고토크 서보모터)
아두이노 메가의 신호를 통해 선바이저 작동시 회전을 담당하게 되는 서보모터이다. 해당 서보모터의 선정시 모터 자체의 무게와 회전시 발생하는 관성 모멘트, 회전으로 인한 출력 샤프트의 마모성 등을 고려하였고, 이를 통해 반복 회전 동작시의 안정성을 확보할 수 있도록 했다.
  • Sub 서보모터
구동되지 않는 상태에서도 차량 주행 중 발생하는 진동에 의해 선바이저가 흔들리거나 펼쳐지는 현상을 방지하기 위해, 접힌 선바이저를 물리적으로 잡아주는 역할을 수행하는 서보모터를 추가로 배치하였다. 해당 모터는 선바이저가 작동하지 않을 때 선바이저를 고정 상태로 유지함으로써, 구조적 안정성과 사용자 안정성을 모두 확보할 수 있도록 한다.
  • 스텝모터
선바이저 위치 제어를 위해 사용된다. 스텝모터의 작동을 통해 전체 선바이저를 특정 Grid로 이동시켜 Glare를 효과적으로 가리게 된다. 구동부에서 가장 큰 무게를 차지하게 되는 모터이다. 따라서, 시스템에 작용할 하중 및 모멘트 등의 물리적 요소들을 고려하여, 요구 스펙을 결정했으며, 무게를 최소화 할 수 있는 모터로 선정하였다.

구동부

구동부는 몸체부, 레일부, 링크부로 구성되어있다.

figure 7. 구동부
  • 몸체부
  • figure 8. 몸체 뚜껑
  • figure 9. 몸체
몸체부는 구조적 안정성과 조립성 등 다양한 요소를 종합적으로 고려하여 설계하였다. 선바이저 로봇에서 가장 큰 하중을 차지하는 부품은 스텝모터이며, 이를 차량과 로봇이 고정되는 지점 바로 아래에 배치함으로써 무게로 인한 돌림힘을 최소화하였다. 무게 중심을 고정점과 최대한 가깝게 두어 링크부의 구동 시 불필요한 영향을 줄이도록 설계하였다. 또한, 스텝모터의 회전력을 직접 링크부로 전달할 수 있도록 레일 고정부를 스텝모터 인접부에 배치하였고, 몸체 내부에는 회로 및 구동 부품이 안정적으로 탑재될 수 있는 공간을 확보하였다. 몸체의 뚜껑은 서보모터를 견고히 고정할 수 있도록 설계되었으며, 본체와 정확하게 체결되도록 제작되었다.
figure 10. 몸체부 내부 회로
  • 레일부
  • figure 11. 레일부 좌측
  • figure 12. 레일부 우측
레일부는 몸체부 좌,우에 각각 연결되어 작동한다. 레일에 슬라이드 롤러가 장착되어 있고, 조인트는 슬라이드 롤러에 고정되어있다. 스텝모터의 구동으로 조인트가 레일을 따라 좌우로 움직이며 동력을 전달할 수 있도록 설계된 구조이다. 벨트가 레일의 정중앙에 배치 되도록 하여 벨트의 손상을 최소화 했다.
  • 링크부
figure 13. 링크부
링크부는 레일부의 조인트와 연결되어 위치한다. 4개의 링크를 결합한 링크 구조를 통해 선바이저가 장착되는 엔드 이펙터의 상/하,좌/우 움직임을 구현하였으며, 엔드 이펙터에는 선바이저를 회전시킬 고토크 서보모터와, 선바이저를 고정할 경첩이 달려 있도록 설계하였다. 링크와 경첩 사이의 마찰이 발생할 수 있는 부분에 금속 와셔 2개를 추가하여 금속 간의 미끄러짐을 통해 마찰을 최대한 줄이고, 부품의 마모를 막아 부드러운 움직임을 구현하였고, 볼트와 구멍 사이의 유격을 최소화하기 위해 공차를 미세하게 고려하여 수치를 설계하였다.
figure 14. 링크 구조
링크 구조의 경우 왼쪽의 링크 2개와 레일부의 경첩, 엔드 이펙터로 만들어지는 링크 구조는 평행사변형 형태를 띄는 것을 확인할 수 있다. 레일부의 경첩의 두 볼트 사이의 거리와 엔드 이펙터의 두 볼트 사이의 거리가 동일하고, 2개의 링크 길이가 동일하기 때문으로, 이로 인해 엔드 이펙터는 항상 레일과 평행한 방향을 유지한다. 그러나 왼쪽의 평행사변형 링크 구조만 존재한다면 여유 자유도가 존재해 회전운동이 발생하므로, 오른쪽에 동일하게 평행사변형 형태의 링크 구조를 만들어 이러한 여유 자유도를 구속하며, 안정적인 링크 구조를 구현했다.

제어함

figure 15. 제어함
제어함 안에는 전력 공급을 위한 SMPS와 라즈베리파이5가 내장되어 있다. SMPS와 라즈베리파이 및 전선들의 크기를 고려하여 적절한 크기로 설계하였으며, 카메라를 고정하기 위해 상부에 돌출부를 제작하였다. 라즈베리파이의 발열이나 차량 외부에서 비치는 햇빛으로 인해 제어함 온도가 상승할 수 있으므로 이를 예방하기 위해 라즈베리파이가 위치할 부분의 상부에 통풍구를 배치하였다.

하드웨어 전체 구상도

  • figure 16. 구동부 전체 모습
  • figure 17. 제어함 전체 모습
전체 구동부의 경우 실제 차량의 선바이저가 위치하는 부분에 고정되며, 제어함의 경우 차량의 대시보드 전방에 위치하게 된다.

하드웨어 제어 코드

  • 제어 코드 개발 배경
구동부의 경우 아두이노 메가를 통해 움직이게 된다. 특히, 로봇 제어의 경우 라즈베리파이에서 시리얼 통신을 통해 보내주는 바이트 신호를 바탕으로 지시를 받으며, 바이트 신호를 정수로 치환하여 움직인다. 다음은 기본 제어 매커니즘이다.
수신 신호 (정수로 치환) 자동 실행
0 선바이저 위치를 2번 위치로 이동 → 선바이저 OFF 알림 송출 → 선바이저 접기(Main 서보모터 작동) → 선바이저 고정(Sub 서보모터 작동)
n (n = 1 to 9) 선바이저 위치를 n번 위치로 이동
1번부터 9번 위치는 하드웨어 링크 구조 특성으로 인해 생기는 사다리꼴 모양의 경로를 의미한다. 그림 상에서 빨간 점은 각 n번 위치에서의 선바이저 중앙 위치를 나타낸 것이다.
figure 18.선바이저 제어 위치
  • 스텝모터 제어
기본 스텝모터 제어를 통해 로봇의 부드러운 움직임을 구현하는 코드이다. 소음이 적게 발생하고, 분주비 설정 및 딜레이 시간에 대한 실험을 통해 스텝모터 제어 코드를 도출하였다.
void moveTo(int target_left, int target_right) {
  int steps_needed = abs(target_left - position_left);// 동일 스텝 수 가정
  bool dir_left = (target_left > position_left) ? HIGH : LOW;
  bool dir_right = (target_right > position_right) ? HIGH : LOW;
  
  digitalWrite(dir_1, dir_left);
  digitalWrite(dir_2, dir_right);
  
  for (int i = 0; i < steps_needed; i++) {
    digitalWrite(steps_1, HIGH);
    digitalWrite(steps_2, HIGH);
    delayMicroseconds(1600);
    digitalWrite(steps_1, LOW);
    digitalWrite(steps_2, LOW);
    delayMicroseconds(1600);
    
    position_left  += (dir_left == HIGH) ? 1 : -1;
    position_right += (dir_right == HIGH) ? 1 : -1;
  }
}
  • 조인트 위치 제어
스텝모터의 특성 상 스텝 수를 활용하여 정확한 각도 제어가 가능하나, 현재 위치에 대해 기억하는 기능은 존재하지 않다. 따라서 이동한 스텝 수에 대해 변화를 기록하여 현재 좌/우 레일에서의 조인트 위치를 기억하는 변수를 도입하여 위치 제어에 사용하도록 코드를 개발하였다. 조인트 위치 변수값에 대응하는 조인트의 실제 위치는 아래 그림으로 명시해 두었다. 이를 바탕으로 각 인덱스에 따른 위치를 테이블로 기록하여, 해당 위치로 이동할 때 테이블에서 위치를 찾아서 이동하도록 구현하였다. 조인트 위치 변수값에 대응하는 조인트의 실제 위치는 아래 그림으로 명시해 두었다.
// ------------- 위치 테이블 (1~9) ---------------------
int positionTable[9][2] = {
  {-1200,    0},   // 1
  {-600,   600},   // 2
  {   0,  1200},   // 3
  {-850,    0},    // 4
  {-425,  425},    // 5
  {   0,  850},    // 6
  {-400,    0},    // 7
  {-200,  200},    // 8
  {   0,  400}     // 9
};

// 위치 추적 변수
int position_left  = -600;   // 왼쪽 모터의 현재 위치 (음수)
int position_right = 600;    // 오른쪽 모터의 현재 위치 (양수)
int currentIndex = 0; // 최신 위치 (1번째)

void moveTo(int target_left, int target_right) {
  int steps_needed = abs(target_left - position_left);  // 동일 스텝 수 가정
  bool dir_left = (target_left > position_left) ? HIGH : LOW;
  bool dir_right = (target_right > position_right) ? HIGH : LOW;
  
  digitalWrite(dir_1, dir_left);
  digitalWrite(dir_2, dir_right);
  
  for (int i = 0; i < steps_needed; i++) {
    digitalWrite(steps_1, HIGH);
    digitalWrite(steps_2, HIGH);
    delayMicroseconds(1600);
    digitalWrite(steps_1, LOW);
    digitalWrite(steps_2, LOW);
    delayMicroseconds(1600);
    
    position_left  += (dir_left == HIGH) ? 1 : -1;
    position_right += (dir_right == HIGH) ? 1 : -1;
  }
}
figure 19.조인트 Position
  • 선바이저 위치 이동 경로 코드
선바이저의 위치가 1번 위치에서 9번 위치로 한 번에 이동할 경우, 대각선 방향의 움직임이 필요하다. 그러나, 실제 운전 상황에서 해의 움직임으로 고려한다면 대각선 방향으로의 움직임보다 상/하, 좌/우의 움직임의 현상이 지배적이기에 정해진 규칙에 따라 상/하, 좌/우 움직임만을 통해 위치 이동을 가능하도록 코드를 개발하였다. 상/하 움직임이 구현될 경우, 선바이저는 항상 좌/우 기준 중앙 위치로 이동한 후 상/하 움직임을 실행하도록 하였다.
// ----------------------- 계단식 이동 구현 -----------------------------

void moveToGridPosition(int targetIndex) {
  int curRow = currentIndex / 3;
  int curCol = currentIndex % 3;
  int tgtRow = targetIndex / 3;
  int tgtCol = targetIndex % 3;

  // 1단계: 현재 층의 중간으로 이동
  if (curCol != 1) {
    int midIndex = curRow * 3 + 1;
    moveTo(positionTable[midIndex][0], positionTable[midIndex][1]);
    currentIndex = midIndex;
  }
  delay(500);

  // 2단계: 다른 층의 중간으로 이동
  if ((currentIndex / 3) != tgtRow) {
    int tgtMidIndex = tgtRow * 3 + 1;
    moveTo(positionTable[tgtMidIndex][0], positionTable[tgtMidIndex][1]);
    currentIndex = tgtMidIndex;
  }
  delay(500);

  // 3단계: 최종 복구
  if (currentIndex != targetIndex) {
    moveTo(positionTable[targetIndex][0], positionTable[targetIndex][1]);
    currentIndex = targetIndex;
  }
}
  • 부저 및 선가드 구동 코드
가드 접힘 및 펼침에 대해 알림을 주기 위해 피에조 부저의 주파수 조절을 사용하여 서로 다른 음을 사용하여 알림음을 구현하였고, 서보모터 구동의 경우 delay를 활용하여 원하는 각도에서 다른 각도로의 움직임을 구현하였다.
// ------------- 부저 멜로디 설정 ---------------------
int arraySize = 5;
int melody[] = {245, 260, 292, 328, 348, 390, 439, 492, 523, 586, 1317};
int song1[]   = {1, 2, 3, 4, 5};
int song2[]   = {5, 4, 3, 2, 1};
int noteDuration[] = {2, 2, 2, 2, 2};

// ----------------------- 썬가드 작동 함수 -----------------------------
void sungard_down() {
  for (int note = 0; note < arraySize; note++){
    int duration = 700/noteDuration[note];
    tone(buzzer, melody[song1[note]], duration);
    delay(duration+30);
  }
  delay(500);
  for (int i = 110; i >=0; i--) {
        mainServo.write(i);
        delay(10);
  }
}

void sungard_up() {
  for (int note2 = 0; note2 < arraySize; note2++){
    int duration2 = 700/noteDuration[note2];
    tone(buzzer, melody[song2[note2]], duration2);
    delay(duration2+30);
  }
  delay(500);
  for (int i = 0; i <= 110; i++) {
        mainServo.write(i);
        delay(10);
  }
}
  • 시리얼 통신 구현 코드
시리얼 통신으로 주고받는 데이터를 바이트 단위로 설정하였고, 바이트 단위의 데이터를 변환하여 사용하기 위해 코드를 추가하였다. 각각의 번호는 개발 과정에서 카메라와 운전자, 선바이저 로봇 그리고 디버깅용 출력 화면의 방향을 통일시키기 위해 해당 순서로 설정하였다.
int get_grid_coords(int index) {
  switch(index) {
    case 0b0000: return 3;
    case 0b0100: return 2;
    case 0b1000: return 1;
    case 0b0001: return 6;
    case 0b0101: return 5;
    case 0b1001: return 4;
    case 0b0010: return 9;
    case 0b0110: return 8;
    case 0b1010: return 7;
    default: return 0;
  }
}
// grid_coords (1~9)
// 1 2 3
// 4 5 6
// 7 8 9

// --- RPi 1바이트 통신 처리 ---
    byte latestRPiByte = 0; // RPi에서 온 가장 마지막 바이트
    bool rpiDataAvailable = false;

    while (Serial.available() > 0) {
      latestRPiByte = Serial.read();
      rpiDataAvailable = true;
    }
//...모터 제어...

하드웨어 개발 과정

  • 기초 제작
비슷한 시제품이 없기에, HW의 제작은 사용할 내부 부품(레일, 모터)등의 치수를 기반으로 제작하였다. 또한, 결과물은 최적설계 연구실의 3D 프린터를 이용하여 제작했기에, 출력되는 결과물의 크기에 따라 공차가 미세하게 달랐다. 결과물을 직접 조립해 보니, 설계 단계에서 생각하지 못한 문제가 발생한 경우도 허다했다. 따라서, 여러번의 3D 프린팅 출력을 통해 보완해 나갔고 여러 Trial and Error 과정을 거치며 진행되었다.
figure 20. 시행 착오
  • 해석
차량에 고정되어 작용하는 로봇의 특성 상, 과속방지턱에서 가해지는 큰 하중들에 취약하다. 따라서 부품에 작용하는 최악의 상황을 가정하여 해석을 진행하였고, 기초 제작된 제품에 피드백을 반영하여 수정하는 과정을 거쳤다.
figure 21. 해석
  • HW 1차 테스트
기초 제작 이후 직접 자동차에 장착해 실제 환경에 대한 초기 테스트를 진행하였다. 설계 했던 전력 공급 회로의 정상 작동 확인과, 차량 내부 간섭 여부등을 파악했다. 이때, 로봇의 크기가 커 운전석 장착시 차량 핸들과 충돌하였고, 로봇 몸체의 레일부가 차량의 백미러와 충돌함을 확인할 수 있었다. 1차 테스트를 기반으로 링크 길이, 레일의 길이들을 축소 시키게 되었다.
  • figure 22. 1차 테스트(차량 외부)
  • figure 23. 1차 테스트(차량 내부)
  • HW 2차 테스트
HW 1차 테스트를 기반으로 로봇의 모델을 개선하여 2차 테스트를 진행하였다. 2차 테스트는 로봇을 운전석에 장착하였을 때의 안정성을 중심으로 테스트를 진행하였고, 장착한 상태로 실제 주행을 해봐도 안정적으로 차량에 부착되어 구동하는 모습을 확인할 수 있었다. 특히, 차량이 심하게 흔들리는 경우의 로봇의 안정성을 확인하고자 과속방지턱을 빠른 속도로 넘어가는 실험 주행을 진행한 결과, 로봇이 천장에서 떨어지지 않고 강하게 부착 되어 있었고, 링크부 역시 손상없이 구동되는 모습을 확인할 수 있었다.
figure 24. 2차 테스트

소프트웨어 설계 및 구현

전체 SW 동작 Flow Chart

figure 25. 전체 SW Flow Chart

Glare Detection

Glare Detection Flow Chart
figure 26. Glare Detection Flow Chart
차량 전방 밝기 상태 판단

터널을 통과하거나 주차장과 같은 실내에서 주행할 때는 선바이저를 필요로 하지 않는다. 이를 반영하기 위해 OpenCV를 통해 영상 정보를 grayscale로 변환하여 전방의 밝기 정보를 파악하고, 임계 값 이상의 밝기에서만 후행 과정이 이루어질 수 있도록 한다. 임계 값은 본 프로젝트에 사용할 라즈베리파이 카메라 모듈 V3으로 다양한 실내외 환경을 촬영하였을 때의 밝기 정보를 기준으로 한다.

추가적으로 카메라가 Glare를 직접적으로 비추게 되면 자동 노출 조절 기능으로 인해 주변부가 어두워지는 상황이 발생한다. 이 때 프레임의 평균 밝기는 낮더라도 Glare가 존재하기 때문에 프레임의 표준편차를 반영하여 인식에 활용한다.

프레임에 대한 표준편차가 작다는 것은 Glare가 존재하지 않아 전체적으로 균일한 밝기 정보를 가진 상태이고, 표준편차가 크다는 것은 Glare로 인해 주변부가 어두워지거나 특정 부분만 밝은 상태를 의미한다.

Glare에 반사되는 노이즈가 많이 인식될 경우를 방지하기 위해 노출 정도를 수동으로 조절하여 카메라가 받아들이는 절대적인 광량을 조절하였다.

Photometric Feature 반영 Glare Detection

[1][1]과 같이 Glare는 높은 밝기, 낮은 채도, 낮은 대비의 광학적 특성을 갖는다. 따라서 이를 바탕으로 Glare를 인식하고자 한다.

figure 27. Glare의 광학적 특성
  • Gphoto 계산
// Photometric map 생성: Intensity(V), Saturation(S), Local Contrast(C) 기반
cv::Mat glare_detector::computePhotometricMap(const cv::Mat&inputRGB) {
    cv::Mat hsv, intensity, saturation;
    cv::cvtColor(inputRGB, hsv, cv::COLOR_BGR2HSV);
    std::vector<cv::Mat>hsv_channels;
    cv::split(hsv, hsv_channels);
    
    intensity =hsv_channels[2];
    saturation =hsv_channels[1];
    
    intensity.convertTo(intensity, CV_32F, 1.0 /255.0);
    saturation.convertTo(saturation, CV_32F, 1.0 /255.0);
    cv::Mat contrast =computeLocalContrast(intensity);
    cv::Mat gphoto =intensity.mul(1.0 -saturation).mul(1.0 -contrast);
    cv::normalize(gphoto, gphoto, 0.0, 1.0, cv::NORM_MINMAX);
    return gphoto;
}
  • 카메라의 영상 정보로부터 밝기 정보(intensity), 채도 정보(saturation), 대비 정보(contrast)를 추출한다. 최종적으로 생성되는 gphoto map의 경우 영상 정보의 intensity*(1-saturation)*(1-contrast)의 값을 계산하여 정규화하고, 이는 Glare의 후보를 나타낸다.
figure 28. Glare의 광학적 특징
Geometric Feature 반영 Glare Detection

낮 시간대에 운전자에게 가장 크리티컬한 Glare는 태양이고 이는 주로 원형으로 존재한다. 따라서 gphoto map을 통해 생성된 Glare 후보들에 대해 원형의 형상을 갖는 부분을 검출하고자 한다.

  • Ggeo 계산
// Geometric map 생성: Gaussian Blur 후 Hough Circle로 glare 후보 검출
cv::Mat glare_detector::computeGeometricMap(const cv::Mat&gphoto) {
    cv::Mat blurred, blurred8u;
    cv::GaussianBlur(gphoto, blurred, cv::Size(13, 13), 5);
    blurred.convertTo(blurred8u, CV_8U, 255);
    std::vector<cv::Vec3f>circles;
    cv::HoughCircles(blurred8u, circles, cv::HOUGH_GRADIENT, 1, 20, 75, 30, 10, 200);
    cv::Mat ggeo =cv::Mat::zeros(gphoto.size(), CV_32F);
    for(const auto&circle : circles) {
        cv::Point center(cvRound(circle[0]), cvRound(circle[1]));
        intradius =cvRound(circle[2]);
      cv::circle(ggeo, center, int(1.5 *radius), cv::Scalar::all(1.0), -1);
}
    cv::normalize(ggeo, ggeo, 0.0, 1.0, cv::NORM_MINMAX);
    return ggeo;
}

gphoto map을 바탕으로 hough circle 알고리즘을 통해 원형의 Glare를 판단한다. 노이즈를 줄이고 이미지를 부드럽게 변환하기 위해 가우시안 블러 처리를 선행한다. ggeo map에 저장되는 원형의 여부 역시 정규화된 값으로 계산된다.

figure 29. Ggeo map
Glare 간 우선 순위 부여
  • Priority 계산
cv::Mat glare_detector::computePriorityMap(const cv::Mat&gphoto, const cv::Mat&ggeo) {
    cv::Mat priority =cv::Mat::ones(gphoto.size(), CV_8U) *3;
    for(int=0; y <gphoto.rows; ++y) {
        for(int=0; x <gphoto.cols; ++x) {
            float=gphoto.at<float>(y, x);
            float=ggeo.at<float>(y, x);
            
	     // 밝고 원형인 glare 존재
            if(p >=0.95f && c>=0.99f) {
                priority.at<uchar>(y, x) =1;
	     } 
	     // 밝지만 원형은 아닌 glare 존재
            else if(p >=0.95f) {
                priority.at<uchar>(y, x) =2;
	     } 
	}
    }
    
    return priority;
}
  • 앞서 계산한 gphoto, ggeo map은 각각, Glare의 광학적 특성과 기하학적 특성을 이용하여 구할 수 있다. 실제로 최종 Glare를 판단하는데 있어 항상 두 특성을 만족하기는 어렵기 때문에 특성 간 우선 순위를 부여하여 구분하고자 한다.
  • 낮 시간대에 운전자를 가장 방해할 수 있는 태양을 인식하기 위해 밝으면서 원형의 성질을 띠는 것을 priority 1로 두어 우선 순위를 최상위로 한다. 이외에도 반사되는 빛, 조명 등에 의한 Glare를 인식하기 위해 밝지만 원형은 아닌 성질을 띠는 Glare를 priority 2로 두었다.
Queue 자료 구조 활용 Glare 좌표 반환

Glare를 인식하는 알고리즘은 매 프레임 단위로 실행된다. 그러나 하나의 프레임마다 인식되는 Glare를 따라 HW가 동작하게 되면 운전자에게 방해가 될 소요가 있다. 따라서 Glare로 인식한 좌표를 즉각적으로 HW에 전달하는 것이 아니라 최근 프레임의 정보를 바탕으로 판단하여 전달한다. 결과적으로 최근 10개의 프레임에 대한 정보를 저장할 queue를 생성하고, Glare 좌표와 유효 여부를 push 하여 최종 Glare의 좌표를 반환한다.

  • Queue 자료 구조 기능
void position_queue::push(const Coord&coord) {
    bool valid = true;
    int valid_count = 0;
    avgCoord =computeAverageOfValid(); // 유효한 glare들의 평균 좌표 반환
    valid = isWithinRange(coord, avgCoord); // 노이즈 여부 판별 
    if(queue_.size() >=max_size_) {
        queue_.pop_front();
}
    queue_.emplace_back(coord, valid);
}

// queue 저장된 glare 정보를 바탕으로 존재 여부 판별
int position_queue::shouldReturnAverage() const{
    int valid_count = 0;
    for(const auto&entryqueue_) {
        if(entry.second) valid_count++;
    }
    
    if(valid_count >= 7){
        return 1;
    }
    else if(valid_count <4){
        return -1;
    }
    else{
        return 0;
    }
}

//compute avg in queue for push
position_queue::Coord position_queue::computeAverageOfValid() const{
    intsum_x =0, sum_y =0, count =0;
    for(const auto&[coord, valid] : queue_) {
        if(valid) {
		sum_x +=coord.x;
		sum_y +=coord.y;
		count++;
	}
    }
    if(count ==0) return{-1, -1}; // 최초 입력은 항상 무효가 아님
   	 return{sum_x /(float)count, sum_y /(float)count};
}

// glare 좌표의 유효성을 판단
bool position_queue::isWithinRange(const Coord& pos, const Coord& avg_pos, int threshold) const {
    if(avg_pos.x <0 || avg_pos.y < 0) threshold = 2000; // glare가 없는 상태에서 새롭게 인식하면 항상 유효한 좌표로 받음

    if(pos.x < 0 || pos.y < 0) return false;            
    
    if(avg_pos.x ==0 && avg_pos.y==0) return true; // 최초 입력이 음수가 아니라면 true로 받음

    return std::abs(pos.x - avg_pos.x) <= threshold && std::abs(pos.y - avg_pos.y) <= threshold;
}
1. push
카메라를 통해 받아들인 영상 정보를 바탕으로 프레임 내에 Glare로 판단되는 좌표를 입력받는다. 입력받은 좌표와 유효 여부를 queue에 저장한다.
2. shouldReturnAverage
queue에 저장된 좌표들의 유효 여부를 판단한다. 최근 10개의 좌표 중 유효하다고 판단되는 좌표가 7개 이상일 경우 Glare가 존재한다고 판단하고, 4~6개가 유효하다고 판단되면 선바이저의 빈번한 이동을 방지하기 위해 이전 Glare의 좌표를 그대로 반환한다. 3개 이하 좌표가 유효하다면 노이즈 혹은 Glare가 사라진 경우로 판단하여 (-1, -1)의 좌표를 반환한다.
3. computeAverageOfValid
Glare가 존재한다고 판단되었을 때, 유효한 좌표에 한하여 평균 좌표를 계산한다.
4. isWithinRange
처음 Glare로 인식한 좌표는 항상 유효하다고 받아들이되, 이후에 전달받는 좌표들은 이전 Glare들의 평균 좌표 기준 Threshold 범위 안에 존재하였을 때만 유효한 좌표로 인식한다. 이는 노이즈가 기존 Glare와 동떨어진 위치에서 인식되었을 때 입력으로 받지 않기 위해 사용한다. 만약 차량의 회전으로 Glare의 위치가 변경된다면 queue가 비워진 이후에 새로운 Glare로 인식하게 된다.
figure 30. Queue 동작 Flow Chart
Test Dataset에서의 Glare Detection 알고리즘 정확도 평가
  • 위에서 제시한 Glare Detection 알고리즘의 성능을 평가하기 위해, [2][2]와 같이 실제 차량 내에서 주행 중에 Glare가 존재하는 이미지 20장과 Glare가 존재하지 않는 이미지 20장, 총 40장에 대해 성능 평가를 진행해 보았다. Glare 존재 여부 및 Glare의 bounding box는 아래와 같이 Human Annotation 작업으로 진행되었다.
figure 31. Test Dataset
  • 해당 Test Dataset에 대하여, Glare 존재 여부에 대한 Binary Classification을 진행하였으며, 결과는 아래와 같다.
figure 32. Test Dataset 평과 결과 (이진 분류 성능)
figure 33. Test Dataset에 대한 Detect 결과 예시(왼쪽 상단으로부터 우측으로, TP, TN, FP, FN 예시)
figure 34. Test Dataset 평가 결과 (특정 IoU 이상의 Precision)

좌표 변환

좌표 변환 Flow Chart
figure 35. 좌표 변환 모듈 흐름도

이 모듈은 위 Glare Detection 모듈을 통해 2차원 카메라 이미지 평면에서 추출된 Glare의 픽셀 좌표를, 3차원 월드 좌표계에서의 기하학적 분석을 통해 최종적으로 아두이노가 이해할 수 있는 이산적인 3x3 그리드 인덱스로 변환한다. 이 과정의 정확성은 시스템 전체의 Glare 차단 성능과 직결되므로, 정밀한 수학적 모델링과 구현이 요구된다. 성능 최적화 및 시스템 통합을 고려하여 전체 로직은 C++와 OpenCV 라이브러리를 사용하여 작성하였다.

OpenCV 내장 함수를 사용한 렌즈 왜곡 보정

카메라 렌즈로 인해 발생하는 이미지의 방사 왜곡(radial distortion) 및 접선 왜곡(tangential distortion)은 픽셀 위치의 정확도를 저해하는 주요 요인이다. 특히, 이미지 중심부에서 멀어질수록 왜곡의 정도가 심해져, 3D 공간으로의 역투영 시 큰 오차를 유발한다. 따라서, 입력된 Glare의 중심 픽셀 좌표는 가장 먼저 렌즈 왜곡 보정 과정을 거친다.

  • undistortPoints
    • 이 보정 과정은 OpenCV 라이브러리에서 제공하는 cv::undistortPoints 함수를 통해 수행된다. 이 함수는 사전에 카메라 캘리브레이션(Camera Calibration)을 통해 획득한 카메라 고유의 내부 파라미터 행렬(Intrinsic Matrix, K)렌즈 왜곡 계수(Distortion Coefficients, D)를 사용한다.
  • 카메라 내부 파라미터 행렬 (K)
figure 36. 카메라 내부 파라미터 행렬
  • fx, fy : 렌즈의 초점 거리를 픽셀 단위로 표현한 값.
  • cx, cy : 이미지의 주점(Principal Point), 즉 렌즈의 광학축이 이미지 센서와 만나는 점의 픽셀 좌표.
  • 왜곡 계수 (D)
    • D = [k1, k2, p1, p2, k3, …]
    • k1, k2, k3 등은 방사 왜곡을, p1, p2는 접선 왜곡을 모델링하는 계수이다.
  • cv::undistortPoints 함수는 입력된 왜곡된 2D 포인트를 이 파라미터들을 이용하여, 왜곡이 제거된 정규화된 카메라 좌표계(normalized camera coordinates)의 포인트로 변환한다. 이 정규화된 좌표는 카메라 중심을 원점으로 하고 초점 거리를 1로 가정한 평면에서의 좌표이다.
  • 이후 함수에 카메라 행렬을 인자로 전달함으로써, 정규화된 좌표를 다시 우리가 사용하는 픽셀 좌표계로 재투영하여 정확한 Glare 중심 좌표를 얻는다. 이 과정은 다음과 같은 C++ 코드로 구현되었다.
cv::Mat sun_center_mat(1, 1, CV_64FC2);
sun_center_mat.atcv::Vec2d(0, 0)[0] = sun_center.first;
sun_center_mat.atcv::Vec2d(0, 0)[1] = sun_center.second;

cv::Mat undistorted_points_mat;
cv::undistortPoints(
      sun_center_mat,                   // 입력: 왜곡된 픽셀 좌표
      undistorted_points_mat,           // 출력: 보정된 좌표를 담을 Mat
      DEFAULT_CAMERA_MATRIX,            // 카메라 내부 파라미터 행렬 (K)
      DEFAULT_DISTORTION_COEFFICIENTS,  // 렌즈 왜곡 계수 (D)
      cv::noArray(),                    // R: Optional rectification transform
      DEFAULT_CAMERA_MATRIX             // P: New camera intrinsic matrix (K와 동일하게 설정)
);
// undistorted_points_mat에는 이제 왜곡이 보정된 픽셀 좌표가 저장됨
cv::Vec2d corrected_sun_vec = undistorted_points_mat.atcv::Vec2d(0, 0);
std::pair<double, double> corrected_sun_center = {corrected_sun_vec[0], corrected_sun_vec[1]};
Ray-plane intersection을 이용하여 좌표 변환

왜곡 보정된 픽셀 좌표를 최종적으로 그리드 인덱스로 변환하기 위해, 3D 기하학에 기반한 Ray-plane intersection 기법을 사용한다. 이 과정은 카메라 좌표계의 방향 벡터를 정의하고, 이를 운전자 시점의 월드 좌표계로 변환하여 3차원 교차점을 계산하는 로직으로 구현되었다.

figure 37. Ray-Plane Intersection 원리. 운전자의 눈에서 시작된 시선이 평면과 만나는 교차점을 계산한다.
  • 전면 유리 평면과의 교차점 계산
    • 픽셀 좌표 정규화 : 왜곡 보정된 픽셀 좌표를 이미지의 주점(cx, cy)을 원점으로 하고, 경계를 [-1, 1] 범위로 갖는 정규화된 이미지 평면 좌표(norm_x, norm_y)로 변환한다. 이는 후속 계산을 이미지 해상도에 독립적으로 만들기 위함이다.
    • 3D 방향 벡터 계산 : 이 정규화된 좌표와 카메라의 수평/수직 화각(FOV) 정보를 삼각함수와 결합하여, 카메라 좌표계 기준의 3차원 방향 벡터 D_vec(du, dv, dw)를 계산한다. 이 벡터는 카메라 렌즈의 중심에서 Glare를 향하는 “카메라의 시선”을 나타낸다. 이때, 카메라가 물리적으로 x축 기준으로 일정 각도만큼 위를 보도록 설치된 점을 반영하기 위해, 계산된 기본 방향 벡터에 x축 회전 행렬을 곱하여 최종 방향 벡터를 얻는다. 이는 카메라의 물리적 방향(orientation)을 수학적으로 보정하는 과정이다.
double norm_x = (x_center_px / img_w - 0.5) * 2.0;
double norm_y = (y_center_px / img_h - 0.5) * 2.0;
double angle_x_rad = to_radians(norm_x * (fov_x_deg / 2.0));
double angle_y_rad = to_radians(norm_y * (fov_y_deg / 2.0));

// 회전 전 기본 방향 벡터 계산 (카메라 전방 +Z 가정)
cv::Vec3d D_no_rotation(std::tan(angle_x_rad), -std::tan(angle_y_rad), 1.0);

// 카메라 Pitch 회전 적용
double pitch_rad = to_radians(DEFAULT_CAMERA_PITCH_DEGREES);
cv::Matx33d rotation_matrix_x(1, 0, 0,
                              0, std::cos(pitch_rad), -std::sin(pitch_rad),
                              0, std::sin(pitch_rad), std::cos(pitch_rad));
cv::Vec3d D_rotated = rotation_matrix_x * D_no_rotation;
cv::Vec3d D_vec = cv::normalize(D_rotated); // 최종 방향 벡터
    • Ray-plane intersection : “태양은 매우 멀리 있어 카메라와 운전자의 시선이 평행하다”는 핵심 가정을 사용하여, 위에서 계산된 D_vec을 운전자 시점의 방향 벡터로 간주한다. 운전자의 눈 위치(DEFAULT_DRIVER_POS)를 광선의 시작점(Origin), D_vec을 방향(Direction)으로 하는 3차원 광선을 정의한다. 이 광선이 차량 전면 유리(월드 좌표계에서 z = DEFAULT_WINDSHIELD_Z로 정의된 평면)와 교차하는 3차원 물리적 지점 P_ws(px, py, pz)를 계산한다.
cv::Point3d intersection = ray_plane_intersection(driver_eye_pos, D_vec, windshield_z);
  • 그리드 인덱스 좌표 변환
    • 계산된 전면 유리와의 교차점 P_ws의 월드 x, y 좌표를, 선바이저가 실제로 작동하게 될 물리적 영역(DEFAULT_GLASS_ORIGIN, DEFAULT_GLASS_SIZE)을 기준으로 3x3 그리드의 최종 인덱스 (grid_x, grid_y)로 변환한다. 이 과정은 교차점의 상대적인 위치를 비례식으로 계산하여 수행되며, 계산된 값이 그리드 범위를 벗어나는 경우 경계값으로 강제 조정하는 로직을 포함한다.
int grid_x = static_cast<int>(((px - x_left_glass) / glass_width) * grid_cols);
int grid_y = static_cast<int>(((y_top_glass - py) / glass_height) * grid_rows);
grid_x = std::max(0, std::min(grid_x, grid_cols - 1));
grid_y = std::max(0, std::min(grid_y, grid_rows - 1));
메인 제어 루프에서의 모듈 호출

메인 제어 루프에서는 Glare가 감지되었을 때, glare_is_detected_flag 플래그를 활성화한다. 이 플래그가 true 일 때만, 감지된 Glare의 중심 픽셀 좌표(avg_glarePos)를 camera_to_driver_coords 함수에 전달하여 최종 그리드 좌표를 계산하도록 구현되었다. 이는 불필요한 연산을 줄이고 시스템 효율을 높인다.

bool glare_is_detected_flag = (avg_glarePos.x != -1 && avg_glarePos.y != -1);
std::pair<int, int> grid_coords = {-1, -1};
if (glare_is_detected_flag) {
     std::pair<double, double> sun_center_for_transform = {
           static_cast<double>(avg_glarePos.x),
           static_cast<double>(avg_glarePos.y)};
     grid_coords = camera_to_driver_coords(sun_center_for_transform);
}

RPi-Arduino 통신

RPi-Arduino Flow Chart
figure 38. 시리얼 통신 흐름도

본 시스템은 효율적인 자원 활용과 실시간성 확보를 위해 분산 제어 아키텍처를 채택하였다. 연산 집약적인 고수준 제어(Glare 인식, 좌표 변환)는 라즈베리파이(RPi)가 담당하며, 실시간성과 정밀성이 요구되는 저수준 하드웨어 제어(모터 구동)는 아두이노 메가(Arduino Mega)가 담당한다. 이 두 이종 프로세서 간의 원활한 정보 교환을 위해, UART 기반의 유선 시리얼 통신 모듈을 C++로 구현하였다.

시리얼 통신을 위한 파일 디스크립터 열기

Linux 기반의 RPi에서 시리얼 포트(/dev/ttyACM0 등)와 통신하기 위해서는, 먼저 해당 장치 파일을 열어 파일 디스크립터(File Descriptor)를 얻어야 한다. 이 과정은 통신을 위한 초기화 단계의 핵심이다.

  • initialize
    • 아두이노와의 시리얼 통신 시작을 위해, 지정된 시리얼 포트 장치 파일을 열고 통신 설정을 구성하는 함수이다.
  • open(시리얼 포트 열기)
    • POSIX 표준 함수인 open()을 사용하여 지정된 시리얼 포트 장치 파일(예: /dev/ttyACM0)을 읽고 쓸 수 있는 모드(0_RDWR)로 연다.
    • 0_NOCTTY : 이 포트가 프로그램을 실행하는 프로세스의 제어 터미널이 되지 않도록 설정한다. 이는 시리얼 통신 프로그래밍에서 예상치 못한 터미널 제어 신호의 간섭을 막기 위한 일반적인 설정이다.
  • 논블로킹 모드 설정
    • fcntl() 함수를 사용하여 열린 파일 디스크립터의 속성을 변경한다. 라즈베리파이의 메인 루프가 아두이노의 상태와 관계없이 항상 신속하게 동작하도록 하기 위해, 시리얼 포트를 논블로킹(Non-blocking) 모드로 설정합니다.
      • fcntl(…, 0)을 통해 현재 파일 상태 플래그를 읽어옵니다.
      • 읽어온 플래그에 0_NONBLOCK 플래그를 추가(OR 연산)합니다.
      • fcntl(…, flags)를 통해 수정된 플래그를 포트에 다시 설정합니다.
    • 이렇게 논블로킹 모드로 설정하면, write() 함수 호출 시 아두이노 측의 수신 준비 여부나 커널 출력 버퍼 상태와 관계없이 즉시 반환됩니다. 만약 버퍼가 가득 차서 데이터를 보낼 수 없다면, write()는 대기하지 않고 오류(EAGAIN 또는 EWOULDBLOCK)를 반환합니다. 이는 RPi 프로그램 전체가 시리얼 쓰기 작업 때문에 멈추는 현상을 방지하여 시스템의 전체적인 실시간성을 보장하는 핵심적인 설정입니다.
bool initialize(const std::string &port_name, speed_t baud_rate) {
     serial_port_fd = open(port_name.c_str(), O_RDWR | O_NOCTTY);
     if (serial_port_fd < 0) { /* 오류 처리 */ return false; }
     // Non-blocking 설정
     int flags = fcntl(serial_port_fd, F_GETFL, 0);
     flags |= O_NONBLOCK;
     fcntl(serial_port_fd, F_SETFL, flags);
     // ...
}
시리얼 모드 설정

1바이트 크기의 패킷으로 구성된 바이너리 데이터를 아두이노에 정확히 전달하기 위해, 운영체제 수준의 문자 처리를 비활성화하는 Raw 모드로 시리얼 통신 모드를 설정한다.

  • tcgetattr로 구조체 선언
    • 현재 시리얼 포트의 터미널 속성(Baud rate, 데이터 비트, 패리티 등)을 읽어와 termios 구조체 tty에 저장한다. 이후 이 구조체의 값을 수정하여 새로운 설정을 적용한다.
  • 1바이트 패킷 데이터 통신을 위한 모드 설정
    • cfmakeraw(&tty) : 이 함수는 tty 구조체를 “Raw” 모드로 설정한다. Raw 모드는 운영체제 수준의 입력/출력 문자 처리(예: 에코, 줄바꿈 준자 변환, 특수 문자 해석 등)를 대부분 비활성화하여, 프로그램이 순수한 바이너리 데이터를 주고받을 수 있도록 한다. 1바이트 명령을 정확히 전송하기 위해 필수적이다.
    • tty.c_cflag : 제어 플래그를 설정한다. 8 데이터 비트, 패리티 없음, 1 스톱 비트 (8N1) 구성을 명시하고, 하드웨어 흐름 제어(CRTSCTS)는 활성화하여 통신의 안정성을 높였다. CLOCAL과 CREAD를 통해 모뎀 제어 신호를 무시하고 수신을 활성화한다.
    • tty.c_lflag, tty.c_iflag, tty.c_oflag : Canonical 모드, 에코, 인터럽트 신호 처리, 소프트웨어 흐름 제어, 출력 후처리 등을 모두 비활성화하여 Raw 데이터 통신을 보장한다.
    • tty.c_cc[VMIN], tty.c_cc[VTIME] : read() 함수의 타임아웃 동작을 제어한다. (현재 쓰기 동작이 중심이므로 기본값 유지)
struct termios tty;
if (tcgetattr(serial_port_fd, &tty) != 0) { /* 오류 처리 */ }
cfmakeraw(&tty);
tty.c_cflag |= (CLOCAL | CREAD | CRTSCTS); // 하드웨어 흐름 제어 활성화
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
// ... 기타 플래그 설정 ...
  • Baud rate, tcflush, tcsetattr
    • cfsetispeed, cfsetospeed 함수로 입출력 Baud Rate를 115200으로 설정한다.
    • tcflush 함수로 포트를 열 때 수신 버퍼에 남아있을 수 있는 이전 데이터들을 모두 버린다.
    • tcsetattr 함수로 수정된 tty 구조체의 설정을 시리얼 포트에 즉시 적용한다. 이 함수가 성공적으로 호출되어야 모든 설정이 반영된다.
Arduino로 데이터 전송

위에서 초기화된 시리얼 포트를 통해 Arduino로 데이터를 전송하도록 하는 함수 sendCommandToArduino를 구현하였다. 이 함수는 고수준의 제어 정보(Glare 유무, 그리드 좌표)를 1바이트 통신 프로토콜에 맞춰 패킹하고 전송하는 역할을 한다.

figure 39. RPi-Arduino 1바이트 통신 프로토콜. RPi가 계산된 정보를 1바이트로 패킹하여 전송하면, Arduino는 이를 해석하여 모터를 제어한다.
  • 전송 데이터 구조(1바이트 command_byte)
    • 최소한의 데이터로 필요한 제어 정보를 전달하기 위해, 총 5비트의 핵심 정보를 하나의 바이트 데이터로 패킹하여 전송한다.
데이터 송신자와 상세 설명
Bit Position Bit Index Field Name Bits Value Description
MSB 7 G (Glare Flag) 1 0 or 1 Glare 감지 유무를 나타내는 플래그 (1 : 감지)
6 Reserved 1 0 예약 비트 (향후 확장용)
5 Reserved 1 0 예약 비트 (향후 확장용)
4 Reserved 1 0 예약 비트 (향후 확장용)
3 C1 (Column MSB) 1 0 or 1 3x3 그리드의 컬럼(x) 좌표 최상위 비트
2 C0 (Column LSB) 1 0 or 1 3x3 그리드의 컬럼(x) 좌표 최하위 비트
1 R1 (Row MSB) 1 0 or 1 3x3 그리드의 로우(y) 좌표 최상위 비트
LSB 0 R0 (Row LSB) 1 0 or 1 3x3 그리드의 로우(y) 좌표 최하위 비트
  • sendCommandToArduino 함수 핵심 로직
    • unsigned char command_byte를 0으로 초기화한다. 이는 Glare가 감지되지 않았거나, 감지되었더라도 유효한 위치를 찾지 못했을 경우 기본적으로 “선바이저 접기” 명령을 보내기 위함이다.
    • 입력받은 glare_detected가 true이고, grid_coords가 유효한 좌표일 때만 명령 생성을 시작한다.
    • 명령 바이트 생성
      • command_byte |= ( 1 << 7 ) : 최상위 비트를 1로 설정하여 Glare가 감지되었음을 표시한다.
      • grid_coords의 col 좌표(0, 1, 2)를 2비트로 변환하고, << 2 비트 시프트 연산을 통해 명령 바이트의 비트 3과 2에 위치시킨다.
      • grid_coords의 row 과표(0, 1, 2)를 2비트로 변환하고, 명령 바이트의 비트 1과 0에 위치시킨다.
    • 디버깅용 콘솔 출력 : 최종적으로 생성된 command_byte의 값을 이진수와 십진수로 콘솔에 출력하여, RPi가 의도한 대로 명령을 생성했는지 개발자가 확인할 수 있도록 한다.
    • Arduino로 명령 바이트 전송 : 구성된 1바이트 명령을 실제로 아두이노로 전송하는 내부 함수 sendByte를 호출한다. sendByte 함수는 write() 시스템 콜을 사용하며, 논블로킹 모드로 설정되어 RPi의 메인 루프가 불필요하게 대기하는 것을 방지한다.
bool sendCommandToArduino(bool glare_detected, const std::pair<int, int>& grid_coords) {
     unsigned char command_byte = 0;
     if (glare_detected) {
          if (grid_coords.first != -1 && grid_coords.second != -1) {
               command_byte |= (1 << 7);
               // ... (col, row 유효성 검사) ...
               unsigned char col_bits = static_cast<unsigned char>(grid_coords.first) & 0x03;
               unsigned char row_bits = static_cast<unsigned char>(grid_coords.second) & 0x03;
               command_byte |= (col_bits << 2);
               command_byte |= row_bits;
          }
     }
return sendByte(command_byte);
}

프로젝트 결과

HW 설계 CAD 파일

Sunvisor_Robot.zip

발표 포스터

figure 40. 최종 발표 포스터

Github

개발 과정 영상

최종 시연 영상

프로젝트 평가

평가 항목 및 결과

  • figure 41. 평가 결과표

    추가 평가 사항

    • 발열 평가
    본 제품은 차량 내부에 설치되어 동작하기 때문에 발열이 심해질 경우 안전상의 문제가 발생할 수 있다. 따라서 라즈베리파이에 Fan을 설치하여 발열을 해소하고자 하였다.
    실제로 프로그램을 30분 실행시켰을 때 CPU의 온도를 측정하였고, Fan을 장착하였을 때 평균 49.3도로 약 30도의 온도가 내려가는 결과를 확인할 수 있다.
    (Fan을 장착하지 않았을 때 80도 이상의 고온으로 동작하여 30분을 채우지 못하고 종료하였다.)
    • figure 42. Fan 장착했을 때 CPU 온도(49.3도)
    • figure 43. Fan 장착하지 않았을 때 CPU 온도(80도 이상)

    역할 분담 및 느낀점

    역할 분담

    figure 44. 역할 분담표

    느낀점

    최*현 :

    실생활에서 겪은 불편함을 해결하기 위해, 내가 배운 공학적 지식을 활용하는 것에서 보람을 느꼈다. 하지만 프로젝트를 진행해나가면서 팀장으로서 부족한 부분도 많이 느꼈던 것 같다. 그럴 때마다, 부족함을 채워준 팀원들에게 감사의 말을 전한다. 특히 낯선 팀원들이 많은 팀에 들어와서, 본인의 역할을 충실하게 수행해준 이*현 팀원, 주말마다 시스템 동작 확인을 위해 차를 끌고 와준 오*택 팀원, 같이 Glare Detection 모듈을 담당해준 조*호 팀원, 멋지게 HW 외관을 만들어준 조*규 팀원 모두에게 감사의 말을 전한다. 팀원들과 함께 고민하고, 새벽까지 프로젝트에 몰입하던 시간들 덕분에 프로젝트를 잘 마무리할 수 있었던 것 같다.

    이*현 :

    길다면 긴 휴학 기간 후 복학한 나에게 임베디드 시스템이라는 과목은 정말 힘들고 낯설지만 유익한 과목이었다. 라즈베리파이를 주로 이용하며 진행되는 수업 및 프로젝트는 무언가를 ‘개발’하려고 할 때 이때까지는 간과해왔던 사항들을 실제로는 어떻게 고려해서 적용해야 하는지 알 수 있게 해주었다. 특히 다른 팀 프로젝트를 진행할 때는 크게 고려하지 않았던 팀적으로 협업을 진행할 때, 팀원들 간의 상호작용이 어떻게 이루어져야 하는가를 이번 기회에 깨달았다. 개발을 진행하며 느꼈던 점은 기존에 익숙했던 Python 언어 기반이 아닌 C++ 언어를 기반으로 하여 모듈을 재작성해야 했던 점이 가장 어려운 점이었는데, 작성하는 동안은 정말 기나긴 역경이었지만 그래도 최종적으로 각자가 만든 모듈들을 통합했을 때 제대로 작동하는 모습을 보니 뿌듯함을 느꼈다. 이번 한 학기 동안 처음 보는 사이임에도 어색함을 느끼지 않도록 도와준 팀원들에게 무한한 감사를 주고 싶다. 또한 프로젝트 진행에 어려움이 있을 때 물신양면으로 도움을 주신 교수님과 조교님들에게도 정말 감사함을 느끼고 있다. 서울시립대학교 기계정보공학과를 4년 간 다니며 가장 힘들었던 한 학기였지만 동시에 가장 많은 지식을 습득할 수 있었던 한 학기였다.

    오*택 :

    운전을 자주 하면서 직접 느낀 고충을 해결하기 위한 프로젝트를 진행하다 보니, 열정적으로, 재미있게 진행하였다. 실제로 사용할 수 있는 제품을 개발하다 보니, 자동차에 직접 달아보고 테스트를 진행하면서 생각하지도 못한 문제사항들이 다수 발생하였고, 현실성을 고려한 프로젝트는 기존의 말로만 하는 프로젝트와 다른 점이 많다는 것을 느꼈다. 기존에 아예 존재하지 않았던 로봇을 처음부터 끝까지 직접 만들어보니 큰 성장을 이룰 수 있었고, 많은 시간을 쏟아부었지만 잘 작동하는 로봇을 보니 뿌듯함에 고생이 추억이 되어 남은 것 같다. 한달 동안 같이 고생하며 함께 하드웨어 개발에 착수한 조*규 팀원에게 정말 감사함을 느끼고, 다른 팀원들 역시 고맙다 동료들아.

    조*호 :

    ‘임베디드 시스템이 무엇인가’를 제대로 느낄 수 있는 시간이었다. 제한된 환경과 자원을 바탕으로 우리가 원하는 동작을 구현해가는 과정이 녹록지 않았지만 그만큼 보람을 느낄 수 있었다. 하나의 완성된 제품을 만들기 위한 HW, SW적 지식을 공부하고, 선행 연구 자료를 바탕으로 우리의 아이디어를 발전해나가는 과정에서 한 단계 발전할 수 있었다. 특히 SW를 개발하며 각 파트를 맡아 팀 단위로 협업하고 통합해가는 과정이 쉽지 않았지만, 그만큼 동료들과 돈독해질 수 있는 시간이 되었다.
    본인이 주로 담당했던 Glare Detection의 경우, OpenCV와 C++ 등 그동안 많이 다뤄보지 않았던 것들에 대해 시행착오가 많았지만 다양한 자료를 조사하며 하나씩 해결해나갈 때 쾌감을 잊지 못할 것 같다. 선배들이 임베디드가 5학점, 6학점 정도의 시간이 투자된다고 힘들지만 그만큼 얻어가는 것이 많았다는 얘기를 들었을 때 처음에는 공감하지 못했다. 그러나 한 학기 동안 정말 많은 시간을 들여 프로젝트를 완성해보고 나니 선배들의 말과 지난 시간들이 머릿속에서 흘러가며 공감할 수 있게 되었다.
    한 학기 동안 고생한 우리 팀원들과 열심히 강의와 피드백을 해주신 교수님, 조교님들께 감사하다는 말을 전하고 싶다.

    조*규 :

    ‘선바이저 로봇’을 주제로 프로젝트를 진행하고자 했을 때, 흥미로웠으나 사실 조금 막막했다. 유사한 시제품이나 참고할 수 있는 문헌이 적어, 초기 설계부터 구현까지 모든 과정을 머릿속에서 구상해야 했고, 이런 점이 프로젝트에서 독특하면서도, 큰 어려움으로 다가왔다. HW 설계를 담당하여 진행을 하며 부족함을 정말 많이 느꼈던 것 같다. 특히, 모터의 움직임에 따라 링크는 회전하되, 엔드 이펙터 부분은 고정되어야 하는 구조적 제약을 만족시킬 수 있는 적절한 매커니즘을 찾기까지 수많은 고민을 반복해야 했다. 매커니즘을 선정하고 나니 다른 문제점이 생겨났고, 이런 과정이 반복되었던 것 같다. 이러한 반복적 시행착오는 결국 나를 성장시키는 값진 과정이 되었고, 실제로 구현된 프로토타입을 차량에 장착하여 주행 테스트를 했을 때의 성취감은 쉽게 잊혀지지 않을 것 같다. 차량에 역전류가 흐르지는 않을까 걱정하며 조심스레 전원을 넣었던 순간과, 햇빛이 운전석에서 정확히 차단되었을 때의 기쁨은 특히 인상 깊었다.
    물론, 혼자였다면 불가능했을지도 모른다고 생각한다. 기술적 문제가 발생 시, 주저 없이 함께 고민해준 오*택 팀원이 든든한 버팀목이 되어줬다. 전력 공급 및 회로 부분에서 문외한인 나에게 많은 배움을 가르쳐 주기도 했다. 물론 팀장으로써, 최*현은 팀원들이 가진 능력과, 팀이 가진 자원들을 최대한 이용하고자 했고, SW 구현을 담당한 이*현, 조*호 팀원은 HW의 한계점들을 고려하여 구현을 하며, 프로젝트가 좋은 방향으로 이끌어질 수 있도록 해주었다. 임베디드 시스템을 활용한 이번 프로젝트는 지금까지 진행했던 과제들 중 가장 장기적이고 실제 적용에 가까운 경험이었다. 이러한 경험은 단지 기술적인 역량뿐만 아니라 실무에 필요한 문제 해결력과 협업 능력을 키울 수 있는 소중한 기회였으며, 앞으로 직업인으로서 성장하는 데 중요한 밑거름이 될 것이라고 믿는다.
    1. Andalibi and D. M. Chandler, “Automatic Glare Detection via Photometric, Geometric, and Global Positioning Information,” Electronic Imaging, vol. 29, pp. 77–82, Jan. 2017
    2. Gray et al., “GLARE: A Dataset for Traffic Sign Detection in Sun Glare,” Dec. 13, 2023