자동차 무선 조종기능 구현
- 1. 블루투스 시리얼 통신으로 데이터 주고 받기
- 2. 시리얼 데이터를 분석하여 명령어 해석하기
- 3. 쓰레드를 활용하여 통신기능 분리하기
- 4. 블루투스 시리얼 통신으로 조종하는 자동차 만들기
- 5. 스위치를 이용하여 비상 정지기능 만들기
- 6. LED로 이동방향 표시하기
- 7. 부저를 이용하여 경적기능 추가하기
1. 블루투스 시리얼 통신으로 데이터 주고 받기
1.1 HM-10 블루투스 4.0 BLE
- TI CC2540 또는 CC2541 블루투스 SOC(시스템 온 칩)를 기반으로 하는 소형 3.3V SMD 블루투스 4.0 BLE 모듈
아두이노, 라즈베리파이 등 호환
- 기본 사양
- CC2540 또는 CC2541 칩 기반
- +2.5v ~ +3.3v
- 최대 50mA 필요
- 활성 상태일 때: 약 9mA 사용
- 수면모드 상태일 때: 50~200uA 사용
- RF 전력: -23dbm, -6dbm, 0dbm, 6dbm
- 블루투스 버전 4.0만 지원
- HC-06 및 HC-05와 같은 Bluetooth 2/2.1 모듈에 연결할 수 없음
- 직렬 UART 연결을 통해 전송되는 AT 명령을 통해 제어됨
- 직렬 연결의 기본 통신 속도: 9600
- 기본 PIN: 000000
- 기본 이름: HMSoft
(그림출처: 디바이스마트)
1.2 블루투스 통신 환경 설정
- Serial Port ➜ 활성화
- Serial Console ➜ 비활성화

- 설정 이유
- 리소스(직렬 통신 포트)의 충돌 방지 및 전용 사용이 목적
- 라즈베리파이에서 블루투스를 사용하려면
- 블루투스 모듈이 사용할 직렬 통신 포트(Serial Port)를 활성화하여 통신 채널을 열어주고
- 이 포트가 다른 용도(Serial Console)로 사용되지 않도록 비활성화하여
- 충돌을 막고 블루투스 전용으로 만들어 주어야 함
- UART (Universal Asynchronous Receiver/Transmitter, 범용 비동기 송수신기)
- 블루투스 모듈은 흔히 UART를 통해 라즈베리파이의 메인 CPU와 통신을 수행함
- SPI(Serial Peripheral Interface), USB(Universal Serial Bus), SDIO(Secure Digital Input/Output), I2C(Inter-Integrated Circuit) 등 다른 통신 수단도 있지만 UART가 가장 기본적인 시리얼 통신의 하나이므로 저속 데이터의 전송 및 제어에 적합함
- 한 번에 하나의 기능만 제대로 수행할 수 있음
- Serial Port를 활성화하는 이유
- 블루투스 모듈과의 통신 채널 확보
- 블루투스 모듈이 라즈베리파이 OS와 데이터를 주고받으려면, 이 통신을 위한 하드웨어적인 통로(직렬 포트, 즉 UART)가 열려 있어야 함
- 블루투스 기능(예: SPP 프로필)이 직렬 통신을 기반으로 작동하기 때문
- 이 포트를 활성화해야 블루투스 통신이 가능해짐
- Serial Console을 비활성화하는 이유
- 직렬 포트 점유 방지
- Serial Console은 라즈베리파이의 UART를 사용하여 부팅 메시지나 터미널(CLI) 접속 등을 제공하는 기능
- 라즈베리파이 부팅 과정이나 운영체제 동작 중 발생하는 로그나 명령 프롬프트를 직렬 케이블을 통해 외부 컴퓨터에서 볼 수 있게 해주는
디버깅/관리용 콘솔- 굳이 직렬 포트를 점유하며 수행해야 할 필요는 없음
- 블루투스와의 충돌 방지
- Serial Console이 활성화되어 있으면, 블루투스 모듈과 Serial Console이 동일한 UART 자원을 서로 사용하려고 경쟁하게 됨
- 이 경우 둘 중 어느 것도 제대로 작동하지 않게 되어 블루투스 통신에 오류가 발생하거나, 콘솔 접속이 불안정해질 수 있음
- 블루투스 기능의 안정성 확보
- Serial Console을 비활성화함으로써 해당 UART 자원을 블루투스 모듈이 전용으로 사용하게 하여, 블루투스 통신의 안정성과 신뢰성을 확보할 수 있음
1.3 라즈베리파이 적용
- 라즈베리파이의 GPIO 14(RXD), 15(TXD)번 핀이 시리얼 통신용으로 할당되어 있음
/dev/serial0의 이름으로 호출됨
#// file: "라즈베리파이 터미널"
ls -l /dev/serial0
ttyAMA10이라는 이름으로 할당됨 (시스템에 따라 다를 수 있음) ➜ 파이썬 코드 작성 시 사용할 serial0 접속명

