[61호]시각장애인을 위한 음성 안내 버스표지판
2020 ICT 융합 프로젝트 공모전 최우수상
시각장애인을 위한 음성 안내 버스표지판
글 | 인하대학교 박민아, 이상우, 이승화
1. 심사평
칩센 데모에서도 인천시 Bus API를 사용하였듯이 기존 시스템과 연계를 하기 위해서는 보완해야 하는 사항이 많아 보이지만, 이러한 부분을 제외한 장치 자체로는 명확한 목적성을 가진 작품으로 생각됩니다. 기획한 의도에 따른 동작을 보여주긴 하였으나, 비콘등의 무선 장치가 절대적인 데이터를 가지거나, 정확한 거리 측정 등을 할 수가 없으므로 이를 보완할 수 있는 방안을 장치에 어떻게 도입할지에 대한 고민이 필요해 보입니다.
펌테크 세심한 관찰력이 반영된 실생활과 밀접한 아이디어와 실용성이 우수한 작품이라고 생각합니다. 기획 의도에 맞게 전체 시스템을 안정적이고 완성도 높게 구현하였고 제출된 보고서 구성 내용도 명확하고, 충실했다고 생각이 듭니다. 전체적으로 기획 의도, 기술 구현도, 완성도 등에서 상당히 뛰어나고 상업적으로 손색이 없는 우수한 작품으로 생각됩니다.
위드로봇 실제 필요한 기능을 잘 구현한 작품입니다. 좀 더 다듬으면 실제 상품화도 가능할 것으로 보입니다.
2. 작품 개요
2.1. 배경 및 목적
21세기에 들어 장애인과 같은 교통약자들의 사회참여 욕구 및 기회가 증가하면서 이들을 위한 이동수단의 확보와 교통정보제공 문제가 사회적 이슈로 대두되고 있다.[1] 그러나 장애인을 위한 대중교통 서비스는 많은 불편함을 호소하며 해결되지 않은 문제로 남아있다.
<표 1>에 따르면 시각장애인은 통행 중에 다른 통행자들에게 도움을 받는 비중이 가장 큰 것으로 나타났다. 시각장애인은 스스로 버스정류장의 정보를 알아내기에 많은 제약이 있는 것을 확인할 수 있었다. 또한 <표 2>에 따르면 대중교통의 안전도 또한 불만족의 경우가 많았다. 지하철의 경우와 비교할 때 만족도의 차이가 매우 컸으며 버스정류장 및 버스 시설의 개선이 이루어져야 할 것으로 확인된다.
MBC 뉴스에서도 실제 버스를 이용하는데 어려움을 겪는 사례를 소개하였다. 시각장애인은 버스 정류시설을 이용하며 도착 버스를 확인하지 못해 혼동을 느끼고 있었으며 이렇게 버스 탑승에 어려움을 겪기 때문에 보통 30~40분의 시간을 허비한다고 대답하였다.
이를 통해 일반 버스정류시설은 시각장애인에게 많은 불편함을 야기시키고 있으며 이를 해결할 시스템이 필요하다고 생각되었다.
2.2. 문제인식
한 시각장애인 단체에서 청와대 국민청원에 시각장애인의 버스 탑승 문제에 있어 개선이 필요하다는 청원을 발의하였다.[3] 이 청원에 따르면 시각장애인 버스 탑승에 있어 문제점은 크게 두가지이다.
(1) 버스정류장 정보를 알 수 없다.
시각장애인이 버스정류장에 도착했을 때 해당 정류장이 어느 정류장인지 확인하기 어렵고, 이 때문에 도착하는 버스 정보를 쉽게 파악하기 어렵다.
(2) 버스번호를 파악할 수 없다.
시각장애인이 버스 도착 정보를 알 수가 없어 버스 탑승에 어려움을 겪는다. 정류장에서 ‘–번 버스가 곧 도착합니다.’라는 안내음이 송출된다고 할지라도 버스가 안내된 순서대로 진입하지 않거나, 많은 버스가 한꺼번에 오는 경우 때문에 특정 버스에 탑승하기 어렵다.
2.3. 해결방안
앞서 제시된 문제를 해결하기 위해 음성인식 기반 버스표지판을 제시하였다. 현재 대부분의 버스정류장에는 이미 음성안내 시스템이 존재하지만 기존 시스템의 문제점은 사용자가 알 수 있는 정보가 오직 잠시 후 도착하는 버스 안내밖에 없다는 것이다. 정류장에 정차하는 버스 정보나 버스가 몇분 후 도착하는지는 시각적인 정보로만 제공되기 때문에 시각장애인이 이용하는 데에는 어려움이 존재한다.
따라서 기존 버스정류장 안내 시스템의 한계를 극복하고 시각장애인이 버스탑승 시 느끼는 불편함을 해결하기 위해 음성인식을 기반으로 시각장애인에게 원하는 정보를 제공하는 시스템을 택하였다. 이 시스템에 대한 간략한 도식은 다음과 같다.
음성안내 버스표지판이 동작하는 방식은 먼저 시각장애인이 비콘을 소지한 상태에서 버스 정류장에 다가가면 버스 음성 안내가 시작된다. 이후 탑승하려는 버스 번호를 마이크에 말하면 이를 인식해 해당 정류장에 정차하는지 여부를 알려주고 남은 시간이 몇분인지 알려준다. 이 탑승버스 번호는 LED에 점등되어 버스 운전기사가 인식하여 시각장애인이 대중교통 서비스를 타는것을 돕도록 한다. 이 정보는 버스기사 뿐만 아니라 주위 사람들도 의식적으로 시각적인 확인이 가능하기 때문에 현재처럼 버스가 오더라도 탑승하지 못하는 문제를 해결할 수 있을 것으로 보인다.
3. 작품 설명
3.1. 주요 동작 및 특징
(1) 사용자 접근 : 하드웨어 사용 전력을 최소화하고 시스템의 남용을 방지하기 위하여 사용자(비콘) 접근시에만 하드웨어가 켜지도록 한다. 사용자가 3m이내에 있는 경우에는 하드웨어에서 “–정류장 입니다.” 소리가 발생하여 정류장 위치를 알리도록 한다.
(2) 탑승 버스 인식 : 표지판에 접근하여 탑승하려는 버스 번호를 마이크에 말하면 버스번호와 도착 예상 시간을 안내한다. 또한 이렇게 인식된 버스 번호는 LED에 표시된다.
(3) 버스 안내 : 버스가 정류장에 도착 예정시 잠시후 도착한다는 안내 메세지를 출력한다. 버스기사는 LED를 통해 탑승하려는 사용자가 있는것을 인지하여 사용자의 탑승을 돕는다.
3.2. 전체 시스템 구성
라즈베리파이는 서버로부터 버스정보를 받아오고 음성 텍스트 변환 및 텍스트 음성 변환을 제어한다. 텍스트->음성 변환 파일은 라즈베리파이 상에 저장되므로 스피커는 라즈베리파이에서 출력되도록 한다.
비콘은 단뱡항 통신으로 페어링이 필요하지 않고 비교적 넓은 범위에서부터 인식가능 하고, 비용적인 측면을 고려해서 통신모듈로 사용하였다. 단, 비콘과 LCD에 대해서는 효율적인 구성 코드 효율적인 사용을 위해 아두이노를 이용하여 제어하였다.
버스 정보 데이터셋은 공공데이터포털(www.data.go.kr)에서 제공하는 인천광역시 버스 정보를 사용하였다. 내부 알고리즘을 간소화하기 위해 데이터를 DB화 하지 않고 필요시에 요청하는 형식을 선택하였다.
4. 단계별 제작 과정
4.1. 간트 차트
4.2. 제작 과정
4.2.1. 하드웨어
아래 첨부한 사진 순서대로 스피커, 마이크 그리고 라즈베리파이를 표지판 내부에 넣고 외부에는 시각장애인 마크 및 LED만 확인할 수 있도록 만들었다.
4.2.2. 소프트웨어 개발환경
Raspberry Pi B
- 개발 언어: Python
- Tool: terminal
- 사용 시스템
1) Google Cloud Platform (Sound To Text & Text To Sound)
2) 인천 Bus API
5. 기대효과
사회적 측면
대중교통 또는 여객시설은 누구나 이용할 수 있는 공공재이다. 하지만 현재 국내의 대중교통 시설과 건물들은 비장애인들에게 초점이 맞춰져 있기 때문에 장애인들은 교통 수단 이용에 많은 제약을 받고 있다. 이동권의 제한은 개인적, 사회적 욕구를 원천적으로 제거시키는 것으로까지 이어진다. 따라서 이동권 관련 문제는 단순히 물리적 제약과 관련 된 것이 아니라 인간의 기본적인 권리와도 연관된 사회적인 문제로 볼 수 있다.[4] 따라서 시각장애인을 위한 음성 안내 버스표지판을 통해 시각장애인의 기본적 권리를 보장하며 사회참여 기회를 증진시킬 수 있을 것이다.
경제적 측면
하나의 표지판 당 기기의 개발비용은 약 10만원 전후로 생산이 가능할 것으로 예상된다. 비용 사용 목록은 다음과 같다. 라즈베리파이3 (Model B+ 기준 40,000원) + LED 표시등 + 비콘(HM-10 Module 기준 8000원)로 측정할 수 있다. 이때 공공재 안내 표지판 및 표지판을 설치하는 비용은 지방 정부나 정부의 보조를 생각하여 제외하고 예상 비용을 측정했다. 동시에 많은 기기를 만들어 내면 더욱 저렴한 금액으로 시각장애인에게 쉽게 도움이 될 수 있다.
인천시 버스 정류장의 개수는 5908개(출처: https://www.data.go.kr/, 인천광역시 버스노선별 정류장 현황, 통계 기준일 2020년 1월)이 기기 한개 생산 단가를 약 10만원으로 가정할 때 5억 908만원으로 한 해의 버스재정지원액의 0.0023%를 넘지 않을 것으로 예측할 수 있었다. 이를 확인하더라도 충분한 발전가능성과 효율을 가지고 있다고 할 수 있다. (출처:https://www.incheon.go.kr/open/OPEN030101)
6. 시연영상
7. 개선방안
시각장애인을 위한 음성안내 표지판에는 몇 가지 개선사항이 존재한다.
1) 라즈베리파이3를 이용하여 표지판을 만들기 때문에 전력의 사용이 원할해야 한다. 마이크로 5핀을 통해 전력을 충분히 끌어 낼 수 있어야 하기 때문에 기존 버스정류장 내의 전력을 이용할 수 있다면 상용화하는데 더 수월할 것이라고 생각한다.
2) LED 제작에 있어서 비용적인 측면에 제한이 있었다. 하지만 비용적인 제한이 없다면 즉, 상용화시킨 표지판에서는 표시등을 더욱 크게하여 시각적으로 잘 보이게 할 수 있을 것이다. 이 방법을 통해서 더욱 완성도가 높은 제품을 만들 수 있을 것으로 기대된다.
3) iBeacon의 인식거리, 음성인식의 정확도, 버스api의 정확도 등 제품의 전체적인 정확도를 향상시킨다면 보다 정확한 결과를 기대할 수 있다.
8. 소스코드
8.1. 메인 코드
import gspeech
import threading
import time
import re
import sys
import requests
import argparse
from luma.led_matrix.device import max7219
from luma.core.interface.serial import spi, noop
from luma.core.render import canvas
from luma.core.legacy import text
from luma.core.legacy.font import proportional, LCD_FONT
import urllib.request
from urllib.request import urlopen
from urllib.parse import urlencode
from urllib.parse import quote_plus
from urllib.parse import unquote
from bs4 import BeautifulSoup
import bs4
from Hi import sound_play
from blesca import find
from google.cloud import texttospeech
#전역변수 선언
busrouteID = []
buslistinform = []
buslist = []
bstopnm = []
target_buses = []
led_tag=0
def demo(w, h, block_orientation, rotate, bus1, bus2):
serial = spi(port=0, device=0, gpio=noop())
print(“Created device”)
device = max7219(serial, width=w, height=h, rotate=rotate, block_orientation=block_orientation)
with canvas(device) as draw:
text(draw, (1, 0), bus1, fill=”white”, font=proportional(LCD_FONT))
text(draw, (1, 8), bus2, fill=”white”, font=proportional(LCD_FONT))
def led_control() :
global led_tag
led_tag = 1
# 정류소 경유노선 목록조회
# 공공데이터포털(data.go.kr)으로 url요청을 해 데이터를 받는다
# 받아오는 정보는 정류장 이름, 정류장에 정차하는 버스의 노선 명칭, 노선 고유 번호
# 서버에 데이터를 받아오지 않고 받아온 html에서 크롤링 하는 방식
# 리턴값은 정차 버스 리스트, 정류장 이름, (2차원 리스트)버스 노선 명칭과 고유번호 이다
# 버스 정류장 고유 정보이므로 최초 한번만 받아옴
def get_busstopinfor(buslistinform, bstopId):
decode_key = unquote(”) #개인 고유 발급 키
url = ‘http://apis.data.go.kr/6280000/busStationService/getBusStationViaRouteList’
queryParams = ‘?’ + urlencode({quote_plus(‘ServiceKey’): decode_key,
quote_plus(‘pageNo’): ’1′,
quote_plus(‘numOfRows’): ’10′,
quote_plus(‘bstopId’): bstopId})
request = urllib.request.Request(url + queryParams)
request.get_method = lambda: ‘GET’
response_body = urlopen(request)
a = response_body.read()
soup = BeautifulSoup(a, ‘html.parser’)
num = soup.find(‘numofrows’)
num = int(num.get_text())
table = soup.find(‘serviceresult’)
for tr in table.find_all(‘msgbody’):
tds = list(tr.find_all(‘itemlist’))
blank = 0
for itemlist in tds:
routeno = itemlist.find(‘routeno’).text
routeid = itemlist.find(‘routeid’).text
getbstopnm = itemlist.find(‘bstopnm’).text
buslistinform.append([routeno, routeid, blank])
buslist.append(routeno)
bstopnm.append(getbstopnm)
return buslist, bstopnm, buslistinform
# 버스 도착정보 목록조회
# 받아오는 정보는 버스 노선 고유번호, 버스 도착시간
# 이 데이터셋에서는 버스 고유번호를 제공하지 않는다. 따라서 버스 노선 고유번호를 받아와 앞서 정류소 경유노선 목록조회에서 받아온
# 2차원 리스트 버스 고유번호 / 버스 노선 고유번호에서 버스 노선 고유번호를 비교해 같은 리스트에 도착시간을 추가한다
# 따라서 2차원 리스트 buslistinform의 요소는 [(버스 고유번호, 버스 노선 고유번호, 버스 도착시간)]
def get_businfor(bustopid):
decode_key = unquote(”) #개인 고유 발급 키
url = ‘http://apis.data.go.kr/6280000/busArrivalService/getAllRouteBusArrivalList’
queryParams = ‘?’ + urlencode({quote_plus(‘ServiceKey’): decode_key,
quote_plus(‘pageNo’): ’1′,
quote_plus(‘numOfRows’): ’10′,
quote_plus(‘bstopId’): bustopid})
request = urllib.request.Request(url + queryParams)
request.get_method = lambda: ‘GET’
response_body = urlopen(request)
a = response_body.read()
soup = BeautifulSoup(a, ‘html.parser’)
table = soup.find(‘serviceresult’)
for tr in table.find_all(‘msgbody’):
tds = list(tr.find_all(‘itemlist’))
for itemlist in tds:
routeid = itemlist.find(‘routeid’).text
arrivalestimatetime = itemlist.find(‘arrivalestimatetime’).text
for i in range(0, len(buslistinform)):
if (buslistinform[i][1] == routeid):
buslistinform[i][2] = arrivalestimatetime
return buslistinform
# 버스 도착시간을 리턴하는 함수. parameter는 버스 번호이며 buslistinform에서 버스 번호를 조회해 버스번호를 리턴함.
def get_bus_arrivetime(busnum):
for i in range(0, len(buslistinform)):
if buslistinform[i][0] == busnum:
return buslistinform[i][2]
def main():
gsp = gspeech.Gspeech()
gsp.pauseMic()
global target_buses
global led_tag
global buslistinform
while True:
find()
gsp.resumeMic()
compare_numbers = [] choosen_bus = []
# Google TTS를 이용하여 정류장 이름+음성안내를 합성해 송출한다
client = texttospeech.TextToSpeechClient()
bus_stop_notice = ‘정류장입니다. 확인할 버스 번호를 말씀해주세요’
bus_stop_name = bstopnm[0]
synthesis_input = texttospeech.types.SynthesisInput(text=bus_stop_name + bus_stop_notice)
voice = texttospeech.types.VoiceSelectionParams(
language_code=’ko-KR’,
ssml_gender=texttospeech.enums.SsmlVoiceGender.NEUTRAL)
audio_config = texttospeech.types.AudioConfig(
audio_encoding=texttospeech.enums.AudioEncoding.MP3)
response = client.synthesize_speech(synthesis_input, voice, audio_config)
bus_notice = ‘bus.mp3′
with open(bus_notice, ‘wb’) as out:
out.write(response.audio_content)
gsp.pauseMic()
sound_play(bus_notice)
gsp.resumeMic()
# 음성인식 될 때까지 대기. 받아온 음성은 stt에 저장된다
stt = gsp.getText()
if stt is None:
break
# 받아온 음성에서 버스번호만 찾아온다. ex) 511번 알려줘 ==> get_numbers = ['511']
get_numbers = re.findall(“\d+”, stt)
# 구글 STT로 인식된 번호가 정류장 정차 버스 리스트에 존재하는지 확인. 확인후 존재한다면 compare_numbers에 입력
compare_numbers = list(set(buslist).intersection(get_numbers))
# 리스트에 존재한다면 구글 TTS로 음성변환
all_check_num = “번 버스”.join(compare_numbers)
# 일치하는 번호가 없는경우 “해당 버스 번호는 없습니다” 음성 인식 출력
if len(compare_numbers) == 0:
mismatch_notice = “/home/pi/mismatch.mp3″
gsp.pauseMic()
sound_play(mismatch_notice)
gsp.resumeMic()
# 물어본 버스가 하나 이상 존재하는 경우 버스번호와 도착시간 음성 인식 변환 후 출력
else:
for i in range(0, len(compare_numbers)):
target_bus = compare_numbers[i] # 타겟버스 번
target_bus_arrivalestimate = get_bus_arrivetime(target_bus)
minute=int(int(target_bus_arrivalestimate) / 60)
second=int(target_bus_arrivalestimate)-(60*minute) #
synthesis_input_match = texttospeech.types.SynthesisInput(text=target_bus
+ “번 버스 약 ”
+ str(minute)
+ “분”
+ str(second)
+ “초 후 도착합니다” )
response_match = client.synthesize_speech(synthesis_input_match, voice, audio_config)
match_notice = ‘match.mp3′
with open(match_notice, ‘wb’) as out:
out.write(response_match.audio_content)
gsp.pauseMic()
sound_play(match_notice)
gsp.resumeMic()
time.sleep(1)
# 물어본 버스 번호가 1개일 경우
if (len(compare_numbers) == 1):
synthesis_input_match = texttospeech.types.SynthesisInput(text=compare_numbers[0]
+ “번 버스 도착 시 bus_choice = “몇 번 버스로 안내해드릴까요??”
synthesis_input_match = texttospeech.types.SynthesisInput(text=bus_choice)
response_match = client.synthesize_speech(synthesis_input_match, voice, audio_config)
choice_notice = ‘choice.mp3′
with open(choice_notice, ‘wb’) as out:
out.write(response_match.audio_content)
sound_play(choice_notice)
stt = gsp.getText()
get_choice = re.findall(“\d+”, stt)
choosen_bus = list(set(compare_numbers).intersection(get_choice))
while (len(choosen_bus) == 0) :
time.sleep(0.5)
stt = gsp.getText()
get_choice = re.findall(“\d+”, stt)
choosen_bus = list(set(compare_numbers).intersection(get_choice))
if len(choosen_bus) > 0 :
break
# 선택한 버스 안내 음성
for i in range(0, len(choosen_bus)):
synthesis_input_match = texttospeech.types.SynthesisInput(text=choosen_bus[i]
+ “번 버스 도착 시 안내해드리겠습니다.”)
response_match = client.synthesize_speech(synthesis_input_match, voice, audio_config)
final_notice = ‘final.mp3′
with open(final_notice, ‘wb’) as out:
out.write(response_match.audio_content)안내해드리겠습니다.”)
response_match = client.synthesize_speech(synthesis_input_match, voice, audio_config)
one_notice = ‘one.mp3′
with open(one_notice, ‘wb’) as out:
out.write(response_match.audio_content)
gsp.pauseMic()
sound_play(one_notice)
gsp.resumeMic()
target_buses.append(compare_numbers[0]) # 타겟 버스 리스트에 추가.
led_control()
print(“led on”)
else: # 물어본 버스가 2개 이상인 경우 선택 음성 안내
gsp.pauseMic()
sound_play(final_notice)
gsp.resumeMic()
target_buses.append(choosen_bus[i]) # 타겟 버스 리스트에 추가
led_control()
print(“led on”)
# 타겟 버스(탑승하려는 버스) 도착시간 확인
while (len(target_buses) > 0 ):
gsp.pauseMic()
for i in range(0, len(target_buses)):
flag = check_bus_arrive(target_buses[i])
# 버스 잔여 시간이 100 초 이하인지 판단
time.sleep(1)
if flag == True: # 100초 미만일경우 버스 도착안내메세지
synthesis_input_match = texttospeech.types.SynthesisInput(text=target_buses[i]
+ “번 버스 잠시 후 도착합니다”)
response_match = client.synthesize_speech(synthesis_input_match, voice, audio_config)
arrive_notice = ‘arrive.mp3′
with open(arrive_notice, ‘wb’) as out:
out.write(response_match.audio_content)
gsp.pauseMic()
sound_play(arrive_notice)
led_control()
time.sleep(7)
target_buses.remove(target_buses[i])
led_control()
break
if (len(target_buses) == 0):
break
continue
# 버스 url송출해 실시간으로 도착정보 가져오는 함수
def get_realtime_businform():
while True:
global buslistinform
get_businfor(163000167) time.sleep(10)
# parameter 값에 대해 도착시간이 90초 미만인지 확인해주는 함수
def check_bus_arrive(target_bus):
target_bus_arrivalestimate = int(get_bus_arrivetime(target_bus))
if (target_bus_arrivalestimate < 90): # 90초 미만이면 True, else False
return True
else:
return False
# led 제어하는 함수
def led_controller():
global target_buses
global led_tag
while True:
parser = argparse.ArgumentParser(description=’matrix_demo arguments’,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(‘–width’, type=int, default=32, help=’Width’)
parser.add_argument(‘–height’, type=int, default=16, help=’height’)
parser.add_argument(‘–block-orientation’, type=int, default=-90, choices=[0, 90, -90],
help=’Corrects block orientation when wired vertically’)
parser.add_argument(‘–rotate’, type=int, default=0, choices=[0, 1, 2, 3], help=’Rotation factor’)
args = parser.parse_args()
# led tag= 1 일때마다 led 표시 값 갱신
if (led_tag==1) :
if len(target_buses) == 1 :
demo(args.width, args.height, args.block_orientation, args.rotate,target_buses[0], “”)
elif len(target_buses) > 1 :
demo(args.width, args.height, args.block_orientation, args.rotate,target_buses[0], target_buses[1])
else :
demo(args.width, args.height, args.block_orientation, args.rotate, “”, “”)
led_tag=0
if __name__ == “__main__” :
get_busstopinfor(buslistinform, 163000167)
t = threading.Thread(target=main)
t2 = threading.Thread(target=get_realtime_businform)
t3 = threading.Thread(target=led_controller)
t.start()
t2.start()
t3.start()
8.2. find() : 비콘 검색함수
from bluepy.btle import Scanner, DefaultDelegate class ScanDelegate(DefaultDelegate): while (beacon ==1): if __name__ == ‘__main__’:
def __init__(self):
DefaultDelegate.__init__(self)
def handleDiscovery(self, dev, isNewDev, isNewData):
if isNewDev:
print (“Discovered device”, dev.addr)
elif isNewData:
print (“Received new data from”, dev.addr)
def find():
beacon = 1
scanner = Scanner()
devices = scanner.scan(3.0)
for dev in devices:
print (“Device %s (%s), RSSI=%d dB” % (dev.addr, dev.addrType, dev.rssi))
for (adtype, desc, value) in dev.getScanData():
print (“%s = %s” % (desc, value))
if(value == ‘Helper’and dev.rssi > -52):
beacon = 0
break
else:
beacon = 1
if beacon ==0:
break
find()
8.3. sound_play() : 음성안내 실행 함수
import pygame
import time
def sound_play(name):
file = name
pygame.mixer.init()
pygame.mixer.music.load(file)
pygame.mixer.music.play()
clock = pygame.time.Clock()
while pygame.mixer.music.get_busy():
clock.tick(3)
pygame.mixer.quit()
· TTS : https://cloud.google.com/text-to-speech/docs/samples?hl=ko
· STT : https://cloud.google.com/speech-to-text/docs/samples?hl=ko
9. 참고문헌
[1] 김원호, 이유화, 김시현, 이유화, 김시현. “시각장애인 대중교통 이용실태 분석 및 대중교통시설 내 보행지원 시스템 구축방안.” 서울도시연구 10 no.3 (2009): 97-114.
[2] MBCNEWS. (2019, 12 25). [소수의견] 버스 탄 시각장애인 봤나요…타고 내리는 게 전쟁 [비디오파일]. 검색경로 https://www.youtube.com/watch?v=saoX-lR-iJ0&feature=emb_title
[3] “시각장애인도 버스를 탈 수 있게 해주세요.” (2020년 3월 1일). 청와대. n.d. 수정, https://www1.president.go.kr/petitions/583770.
[4] “인간공학적 시선으로 본 장애인의 이동권.” LG챌린저스. n.d. 수정, 2020년 3월 20일 접속, http://www.lgchallengers.com/wp-content/uploads/global/global_pdf/S0410.
[61호]Can You See Me?(랜덤 배열을 활용한 투명 도어락)
2020 ICT 융합 프로젝트 공모전 대상
Can You See Me?(랜덤 배열을 활용한 투명 도어락)
글 | 광운대학교 나영은 선아라 신동빈 조수현 최희우
심사평
칩센 훌륭한 작품을 개발하였습니다. 작품의 의도, 개발 과정, 보고서 모두 대단하다는 말밖에 할 수 없을 듯 합니다. 여러 가지 기능을 구현하고, 비밀번호 노출에 대한 강화를 위해 블루투스를 이용하여 스마트폰을 거치는 과정이 있었을 듯합니다. 다만 스마트폰을 반드시 이용해야하는 것이 제약이 될수 있으므로 도어락 패드를 display 터치패드 형태로 구성하여 랜덤하게 위치 이동이 이루어지면 지원자(팀)가 원하는 소기의 목적은 달성 가능하지 않을까 합니다.
펌테크 실생활에 사용이 될 수 있는 실용성, 아이디어, 창의성이 돋보이는 작품으로 출품된 작품은 충분히 실생활에 바로 적용이 가능할 수 있는 상업성이 뛰어난 작품이라고 생각됩니다. 전체적으로 기획의도에 맞게 시스템을 꼭 필요한 기능으로 목적에 맞는 최적의 시스템으로 잘 구성하였고 기술 구현도, 완성도 등에서도 상당히 뛰어나고 훌륭한 작품으로 생각됩니다.
위드로봇 재미있는 아이디어입니다만, 실용성 측면에서 좀 더 고민이 필요합니다.
1. 작품 개요
기존의 도어락이나 비밀번호 인증 방식은 키패드가 보이는 출력부와 비밀번호를 입력하는 입력부가 일체형으로 되어있다. 이러한 특징 때문에 지문의 흔적 등으로 인해 제삼자에게 비밀번호가 해킹을 당할 위험이 다분하다. 이러한 점에서 본 제품은 입력부를 투명한 키패드로, 출력부를 안드로이드로 나누어 배치하여 그런 위험을 사전에 방지하는 것에 의의가 있다.
출력부에서 키패드의 위치 출력 시 시도할 때마다 그 배치를 랜덤으로 배치하여 보안성을 기하급수적으로 높인다.
배터리 시장이 커지고 있는 사회 흐름에 맞춰 본 제품도 배터리 효율을 극대화할 수 있는 방법을 고려했다. 제품의 특성상 사용자가 사용하는 시간보다 사용하지 않는 대기 모드 시간이 더 길기 때문에, 대기 시간에 저전력 모드를 활용해 사용하는 전력을 최소화하였다.
본 제품의 경우, 매번 키패드에 매핑된 숫자와 전송버튼의 위치가 달라지기 때문에 무수한 경우의 수가 도출된다. 따라서 본 제품의 비밀번호가 해킹될 확률은 프로그램 시뮬레이션을 통하여 도출된 통계적인 확률로 제시를 한다.
기본적인 통신방식은 보안성이 높은 블루투스 4.0을 기반으로 사용하며 본 제품의 MCU는 Atmega128을 활용한다.
2. 작품 설명
2.1. 주요 동작 및 특징
2.1.1. 투명 터치 키 패드
비밀번호 입력키 패드는 투명 터치 필름을 이용하였다. 투명 터치 필름을 이용하게 되면 각각의 키 패드 각각의 숫자가 노출되지 않아 Door lock에 접근하는 사람은 어디 위치에 어떤 숫자나 기호가 있는지 알 수 없다. 각 자리 값에 해당하는 숫자는 오직 안드로이드 어플에서 확인할 수 있으며 매번 갱신된다. 그렇기 때문에 보안성 측면에서 더욱 우수하다.
2.1.2. 안드로이드 앱을 통한 키 배열 위치 확인 및 비밀번호 검사
키 배열 위치(이하, 고유 자리값이라고 하겠다.)는 터치 키패드의 12-KEY 각각의 자리를 의미한다. 안드로이드 앱에서는 키패드의 위치가 매번 랜덤으로 바뀐다.
도어락과 블루투스 연결 시도 후, 연결이 완료되면 도어락과 데이터를 주고받으며 본격적인 기능이 활성화된다.
사용자는 위 그림1-2와 같이 안드로이드 앱에서 숫자와 기호의 위치를 확인한 뒤, 이에 맞는 터치 센서의 고유 자리 값을 눌러 비밀번호를 입력한 뒤, Send 위치를 누르게 되면 비밀번호 고유 자리 값 버퍼가 안드로이드 앱으로 전송이 된다.
비밀번호 고유 자리 값 버퍼와 기존에 설정된 비밀번호를 안드로이드 앱에서 비교한 뒤, 다시 도어락으로 성공 혹은 실패 여부를 전송해 준다.
전송 버튼(Send) 위치 또한 매번 갱신되기 때문에 외부인의 경우 비밀번호를 뚫는 것이 매우 힘들다. 비밀 번호 변경은 안드로이드 앱에서만 가능하다.
2.1.3. 액추에이터
모터
비밀번호가 일치했다는 신호를 블루투스 모듈로부터 받은 경우, 모터를 활성화시켜 문 잠금을 해제한다. 이후 리드 스위치 인식에 따라 문이 열렸다가 닫힘이 인식되면 다시 문을 잠그고, 모터를 비활성화 시킨다. 모터의 비활성화는 서보모터에 공급되는 PWM 신호를 끊어주는 행위이다. 이는 실내에서 문을 직접 열 수 있도록 하기 위함 이기도 하며, 예기치 못하게 모터의 혼(Horn)이 걸려서 Stall Current가 흐르는 상황을 방지하기 위함도 있다.
비밀번호가 일치하지 않으면 모터는 초기 상태를 유지한다.
부저
현재 도어락에서 발생하는 모든 상황(이하, Status)을 이용자가 손쉽게 파악하고자 하는 목적으로 각 음계의 주파수를 모두 계산하여 이 제품에서 사용되는 효과음들을 모두 직접 제작했다. 여러가지 기기의 상태에 따른 부저 음을 구현하였는데, 그 내용은 아래와 같다.
(구현에 있어, 자세한 과정은 제작 과정에 명시하였다.)
RGB LED
현재의 Status를 확인하는 데 있어 부저 다음으로 중요한 요소이다. 도어락에서 소리 나는 것이 불쾌한 이용자가 있다면, 부저를 음소거모드(MUTE)로 전환하고 현재의 Status를 부저 말고도 다른 요소로 파악할 수 있어야 하기 때문이다.
부저와 마찬가지로 여러 상황에 따른 효과를 만들었는데, 아래와 같다.
(구현에 있어, 자세한 과정은 제작 과정에 명시하였다.)
2.1.4. 저전력
현대 사회에 가장 이슈화되는 주제로 배터리를 들 수 있다. 이런 시대의 흐름에 맞추어 본 제품에서도 배터리의 성능을 극대화하기 위하여 저전력 설계를 기반으로 제품을 만들었다. 저전력 설계에서 빠질 수 없는 중요한 기능이 슬립 모드이다. 거의 모든 MCU에선 슬립모드 기능을 제공하며, 클럭을 차단하거나 일부 Peripheral을 제한하는 것을 통해 전력을 절약하는 모드를 말한다.
도어락에서 30초 간 별 다른 동작(터치, 어플 자리 값 갱신 등)을 하지 않을 시, 슬립모드(POWER-DOWN)로 진입하도록 구현하였으며, 또한 슬립모드로 진입할 때, 거의 모든 주변회로들이 OFF되도록 하였다.
하지만 슬립모드에서도 여전히 저전력 제품이라고 할 수 없을 정도의 전류(~20mA)가 흐르고 있으며, 자세한 내용은 아래 고찰에서 다루었다.
2.1.5. 키패드 입력
무음모드 설정
Mute 위치를 더블 클릭(연속으로 두 번 TOUCH)하게 되면 무음 진입 효과음과 함께 무음모드로 진입한다. 같은 과정을 다시 진행할 경우 벨소리 진입 효과음과 함께 벨소리 모드로 진입한다.
비밀번호 성공 시
키패드에서 비밀번호를 입력하고 안드로이드 앱 상의 Send 위치를 누르게 되면 설정한 비밀번호와 비교하게 된다. 그리고 자릿값과 비밀번호가 일치할 경우, 부저와 RGB가 SUCCESS로 반응하고 서보모터가 돌아가면서 잠금이 해제된다.
이때, 문을 열게 되면 리드스위치가 문이 열렸음을 감지하며, 이후 다시 닫을 경우 또한 문이 닫힘을 감지하여 다시 RGB, 부저가 닫힘 반응을 한 뒤, 일정 시간 후 서보모터가 돌아 잠금이 활성화 된다.
비밀번호 실패 시
키패드에 입력한 비밀번호가 잘못된 비밀번호 일시 성공했을 때와는 다르게 나타난다. 우선 모터가 돌아가지 않고 RGB와 BUZZ 가 FAIL에 반응하고 서보모터가 돌아가지 않아 문이 열리지 않는다.
키패드의 입력한 비밀번호가 틀릴 때에는 실패 신호와 동시에 앱에서는 셔플이 되며, 도어락에서는 실패 Stack을 쌓는다. 그리고 이 비밀번호 실패 Stack이 5회가 되면, SIREN 소리와 RGB의 빨간불이 나타나면서 반응하게 된다. 그리고 도어락을 제한 한 후, 안드로이드로 제한 신호를 전송하게 되어 일정시간이 지난 후 슬립모드로 들어가게 된다.
2.2. 전체 시스템 구성
위 그림2-1와 같이 전체적인 시스템을 플로우 차트를 통해 간략히 표현하였다. 적색으로 표현된 텍스트는 ‘주요 동작 및 특징’ Part에서 액추에이터 부분(RGB, BUZZER)에 대한 표현이다.
그림 2-2에선 현재 핀에서의 입출력 기능들을 설명한다. 다음과 같다.
· PA0~2의 경우 RGB LED와 연결되어 PWM 출력 핀으로 사용하였다.
· PA5의 경우 Reed Switch 입력 핀으로 사용한다.
· PA7의 경우 릴레이 스위치 제어 용도로 사용한다.
· PB5의 경우 서보모터 PWM 출력 핀으로 사용한다.
· PC0의 경우 디버그 포트로 사용한다.
· PD0의 경우 터치 키패드 입력 이벤트에 대한 인터럽트 입력 핀으로 사용한다.
· PD2의 경우 UART1의 Rx핀, PD3의 경우 UART1의 Tx핀으로 사용함.
· PE0의 경우 UART0의 Rx핀, PE1의 경우 UART0의 Tx핀으로 사용한다.
· PE3의 경우 부저의 CTC 출력 핀으로 사용한다.
2.3. 개발 환경
- 개발 언어: JAVA, C
- 개발 Tool: Android Studio, Atmel Studio
- 디버깅 Tool: Beyond Compare(코드 병합 및 디버깅), Saleae Logic(uart 데이터 포맷 분석), TeraTerm(통신 디버깅 및 장치 설정), 안드로이드 디바이스
도어락 장치부의 펌웨어를 작성하기 위한 개발환경으로 Atmel Studio를 사용하였다.
안드로이드 기반 어플리케이션을 제작하고자 Android Studio에서 개발작업을 진행하였다.
원활한 개발을 위해, 디버깅 관련 Tool들을 적극적으로 이용했다. 코드 보수 및 병합하는 과정에서 에러가 발생할 경우 Beyound Compare을 통해 이전 코드와 비교하여 에러 혹은 버그를 빠르게 파악할 수 있었다. Saleae Logic 툴을 통해 시리얼 통신 데이터 및 PWM 신호 등을 관측하였다.
3. 단계별 제작 과정
3.1. 서보모터 구동을 위한 50Hz PWM 신호 만들기 (Timer1)
서보모터를 제어하는 데 있어, 20ms주기를 갖는 PWM 신호를 만들어 주어야 한다. 보통 16-bit 분해능을 갖는 타이머 카운터를 이용하여 서보모터를 제어하는 것이 일반적이다. 8-bit 타이머 카운터를 이용하게 되면 분해능이 낮아 정확한 각도 제어가 16-bit 타이머 카운터에 비해 쉽지 않다.
그렇기 때문에 16-bit 분해능을 갖는 Timer/Counter1을 이용하였다.
추후에 고찰에서 설명하겠지만, 초기에는 시스템 클럭 16MHz 기준으로 코드를 작성했지만, 이후 8MHz 크리스탈로 변경하여 Timer 관련 레지스터 설정들을 모두 변경하였다. 설정은 아래와 같이 하였다.
TCCR1A & TCCR1B Register
COM: 10 ▶ default HIGH, compare match LOW
WGM: 1110 ▶ FAST PWM, TOP: ICR1
CS: 010 ▶ 8 Pre-scaling.
ICR1 Register
ICR1=19999 ▶ ICR1H = (19999>>8), ICR1L=19999&0xff
50Hz짜리 PWM 신호를 만들기 위한 레지스터 세팅은 위와 같으며, OCR1 레지스터(OCR1H, OCR1L)를 통해 TCNT레지스터와 비교매치를 발생시켜 PWM 신호를 생성하면 된다.
코드에서 사용하기 쉽도록 함수화 하였는데, 아래와 같다.
TimerCounter1에서 제공하는 PWM 출력 채널은 A, B, C 3개 를 제공하는데, A채널(PB5)을 이용하였다. OCR값을 바꿔 줌에 따라 Duty Cycle이 바뀌게 된다. 결과는 아래 그림 4와 같다.
원하는 Angle을 손 쉽게 사용하기 위해 함수화 하였는데, 아래 그림5와 같다.
0도일 경우 2.5%의 Duty를 갖는 신호를, 180도일 경우 12.5%의 Duty를 갖는 PWM신호를 공급하도록 했다. 서보모터 제조사마다 Duty에 따른 각도가 다를 수 있으며, 아래 자료를 참고하여 만들었다.
convertServoAngle(angle)이라는 함수를 사용하면 converted된 값을 return해준다. 이 값을 OCR에 넣어주면 작동하도록 구현하였다.
3.2. RGB LED 각각 밝기 제어를 하기 위한 PWM 출력 채널 구현(Timer0, GPIOA)
RGB LED는 3개의 LED가 내장되어 있어 PIN OUT을 확인해보면, GND는 공통이며, R, G, B 각각 입력 핀을 갖고 있다. 그렇기 때문에 RGB LED를 통해 색을 표현하고 싶다면 PWM 신호를 통해 구현이 가능하다. 다만, 현재 사용하는 타이머 개수 제한으로 인해 무작정 사용할 수 없다. Timer1의 경우, SERVO 모터용으로 사용하고 있으며, TIMER3의 경우 BUZZER를 사용할 것이므로 CTC모드를 사용해야 한다. 그리고 나머지 8-bit 타이머의 경우 PWM 출력 채널이 1 개 밖에 없으므로, PWM 출력 핀을 사용하는 것은 좋은 생각이 아니다. 그리하여, Tiemr0에서 발생하는 인터럽트 루틴에서 GPIO로 PWM 신호를 만들어 주는 방법을 고안했다. 설정은 아래와 같이 하였다.
TCCR0
COM: 00 ▶ OC0 Disconnected.
WGM: 10 ▶ CTC Mode.
CS: 001 ▶ No Pre-scaling.
TIMSK
OCIE0: 1 ▶ Output compare Interrupt Enable.
OCR0 = 250
마찬가지로, 16MHz 크리스탈을 8MHz로 변경했으므로, 이와 맞는 코드로 수정된 상태이다.
분주를 하지 않으므로 8MHz를 Timer Clock으로 사용하며, CTC 모드에서 비교매치 발생 시, Clear됨과 동시에 비교매치 인터럽트를 허용해주었으므로, TCNT와 OCR이 같아지는 지점에서 비교매치 인터럽트가 발생한다. 계산해보면 ISR로 진입하는 데 32kHz마다 진입을 하게 된다. (기존 16MHz 크리스탈로 설정했을 땐 16kHz 속도로 ISR에 진입했었다.) 16kHz를 설정하여 코드를 작성할 예정이므로 ISR을 2번 진입했을 때 1번 동작하도록 Flag 처리를 따로 해주었다.
구현 코드는 아래와 같다.
ISR(TIMER0_COMP_vect) //32khz 속도로 들어옴.
{
static unsigned char ticks=0;
static int twice=0;
twice++;
if(twice%=2) return;
//16khz 속도로 들어옴.
if(ticks!=255) {
if(ticks == led_R) PORTA &= ~(0×01);
if(ticks == led_G) PORTA &= ~(0×02);
if(ticks == led_B) PORTA &= ~(0×04);
}
else {
led_B=led_B_buf;
led_G=led_G_buf;
led_R=led_R_buf;
PORTA|=( ((led_B!=0?1:0)<<2) | ((led_G!=0?1:0)<<1) | ((led_R!=0?1:0)<<0) );
}
. . . 생략 . . .
ticks++; //255 이후 Clear.
}
setRGB(r,g,b) 함수를 사용하여 PA0,1,2 핀의 PWM 출력 상태를 확인해봤을 때, 아래 그림8과 같다.
PWM 주기는 16.13ms이며, 이는 ISR상에서 ticks 변수가 255까지 카운팅되고 Clear되는 시간이다. (16kHz ▶ 62.5us*256 ≒ 16ms)
setRGB(r,g,b)에 들어가는 파라미터 값은 0~255를 넣어주면 되며, 0을 넣어주면 PWM신호가 출력되지 않도록 코딩해두었다.
3.3. 부저 구동을 위해 주파수 가변 함수 구축(Timer3)
사람의 인체에서 귀와 눈을 비교해볼 때 귀가 더욱 민감하다. 그렇기 때문에 RGB LED 대비 BUZZER의 출력 주파수에 대한 분해능은 매우 정밀할 필요가 있다. 그렇기 때문에 16-bit 카운터를 이용하여 구현하였다.
TIMER3을 이용하였으며 아래와 같이 설정하였다.
TCCR1A & TCCR1B Register
COM: 01 ▶ Toggle (in CTC)
WGM: 1100 ▶ CTC, TOP: ICR3
CS: 010 ▶ 8 Pre-scaling.
설정해둔 ICR값에 TCNT가 도달하게 되면 즉시 Clear가 되며, PWM 출력핀은 Toggle하게 된다.
즉, 원하는 주파수에 대한 ICR값을 찾아주게 되면 원하는 부저음을 낼 수 있게 된다.
setICR3(num)함수에 1706을 넣어주게 되면 853값이 ICR3H,L 레지스터로 나뉘어 들어가게 되며 출력은 결국 대략 1,172Hz로 토글하게 되며, 부저 출력은 586Hz 출력이 나가게 된다. 해당 출력은 아래 그림과 같다.
여러 소리들을 직접 찾아내어 아래와 같이 모두 Define하였다.
3.4. Tick Timer 구축(Timer0)
임베디드 시스템에서 Tick Timer는 필수적이다. 해당 프로젝트에선 RGB LED 구현, 여러 액추에이터 동시 제어, 센서 데이터 갱신 속도, 더블클릭 등을 구현하기 위해 아주 중요한 역할을 한다. 위 3.2에서 Timer0에 대한 레지스터 세팅에 의해 32kHz의 속도로 ISR에 진입한다. twice라는 flag를 만들어 16kHz마다 Tick 관련 코드에 진입할 수 있도록 구성하였다. 아래 코드에서 Tick 변수들은 각각 액추에이터나 기능들을 제어하는 데 있어 필수적으로 사용되는 타이머들이다.
3.5. BUZZER 동작과정
BUZZER는 다음과 같이 구성되었다.
BUZZER가 실험됨과 동시에 TICK.buzz_1ms가 카운터 되면서 setSoundClip()함수에 있는 switch-case문이 실행된다.
도어락이 특정 동작을 할 때 원하는 부저음을 case별로 만든 후, 이를 실행시켜 소리가 나도록 구성한다.
TICK.buzz_1ms가 카운트됨과 동시에 if-else문을 사용하여 시간별로 짜 놓았던 (TICK.buzz_1ms==(지정된시간))이 되면 setSound()로 정의해 놓았던 소리가 나오도록 한다. 모든 소리가 재생이 끝나면 TICK.buzz_1ms=0으로 해주어 다음 부저 소리단계를 실행시킬 수 있도록 한다.
3.6. RGB LED동작과정
RGB는 다음 틀과 같이 구성되었다.
RGB가 실행됨과 동시에 위에서 만들어 주었던 TICK.rgb_1ms가 카운터 되면서 RGB_Drive();에 있는 switch-case문이 실행된다. 도어락이 특정 동작을 할 때, 원하는 LED를 case별로 만든 후, 이를 실행시켜 RGB가 빛이 나도록 구성한다.
TICK.rgb_1ms가 카운트 됨과 동시에 if-else문을 (TICK.rgb_1ms==(지정시간))이 되면 set(R,G,B)가 실행되어 색이 시간별로 바뀌거나 빛을 낸다. BUZZER와 마찬가지로 TICK.rgb_1ms=0으로 셋해주어 다음 RGB가 반응할 때, 다시 1부터 카운터 되도록 한다.
3.7. Motor 동작과정
MOTOR는 다음과 같은 구성으로 동작한다.
키패드를 이용해 비밀번호 버퍼를 채워 나가고 이 버퍼가 기존 비밀번호와 일치하게 되면, 부저와 RGB가 동시에 반응함과 동시에 MOTOR가 돌아가 사용자가 문을 열수 있게 된다. 그리고 리드스위치가 자석이랑 떨어짐으로써 HIGH로 인식하게 된다. 사용자가 문을 닫지 않는 이상 이는 변함이 없다.
사용하자 문을 닫게 되면, 리드스위치가 자석과 닿아 LOW로 신호가 바뀌어 MOTOR가 다시 돌아가 도어락이 닫히게 된다.
반대로, 입력한 버퍼가 기존 지정해 놓았던 비밀호와 다를 때에는 모터가 돌아가지 않아 사용자가 문을 열 수 없게 된다. 그리고 1초 뒤, PWM 신호를 끄게 되어 처음부터 비밀번호 버퍼를 입력할 수 있게 된다.
3.8. 슬립모드 구현
저전력으로 구동되는 모든 제품에 있어 슬립모드가 들어가는 건 대부분 필수 불가결하다. 도어락 역시 해당 기능이 포함되어야 하며, 현재 사용하고 있는 MCU(ATMEGA128)에서도 여러 가지 슬립모드를 제공한다. 그 중, 모든 Clock을 끊어 거의 모든 Peripheral 기능들을 차단(외부 인터럽트와 I2C Address match만 활성화되며 나머지는 모두 Block.)하여 가장 전력 소모가 낮은 POWER-DOWN 모드를 사용하였다.
기본 구성은 다음과 같다. 터치 입력 혹은 블루투스로부터 받는 신호. 즉 UART 수신 데이터가 없는 이상 sleep관련 Tick Timer를 계속 Count한다. Tick에 의해 30초가 지난 것을 판단하게 되면 자동으로 슬립모드로 빠지게 하였다. 만일 UART 수신 데이터 아무 것이든 감지가 되면 Tick을 Clear한다.
#include <avr/sleep.h>
. . . 생략 . . .
int main(void){
. . . 생략 . . .
while (1) {
. . . 생략 . . .
if(TICK.sleep_tim==29000) setSoundClip(BUZZ_SLEEPOUT); //슬립모드 빠질 준비
else if(TICK.sleep_tim>=30000){
gotoSleep();//슬립모드 진입
. . . 생략 . . .
}
}
. . . 생략 . . .
void gotoSleep(){
sleep_flag=1;
PORTA &= ~(0×80); //릴레이 OFF
PORTA &= ~(0×07); //RGB OFF
set_sleep_mode (SLEEP_MODE_PWR_DOWN); //only interrupt
sleep_enable ();
sei();
sleep_cpu();
. . . 생략 . . .
IsWakeup=1;
}
void sleep_stack_clear(){
TICK.sleep_tim=0;
}
sleep_cpu() 함수가 호출되면 그 즉시 슬립모드로 진입하며, 그 전까지 갖고 있던 메모리 데이터들은 모두 유지되어 있다. 외부 인터럽트 등에 의해 Wakeup하게 되면, 기존 데이터는 모두 유지하며, sleep_cpu()함수 이후 라인에서 마저 진행하게 된다.
결론적으로는 슬립모드에서 마저 20mA라는 큰 전류가 흐르고 있다. 이는, wakeup에 대한 외부 인터럽트 신호를 주기 위하여 터치 센서를 활성화(대기 전류 10mA)시켰으며, 3.3V 레귤레이터(소모전류 10mA)에서 소모하는 전류까지 포함하여 20mA 정도가 소비되었다.
3.9. UART 문자, 문자열 송수신 함수 구현
현재 UART1, UART0를 사용하는 데 있어 문자열을 주고받는 함수가 구현되어야 함은 필수적이다.
수신부, 송신부 모두 Interrupt 방식으로 데이터를 주고받도록 구현했다. (polling 방식의 경우 예기치 못한 비효율적인 상황들이 발생할 가능성이 크다고 판단했음.)
수신의 경우 수신완료 인터럽트가 발생되면 임시 버퍼에 우선 저장하는 방식으로 구현한 뒤, 필요할 때 가져다 쓰는 방식으로 구현했으며, 문자열이 들어오는 상황인 경우 배열 버퍼를 만들어 데이터를 계속해서 수신받고 문자의 끝 신호가 들어오게 되면, 기존에 만들어 둔 수신완료 Flag를 띄워 문자열이 수신되었음을 판단하도록 코드를 구성하였다.
송신의 경우 평소에는 송신완료 인터럽트를 꺼두었으며, 송신 명령을 내리면 그때 송신완료 인터럽트를 켜주는 방식으로 구현을 했다. 문자열의 경우 매번 이후 인덱스를 연속적으로 전송하도록 구현했으며, 문자열 마지막에 붙는 NULL문자로 문자의 끝을 확인한 뒤, 송신완료 인터럽트를 다시 끄도록 했다. 구현한 코드는 아래와 같다.
//수신부 문자열 수신 구현
ISR(USART0_RX_vect) // { <, X, Y, Z, > }
{//5Byte Fixed data.
static u8 cnt=0;
u8 _buff=UDR0; //fifo 방식에 대한 예방 차원으로 버퍼사용
switch(cnt){
case 0: if(_buff==’<’){ //문자열의 첫 데이터??
TOUCH.sensorData[cnt]=_buff;
cnt++; //다음 요소 수신 충족
} break;
case 4: if(_buff==’>’){ //문자열의 마지막 데이터??
TOUCH.sensorData[cnt]=_buff;
cnt=0; //인덱스 초기화
UCSR0B &= ~(0×80); //잠시 수신 인터럽트 끔
TOUCH.receive_cplt_sensor=1; // 문자열 수신완료 플래그 HIGH
}break;
default: //data 부분
TOUCH.sensorData[cnt]=_buff;
cnt++;
break;
}
}
위 코드의 경우 터치 키패드(UART0) 데이터 패킷를 문자열의 형태로 받는 루틴이다. 받는 데이터의 패킷이 고정되어 있으므로 위와 같이 구현된다. 따로 특정 함수를 사용할 필요 없이 데이터를 사용하고자 할 때 TOUCH.receive_cplt_sensor Flag만 잘 처리해주면 된다. 만일 데이터가 고정되어 있지 않다면, 문자의 시작부분과 문자의 끝부분을 미리 규약하여 데이터를 받으면 된다.
송신부에 대한 구현은 아래와 같이 진행했다.
//송신부 문자열 송신 구현
. . . 생략 . . .
ISR(USART1_TX_vect){
if(BT.tx1Buf[BT.tx1Cnt]==”) //문자열 마지막부분은 보내지 않는 것이 특징.
{
UCSR1B &= ~0×40; //Tx 송신완료 interrupt disabled.
memset((char*)BT.tx1Buf,0,sizeof(BT.tx1Buf));
}
else UDR1 = BT.tx1Buf[BT.tx1Cnt++]; // 0은 이미 보냈고 1이 보내지고 2로 증가, 2보내지고 3으로 증가되고 다음 루틴에서 인터럽트 비활성화
}
. . . 생략 . . .
void uart1_BT_tx_string(char * data){
int _len=strlen(data); //문자열 길이 반환
strncpy((char*)BT.tx1Buf,data,_len); //데이터를 잠시 버퍼에 저장
while(!(UCSR1A & (1<<UDRE1))); //UDR1레지스터가 비워질 때까지 대기
UDR1=BT.tx1Buf[0]; //버퍼의 첫 주소에 있는 문자 전송
BT.tx1Cnt=1; //ISR상에서 사용할 cnt 시작 값
BT.tx1CntMax=_len+1; //ISR상에서 사용할 cnt max 값 (문자열 뒤에 붙는 도 포함)
UCSR1B |= (1<<TXCIE1);//송신완료 인터럽트 활성화
}
아래 3.10 ~ 3.14가 UART 관련 제작 과정이며, 문자열 송, 수신부를 구현한 것으로 계속 사용한다.
3.10. 터치 키패드 센서 테스트
터치 키패드를 이용하는 데 있어 컨트롤러를 이용하면 수월하다. UART로 데이터를 주고받으면 손 쉽게 데이터를 얻어낼 수 있다.
UART0 채널에서 터치 센서 컨트롤러로 ‘R’을 전송하면 컨트롤러에서는 “<ABC>”와 같은 형태의 데이터를 UART0 채널로 송신한다. 아래 그림 17을 보면, ‘A’는 첫번째 열, ‘B’는 두번째 열, ‘C’는 세번째 열을 뜻한다. 수신받은 데이터 중 ‘<’는 데이터의 시작 byte, ‘A’, ’B’, ‘C’의 경우 터치된 데이터이다. ‘>’는 수신받은 데이터의 끝을 알려주는 byte이다.
위 수신 받은 데이터를 parsing해주는 과정은 다음과 같다. 가령, 위 그림에서 6이라고 적은 위치의 키패드가 눌렸다면 그 위치에 Mapping되는 비트는 1이 되며, 아니라면 0이 된다. 즉 ‘B’와 ‘C’는 아무것도 눌리지 않았기 때문에 Binary로 0000 데이터가 들어오며, ‘A’는 Binary로 0100가 들어오는 것으로 해석할 수 있다. 하지만 사용자 입장에서 가독성을 고려하여 데이터를 받을 땐 아스키의 형태로 수신된다. 즉, ‘4’(0×34), ‘0’(0×30), ‘0’(0×30)이라는 문자 형태의 데이터를 송신하여 결국 패킷은 ‘<’, ‘4’, ‘0’, ‘0’, ‘>’. 총 5 Byte가 String 형태로 들어오게 된다.
그림 17에서의 6이라고 적힌 자리를 누르게 되면 아래 그림18과 같은 데이터가 수신된다.
펌웨어에선 20ms마다 센서 데이터를 요청하도록 구현하였으며, 위 그림19와 같이 데이터가 갱신되어 들어오는 것을 확인할 수 있다. (터치 센서가 연결되는 컨트롤러의 메탈 노드를 터치한 사진이다.)
3.11. 터치 데이터 수신 및 파싱
위 3.10에서 진행한 데이터 패킷 수신이 정상적으로 이루어졌으므로, 이제 데이터 패킷을 Parsing하는 과정을 진행해야 한다. 키패드가 눌림에 따라 ‘<’, ‘>’를 제외한 3byte의 데이터를 12byte 배열 각각에 문자 ‘1’ 또는 ‘0’을 넣어주도록 하였다. 20ms마다 데이터를 갱신하며, 키패드가 눌리기 전에는 계속해서 000000000000가 string 형태의 문자열로 반환된다. 키패드가 눌리면 그 곳에 Mapping되는 위치에 해당하는 자리만 1이 되어 “010000000000”과 같은 문자열을 반환한다.
유의해야 하는 점은, “<ABC>” 패킷을 수신할 떄 A,B,C 각각은 Hexadecimal이 아닌 Ascii이다. 즉, 패킷의 인덱스를 그대로 따와서 코드를 작성하면 안되며 매번 비교하여 Hexadecimal로 변환한 값을 이용해야 한다.
이후 해당 값을 통해 원하는 위치를 클릭했을 때 배열에 값이 들어가도록 Parsing을 진행한다.
구현하는데 있어 작성한 코드는 아래와 같다.
int main(void){
. . . 생략 . . .
while (1) {
. . . 생략 . . .
if(TICK.t_1ms>=10){ // check per 20ms
TICK.t_1ms=0;
uart0_sensor_tx_char(‘R’);
if(TOUCH.receive_cplt_sensor){ // 데이터 버퍼에 정상적으로 데이터가 들어왔을 때
TOUCH.receive_cplt_sensor=0;
dataParsing();
}//parsing end
}//tick end
. . . 생략 . . .
}//while end
}
void dataParsing(){
for(int i=0; i<3; i++){
switch(TOUCH.sensorData[i+1]) {
//Ascii Data를 Hexa decimal 데이터로 바꿔주는 과정
case ’0′: TOUCH.DataBuff[i+1]=0×00;
break;
case ’1′: TOUCH.DataBuff[i+1]=0×01;
break;
case ’2′: TOUCH.DataBuff[i+1]=0×02;
break;
case ’3′:TOUCH.DataBuff[i+1]=0×03;
break;
case ’4′:TOUCH.DataBuff[i+1]=0×04;
break;
case ’5′:TOUCH.DataBuff[i+1]=0×05;
break;
case ’6′:TOUCH.DataBuff[i+1]=0×06;
break;
case ’7′:TOUCH.DataBuff[i+1]=0×07;
break;
case ’8′:TOUCH.DataBuff[i+1]=0×08;
break;
case ’9′:TOUCH.DataBuff[i+1]=0×09;
break;
case ‘A’:TOUCH.DataBuff[i+1]=0x0a;
break;
case ‘B’:TOUCH.DataBuff[i+1]=0x0b;
break;
case ‘C’:TOUCH.DataBuff[i+1]=0x0c;
break;
case ‘D’:TOUCH.DataBuff[i+1]=0x0d;
break;
case ‘E’:TOUCH.DataBuff[i+1]=0x0e;
break;
case ‘F’:TOUCH.DataBuff[i+1]=0x0f;
break;
}
}
//16진수로 변환한 데이터를 토대로 패킷 데이터를 Parsing.
TOUCH.KEY[9]=((TOUCH.DataBuff[1]&0b1000)!=0) ?PRESS:RELEASE; //PRESS : ‘1’
TOUCH.KEY[6]=((TOUCH.DataBuff[1]&0b0100)!=0) ?PRESS:RELEASE; //RELEASE : ‘0’
TOUCH.KEY[3]=((TOUCH.DataBuff[1]&0b0010)!=0) ?PRESS:RELEASE;
TOUCH.KEY[0]=((TOUCH.DataBuff[1]&0b0001)!=0) ?PRESS:RELEASE;
TOUCH.KEY[10]=((TOUCH.DataBuff[2]&0b0001)!=0) ?PRESS:RELEASE;
TOUCH.KEY[7]=((TOUCH.DataBuff[2]&0b0010)!=0) ?PRESS:RELEASE;
TOUCH.KEY[4]=((TOUCH.DataBuff[2]&0b1000)!=0) ?PRESS:RELEASE;
TOUCH.KEY[1]=((TOUCH.DataBuff[2]&0b100)!=0) ?PRESS:RELEASE;
TOUCH.KEY[11]=((TOUCH.DataBuff[3]&0b0001)!=0) ?PRESS:RELEASE;
TOUCH.KEY[8]=((TOUCH.DataBuff[3]&0b0010)!=0) ?PRESS:RELEASE;
TOUCH.KEY[5]=((TOUCH.DataBuff[3]&0b0100)!=0) ?PRESS:RELEASE;
TOUCH.KEY[2]=((TOUCH.DataBuff[3]&0b1000)!=0) ?PRESS:RELEASE;
TOUCH.KEY[12]=’\n’; //dummy code (터미널프로그램에서 확인하기 쉽도록)
TOUCH.KEY[13]=0;
UCSR0B |=(1<<RXCIE0);
}
위 함수는 20ms마다 호출되도록 하였으며, 문자열 수신 완료 flag(TOUCH.receive_cplt_sensor)가 활성화된 뒤에 Parsing 작업을 진행한다. (데이터가 충돌하는 것을 방지)
파싱은 크게 두 과정으로, Hexa data로 변환한 뒤에 12byte 버퍼에 저장을 하게 된다. 해당 버퍼를 터미널로 출력해보면 아래와 같다.
이제 그림 20에서의 데이터를 가지고 키 입력을 인식하며, 이를 통해 비밀번호 입력 로직을 구현하면 된다.
진행하기 앞서, 터치 센서는 정전용량식으로 구현된 센서이며, 민감도 조절은 필수적이다.
아크릴 판 안쪽에 터치 센서를 부착할 계획이므로 더욱 민감하게 센서 인식이 되도록 수정해주어야 한다.
이는 MCU에서 처리해주지 않았으며, 터미널 프로그램에서 직접 터치 센서 컨트롤러에 명령을 내려 제어하는 방식으로 진행했다. USB to Serial 케이블로 PC와 센서를 직접 연결하여 진행하면 된다.
Sensitivity는 100이면 가장 둔감하며, 0에 가까울수록 민감해진다. 0의 경우 터치 센서가 커패시턴스 성분에 의해 접근만 해도 터치 인식을 하게 된다. 적당한 사이 값을 찾는 과정을 아래 그림 22와 같이 진행했다.
민감도가 높으면 터치 센서에 닿지 않고 가까워지기만 해도 인식이 되고, 민감도가 낮으면 터치 센서가 아닌 버튼과 다름이 없다. 키보드의‘R 키를 눌러 데이터를 전송받으며 터치 센서 민감도를 설정했다. 민감도를 30으로 설정하여 아크릴 판 위에 놓고 테스트해보니 터치가 원활하게 인식했다.
3.12. 블루투스 모듈 테스트
처음에는 HC-06를 사용하여 구현을 거의 마친 상태에서 갑작스럽게 고장이 났다. (모터에 의한 역전압으로 추정된다. 그래서 레귤레이터를 통해 동일한 전원으로 공급되지 않도록 모터만 분리하였다.) 어쩔 수 없이 수중에 남아있던 HC-06 모듈을 사용했다.
Default 세팅은 – BAUD: 38400bps – 8 Bit, 1 Stop, No Parity이다. PIN: 1234 NAME: HC-06
기존에 Parsing해둔 센서 데이터를 터미널 프로그램에서 확인하지 않고 블루투스 터미널 프로그램으로 확인해보았다.
UART1 채널에 대해 구현해둔 문자열 송신 함수를 사용하여 데이터를 송신해보면 위 그림 23처럼 출력이 된다. (위 그림 23은 HC-05를 사용했을 당시 촬영했던 사진자료이다.)
3.13. 데이터 프로토콜 규약
블루투스와 데이터를 주고받는 데 있어 한 가지 신호만 오가는 것이 아니므로, 데이터에 대한 규약이 필요하다. 본 제품의 경우, 앱에서 UART1채널로 데이터를 보낼 때 특정 id를 규약하여 데이터를 받아냈다.
앱에서 펌웨어로 데이터를 송신하는 경우, 문자를 송신하는데, 1Byte에서 앞 뒤로 4bit씩 쪼개 앞 4bit는 id. 뒤 4bit는 data로 정의하여, (비밀번호 성공, 실패)/(Send버튼 위치)/(Mute버튼 위치)에 대한 데이터들을 모두 구분했다. 펌웨어 단에서 데이터를 수신 받을 때 id로 먼저 어떤 데이터들이 들어오는지 필터링하여 엉뚱한 데이터가 들어오지 않도록 하였다.
앱에서 펌웨어 데이터를 수신하는 경우, 문자열을 수신해야 하는데, 펌웨어 단에서는 (입력한 비밀번호 byte수 +2 byte)만큼을 전송한다. 앱에서는 ‘<’와 ‘>’를 감지하여 그 사이 데이터를 꺼내오도록 규약하였다.
3.14. 비밀번호 자리 값 버퍼 입력 및 전송 로직 구현 (블루투스 문자열 송신)
비밀번호 버퍼 넣는 과정에선 센서 파싱 데이터에서 터치가 발생하는 경우. 특히, 키패드가 눌리는 Rising edge가 발생하는 순간의 문자만 비밀번호 입력으로 인정하도록 코드를 작성해야 한다.
뿐만 아니라 12개의 키패드 중, 2개는 전송버튼과 무음모드 버튼이다. 그러므로 터치에 대한 이벤트가 크게 3가지로 나뉘어 지도록 구현해야 한다.
위와 같은 몇 가지 고려사항을 토대로 코드를 구현하면 되는데, 구현한 코드는 아래와 같다.
int main(void)
{
. . . 생략 . . .
while (1) {
. . . 생략 . . .
for(int i=0; i<12;i++){//각 키 위치마다 버튼 눌림 여부 체크 (Rising edge 체크)
if(TOUCH.KEY[i]==PRESS){ //눌려있는 상태인지 체크
if(pressed_flag[i]==0){ //바로 이전 데이터가 안눌려있던 상태였는지 체크. 맞다면 Rising edge
switch(i){ //해당 반복문 인덱스에 case 접근
//블루투스로부터 Mute버튼과 Send 버튼의 위치를 수신받아
//해당 위치의 경우 음소거진입버튼 혹은 전송버튼으로 사용. 아닌 경우 일반 입력 버튼
case 0: if(BT.MuteBtnLoc==0×00) MuteMode_toggle();
else if(BT.SendBtnLoc==0×00)SendPW();
else ClickSensor(’0′);
break;
case 1: if(BT.MuteBtnLoc==0×01) MuteMode_toggle();
else if(BT.SendBtnLoc==0×01)SendPW();
else ClickSensor(’1′);
break;
case 2: if(BT.MuteBtnLoc==0×02) MuteMode_toggle();
else if(BT.SendBtnLoc==0×02)SendPW();
else ClickSensor(’2′);
break;
case 3: if(BT.MuteBtnLoc==0×03) MuteMode_toggle();
else if(BT.SendBtnLoc==0×03)SendPW();
else ClickSensor(’3′);
break;
case 4: if(BT.MuteBtnLoc==0×04) MuteMode_toggle();
else if(BT.SendBtnLoc==0×04)SendPW();
else ClickSensor(’4′);
break;
case 5: if(BT.MuteBtnLoc==0×05) MuteMode_toggle();
else if(BT.SendBtnLoc==0×05)SendPW();
else ClickSensor(’5′);
break;
case 6: if(BT.MuteBtnLoc==0×06) MuteMode_toggle();
else if(BT.SendBtnLoc==0×06)SendPW();
else ClickSensor(’6′);
break;
case 7: if(BT.MuteBtnLoc==0×07) MuteMode_toggle();
else if(BT.SendBtnLoc==0×07)SendPW();
else ClickSensor(’7′);
break;
case 8: if(BT.MuteBtnLoc==0×08) MuteMode_toggle();
else if(BT.SendBtnLoc==0×08)SendPW();
else ClickSensor(’8′);
break;
case 9: if(BT.MuteBtnLoc==0×09) MuteMode_toggle();
else if(BT.SendBtnLoc==0×09)SendPW();
else ClickSensor(’9′);
break;
case 10: if(BT.MuteBtnLoc==0x0a) MuteMode_toggle();
else if(BT.SendBtnLoc==0x0a)SendPW();
else ClickSensor(‘A’);
break;
case 11: if(BT.MuteBtnLoc==0x0b) MuteMode_toggle();
else if(BT.SendBtnLoc==0x0b) SendPW();
else ClickSensor(‘B’);
break;
} //if(pressed_flag[i]==0) end
pressed_flag[i]=1; //눌려 있는 상태임을 표시. 해당 루프에 다시 접근할 경우 눌려 있는 상태라면
// 위 case 구문에 접근하지 못하도록 Rising edge 신호에서만 처리함.
}
}// if(TOUCH.KEY[i]==PRESS) end
else pressed_flag[i]=0; //버튼을 뗐을 경우에만 다시 Clear하여 재 접근이 가능하도록
} // for(int i=0; i<12;i++) end
. . . 생략 . . .
}// while end
}//main end
Case 내 총 3가지의 이벤트가 존재하며 각각에 대한 함수는 아래와 같다.
//터치 시 비밀번호를 버퍼에 저장하는 함수
void ClickSensor(char number){
TOUCH.PW[pw_i]=(unsigned char)number;
setSoundClip(BUZZ_BEEP);
setStateRGB(RGB_TOUCHED)
pw_i++;
mode_change_stack=0;
}
//터치 시 어플로 전송하는 함수
void SendPW(){
TOUCH.PW[0]=’<’;
TOUCH.PW[pw_i]=’>’;
uart1_BT_tx_string((char *)TOUCH.PW);
memset((char*)TOUCH.PW,0,sizeof(TOUCH.PW));
pw_i=1;
mode_change_stack=0;
}
//더블 클릭 시 음소거모드 <-> 벨소리모드 진입하는 함수
void MuteMode_toggle() //500ms 안에 연속 두번 들어온다면 그때 모드 전환이 이뤄짐.
{
if(TICK.doubleClick_tick_1ms>500) {
mode_change_stack=0;
mode_change_stack++;
clickTimerON_SW=1;
} //평상시, 혹은 0.5초 이후 터치 시도
else mode_change_stack++; //0.5초 내로 추가 터치
if(clickTimerON_SW==1){ //첫 터치 시작 여부 감지
clickTimerON_SW=0;
TICK.doubleClick_tick_1ms=0;
} //start count tick
if(mode_change_stack==2){
if(MuteModeFlag==MUTE_ENABLE){setSoundClip(BUZZ_SILENCE);MuteModeFlag=MUTE_DISABLE;}
else if(MuteModeFlag==MUTE_DISABLE){setSoundClip(BUZZ_BELLIN);MuteModeFlag=MUTE_ENABLE;}
wait_1s_flag=1; //효과음 재생하고나서 무음모드, 벨소리모드 진입.
}
}
입력한 비밀번호를 버퍼에 저장한다. 전송 버튼을 누르면 SendPW()가 호출되어 저장된 비밀번호 앞에 ‘<’를, 뒤에 ‘>’를 넣고 블루투스로 전송한다.
아래 그림 24는 UART 1번 채널의 Tx 라인을 Logic Analyzer로 측정한 결과이다.
데이터가 UART1 채널을 타고 정상적으로 전송되는 것을 확인할 수 있다. 블루투스 터미널에서도 같은 방식으로 진행하면 된다.
3.15. 문 열림 닫힘 신호 감지
잠금이 해제되고 문을 열고나서 다시 닫을 때 모터가 다시 돌아 잠금모드로 복귀해야 한다. 문이 열리고 닫힘을 인식하기 위해선 홀센서와 같이 자기장을 감지하는 센서가 필요했다. 자기적으로 스위칭을 해주는 Reed Switch를 사용하였으며, 자기장에 영향을 받는 상황이면 스위치가 닫히며, 아닌 경우 스위치가 열리게 된다. Pull-up 저항이 달려 있는 모듈을 사용하므로 따로 풀 업 여부는 고려해주지 않아도 됐다.
다만 문제점은, 자기장에 의한 기계적인 스위칭으로 인한 채터링 노이즈를 해결해야 정상적으로 동작한다. 만일 이러한 문제에 대한 케어가 없다면, 작성한 코드 한에서 정상적인 동작을 할 확률이 매우 낮아진다.
코드로 해결하는 방법도 있으며, 회로적으로 해결하는 방법도 있다. 우리의 경우 센서 출력단에 디 커플링 커패시터(10uF 정도 달았음.)를 달아주는 것을 통해 기계적인 채터링 노이즈를 그라운드 쪽으로 바이패스했다.
노이즈는 해결되었으며, 아래는 동작을 위한 코드이다.
void motor_drive()
{//door_open_flag
int reedSW_buff=(PINA&0×20); //문이 열리면 HIGH, 닫히면 LOW
if((reedSW_buff)==0×20) door_open_flag=DOOR_OPEN_STATE;
else if((reedSW_buff)!=0×20)door_open_flag=DOOR_CLOSE_STATE;
if(motor_flag)
{ //문닫혀있는 상태 : DOOR CLOSE STATE > 잠금 풀리고 > 문 열리면 DOOR OPEN STATE
if(door_open_flag==DOOR_OPEN_STATE){ // ____—- LOW ▶ HIGH
reedSW_state=HIGH;
}
//falling edge일 때 잠금모드 진입해야지
else if(door_open_flag==DOOR_CLOSE_STATE) {
if(reedSW_state==HIGH)// —-____ HIGH ▶ LOW일때만
{
reedSW_state=LOW;
command_door_lock=1;
TICK.motor_1ms=0;
//문닫아명령 tick=0부터 이제 3초 세기 시작, 그리고 sleep 타이머도 세팅
}
}
}
if(command_door_lock==1)
{
if(TICK.motor_1ms==1500)setServoAngle(CLOSE); //close the door
else if(TICK.motor_1ms==2000)setSoundClip(BUZZ_CLOSEDOOR);
else if(TICK.motor_1ms==2500)
{
TCCR1A&=~(1<<COM1A1); //모터 PWM 신호 끊음.
command_door_lock=0;
motor_flag=0;
TICK.sleep_1ms=25000; //25초로 세팅하여 5초 뒤에 슬립모드에 빠지도록 설정.
}
}
}
3.16. 회로제작과정
3.17. 제품제작과정
4. 기타
4.1. 회로도 (PSpice)
4-2. 안드로이드 코드
4-3. MCU 펌웨어 코드
5. 고찰
5.1. 저전력 관련 문제
프로젝트 초기에 하드웨어 설계 중, 부품 선정에 있어 MCU 및 모듈 등에 대한 선정에 부족함이 있었던 것 같다. 블루투스 모듈의 경우 MCU가 슬립모드 상태가 돼도 계속 활동해야 했는데, 본 제품의 경우 슬립모드가 되면 블루투스 모듈 또한 전원 공급을 차단했다. 대기 전류가 너무 많이 흘렀기 때문이다. BLE 타입의 모듈을 사용하여 대기 전력은 매우 낮은 상태를 유지하도록 해야 하는 것이 상황에 가장 적절한 것 같다. 전원 스위칭하는 목적으로 릴레이 모듈을 사용했는데, 이 또한 목적에 적합하지 않은 선택이었다고 생각한다. 릴레이 모듈은 고전력 회로를 스위칭하는 용도가 주된 이유이지만 해당 제품에선 사용할 이유가 없다고 생각했다. 차라리 PNP BJT를 이용하여 스위칭을 했다면 적합했을 것 같다.
레귤레이터는 LM2576-3.3 스위칭 레귤레이터를 사용했으며, 리니어 레귤레이터에 비해 전력 소모가 적다는 이유에서 사용하였다. 처음에는 5V 출력인 레귤레이터를 사용했지만, 전력을 소모하는 대부분의 모듈이(Atmega128, Touch Controller, Reed SW, Relay SW, BlueTooth Module, RGB, BUZZ) 3.3V로 동작하는 것으로 확인하여 전력 절약을 목적으로 3.3V로 바꾸게 되었다.
하지만 레귤레이터만을 연결하여 소모하는 전류를 측정해본 결과, 10mA정도가 흐르는 것으로 확인하였다. 사실상 저전력 제품에서 10mA가 계속 흐른다는 것은 인정될 수 없는 것이라고 생각한다. 뿐만 아니라 본 제품에서 사용하고 있는 터치센서 또한 슬립모드에서 활성화되어 있기 때문에 컨트롤러 자체에서 10mA를 소모하여, 도합 20mA가 슬립모드에서 흐르고 있는 것을 확인되었다.
이것에 대한 해결책은 다음과 같다.
레귤레이터의 경우 ‘Low Quiescent Current LDO’를 찾아볼 것. ▶ST730의 경우 3.3V 출력에 최대 300mA까지 출력함. 우리 제품의 경우 서보모터의 Stall Current를 배제하면 300mA는 인정 범위에 들어올 것으로 생각이 된다. 다만, 해당 LDO는 DIP타입이 아니기 때문에 우리와 같이 만능기판에 납땜하여 만들기는 힘들다. PCB로 제작할 기회가 생긴다면 제품 크기 축소화와 동시에 전력 감축까지 가능할 것 같다는 생각이 들었다.
3.3V로 낮춰주면서 발생하는 문제는 Atmega128에서는 3.3V에서 16MHz 클럭을 보장해주지 못한다는 데이터시트에서의 말이 있다. Overclock으로 통신에 있어 큰 문제가 생길 수도 있다는 말로 해석을 했다. 이에 대한 해결책으로는 아래 그래프를 보면 금방 알 수 있다.
결론적으로는 외부 크리스탈을 8MHz로 교체하면 된다. 일석이조로 클럭에 대한 안정화를 얻어감과 동시에 클럭을 낮춤으로 동작 전력을 낮출 수 있다는 장점까지 얻어간다. 본 제품의 경우 대부분의 Peripheral들에 대해 클럭을 분주해서 사용하므로, 16MHz 속도의 퍼포먼스까지는 필요 없다. 8MHz로 교체함으로 두 가지 장점을 가져왔다.
마지막으로, 저전력 회로를 설계함에 있어 사용하지 않는 모든 핀들 PULL UP 시켜주어야 한다. 따로 설정되지 않은 상태라면 쓸모 없는 누설전류가 흐를 가능성이 생겨 전력 소모가 발생할 수 있다.
6. 참고 문헌
· Atmega128 Data sheet (ATMEL): Unconnected pins (p70)
· Atmega128 Data sheet (ATMEL): Typical Characteristics (p333)
· Atmega128 Data sheet (ATMEL): Electrical Characteristics-Speed Grades (p320)
· https://kogun.tistory.com/33
· https://m.blog.naver.com/PostView.nhn?blogId=ansdbtls4067&logNo=220640400445&proxyReferer=https%3A%2F%2Fwww.google.com%2F
· https://binworld.kr/36
· https://miobot.tistory.com/23
· http://www.makeshare.org/bbs/board.php?bo_table=arduinomotor&wr_id=5
· https://blog.naver.com/6k5tvb/120055584295
· https://mjk90123.tistory.com/22
[61호]파파고 API를 이용한 점자 번역기
2020 ICT 융합 프로젝트 공모전 최우수상
파파고 API를 이용한 점자 번역기
글 | 국민대학교 이동윤
1. 심사평
칩센 점자 변환 장치는 다수 본 경험이 있지만, 이 작품처럼 번역이 가능한 장치는 처음 접하는 듯 합니다. 온전히 새로운 것을 만드는 것도 대단하지만, 작품과 같이 기존의 보유 시스템의 융합을 통해 새로운 것을 내보이는 것 또한 굉장한 것으로 생각합니다. 사용 대상자가 될 시각 장애인이 인지 가능한 단위로 점자판의 요철을 촘촘히 가져간다면 충분히 멋진 제품으로 진행이 가능할 것으로 보입니다.
펌테크 작품의 아이디어와 창의성이 돋보이며 추후 작품 완성도를 높일 경우 실제 시각장애인을 위한 의사소통 장치로도 충분히 활용이 가능할 작품이라고 생각합니다. 작품의 기획의도, 기술 구현도, 완성도 등에서 우수한 작품으로 생각됩니다.
위드로봇 높은 완성도를 보이는 작품입니다. 점자를 표시하는 부분은 실제 시각 장애인이 사용하기에는 부족하지만 별도의 햅틱 장치와 연동이 된다면 더욱 완성도가 올라갈 것으로 보입니다.
2. 작품 개요
2.1. 제작동기
시각장애인이 세상과 소통하면서 가장 의존하고 있는 수단이 점자이다. 그로 인해 많은 책들이 점자책으로 출판되기도 했고 시각장애인을 위해 점자 키보드가 나오기도 하였다.
시각장애인을 위해 점자를 활용한 다양한 제품이 발명되고 출시되고 있지만, 아직 비장애인들과 같은 편리함을 느끼지 못하는 것이 현실이다. 특히 영어 점자와 한글 점자의 체계가 다르기 때문에 영어 점자에 대해 따로 배우지 않는다면 전혀 다른 의미로 해석하는 등 많은 불편함이 존재했다.
그래서 영어 점자로 만들어진 점자를 한글 점자로 바꾸는 프로젝트를 구상하게 되었다.
우리나라의 시각장애인의 수는 2018년 통계청에서 발표한 자료에 따르면 252,957명에 이른다. 하지만 시각장애인에 대한 교육방식은 점자책과 오디오를 통해 이루어지고 있다. 특히 언어교육에서 영어점자와 한글점자의 체계가 다르기 때문에 비장애인과는 다르게 영어점자와 영어를 함께 배워야하는 이중고를 겪어 많은 시각장애인들이 어려움을 겪고 있다.
더욱 심각한 것은 대학이나 대학원 과정의 점자책은 소설이나 교양서적에 비하여 턱없이 부족하다는 것이다. 또한 전공서적을 점자책으로 만들기 위해서는 3~4개월의 시간이 들기 때문에 시각장애인의 향학열을 꺾고 있다.
이러한 상황에서 비교적 우리나라 보다 점자책의 폭이 넓고 수요가 많은 외국서적을 직접 번역해 주는 점자 번역기는 시각장애인들의 전문지식을 넓히는 데 도움이 될 것이라고 생각한다.
또한 점자 번역기는 학업에만 적용되지 않고 다양한 분야에서 적용될 것으로 보인다.
현재 여행객들을 위한 번역기나 스마트폰 애플리케이션이 많이 개발되고 있다. 반면에 시각장애인들은 소리로 번역된 언어를 듣는 방법 외에는 대안이 없다. 하지만 표지판이나 손잡이 심지어 화장실에도 점자로 표시되어 있어 소리로만 모든 정보를 얻는 데 어려움이 있다. 이러한 상황에서 점자 번역기를 사용한다면 비장애인들의 도움 없이 여행이 가능할 것으로 보인다.
2.2. 관련 특허 분석
본 발명은 점자를 변환하여 출력하는 장치에 있어서, 사용자의 조작에 의 해 한국어 단어/문장에 해당하는 키 또는 명령을 입력받은 후 한국어 점자 패턴을 생성하는 점자 입력부; 오디오 데이터를 가청음으로 변환하여 출력하는 오디오 출력부; 및 상기 장치의 전반적인 동작을 제어하는 제어 수단으로서, 상기 점자 입력부로부터 상기 한국어 점자 패턴을 입력받아 인식한 후 상기 한국어 특정 단어/문장으로 변환하며, 상기 한국어 특정 단어/문장을 기 설정된 외국어로 번역하여 외국어 단어/문장을 생성하고, 상기 외국어 단어/문장을 외국어 오디오 데이터 변환한 후 상기 오디오 출력부로 출력하도록 제어하는 제어부를 포함하는 것을 특징으로 하는 한글에 대한 점자를 외국어로 번역하여 오디오로 출력하는 장치를 제공한다.
본 발명은 지능형 점자 번역 장치에 관한 것이다. 보다 상세하게는 시각 및 청각장애인들이 쉽고 편리한 점자문자사용을 할 수 있도록 지능형 점자 번역 장치를 제공하여 점자를 스캔하는 경우, 점자를 인식한 후 점자를 DB에 저장된 점자정보와 매칭하여 음성으로 변환하여 출력함으로써, 시각 및 청각장애인들이 보다 편리하게 점자정보를 읽을 수 있도록 하는 지능형 점자 번역 장치에 관한 것이다.
3. 작품 설명
3.1. 주요 동작 및 특징
· 영문점자를 카메라로 영상을 받아 노트북으로 전송한다.
· 노트북에서 영상처리를 통해 영문점자를 영문으로 변환한다.
· 파파고 API를 이용해 영문을 한글로 번역한다.
· 한글을 각각의 자모에 맞게 변환을 한다.
· 변환한 데이터를 노트북에서 아두이노로 시리얼통신을 이용해 전송한다.
· 전송받은 데이터를 이용해 아두이노가 서보모터를 제어한다.
영상처리에서의 점자 판독부
· 요철확인 : 점자 명도영상 획득, 잡음제거, 점자판독
· 좌표 가로줄 정렬 : 컨투어, 컨투어 좌표 중심값, 가로줄 정렬 및 기울기 교정
· 점자판별 : 세로줄 정렬 및 근사화, 6점 단위 구성
영상처리 결과
3.2. 전체 시스템 구성
3.2.1. 제어부
카카오 api를 이용하여 점자를 해석한 후 이를 pc가 시리얼 통신으로 아두이노에 정보를 넘겨준다. 아두이노에서 수신된 값에 해당하는 소모터를 동작시킴으로써 점자를 표기한다.
3d 모델링을 통해 서보모터로 요철을 표현하였다.
3.2.2. 작품 결과
3.2.3. 기능 및 제한 사항
· 테블릿의 영문 점자를 한글 점자로 변환하여 물리적으로 표현할 수 있다.
· 한 번에 카메라에 들어오는 영상의 제한이 있기 때문에 문장 단위로 해석이 가능하다.
3.2.4. 개발환경
· Python , C 언어 사용
· Arduino : 점자 표현
· Visual: 기본적인 영상처리 툴
· PyCham: 파이선 에디터
· pyqt5 desinger: UI 제작 툴
· inventor :3D 모델링
· cura : 3D 모델 출력
· 사용 노트북 사양 : CPU : i5-7200, RAM : 8GB, OS : Windows
4. 단계별 제작과정
4.1. 일자별
2019년 11월 14일 : 영상처리를 이용하여 사회적으로 많은 도움이 될 작품을 구상했다. 영상처리 기술을 사용하여 고가의 점자 번역기를 저렴하게 구현하기로 했다. 파이썬 기반으로 영상처리를 하고 아두이노와 시리얼 통신을 하기로 했다.
2019년 11월 21일 : 조사한 내용을 바탕으로 실제 작품을 구현할 수 있는지 Flow Chart를 만들어 보았다. 파파고 API를 노트북과 연동하였다.
2019년 11월 26일 : 시리얼 통신을 할 때, 아스키코드를 이용하여 상황별 분류를 좀 더 간단히 하였다.
2019년 11월 28일 : 3D 프린터를 이용하여 하드웨어를 제작했다. 하드웨어는 고장 시, 점검에 용의하도록 개패식으로 제작하였다. 수월한 영상처리가 가능하게 하도록 UI에서 ROI가 보이도록 수정했다.
2019년 11월 30일 : 사람이 손으로 점자를 인식하기 좋게 모터 값을 조절했다.
2019년 12월 3일 : 영상처리 시, 점과 점 사이의 간격을 계산하는 기준 값을 조절하여 영어 번역을 할 때 띄어쓰기가 되지 않는 문제를 해결했다. 최적화를 위해 한글 점자에서 사용하지 않는 자음 ‘ㅇ’는 생략하도록 데이터를 처리했다.
4.2. 제작 고려요소 및 변경사항
· 원가 : 기존의 점자 정보 단말기는 450만원을 호가할 정도로 비싸다. 이를 비교적 저렴한 MCU와 모터를 사용하고 영상처리 기술을 이용하여 경제성이 있는 결과물을 향상시켰다.
· 신뢰성 : 점자를 인식하기 쉽게 점자 블록의 튀어나오는 정도를 조절했다. 각 서보모터의 내구도와 성능의 차이가 있었기 때문에 서보모터의 각도를 모터마다 설정해주었다.
· 하드웨어를 만들 때 3D프린터를 사용하여 좀 더 정밀하고 깔끔한 외관을 만들었다.
· 노트북 전원으로 아두이노와 6개의 서보모터를 동작할 때, 전력이 부족할 것을 우려했지만 동작하는데 무리가 없어 별도의 전원장치는 사용하지 않기로 했다.
· 영어 점자를 해석하는 과정에서 띄어쓰기한 부분이 인식이 잘 되지 않아 띄어쓰기의 여부를 결정하는 기준 값을 조절하여 띄어쓰기 인식률을 높혔다.
· 알고리즘의 속도를 높이기 위해서 불필요한 형변환 부분을 삭제하였다.
· 점자에서 자음 ‘ㅇ’은 존재하지 않기 때문에 이를 무시하는 방향으로 알고리즘을 수정했다.
elif(korean_to_be_englished(response_text [cnt:cnt+1]).pop(O).pop(pp)==’o’):
print(‘넘기기’)
5. 기타
5.1. 아두이노 코드
5.1.1. 셋업부
void setup() {
Serial.begin(9600); // 보오레이트를 9600으로 설정한다.
myservo2.attach(2); // 서보ㅅ모터를 동작할 수 있게 설정한다. 2,3,4,5,6,7 같은 방식으로 지정
ser2(1); // 서보모터의 각도를 정해주는 함수이다.
pinMode(8,OUTPUT); // 셋업이 되었으면 8번 핀에 빛을 낸다.
}
5.1.2. 서보모터 각도 설정부
void ser2(int a){
if(a==0)
myservo2.write(100); // low값이 들어오면 서보모터의 각도를 100으로 설정한다.
else
myservo2.write(75); // high값이 들어오면 서보모터의 각도를 75으로 설정한다.
}// 위와 같은 방식으로 나머지 서보모터도 지정
5.1.3. 동작부
void loop() {
if(Serial.available()) // 시리얼 통신의 신호가 들어오면 동작한다.
{
int swt = (int)Serial.read(); // swt 변수에 시리얼 통신으로 수신된 값을 저장한다.
if(swt==97) // 수신된 값이 ‘a’라면 아래와 같이 서보모터를 동작한다.
{
ser2(0);
ser3(1);
ser4(0);
ser5(0);
ser6(0);
ser7(0);
}
// 이후 수신된 값에 따라 서보모터를 동작하게 한다.
}
5.2. Python 코드
5.2.1. 영상처리부
img_gray = cv.cvtColor(img_color, cv.COLOR_BGR2GRAY) #gray 이미지 변경
ret, img_binary = cv.threshold(img_gray, 127, 255, 0) #이미지 이진화
Contours, hierarchy = cv.findContours(img_binary, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE) #컨투어 찾기
Str=[]
Contours.pop()
For cnt in contours:
cv.drawContours(img_color, [cnt], 0, (255, 0, 0), 3) #컨투어 그리기 blue
times=0
STACK=[] #location _stack
x_dot=[] #x _location stack
y_dot=[] #y _location stack
for cnt in contours:
M = cv.moments(cnt) #중심모멘트
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
STACK.append([cx,cy])
x_dot.append(cx) #x 중심 모멘트
y_dot.append(cy) #y 중심 모멘트
cv.circle(img_color, (cx, cy), 10, (0,0,255), -1)
times=times+1
y_max=STACK[len(STACK)-1][1]
y_min=STACK[0][1]
y_gap=y_max-y_min # 판단을 위한 거리측정
y_avg=int(y_max+y_min)/2
x_dot=list(set(x_dot))
x_dot.sort()
no_need_x=[]
change_x=[]
x_gap=int(y_gap/2) #x_gap 구함
last_gap=int(y_gap*10/12) #단어 사리 거리 구함
for count in range(0 ,len(x_dot)) : #X 좌표 보정
if x_dot[count]-x_dot[count-1]<x_gap/3 and count>0 : # 차이 얼마 안나는 x값
no_need_x.append(x_dot[count]) # 대체 해 줄 x값 저장
change_x.append(x_dot[count-1])
if count+1<len(x_dot)-2 :
if x_dot[count+1]-x_dot[count-1]<x_gap/3 :
no_need_x.append(x_dot[count+1])
change_x.append(x_dot[count-1])
y_top=[] y_middle=[] y_bottom=[]
for count in range(0 ,times) : #Y 값에 따라 X 좌표 저장
if STACK[count-1][1]<y_avg-y_gap/4: #TOP
if no_need_x.count(STACK[count-1][0])>0:
location=no_need_x.index(STACK[count-1][0])
y_top.append(change_x[location])
if x_dot.count(STACK[count-1][0])>0:
x_dot.remove(STACK[count-1][0])
else :
y_top.append(STACK[count-1][0])
elif STACK[count-1][1]>y_avg+y_gap/4: #BOTTOM
if no_need_x.count(STACK[count-1][0])>0:
location=no_need_x.index(STACK[count-1][0])
y_bottom.append(change_x[location])
if x_dot.count(STACK[count-1][0])>0:
x_dot.remove(STACK[count-1][0])
else :
y_bottom.append(STACK[count-1][0])
else :
if no_need_x.count(STACK[count-1][0])>0: #MIDDLE
location=no_need_x.index(STACK[count-1][0])
y_middle.append(change_x[location])
if x_dot.count(STACK[count-1][0])>0:
x_dot.remove(STACK[count-1][0])
else :
y_middle.append(STACK[count-1][0])
for count in range(1 ,len(x_dot)) : # X값들 사이의 거리를
if x_dot[count-1]<x_gap: #단어 시작점 구함 first_line.append(x_dot[count-1])
cv.rectangle(img_color,(int(x_dot[count-1]),int(y_min)),(int(x_dot[count-1]+x_gap),int(y_max)),(255,0,0),1)
elif x_dot[count]-x_dot[count-1]<(last_gap+x_gap)/2:
first_line.append(x_dot[count-1])
cv.rectangle(img_color,(int(x_dot[count-1]),int(y_min)),(int(x_dot[count]),y_max),(255,0,0),1)
elif x_dot[count]-x_dot[count-1]<last_gap+(x_gap)/2:
first_line.append(x_dot[count])
cv.rectangle(img_color,(int(x_dot[count]),int(y_min)),(int(x_dot[count]+x_gap),int(y_max)),(255,0,0),1)
cv.rectangle(img_color,(int(x_dot[count-1]-x_gap),int(y_min)),(int(x_dot[count-1]),int(y_max)),(255,0,0),1)
elif (first_line.count(x_dot[count-1])>0) and (x_dot[count]-x_dot[count-1] <last_gap+3*(x_gap)/2):
first_line.append(x_dot[count])
cv.rectangle(img_color,(int(x_dot[count]),int(y_min)),(int(x_dot[count]+x_gap),int(y_max)),(255,0,0),1)
first_line=list(set(first_line))
first_line.sort()
second_line=set(x_dot)-set(first_line) #시작 줄 아니면 2번째 줄
second_line=list(second_line)
second_line.sort()
for count in range(0 ,len(first_line)) :
if count==0 :
if(first_line[count+1]>second_line[index_second]):
number=int(y_top.count(first_line[count]))*100000+int(y_top.count(second_line[index_second]))
*10000+int(y_middle.count(first_line[count]))*1000+int(y_middle.count(second_line[index_second]))
*100+int(y_bottom.count(first_line[count]))*10+int(y_bottom.count(second_line[index_second]))
dot_num.append(number)
index_second+=1
else :
number=int(y_top.count(first_line[count]))*100000+int(y_middle.count(first_line[count]))
*1000+int(y_bottom.count(first_line[count]))*10
dot_num.append(number)
elif( count< len(first_line)-1):
if (first_line[count]-first_line[count-1]>+x_gap*2+last_gap*2) :
dot_num.append(-1)
if(first_line[count+1]>second_line[index_second]):
number=int(y_top.count(first_line[count]))*100000+int(y_top.count(second_line[index_second]))
*10000+
int(y_middle.count(first_line[count]))*1000+int(y_middle.count(second_line[index_second]))
*100+int(y_bottom.count(first_line[count]))*10+int(y_bottom.count(second_line[index_second]))
dot_num.append(number)
index_second+=1
else :
number=int(y_top.count(first_line[count]))*100000+int(y_middle.count(first_line[count]))
*1000+int(y_bottom.count(first_line[count]))*10
dot_num.append(number)
else :
last_in=x_dot.pop()
if(last_in==first_line[count]) :
number=int(y_top.count(first_line[count]))*100000+int(y_middle.count(first_line[count]))
*1000+int(y_bottom.count(first_line[count]))*10
dot_num.append(number)
else :
number=int(y_top.count(first_line[count]))*100000+int(y_top.count(second_line[index_second]))
*10000+int(y_middle.count(first_line[count]))*1000+int(y_middle.count(second_line[index_second]))
*100+int(y_bottom.count(first_line[count]))*10+int(y_bottom.count(second_line[index_second]))
dot_num.append(number)
index_second+=1
Alpa=[100000,101000,110000,110100,100100,111000,11100,101100,11000,11100,100010,101010,110010,110110,
100110,111010,111110,101110,11010,11110,100011,101011,11101,110011,110111,100111]
sen_str=[]
for count_dot_num in range(0,len(dot_num)):
for count_alpa in range(0 ,len(Alpa)) :
if dot_num[count_dot_num]==Alpa[count_alpa]:
dot_num[count_dot_num]=Alpa.index(Alpa[count_alpa])
if(dot_num[count_dot_num]==-1) :
sen_str.append(‘ ‘)
elif(dot_num[count_dot_num]==0) :
sen_str.append(‘a’)
elif(dot_num[count_dot_num]==1) :
sen_str.append(‘b’)
elif(dot_num[count_dot_num]==2) :
sen_str.append(‘c’)
elif(dot_num[count_dot_num]==3) :
sen_str.append(‘d’)
/* 초성,중성,종성의 리스트를 만들어준다. */
CHOSUNG_LIST = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']
JUNGSUNG_LIST = ['ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ']
JONGSUNG_LIST = [' ', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']
def korean_to_be_englished(korean_word): // 번역함수입니다.
r_lst = []
for w in list(korean_word.strip()):
## 영어인 경우 구분해서 작성함.
if ‘가’<=w<=’힣’:
## 588개 마다 초성이 바뀜.
ch1 = (ord(w) – ord(‘가’))//588
## 중성은 총 28가지 종류
ch2 = ((ord(w) – ord(‘가’)) – (588*ch1)) // 28
ch3 = (ord(w) – ord(‘가’)) – (588*ch1) – 28*ch2
r_lst.append([CHOSUNG_LIST[ch1], JUNGSUNG_LIST[ch2], JONGSUNG_LIST[ch3]])
else:
r_lst.append([w])
return r_lst
PORT = ‘COM6′ // 포트를 설정해줍니다.
BaudRate = 9600 // 보오레이트를 9600으로 설정합니다.
ARD= serial.Serial(PORT,BaudRate) // ARD를 시리얼통신 변수로 설정한다.
client_id = “P6ynPpUq#######” // NAVER API의 ID를 입력해준다.
client_secret = “g##########0″ // NAVER API의 PassWard를 입력해준다.
/* Naver API로 영어 문자를 전송하고 변역한다. */
encText = urllib.parse.quote(str_alpa)
data = “source=en&target=ko&text=” + encText
url = “https://openapi.naver.com/v1/papago/n2mt”
request = urllib.request.Request(url)
request.add_header(“X-Naver-Client-Id”,client_id) request.add_header(“X-Naver-Client-Secret”,client_secret)
response = urllib.request.urlopen(request, data=data.encode(“utf-8″))
rescode = response.getcode()
/* 번역된 수신값에 필요한 값만 뽑아낸다. */
if(rescode==200):
response_body = response.read()
response_text = response_body.decode(‘utf-8′)
response_text = json.loads(response_text)
response_text = response_text['message']['result']['translatedText']
print(response_text)
t = 0
cnt = 0
pp = 0
send = ‘a’
while(t == 0):
pp = 0
tim = 2
/* 번역된 값이 필요없는 값이면 넘겨준다 */
if(response_text[cnt:cnt+1]==’ ‘):
print(‘넘기기’)
elif(response_text[cnt:cnt+1]==’,'):
print(‘넘기기’)
elif(response_text[cnt:cnt+1]==’.'):
print(‘끝’)
t=1
elif(response_text[cnt:cnt+1]==’!'):
print(‘끝’)
t=1
else:
/* 이후 뽑아진 초성 값에 따라 정해진 알파벳을 전송한다. */
if(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(0)==’ ‘):
pp+=1
print(‘넘기기’)
elif(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(0)==”):
pp+=1
print(‘넘기기’)
elif(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp)==’ㅇ’):
pp+=1
print(‘넘기기’)
else:
print(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp))
if(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp)==’ㄱ’):
send = ‘a’
pp+=1
time.sleep(2) // 2초의 delay를 넣는다.
Trans = send
#Trans = str(Trans)
Trans = Trans.encode(‘utf-8′)
ARD.write(Trans) // utf-8로 인코딩하여 전송한다.
/* 이후 뽑아진 종성 값에 따라 정해진 알파벳을 전송한다. */ if(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp)==”):
pp+=1
print(‘넘기기’)
else:
print(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp))
time.sleep(2)
Trans = send
#Trans = str(Trans)
Trans = Trans.encode(‘utf-8′)
ARD.write(Trans)
/* 이후 뽑아진 종성 값에 따라 정해진 알파벳을 전송한다. */
if(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp)==”):
pp+=1
else:
print(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp))
if(korean_to_be_englished(response_text[cnt:cnt+1]).pop(0).pop(pp)==’ㄱ’):
send = ‘o’
#pp+=1
time.sleep(tim)
Trans = send
#Trans = str(Trans)
Trans = Trans.encode(‘utf-8′)
ARD.write(Trans)
cnt+=1
else:
print(“Error Code:” + rescode)
[61호]음성으로 제어하는 간접등 만들기
아두이노 DIY 무작정 따라하기 (중·고급편)
음성으로 제어하는 간접등 만들기
음성인식 모듈을 활용하여 내 목소리로 간접등을 끄고 켜고 스마트한 나만의 공간 만들기!
글 | 디바이스마트 정대진, 박진아
스마트 스피커로 집안을 제어하거나, 집안의 전기 사용량, 온·오프 등을 앱으로 관리할 수 있게 만들어주는 사물인터넷, 스마트 홈에 대한 관심이 날로 높아지며 일상생활에 많은 변화를 주고 있습니다.
그렇다면 스마트 홈이란 정확히 무엇일까요? 스마트 홈은 사물인터넷 등을 이용해서 가정 내의 시스템들을 자동/원격으로 조작 가능한 환경을 구축한 것을 의미합니다.
저희는 사무실이라는 공간적 제약이 있었기에 이번 프로젝트는 회의실 화이트보드위에 간접등을 설치하고 음성 인식 모듈을 통해 음성으로 불 켜짐과 꺼짐을 제어해 보았습니다. 집에서 따라하실 분은 신발장 하부나, 책상아래, 주요 장식장 등 원하는 위치에 설치해보고 따라하는 시간을 가져보시기 바랍니다.
1. 음성 제어 간접등 만들기에 필요한 제품소개
필수 준비물 제품은 각 각 한개씩 사용하였으며, 프로젝트에 사용되는 모든 제품은 디바이스마트에서 구매가 가능하오니 디바이스마트 검색창에 상품번호를 입력해보시기 바랍니다.
2. 음성인식 모듈에 명령어 세팅하기
가장 먼저 이번 프로젝트에서 제일 중요한 음성인식 모듈에 간접등을 음성으로 끄고, 켤 수 있도록 음성 명령어를 세팅해주도록 하겠습니다.
2.1. 음성 인식 모듈 연결하기
이번에 사용한 음성 인식 모듈은 최대 15개의 음성 명령어를 저장할 수 있습니다.
음성 명령은 3개의 그룹으로 나뉘며, 각 5개의 음성 명령이 저장되기에 그룹 별로 음성 명령어를 모듈에 저장 한 후, 사용할 그룹을 선택해야 합니다.
음성인식 모듈을 PC와 연결하기 위해서 위와 같이 USB to TTL 컨버터와 모듈을 연결해야 합니다. 이때 Rx와 Tx가 교차되어 연결해야 합니다.
음성 인식 모듈과 USB to TTL 컨버터를 연결한 후 PC USB포트에 USB to TTL 컨버터를 꽂아주면 PC의 장치관리자에서 포트 번호를 확인 할 수 있습니다. 저희는 포트 번호가 COM22입니다.
2.2. 음성인식 모듈에 명령어 저장하기
음성 인식 모듈의 데이터 시트에서는 PC와의 연결 할 때, AccessPort라는 시리얼 통신 프로그램 사용을 권하고 있지만 다른 시리얼 통신 프로그램을 사용해도 가능합니다. 이 프로젝트에서는 음성 인식 모듈의 데이터 시트에 권장하는 AceesPort라는 프로그램을 사용하며 음성 인식 명령어를 녹음하는 방법에 대해 알려드리겠습니다.
프로그램은 http://www.sudt.com/en/ap/download.htm에서 다운로드 받으실 수 있습니다.
프로그램 다운로드 페이지를 보시면 오른쪽 하단에 있는 AccessPort137.zip을 확인할 수 있습니다. 클릭해서 다운받아준 후, 압축을 해제하면 AccessPort.exe파일이 있습니다. 해당 프로그램을 실행시켜줍니다.
AccessPort 프로그램을 실행 시킨 후, 왼쪽 상단에 있는 노란색과 초록색의 톱니바퀴 모양의 Configuration 아이콘을 눌러줍니다.
그럼 Options 창이 뜹니다. 여기서 USB to TTL 컨버터와 연결된 Port 번호를 입력하고, Baud Rate는 9600으로 맞춰줍니다. Send display는 Hex format으로 Receive display는 Char Format으로 설정해준후 OK 버튼을 눌러줍니다.
이 후 원래창 아래쪽에 Hex 명령어로 ‘AA36’을 Send 버튼을 통해 보내주었을 때, Common Mode라는 답변이 온다면, 제대로 연결된 것입니다.
Hex 명령어로 ‘AA11’을 보내면, 음성 명령어 녹음이 시작됩니다. 음성 인식 모듈의 마이크에 대고 명령어를 녹음해주시면 됩니다. 각 명령의 길이는 최대 1300ms(1.3초)이므로, 짧은 단어로 명령을 넣는게 좋습니다. 녹음을 시작하면, 한 그룹의 5가지 음성 명령 녹음을 끝내기 전까지 녹음 과정을 멈출 수 없는점 유의해 주시기 바랍니다.
START 라는 답변이 오면 명령어를 녹음해주시면 됩니다. No Voice, Different등을 통해 이전 녹음과 다른지, 녹음이 되고 있는지 확인이 가능합니다. Finish one이 나오면 1개의 명령어가 녹음되었다는 뜻입니다. 순차적으로 5개가 모두 녹음 완료되면, Group 1 finish라는 답변이 옵니다.
저희의 경우에는 불켜, 꺼줘, 하나, 둘, 셋 5가지를 녹음한 후 아두이노를 통해 ‘불켜’, ‘꺼줘’ 명령어만 사용하여 간접등을 제어하였습니다. 실제로 하나, 둘, 셋 같은 단어가 인식률이 더 좋았으며 파동을 인식하는 모듈이기에 불켜와 같은 문장을 명령어로 설정할 경우 똑같은 음정으로 명령어를 발음하여야 인식이 더 잘되는점 참고해서 원하는 명령어를 입력해보시기 바랍니다.
이 후 Hex 명령어로 ‘AA21’을 보내주면, Group1 Imported라는 답변이 옵니다. 첫번째 그룹의 5개의 명령어 입력이 끝났습니다.
이 프로그램에서 사용 가능한 명령어 리스트는 아래와 같으며, 이외의 추가적인 명령어는 QR코드에 관련자료 Voice_Recognize_manual.pdf를 참고하시기 바랍니다.
Delete Group 1 – 0xAA01
Delete Group 2 – 0xAA02
Delete Group 3 – 0xAA03
Delete All Groups – 0xAA04
Record Group 1 – 0xAA11
Record Group 2 – 0xAA12
Record Group 3 – 0xAA13
Import Group 1 – 0xAA21
Import Group 2 – 0xAA22
Import Group 3 – 0xAA23
Switch Common Mode – 0xAA36
Switch Compact Mode – 0xAA37
Common Mode는 음성 명령값을 ‘Result:11’과 같은 ASCII 코드 값으로 전달해 주지만, Compact Mode는 음성 명령값을 ‘11’과 같은 Hex값으로 전달해줍니다.
따라서 처음 PC와 음성 인식 모듈을 연결하여, 음성 명령을 녹음 할 때는 Common Mode(AA36)를 사용하여 녹음하고 이후 아두이노등의 플랫폼에서 사용 할때는 Compact Mode(AA37)로 사용해야 합니다.
3. 하드웨어 구성 하기
회로도와 상세연결도식표를 보며 아두이노를 브레드보드, 음성인식 모듈, 릴레이모듈에 연결해줍니다.
저희는 회의실에 실제로 장착하는데 편리하고자 음성 인식 키트를 구매하면 같이 오는 케이스에 구멍을 뚫고 모든 하드웨어를 넣어 정리해주었으나 필수적인 작업은 아닙니다.
전선의 경우, LED 스트립 설치 시 전선길이가 부족했기에 안내해드렸던 추가전선과 LED 스트립선을 납땜, 수축튜브를 활용하여 연결해주었습니다.
작업이 완료된 LED 스트립은 항상 불이 켜져있는 상태가 아닌 아두이노에서 신호를 주었을 때만 불이 들어와야 하기 때문에 중간에 릴레이를 거쳐서 DC잭에 연결하여 줍니다.
DC잭에서 출력되는 전압은 아두이노의 VIN핀, GND로 연결하여 아두이노에 전원을 공급해주고, 또한 릴레이를 거쳐 LED스트립에 전원을 공급해 줍니다. LED스트립의 플러스 선(빨간선)은 다이렉트로 DC잭의 플러스 선으로 연결해주고, LED스트립의 마이너스 선(검정선)은 릴레이의 NO(Normal Open)단자에 연결해줍니다. 또 릴레이의 COM(Common)단자에는 DC잭의 마이너스 선으로 연결해주면 됩니다.
전원 어댑터와 연결된 DC 잭을 통해 아두이노와 LED 스트립에 전원을 공급하게 됩니다. 또 릴레이 모듈과 음성 인식 모듈에도 5V를 인가해줘야 하기 때문에 아두이노의 5V 출력 핀에서 브레드 보드를 통해 전원을 나눠서 공급할 수 있게 연결을 해줬습니다.
저희는 전선처리를 더욱 깔끔하게 하기위해 필수사항은 아니나 DC 잭에 연결하는 전선앞을 펜홀단자로 작업 후에 연결하였습니다.
완성된 모습은 위와 같습니다. 점퍼선들로 복잡해 보이지만 회로도를 보고 따라서 천천히 작업하다보면 쉽게 하실 수 있습니다.
4. 아두이노 설치 및 소스코드 업로드하기
하드웨어까지 완료되었다면 이제 음성으로 LED 스트립이 잘 제어될 수 있게 코딩작업을 진행해보도록 하겠습니다. 앞서 말씀드렸듯이 작업과정에 따라 하드웨어 구성보다 코딩작업이 먼저 진행되어도 무관함합니다.
4.1. 아두이노 통합개발환경(IDE)
아두이노 IDE(Integrated Development Environment)는 아두이노 프로그램을 작성하고 컴파일할 수 있는 통합 개발 환경입니다. 아두이노 공식 홈페이지에서 무료로 제공하고 있어서 편리하게 사용할 수 있습니다.
4.2. 아두이노 통합개발환경(IDE) 설치
아두이노 통합환경은 아두이노 공식 홈페이지(https://www.arduino.cc)에서 다운 받을 수 있습니다.
아두이노 공식 홈페이지에 접속하신 후 SOFTWARE-DOWNLOADS를 클릭해 주세요. Download the Arduino IDE 부분에서 각자 운영체제의 알맞은 버전의 파일을 선택해서 다운받으시면 됩니다. 제가 다운로드 받을 시점에서 최신 버전은 1.8.13입니다.
파일을 선택하시면 아두이노 소프트웨어에 대한 기부를 권하는 창이 뜹니다. 기부를 하지 않으시려면 JUST DOWNLOAD를 클릭하시면 다운로드 됩니다.
4.3. 아두이노 통합개발환경(IDE) 기본 세팅
IDE를 사용하는 방법에 대해 알려드리겠습니다.
IDE 환경에서 아두이노 보드(Board)와 포트(Port)를 맞춰줘야 합니다.
보드를 선택하는 방법은 메뉴바 – 툴 – 보드 – Arduino Uno 보드를 선택해 주면 됩니다.
포트 선택하는 방법은 메뉴바 – 툴 – 포트에서 아두이노에 해당하는 COM 포트를 선택해 주면 됩니다.
4.4. 소스코드 업로드하기
이번 프로젝트에 사용 할 소스코드 전체는 아래와 같습니다.
/*
www.devicemart.co.kr
Voice Recognition Module test
*/
#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3);
int relay= 7;
byte voice_recogn= 0;
void setup()
{
Serial.begin(9600);
mySerial.begin(9600);
Serial.println(“wait settings are in progress”);
pinMode(relay, OUTPUT);
delay(1000);
mySerial.write(0xAA);
mySerial.write(0×37);
delay(1000);
mySerial.write(0xAA);
mySerial.write(0×12);
Serial.println(“The settings are complete”);
}
void loop()
{
while(mySerial.available())
{
Serial.println(“voice input”);
voice_recogn=mySerial.read();
switch(voice_recogn)
{
case 0×11:
digitalWrite(relay,HIGH);
Serial.println(“relay turn on”);
break;
case 0×12:
digitalWrite(relay,LOW);
Serial.println(“relay turn off”);
break;
case 0×13:
//
break;
case 0×14:
//
break;
case 0×15:
//
break;
}
}
}
업로드 하는 방법은 아두이노 IDE 툴에서 보드와 포트가 정확히 선택되었는지 확인하신 후, 위쪽 오른편에 있는 화살표 모양을 클릭해 주시면 업로드가 진행됩니다.
업로드가 완료되면 아래편에 업로드 완료라고 표시가 됩니다.
소스 코드에 대해 설명드리겠습니다.
#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3);
int relay= 7;
byte voice_recogn= 0;
음성 인식 모듈과 아두이노를 Serial 통신으로 연결하여 사용하기 위해, 소프트웨어 시리얼 라이브러리를 포함시키고, 디지털 2번핀 3번핀을 각각 Rx핀 Tx핀으로 할당시켜주는 코드입니다. 릴레이를 제어하기 위해 디지털 7번핀에 relay 변수를 선언하고, 음성을 인식하기 위한 변수 voice_recogn를 초기값 0으로 선언해주었습니다.
void setup()
{
Serial.begin(9600);
mySerial.begin(9600);
Serial.println(“wait settings are in progress”);
pinMode(relay, OUTPUT);
delay(1000);
mySerial.write(0xAA);
mySerial.write(0×37);
delay(1000);
mySerial.write(0xAA);
mySerial.write(0×12);
Serial.println(“The settings are complete”);
}
보드에 전원이 들어왔을 때 혹은 리셋 되었을 때 한번만 동작하는 Setup()함수에 작성해야 할 내용입니다. Serial.begin()함수와 mySerial.begin()함수를 9600보오 레이트로 통신 속도로 설정해줍니다. relay 변수를 출력모드로 설정해주고, Serial통신으로 연결된 음성 인식 모듈에 명령어를 넣어 Compact Mode로 설정, Group2에 연결된 명령어를 가져와 줍니다.
void loop()
{
while(mySerial.available())
{
Serial.println(“voice input”);
voice_recogn=mySerial.read();
switch(voice_recogn)
{
case 0×11:
digitalWrite(relay,HIGH);
Serial.println(“relay turn on”);
break;
case 0×12:
digitalWrite(relay,LOW);
Serial.println(“relay turn off”);
break;
case 0×13:
//
break;
case 0×14:
//
break;
case 0×15:
//
break;
}
}
}
loop()함수에 들어가는 내용은 아두이노에 전원이 들어가 있는 한 계속 반복됩니다.
처음 While(mySerial.availble())을 통해 음성 인식 모듈로 음성이 인식되면 voice_recogn변수에 음성 인식 모듈을 통해 읽은 데이터를 저장하고, 저장한 명령어가 맞다면 switch 문을 통해 동작을 제어 할 수 있습니다. 이번 예제에서는 음성 인식 모듈로 릴레이를 on/off만 제어하기 때문에 1번 그룹의 case 0×11(명령어1), case 0×12(명령어2)만 사용하여 릴레이를 on/off하는 동작을 제어하였고, 사용하지 않을 명령어 case 0×13(명령어3), case 0×14(명령어4), case 0×15(명령어5)는 주석처리(//) 하였습니다.
5. 불키고, 끄고 확인하고 완성하기
하드웨어 구성과 코딩작업이 완료된 후에는 LED 스트립은 필요한 장소에 부착합니다.
LED스트립은 원하는 길이 만큼 컷팅이 가능하며 뒷면이 접착식으로 되어 원하는 장소에 부착이 간편합니다. 설치가 완료되고 나면 전원을 공급한 후 입력한 명령어에 따라 불이 켜지고 꺼짐을 확인해봅니다.
완성된 모습입니다. 회의실 한편이 은은한 조명으로 멋스러워졌습니다.
만일 명령어를 말했을때 불이 들어오지 않는다면?
1. LED 스트립과 릴레이가 제대로 동작하는지 확인해 보세요.
음성 인식에 대한 코드는 사용하지 않고, 1초마다 릴레이를 On/Off하는 아래 소스코드를 아두이노에 직접 업로드 해줍니다. 이를 통해 LED가 켜졌다 꺼졌다를 반복하는지를 보며 릴레이와 LED스트립이 제대로 동작하는지 확인할 수 있습니다.
이 코드를 사용해서 릴레이가 제대로 동작하지 않는다면 배선을 다시 한번 점검해보시거나 모듈의 불량을 의심하여 모듈을 교체해줍니다.
2. 음성인식 모듈이 제대로 작동하는지 확인해 보아야합니다.
아두이노와 연결했을 때 아두이노 시리얼 모니터를 통해 음성인식 모듈에 명령어가 제대로 인식되고 있는지 확인 가능합니다.
아두이노 IDE에서 오른쪽 위에 있는 시리얼 모니터 버튼을 클릭해주세요.
아두이노에서 음성 인식 모듈에 명령어를 불러오기가 끝나면 위 화면과 같이 ‘The settings are complete’라고 출력이 됩니다.
음성을 인식해 주면 아래와 같이 ‘voice input’이라는 글자가 출력이 되고, 첫번째와 두번째 명령어를 인식하면 ‘relay turn on/off’로 글자가 출력이 됩니다.
모듈이 제대로 작동하는데도 명령어를 인식하지 못한다면 더쉽고 명확한 단어로 명령어를 변경하시거나 녹음한 음정과 똑같은 음정으로 발음해보시기 바랍니다. 모듈이 제대로 작동하지 않는다면 모듈의 불량을 의심하여 모듈을 교체하기시 바랍니다.
자, 이제 모든 과정이 끝났습니다. 구독자 분들이 이번 프로젝트를 함께하며 음성인식 기능과 사물인터넷에 대해 조금이나마 쉽게 생각할 수 있는 기회가 되셨길 바라며 다음에도 더 쉽고 재미난 프로젝트로 찾아뵙도록 하겠습니다!
[61호]우울증 치료로봇 IWMYS (I will make you smile)
2020 ICT 융합 프로젝트 공모전 우수상
우울증 치료로봇 IWMYS (I will make you smile)
글 | 성균관대학교 김승년, 임혁수, 최윤수
1. 심사평
칩센 과연 우울증이 치료될지는 모르겠지만, 우선 보고서에 포함된 작품과 시연 동영상 기준으로는 흥미롭고 자연스레 웃음이 나는 작품입니다. 여러 면에서 함께 공동 개발자간에 재미있게 작품 개발이 진행되지 않았나하는 막연한 생각도 듭니다. 개발의 난이도나 기능을 따져보자면 아주 복잡도가 높지는 않지만, 반드시 좋은 작품이나 제품은 복잡해야한다라는 것을 보기좋게 뒤집을수 있다는 것을 보여주는 좋은 작품입니다.
펌테크 작품의 아이디어와 창의성, 실용성이 돋보이는 작품으로 생각됩니다. 군더더기 없이 꼭 필요한 기능으로 목적에 맞는 최적의 시스템으로 잘 구성하였고 추후 작품의 완성도를 높일경우 상업적으로 충분한 가치를 가질 수 있는 제품이 될수 있을것이라 생각됩니다. 전체적으로 작품의 기획의도, 기술 구현도, 완성도 등에서 높을 평가를 주고 싶은 작품입니다.
위드로봇 관련 문헌 조사에서부터 구현까지 완성도가 매우 높은 작품입니다. 얼굴 인식에 대한 결과 분석이 빠진 것이 아쉽습니다.
2. 작품개요
이 작품을 만들 때, 크게 2가지를 고려했다. 첫째로, ‘사회적으로 도움이 되는 제품’ 을 만들고 싶었다. 요즘 심각한 사회적 문제가 무엇이고, 이를 어떻게 하면 획기적으로 해결할 수 있을지 팀원들과 이야기를 나누었다. 그 결과 심각한 사회문제인 우울증을 해결해 보는 게 어떨까? 하는 목표를 가졌다. SNS 서비스의 발달로 서로가 늘 연결되어 있는 것처럼 이 사회에도 우울증은 증가하고 있다. 건강보험심사평가원에서 제공한 그래프를 보면 우울증이 계속해 늘어나고 있는 경향을 알 수 있다.
우리는 웃음치료법을 통해 우울증을 치료하고자 했다. 우리 몸은 웃을 때 ‘엔도르핀’이라는 호르몬이 나와서 고통이나 아픔을 잊게 해주는데, 이를 유도하기 위해서 스피커로 격려가 되는 말과, 어이가 없는 개그와, 랜덤으로 재생되는 경쾌한 음악을 재생시킴으로써 최종적으로 우울하거나 슬픈 일이 있을 때 당사자로 하여금 웃음을 짓게 하여 마치 사람과 소통하듯이 듣는 이로 하여 따뜻함을 느낄 수 있게 만들었다. 우리의 로봇이 행복한 사람들이 더 많아지는데 많이 쓰였으면 한다.
2번째로 ‘로봇의 작동방식’을 고민했다. 작동방식이 복잡하면 어르신이나, 아이들이 로봇의 작동에 어려움을 느낄 수가 있다. 단순하면서 우울함을 해소해줄 수 있는 작동방식이 필요했다.
“웃으면 복이 온다” 라는 격언이 있다. 행복해지는 가장 첫 단계는 웃는 것이라고 생각했다. 그래서 우리 로봇은 ‘억지 미소’에 작동하도록 설계되었다. 우울한 사용자가 로봇에 억지 미소를 지어주면, 음성(우리의 녹음된 목소리, 아재개그), 텍스트 메시지(힘내세요, 웃어주세요)가 출력되어 그 ‘억지 미소’를 ‘진짜 웃음’으로 바꾸어준다. 우리의 로봇은 미소 짓는 자에게 진정한 웃음을 선사한다.
3. 작품 설명
3.1. 주요 동작 및 특징
3.1.1. 주요 동작
3.1.2. 특징
OpecvCV의 Haar Cascade 알고리즘을 사용해 웃음을 검출합니다. Haar Cascade는 머신러닝 기반의 인식 방법이므로, 학습된 모델이 필요합니다. OpenCV의 github repository에서 haarcascade_frontalface_default.xml와 haarcascade_smile.xml를 다운받아 얼굴과 표정 검출에 사용했습니다.
라즈베리파이에서 웃음을 인식하면, 6가지의 아재개그와 7가지의 위로의 말을 랜덤으로 재생합니다.
아두이노는 라즈베리파이와 시리얼 통신으로 연결되어 있습니다. 라즈베리파이는 웃지 않을 때는 숫자 ‘0’을, 웃고 있을 때는 숫자 ‘1’을 전송합니다.
아두이노에서 ‘1’의 신호를 받으면, 스피커 모듈을 통해 음악을 연주하고, LCD 디스플레이에 6가지의 위로의 말을 랜덤으로 보여줍니다.
3.2. 전체 시스템 구성
3.2.1. 시스템 개요도
1단계: 카메라 수신부
2단계: 라즈베리파이
3단계: 아두이노
4단계: LCD 디스플레이 + 스피커 재생
3.2.2. 1단계-카메라 수신부
카메라 종류: Logitech WebCam C170
1.카메라 영상을 실시간으로
라즈베리파이로 전송
3.2.3. 2단계-라즈베리파이
-라즈베리파이4
1. 전송받은 영상을 실시간으로 검출
2. 얼굴을 파란색 테두리로 검출
3. 얼굴 내에서 미소를 노란색 테두리로 검출
4. 미소가 발견되면 아두이노로 시리얼신호 전송
-웃음 인식 모습
3.2.4. 3단계-아두이노
-아두이노 UNO
1.시리얼신호를 입력 받으면 스피커와 LCD를 작동시킴
3.2.5. 4단계-스피커, LCD
라즈베리파이 스피커
-웃으면 아래 중 한 개 랜덤 출력
1오늘 하루 힘들었지? 고생했어~
2.내일도 좋은 일만 있을거야
3.행복한 하루 되세요~
4.이리와~ 안아줄게
5.넌 정말 소중한 사람이야
6.지금 이대로도 괜찮아
7.내가 필요할 때, 항상 옆에 있을게!
(이하 기계음)
1.사슴이 눈이 좋으면? 굿아이디어
2.맥주가 죽기전에 남긴 말은? 유언비어
3.나무를 톱으로 캐면? 트리케라톱스
4.반성문을 영어로 하면? 글로벌
5.가장 안전한 감옥은? 안전제일
6.석유가 도착하는데 걸리는 시간은? 오일
아두이노 LCD 모니터
웃으면 아래 중 한 개 랜덤 출력
1. There will be only good thing
2. Have a happy day
3. You are so precious
4. You did a Great job!!
5. Come here, I’ll give you a hug
6. It is OK as it
-웃지 않으면
Smile ^-^ 출력
아두이노 스피커
웃으면 다음 노래 중 한 개 랜덤 재생
1.비행기
2.학교종
3.3. 개발환경(개발 언어, Tool, 사용 시스템 등)
3.3.1. 모델링 Tool
제품의 틀을 만들기 위해, 오토데스크의 ‘Inventor’와 ‘Thinkercad’를 이용했다. 치수와 자석을 넣는 세밀한 작업의 경우 원통 및 사각형은 Inverntor로, 면 단위의 전반적 3D작업에는 Thinkercad로 개체합치기 및 개체 나누기로 작업을 진행했다. 3D 프린터로 출력 시 Cubicreator는 ‘큐비콘 듀오’로 위의 뚜껑부분을 출력할 때 사용했다. 아래 부분은 2 부분으로 나누어 Cura로 작업한 후 엔더3 pro를 이용해 출력하였다.
3.3.2. software
3.3.3. 부품
4. 단계별 제작 과정
제작과정은 크게 3개로 나눠져 있다. 1) 라즈베리파이를 통한 비전인식과 녹음된 음성파일 재생, 2) 아두이노를 통한 LCD디스플레이의 텍스트 출력 및 스피커의 음악출력, 3) 마지막으로 이러한 모듈을 수용할 로봇의 디자인과 모듈의 고정
4.1. 표정인식 VISION 시스템의 구축
1. 윈도우 PC에서 웃음검출기 작동 확인
1. 아나콘다 가상환경을 생성
2. OpenCV, numpy 설치
3. 웃음검출기 구현
2. 라즈베리파이로 옮기기
1. 코드를 라즈베리파이로 옮김
2. OpenCV, numpy 설치
3. 작동 확인
3. 아두이노와 시리얼 통신 확인
1. sudo pip3 install pyserial
2. ls /dev/tty* 명령어로 ttyACM0 포트에 연결됨을 확인
3. blink 예제를 변형해, 시리얼 통신이 제대로 되는지 확인
4. pygame으로 목소리 재생 구현
1. 위로의 말, 아재개그 문구 선정
2. 녹음
3. pygame을 이용해 재생할 수 있도록 구현
4.2. 아두이노
배경음악 2종류 함수 생성 ▶ LCD 화면 출력 6종 함수 생성 ▶ 라즈베리파이로부터 serial통신 값 수신 ▶ 랜덤 변수에 따라 화면, 음악 동시출력. 라즈베리 파이에서 받은 Serial신호가 -1(신호없음) 일 때, 아무 반응도 하지 않는다. 0(무표정) 일 때, 웃음을 유도하는 문구 출력 ‘^-^’. 1(웃는표정) 일 때, random변수를 받아 여러 종류의 위로, 응원의 말과 함께 귀여운 배경음을 출력한다.
1) talkie라이브러리를 활용한 스피커제어 시도
특정 음이 아닌 녹음된 자연어를 출력하기 위해 음성데이터를 직접 만들었다. 텍스트를 음성파일로, mp3파일을 wav파일로 convert하고, normalize과정까지 거쳐 다듬은 후 비트맵으로 변환시켰다. 무료 제공된 talkie라이브러리를 사용해 코드에 적용시켰지만 음질이 매우 떨어졌다. 이 후 앰프모듈 추가, 스피커 스펙 변경, 음성데이터 수정 등의 방법을 통해 이를 극복하려 했지만 끝내 비트맵으로 변환하는 과정에서 발생하는 라이브러리 자체의 한계점이라 판단하고, 라즈베리 파이 내에서 mp3파일을 직접 적용하는 방법을 선택했다.
2) LCD 한글 출력을 위한 시도
가장 많이 쓰이는 저렴한 16×02 LCD에서는 한글출력을 지원하지 않는다. 직접 좌표를 설정해 한글을 임의로 표시할 수 있지만, CGRAM에 8개의 바이트 배열만을 저장할 수 있기 때문에 짧은 문장도 표현하기 쉽지 않고 깨져서 표현된다. 개인 라이브러리를 사용하거나, 한 글자씩 출력하는 방법을 시도해보았지만 가독성이 떨어지면 위로의 말을 전달하는 목적과 맞지 않아 쉬운 영어문장과 귀여운 이모티콘으로 대체했다.
4.3. 3단계 디자인
이 작품을 모델링 하는 과정에는 우울증을 치료해야 하기 때문에 각이 있는 모델 보다는 최대한 둥근 면적을 많이 포함하는 모델을 고려했고, 더 나아가 나무와 숲을 연상시키는 색을 입히면 어떨까? 라는 색채적인 측면도 고려했다. 머리 부분에는 구멍을 파서 꽃을 장식하고자 했다. Inventor로 대략적인 형상의 모델링을 진행했다. Inventor로는 면 단위의 볼륨감을 디자인하는 데 부족함을 느껴, Tinkercad를 이용해 기본 형상을 붙이고 자르는 방식으로 모델링을 했다.
이는 로봇 하체의 뒷모습이며 엉덩이 부근에 구멍을 뚫어 외부 전압을 공급할 수 있도록 설계하였다. 이때 Inverntor로 원통형 3개를 작업한 뒤 큰 원통형에 모깍기로 기울기를 준 다음 원뿔형을 만들었다. 이후 2개의 원기둥을 다리로 붙인 다음 원기둥의 반만큼만 기울기를 주어 원뿔 형태로 깎았다. 많은 장치들이 들어가야 하는 만큼 큰 수용 능력이 필요했고, 로봇이 사람처럼 두 팔과 두 다리를 가지길 원했다. 이에 다리의 크기를 최대한 줄여서 몸통부분이 크도록 조절하였다.
아래는 로봇 하체의 앞모습이다. 앞을 디자인 할 때에는 우울증 치료의 목적과 맞게 상대방이 사랑 받고 있다는 느낌을 주기 위해 하트 모양을 선택했으며, 손 모양은 Inverntor로는 작업하기가 어려워서 팔과 하트는 Thinkercad에서 작업을 했다. 격언을 띄워 주기 위한 아두이노 LCD 디스플레이를 배치하기 위해 하트 앞부분에 구멍을 냈으며, 손 모양은 몸체와 하트를 연결하는 역할을 한다.
이 사진은 로봇 상체 사진이며 눈 쪽에는 카메라를 설치하기 위해 구멍을 뚫었고 머리 부분에는 하트모양의 구멍과 눈 위쪽 부근에도 하트모양을 달았다. 머리 윗부분에 구멍을 파서 꽃을 심어, 사용자가 편안하게 느끼도록 디자인했으며, 인테리어 용품으로도 손색없도록 아름답게 디자인하기 위해 노력했다.
결합할 때에는 나사나 볼트를 사용하는 것 보다 (재질이 PLA라서 깨질 경우 다칠 우려가 있음) 자석을 이용해서 결합을 한다면, 쉽게 분리 및 조립이 가능하고 내부 기기의 문제가 생겼을 시 쉽게 대처가 가능해서 3D프린트 출력 시 레이어 스톱 기능을 넣어서 자석을 순간접착제로 부착시킨 뒤 말리고 출력을 진행시켰다. 아랫부분 자석4개 윗부분 자석4개 총 8개를 사용했다.
4.4. 도색 및 조립
도색할 때에는 스프레이를 이용했다. 화분과 같은 인상을 주고자 밤색과 녹색 2가지색을 채용했으며, 피부는 녹색으로 그 외에는 전부 밤색으로 도색했다. 도색을 할 때에는 원치 않는 부분까지 침범할 우려가 있어서 칠하고자 하는 부분을 제외하고는 전부 테이프로 감아서 스프레이로 작업했으며, 그 외 흘러내린 부분은 붓으로 작업을 했다.
로봇의 머리 부분에는 좌측 사진과 같이 꽃으로 장식을 하였다. 스펀지의 중앙에 구멍을 내고 글루건을 이용해서 꽃을 고정시켰다.
꽃을 통해 사용자가 편안하고 자연적인 분위기를 느꼈으면 했다.
5. 기타
5.1. 전체 회로도
5.2. 웃음 검출을 위한 Raspberry pi 코드
import numpy as np
import cv2
import serial
import pygame
import random
print(“import clear”)
faceCascade = cv2.CascadeClassifier(‘haarcascades/haarcascade_frontalface_default.xml’)
smileCascade = cv2.CascadeClassifier(‘haarcascades/haarcascade_smile.xml’)
print(“recall model”)
cap = cv2.VideoCapture(0)
cap.set(3,640)
cap.set(4,480)
ser = serial.Serial(‘/dev/ttyACM0′, 9600)
print(“camera & serial connected”)
music_file1 = “sound/didgreat.mp3″
music_file2 = “sound/goodTomorrow.mp3″
music_file3 = “sound/happyDay.mp3″
music_file4 = “sound/hugYou.mp3″
music_file5 = “sound/okayAsItIs.mp3″
music_file6 = “sound/myPrecious.mp3″
music_file7 = “sound/standByYourSide.mp3″
music_file8 = “sound/joke1.mp3″
music_file9 = “sound/joke2.mp3″
music_file10 = “sound/joke3.mp3″
music_file11 = “sound/joke4.mp3″
music_file12 = “sound/joke5.mp3″
music_file13 = “sound/joke6.mp3″
music = [music_file1, music_file2, music_file3, music_file4, music_file5, music_file6, music_file7, music_file8, music_file9, music_file10, music_file11, music_file12, music_file13]
while True:
ret, img = cap.read()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.2,
minNeighbors=5,
minSize=(20, 20)
)
for (x,y,w,h) in faces:
cv2.rectangle(img, (x,y), (x+w, y+h),(255,0,0),2)
roi_gray = gray[y:y+h, x:x+w]
roi_color = img[y:y+h, x:x+w]
smile = smileCascade.detectMultiScale(
roi_gray,
scaleFactor = 1.5,
minNeighbors=15,
minSize=(25, 25),
)
if len(smile)==0:
print(ser.write(“0″.encode()))
else:
for (x2, y2, w2, h2) in smile:
cv2.rectangle(roi_color, (x2, y2), (x2 + w2, y2 + h2), (0, 255, 255), 2)
print(ser.write(“1″.encode()))
music_number = random.randint(0, 12)
print(music_number)
pygame.mixer.init()
pygame.mixer.music.load(music[music_number])
pygame.mixer.music.play()
clock = pygame.time.Clock()
while pygame.mixer.music.get_busy():
clock.tick(30)
pygame.mixer.quit()
cv2.imshow(‘faceWithSmile’, img)
k= cv2.waitKey(30) & 0xff
if k == 27:
break
cap.release()
cv2.destroyAllWindows()
5.3. 1602LCD화면 출력과 스피커제어를 위한 Arduino 코드
#include <LiquidCrystal_I2C.h>
#include<Wire.h>
long randNumber;
int speaker=3;
LiquidCrystal_I2C lcd(0×27,16,2);
void airpl(){ //비행기
tone(speaker,659);
delay(350);
tone(speaker,587);
delay(150);
tone(speaker,523);
delay(200);
noTone(speaker);
delay(50);
tone(speaker,587);
delay(200);
noTone(speaker);
delay(50);
tone(speaker,659);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,659);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,659);
delay(200);
}
void schbell(){ //학교종
tone(speaker,784);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,784);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,880);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,784);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,784);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,784);
delay(200);
noTone(speaker);
delay(80);
tone(speaker,659);
delay(300);
}
void Hg1(){
lcd.setCursor(1,0);
lcd.print(“There will be”);
lcd.setCursor(0,1);
lcd.print(“only good thing”);
lcd.init();
}
void Hg2(){
lcd.setCursor(0,0);
lcd.print(“Have a happy day”);
lcd.init();
}
void Hg3(){
lcd.setCursor(3,0);
lcd.print(“You are so”);
lcd.setCursor(5,1);
lcd.print(“precious”);
lcd.init();
}
void Hg4(){
lcd.setCursor(0,0);
lcd.print(“You did a”);
lcd.setCursor(1,1);
lcd.print(“Great job!!”);
lcd.init();
}
void Hg5(){
lcd.setCursor(0,0);
lcd.print(“Come here, I’ll”);
lcd.setCursor(1,1);
lcd.print(“give you a hug”);
lcd.init();
}
void Hg6(){
lcd.setCursor(0,0);
lcd.print(“It is OK as it”);
lcd.init();
}
void setup(){
Serial.begin(9600);
lcd.init();
lcd.backlight();
randomSeed(analogRead(5)); //랜덤seed생성
}
void loop() {
if(Serial.available()){
int swt=(int)Serial.read(); //기분값입력
if(swt==48){ //ASCII(48)=0 (무표정)
lcd.setCursor(3,0);
lcd.print(“Smile ^-^”);
}
else if(swt==49){ //ASCII(49)=1 (웃는표정happy)
randNumber=random(1,7);
Serial.println(randNumber);
if(randNumber==1){ //1) 내일도 좋은일만 있을거야
Hg1();
schbell();
}
else if(randNumber==2){//2) 행복한하루되세요
Hg2();
airpl();
}
else if(randNumber==3){//3) 넌정말소중한사람
Hg3();
schbell();
}
else if(randNumber==4){//4) 오늘하루힘들었지고생했어
Hg4();
airpl();
}
else if(randNumber==5){//5) 이리와안아줄게
Hg5();
schbell();
}
else{ //6)지금이대로도괜찮아
Hg6();
airpl();}
}}
}
5.4. 참고문헌
1. OpenCV github repository:
2. 10, 20대 우울증 환자 현황; 2019년11월29일;건강보험심사평가원
5.5. 작품 작동 시연 동영상
[1] https://www.youtube.com/watch?v=Ulte1ao_2Tc[2] https://www.youtube.com/watch?v=qkcn13aGhbk
[3] https://www.youtube.com/watch?v=Ot1GmvqJPso
동영상에서 VNC viewer로 라즈베리파이에 원격접속해 웃음을 인식하는 모습을 촬영하였습니다. 이때, VNC viewer 자체의 딜레이와 라즈베리파이의 느린 연산으로 인한 딜레이가 겹쳐, 모니터 화면에 버퍼링이 있습니다. 하지만 스피커를 통해 정상작동하고 있음을 확인할 수 있습니다.