휴대용 원격 감시 카메라 로봇 시스템
부품 및 소모품
부품 수량
SparkFun 아두이노 프로 미니 328 - 5V/16MHz 1
타이어 + 기어박스 DC 모터 + 브라켓 1
SparkFun 듀얼 H-브리지 모터 드라이버 L298 1
라즈베리 파이 3 모델 B 1
라즈베리 파이 카메라 모듈 1
필요 도구
USB-시리얼 FTDI 어댑터 (FT232RL)
아두이노 IDE
드릴
정밀 톱
드라이버 세트
납땜 인두
와이어 커터
프로젝트 소개
로봇 설계·제작부터 리눅스 고급 설정(라즈베리 파이), 안드로이드 앱 개발, 와이파이 원격 제어까지 다루는 종합 프로젝트입니다. 난이도는 높지만 배울 점이 많습니다.
탐구 기술 및 개념
개발 플랫폼: 아두이노, 라즈베리 파이, 안드로이드
전자공학: H-브리지, 트랜지스터로 대용량 부하 구동, 적외선 센서
리눅스: Docker, Docker Compose, systemctl 서비스 설정, 비디오 스트리밍
프로그래밍: 안드로이드 앱, Python, 아두이노 언어, 시리얼 통신, MQTT
1단계: 필요 부품
기계 부품: 아크릴/플라스틱 시트, 접착제, 타이어+기어드 모터+브라켓, 소형 너트/볼트/육각 스페이서, 전방향 바퀴 2개, 소형 LED 플래시라이트(헤드라이트용)
전자 부품: 아두이노 프로 미니 328P, 적외선 장애물 센서 2개, PCB, NPN 트랜지스터(플래시라이트 구동), L7805CV 5V 레귤레이터, L298 H-브리지, 220옴 저항, USB/마이크로 USB 커넥터, 각종 와이어, 3.3V 레귤레이터(아두이노-라즈베리 파이 통신용), 수/암 PCB 커넥터, 온오프 스위치, XT-60 암 커넥터, 2S 1300mAh 리포 배터리, 5V 배터리 팩, 라즈베리 파이 3, 마이크로 SD 카드, 파이 케이스, 파이 카메라
2단계: 로봇 플랫폼 제작
사양:
전륜 2개 DC 모터로 독립 구동
후륜 360도 자유 회전 캐스터
좌우 모터 속도 차이로 조향, 제자리 회전 가능
상단 라이트, 전자부품/배터리/파이 케이스 수납 공간, 지상고 확보
베이스 플레이트: 18x13cm, 모터 브라켓으로 고정, H-브리지는 바닥면 중앙 장착, 후륜은 2cm 육각 스페이서로 고정. H-브리지 근처에 상단 배선용 큰 구멍 필요.
상단 커버: 'ㄱ'자 플레이트 2개(12x13cm, 6.5x13cm) 접합. 전자부품 커버, 헤드라이트 마운트, 파이 케이스 지지대 역할. 6cm 육각 스페이서로 베이스와 결합.
3단계: 전자 회로 제작
핀아웃 (아두이노 프로 미니)
LED 플래시라이트: D3
좌측 모터: PWM(D5), EN1, EN2(A4, A5)
우측 모터: PWM(D6), EN1, EN2(A3, A2)
적외선 센서: 전방(A0), 후방(A1)
라즈베리 파이 통신: TX D11, RX D10
PCB 조립 순서
적외선 센서 전/후방 장착 (L자 브라켓 활용)
헤드라이트: 5V LED 플래시라이트 개조 후 상단 중앙 부착, 배선 홀 드릴링
라즈베리 파이 케이스 조립 (카메라 연결, 최신 라즈비안 설치)
메인 PCB 제작 (프리칭 제공): 커넥터 컷팅→납땜→KF301-2P 커넥터→NPN 트랜지스터+저항→L7805CV→3.3V 레귤레이터→아두이노 핀 헤더→배선→플래시라이트 커넥터
커넥터 제작: 5V USB 배터리 팩→아두이노/파이, 리포 배터리→전원 보드(스위치 포함), IR 센서/H-브리지/모터 연결
확대경과 멀티미터로 검사 후 아두이노 장착
4단계: 아두이노 코드
왜 중간 아두이노 레이어가 필요한가?
모듈성: 파이 없이 아두이노 단독 재사용 가능
안전성: 3달러 아두이노가 35달러 파이 보호
실시간성: OS 간섭 없이 PWM 제어, 센서 폴링 안정적
페일세이프: 파이 스크립트 오류로 폭주 방지
디버깅 용이: 분리된 시스템
주요 기능:
시리얼로 모터/라이트 명령 수신 후 구동
M:-25:16; → 좌 -25(17%), 우 16(32%), 전진
M:44:19; → 좌 44(38%), 우 19(5%), 전진
L:1; 라이트 온, L:0; 오프
전/후방 적외선 센서 폴링(초당 수회) → 거리 데이터 시리얼 전송 (F=5:B=3; 형식)
라이브러리: TextMotorCommandsInterpretter
// 핵심 코드 발췌
#include <SoftwareSerial.h>
#include <TextMotorCommandsInterpretter.h>
const char MOTOR_COMMAND = 'M';
const char LIGHT_COMMAND = 'L';
const long maxDurationForMottorCommand = 300;
const byte maxPwmValue = 230;
const long transmitingInterval = 500;
const int maxObstacleDetection = 1000;
const int minObstacleDetection = 500;
const byte FLASH_PIN = 3;
const byte RIGHT_MOTOR_PWM_PIN = 5;
const byte RIGHT_MOTOR_EN1_PIN = A4;
const byte RIGHT_MOTOR_EN2_PIN = A5;
const byte LEFT_MOTOR_PWM_PIN = 6;
const byte LEFT_MOTOR_EN1_PIN = A3;
const byte LEFT_MOTOR_EN2_PIN = A2;
const byte FRONT_DISTANCE_SENSOR = A0;
const byte BACK_DISTANCE_SENSOR = A1;
SoftwareSerial masterComm(11, 10); // RX, TX
TextMotorCommandsInterpretter motorCommandsInterpretter(-50, 50, -50, 50);
String currentCommand;
long lastCheckedTime;
long lastTransmitTime;
boolean inMotion = false;
void setup() {
Serial.begin(9600);
masterComm.begin(9600);
masterComm.setTimeout(10);
pinMode(FLASH_PIN, OUTPUT);
pinMode(LEFT_MOTOR_PWM_PIN, OUTPUT);
pinMode(LEFT_MOTOR_EN1_PIN, OUTPUT);
pinMode(LEFT_MOTOR_EN2_PIN, OUTPUT);
pinMode(RIGHT_MOTOR_PWM_PIN, OUTPUT);
pinMode(RIGHT_MOTOR_EN1_PIN, OUTPUT);
pinMode(RIGHT_MOTOR_EN2_PIN, OUTPUT);
lastCheckedTime = millis();
lastTransmitTime = millis();
}
void loop() {
if (masterComm.available() > 0) {
currentCommand = masterComm.readString();
processCommand();
}
if (inMotion && millis() - lastCheckedTime > maxDurationForMottorCommand) {
stopMotors();
}
if (millis() - lastTransmitTime > transmitingInterval) {
lastTransmitTime = millis();
masterComm.print(getObstacleData());
Serial.print(analogRead(BACK_DISTANCE_SENSOR)); Serial.print("---");
Serial.println(getObstacleData());
}
}
String getObstacleData() {
int frontDistance = analogRead(FRONT_DISTANCE_SENSOR);
int backDistace = analogRead(BACK_DISTANCE_SENSOR);
frontDistance = map(frontDistance, maxObstacleDetection, minObstacleDetection, 0, 10);
backDistace = map(backDistace, maxObstacleDetection, minObstacleDetection, 0, 10);
return String("F=" + String(frontDistance) + ":B=" + String(backDistace) + ";");
}
void processCommand() {
switch (currentCommand.charAt(0)) {
case MOTOR_COMMAND: steerCar(); break;
case LIGHT_COMMAND: toggleLight(currentCommand.charAt(2)); break;
}
}
void steerCar() {
motorCommandsInterpretter.analizeText(currentCommand);
float percentLeftMotor = motorCommandsInterpretter.getPercentLeft();
float percentRightMotor = motorCommandsInterpretter.getPercentRight();
Serial.write("Left="); Serial.println(percentLeftMotor);
Serial.write("Right="); Serial.println(percentRightMotor);
setMotorsDirection(motorCommandsInterpretter.getDirection());
analogWrite(LEFT_MOTOR_PWM_PIN, percentLeftMotor * maxPwmValue);
analogWrite(RIGHT_MOTOR_PWM_PIN, percentRightMotor * maxPwmValue);
inMotion = true;
lastCheckedTime = millis();
}
void setMotorsDirection(boolean forward) {
if (forward) {
digitalWrite(LEFT_MOTOR_EN1_PIN, HIGH);
digitalWrite(LEFT_MOTOR_EN2_PIN, LOW);
digitalWrite(RIGHT_MOTOR_EN1_PIN, HIGH);
digitalWrite(RIGHT_MOTOR_EN2_PIN, LOW);
} else {
digitalWrite(LEFT_MOTOR_EN1_PIN, LOW);
digitalWrite(LEFT_MOTOR_EN2_PIN, HIGH);
digitalWrite(RIGHT_MOTOR_EN1_PIN, LOW);
digitalWrite(RIGHT_MOTOR_EN2_PIN, HIGH);
}
}
void stopMotors() {
Serial.println("Stopping motors");
analogWrite(LEFT_MOTOR_PWM_PIN, 0);
analogWrite(RIGHT_MOTOR_PWM_PIN, 0);
inMotion = false;
}
void toggleLight(char command) {
Serial.println("Toggle light");
if (command == '1') digitalWrite(FLASH_PIN, HIGH);
else digitalWrite(FLASH_PIN, LOW);
}
FTDI 어댑터로 업로드. 블루투스 모듈(HC-05 등)로 테스트 가능.
5단계: 라즈베리 파이 프로젝트 설치 및 설정
아키텍처:
안드로이드 앱: uv4l 스트리밍을 웹뷰로 표시, 조작 → MQTT 발행
라즈베리 파이: uv4l로 카메라 스트리밍, Docker 컨테이너에서 Python 서버가 MQTT 구독 → 시리얼로 아두이노 전달
아두이노: 센서 데이터 시리얼 전송 → Python → MQTT → 앱 표시
사전 준비: 라즈비안 설치, 카메라 활성화, SSH 활성화, 포트 포워딩(외부 접속 시: 9090 uv4l, 1883 mosquitto).
사전 요구사항
# git 설치 후 홈 디렉토리에서 클론 (경로 중요: docker-compose.yml에 하드코딩됨)
git clone https://github.com/danionescu0/robot-camera-platform.git
# 시리얼 콘솔 비활성화 필요
uv4l 스트리밍 설치
chmod +x uv4l/install.sh uv4l/start.sh
sh ./uv4l/install.sh
uv4l/start.sh에서 비밀번호, 포트, 프레임레이트, 해상도, 회전 등 설정.
설정 파일 수정
config.py: 모스키토 비밀번호, 시리얼 포트/보드레이트 설정
docker-container/mosquitto/Dockerfile: RUN mosquitto_passwd -b /etc/mosquitto/pwfile user your_password 본인 계정으로 변경
uv4l 테스트
sh ./uv4l/start.sh
# 브라우저에서 https://your_ip:9090/stream 확인
sudo pkill uv4l # 중지
Docker & Docker Compose 설치
라즈베리 파이 공식 블로그 참조.
부팅 시 자동 시작 (systemd)
# systemctl 폴더의 파일들을 /etc/systemd/system/로 복사
sudo systemctl enable robot-camera.service
sudo systemctl enable robot-camera-video.service
sudo reboot
# 상태 확인
sudo systemctl status robot-camera.service
sudo systemctl status robot-camera-video.service
6단계: 안드로이드 앱 설정 및 빌드
git clone https://github.com/danionescu0/android-robot-camera
개발자 옵션 활성화
안드로이드 스튜디오 설치
프로젝트 임포트
./app/src/main/values/passwords.xml에서 MQTT/스트리밍 호스트/자격증명 설정
앱 실행
7단계: 로봇 사용 및 디버깅
앱 사용법
좌측: uv4l 스트리밍 영상
우측: 조이스틱(방향 제어), 헤드라이트 토글 버튼
우측 상단: 상태 표시 (장애물 화살표, 배터리, MQTT 연결 상태)
디버깅 레이어별 가이드
아두이노 레이어: FTDI로 2번째 시리얼 연결(RX↔D11, TX↔D10), 모터/라이트 명령 직접 전송 테스트. 배선 확인, 모터 방향 반전 시 배선 스왑.
라즈베리 파이 레이어: docker ps로 컨테이너 확인, netstat -nltp로 9090/1883 포트 확인, ls -l /dev/ttyS0로 시리얼 포트 확인, picocom으로 직접 시리얼 테스트, 시리얼 콘솔 비활성화 확인, 외부에서 스트리밍/MQTT 접근 확인.
안드로이드 레이어: 디버깅 모드 확인, passwords.xml 설정 확인, 외부 접속 확인, 앱 우측 상단 'Connected' 확인, 안드로이드 디버거로 에러 확인.
8단계: 확장 아이디어
색상 추적 로봇: 특정 색상/크기 객체 추적. pip3 install -r navigation/requirements.txt 후 config_navigation.py에서 HSV 범위/크기 임계값 설정. VNC로 python3 object_tracking.py colored-object --show-video 실행.
기타 확장: 얼굴 인식 추적, 순찰 및 움직임 감지, 3G 모뎀으로 외부 제어/위험 지역 탐사 등.
복잡한 프로젝트라 오류 있을 수 있습니다. 댓글로 문의 주세요. 유튜브 채널도 구독 부탁드립니다.
코드
회로도