- 시리얼 통신 테스트: 전송되는 데이터가 없으므로 빈 데이터만 표시됨
#// file: "bluetooth_test.py"
import serial
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
try:
while True:
data = bleSerial.read()
print(data)
except KeyboardInterrupt:
pass
bleSerial.close()

1.4 무선 조종을 위한 스마트폰 앱 설치
- 안드로이드 기종
- 플레이스토어에서
Serial Bluetooth Terminal검색하여 설치
➜ 
Serial Bluetooth Terminal에서 라즈베리파이의 블루투스 탐색 및 연결- 주로 MLT-BT05, HM-10, BT05 등의 이름으로 검색됨



- 플레이스토어에서
- 아이폰 기종의 경우
- App Store에서
ble automation앱을 검색 후 설치할 것- 무료 터미널 앱이지만 가끔 광고가 나타남
- 사용법은 동일하지만 라즈베리파이에서 전송한 값을 확인할 수 없음
- 아이폰용 앱(ble automation)에서 전송 값을 표시하는 기능을 지원하지 않음

- App Store에서
1.5 라즈베리파이 ➜ 스마트폰 데이터 전송 확인
#// file: "bluetooth.py"
import serial
import time
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
try:
while True:
sendData = "I am raspberry \r\n"
bleSerial.write( sendData.encode() )
time.sleep(1.0)
except KeyboardInterrupt:
pass
bleSerial.close()

2. 시리얼 데이터를 분석하여 명령어 해석하기
- 시리얼 통신 데이터를 한 줄씩 받아 출력하기
- 받은 값은 bytes형이므로 일반적으로 사용할 수 있는 문자열로 변경하여 처리
#// file: "serial_command.py"
import serial
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
try:
while True:
data = bleSerial.readline()
data = data.decode()
if data.find("go") >= 0:
print("ok go")
elif data.find("back") >= 0:
print("ok back")
elif data.find("left") >= 0:
print("ok left")
elif data.find("right") >= 0:
print("ok right")
elif data.find("stop") >= 0:
print("ok stop")
except KeyboardInterrupt:
pass
bleSerial.close()
3. 쓰레드를 활용하여 통신기능 분리하기
- 라즈베리파이에서 블루투스 통신을 할 때,
- 데이터가 도착할 때까지 대기하거나,
- Timeout으로 설정한 시간이 지나면
- 다음 코드로 넘어감
- 이러한 대기 시간은 while문으로 반복할 때, 동작 시간이 시리얼 통신에 의해 주기가 틀어질 수 있음
- 통신 부분만 추출하여 별도의 모델을 만들고
- 쓰레드를 이용하여 다수의 동작을 수행할 수 있도록 함
- 쓰레드(Thread)
- 컴퓨터 프로그램이 작업을 수행하는 가장 작은 단위
- 하나의 프로그램(프로세스) 안에서 여러 작업을 동시에 처리할 수 있게 해주는 기능
- 여러 쓰레드가 같은 메모리 영역에 동시 접근할 경우, 데드락(DeadLock)과 같은 문제가 발생할 수 있음
- 동기화(Synchronization) 기술이 요구됨
- 쓰레드(Thread)
#// file: "thread_comm.py"
import threading
import serial
import time
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
gData = ""
def serial_thread():
global gData
while True:
data = bleSerial.readline()
data = data.decode()
gData = data
def main():
global gData
try:
while True:
print("serial data:",gData)
time.sleep(1.0)
except KeyboardInterrupt:
pass
if __name__ == '__main__':
task1 = threading.Thread(target = serial_thread)
task1.start()
main()
bleSerial.close()
4. 블루투스 시리얼 통신으로 조종하는 자동차 만들기
- 프로세스
- 블루투스 시리얼 통신을 이용하여 명령어 데이터를 수신
- 명령어 부분만 해석
- 명령어에 해당하는 제어 함수 호출
- 스마트폰 앱에서 각 명령을 버튼에 할당
#// file: "bluetooth_control.py"
import threading
import serial
import time
from gpiozero import DigitalOutputDevice
from gpiozero import PWMOutputDevice
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
gData = ""
PWMA = PWMOutputDevice(18)
AIN1 = DigitalOutputDevice(22)
AIN2 = DigitalOutputDevice(27)
PWMB = PWMOutputDevice(23)
BIN1 = DigitalOutputDevice(25)
BIN2 = DigitalOutputDevice(24)
def motor_go(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_back(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_left(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_right(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_stop():
AIN1.value = 0
AIN2.value = 1
PWMA.value = 0.0
BIN1.value = 0
BIN2.value = 1
PWMB.value = 0.0
def serial_thread():
global gData
while True:
data = bleSerial.readline()
data = data.decode()
gData = data
def main():
global gData
try:
while True:
if gData.find("go") >= 0:
gData = ""
print("ok go")
motor_go(0.5)
elif gData.find("back") >= 0:
gData = ""
print("ok back")
motor_back(0.5)
elif gData.find("left") >= 0:
gData = ""
print("ok left")
motor_left(0.5)
elif gData.find("right") >= 0:
gData = ""
print("ok right")
motor_right(0.5)
elif gData.find("stop") >= 0:
gData = ""
print("ok stop")
motor_stop()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
task1 = threading.Thread(target = serial_thread)
task1.start()
main()
bleSerial.close()
PWMA.value = 0.0
PWMB.value = 0.0
5. 스위치를 이용하여 비상 정지기능 만들기
- 무선통신은 항상 접속이 끊어짐을 주의해야 함
- 통신이 끊어지더라도 차량의 스위치를 누르면 차량이 멈추도록 제어함
#// file: "emergency_stop.py"
import threading
import serial
import time
from gpiozero import DigitalOutputDevice
from gpiozero import PWMOutputDevice
from gpiozero import Button
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
gData = ""
SW1 = Button(5, pull_up=False)
SW2 = Button(6, pull_up=False)
SW3 = Button(13, pull_up=False)
SW4 = Button(19, pull_up=False)
PWMA = PWMOutputDevice(18)
AIN1 = DigitalOutputDevice(22)
AIN2 = DigitalOutputDevice(27)
PWMB = PWMOutputDevice(23)
BIN1 = DigitalOutputDevice(25)
BIN2 = DigitalOutputDevice(24)
def motor_go(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_back(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_left(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_right(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_stop():
AIN1.value = 0
AIN2.value = 1
PWMA.value = 0.0
BIN1.value = 0
BIN2.value = 1
PWMB.value = 0.0
def serial_thread():
global gData
while True:
data = bleSerial.readline()
data = data.decode()
gData = data
def main():
global gData
try:
while True:
if gData.find("go") >= 0:
gData = ""
print("ok go")
motor_go(0.5)
elif gData.find("back") >= 0:
gData = ""
print("ok back")
motor_back(0.5)
elif gData.find("left") >= 0:
gData = ""
print("ok left")
motor_left(0.5)
elif gData.find("right") >= 0:
gData = ""
print("ok right")
motor_right(0.5)
elif gData.find("stop") >= 0:
gData = ""
print("ok stop")
motor_stop()
if SW1.is_pressed == True or SW2.is_pressed == True or SW3.is_pressed == True or SW4.is_pressed == True :
motor_stop()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
task1 = threading.Thread(target = serial_thread)
task1.start()
main()
bleSerial.close()
PWMA.value = 0.0
PWMB.value = 0.0
6. LED로 이동방향 표시하기
#// file: "move_direction.py"
import threading
import serial
import time
from gpiozero import Button
from gpiozero import DigitalOutputDevice
from gpiozero import PWMOutputDevice
from gpiozero import LED
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
gData = ""
SW1 = Button(5, pull_up=False )
SW2 = Button(6, pull_up=False )
SW3 = Button(13, pull_up=False )
SW4 = Button(19, pull_up=False )
PWMA = PWMOutputDevice(18)
AIN1 = DigitalOutputDevice(22)
AIN2 = DigitalOutputDevice(27)
PWMB = PWMOutputDevice(23)
BIN1 = DigitalOutputDevice(25)
BIN2 = DigitalOutputDevice(24)
LED1 = LED(26)
LED2 = LED(16)
LED3 = LED(20)
LED4 = LED(21)
def motor_go(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_back(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_left(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_right(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_stop():
AIN1.value = 0
AIN2.value = 1
PWMA.value = 0.0
BIN1.value = 0
BIN2.value = 1
PWMB.value = 0.0
def serial_thread():
global gData
while True:
data = bleSerial.readline()
data = data.decode()
gData = data
def main():
global gData
try:
while True:
if gData.find("go") >= 0:
gData = ""
print("ok go")
motor_go(0.5)
LED1.on()
LED2.on()
LED3.off()
LED4.off()
elif gData.find("back") >= 0:
gData = ""
print("ok back")
motor_back(0.5)
LED1.off()
LED2.off()
LED3.on()
LED4.on()
elif gData.find("left") >= 0:
gData = ""
print("ok left")
motor_left(0.5)
LED1.on()
LED2.off()
LED3.on()
LED4.off()
elif gData.find("right") >= 0:
gData = ""
print("ok right")
motor_right(0.5)
LED1.off()
LED2.on()
LED3.off()
LED4.on()
elif gData.find("stop") >= 0:
gData = ""
print("ok stop")
motor_stop()
LED1.off()
LED2.off()
LED3.off()
LED4.off()
if SW1.is_pressed == True or SW2.is_pressed == True or SW3.is_pressed == True or SW4.is_pressed == True :
motor_stop()
LED1.off()
LED2.off()
LED3.off()
LED4.off()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
task1 = threading.Thread(target = serial_thread)
task1.start()
main()
bleSerial.close()
PWMA.value = 0.0
PWMB.value = 0.0
LED1.off()
LED2.off()
LED3.off()
LED4.off()
7. 부저를 이용하여 경적기능 추가하기
- 프로세스
- 부저에 사용할 핀 추가
- bz_on, bz_off 명령어를 입력받아서 제어
#// file: "buzzer.py"
import threading
import serial
import time
from gpiozero import Button
from gpiozero import DigitalOutputDevice
from gpiozero import PWMOutputDevice
from gpiozero import LED
from gpiozero import TonalBuzzer
bleSerial = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=1.0)
gData = ""
SW1 = Button(5, pull_up=False )
SW2 = Button(6, pull_up=False )
SW3 = Button(13, pull_up=False )
SW4 = Button(19, pull_up=False )
PWMA = PWMOutputDevice(18)
AIN1 = DigitalOutputDevice(22)
AIN2 = DigitalOutputDevice(27)
PWMB = PWMOutputDevice(23)
BIN1 = DigitalOutputDevice(25)
BIN2 = DigitalOutputDevice(24)
LED1 = LED(26)
LED2 = LED(16)
LED3 = LED(20)
LED4 = LED(21)
BUZZER = TonalBuzzer(12)
def motor_go(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_back(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_left(speed):
AIN1.value = 1
AIN2.value = 0
PWMA.value = speed
BIN1.value = 0
BIN2.value = 1
PWMB.value = speed
def motor_right(speed):
AIN1.value = 0
AIN2.value = 1
PWMA.value = speed
BIN1.value = 1
BIN2.value = 0
PWMB.value = speed
def motor_stop():
AIN1.value = 0
AIN2.value = 1
PWMA.value = 0.0
BIN1.value = 0
BIN2.value = 1
PWMB.value = 0.0
def serial_thread():
global gData
while True:
data = bleSerial.readline()
data = data.decode()
gData = data
def main():
global gData
try:
while True:
if gData.find("go") >= 0:
gData = ""
print("ok go")
motor_go(0.5)
LED1.on()
LED2.on()
LED3.off()
LED4.off()
elif gData.find("back") >= 0:
gData = ""
print("ok back")
motor_back(0.5)
LED1.off()
LED2.off()
LED3.on()
LED4.on()
elif gData.find("left") >= 0:
gData = ""
print("ok left")
motor_left(0.5)
LED1.on()
LED2.off()
LED3.on()
LED4.off()
elif gData.find("right") >= 0:
gData = ""
print("ok right")
motor_right(0.5)
LED1.off()
LED2.on()
LED3.off()
LED4.on()
elif gData.find("stop") >= 0:
gData = ""
print("ok stop")
motor_stop()
LED1.off()
LED2.off()
LED3.off()
LED4.off()
elif gData.find("bz_on") >= 0:
gData = ""
print("ok buzzer on")
BUZZER.play(391)
elif gData.find("bz_off") >= 0:
gData = ""
print("ok buzzer off")
BUZZER.stop()
if SW1.is_pressed == True or SW2.is_pressed == True or SW3.is_pressed == True or SW4.is_pressed == True :
motor_stop()
LED1.off()
LED2.off()
LED3.off()
LED4.off()
BUZZER.stop()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
task1 = threading.Thread(target = serial_thread)
task1.start()
main()
bleSerial.close()
PWMA.value = 0.0
PWMB.value = 0.0
LED1.off()
LED2.off()
LED3.off()
LED4.off()
BUZZER.stop()