소리와 불빛이 나는 계단 프로토타입

  1. 프로토타입의 사진/영상 및 코드

– LED는 14번 핀이 HIGH 일 때 3.3V를 공급받는다.
– Sharp IR 센서가 출력하는 데이터(전압)는 ADC를 통해 디지털화 되어 파이에 전달된다. 이 과정에서 spidev 라이브러리에 있는 spi 객체가 사용된다.

import time

# 적외선 센서를 사용하기 위한 사전 작업
import spidev 
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 100

# 음악을 재생하기 위한 부분
import pygame 
pygame.init()
pygame.mixer.init()

sound_channel = pygame.mixer.Channel(0)
sound = "piano-C4.wav" # 음에 따라 다른 파일 필요(C4, D4)
sound = pygame.mixer.Sound(sound); 

# LED 재생을 위한 부분
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM) # GPIO 핀 세팅 방식 설정
GPIO.setwarnings(False)
GPIO.setup(14, GPIO.OUT, initial = GPIO.LOW) # 14번 핀이 HIGH로 설정될 때, LED가 켜진다

def readChannel(channel) :
  val = spi.xfer2([1, (8 + channel) << 4, 0])
  data = ((val[1] & 3) << 8) + val[2]
  return data

# 거리 측정 함수
def measure_distance() :
  v = (readChannel(0) / 1023.0) * 3.3
  dist = 16.2537*v**4 - 129.893*v**3 + 382.268*v**2 - 512.611*v + 301.439
  return dist

# 측정하고자 하는 위치에 발이 존재하는지를 알려줌
def should_play() :
    distance = measure_distance()
    if distance < 60 :
        return True
    return False
    
# sound channel을 통해 소리 재생
def sound_on() :
    sound_channel.play(sound)

# 소리 재생 중지
def sound_off() :
    sound_channel.stop()
    
try:
    music_on = False
    was_on = False
    
    while True:
        time.sleep(0.1) # 0.1초에 한 번 씩 재생
        music_on = should_play() # 음악이 켜져야 하는가?

        if music_on == was_on : # 이전 상태와 지금 상태가 같다면, 변화가 필요없다.
            pass
        elif music_on : # 음악이 재생되어야 한다면,
            sound_on() # 소리 재생
            GPIO.output(14, GPIO.HIGH) # LED on
        else : # 음악이 꺼져야 한다면,
            sound_off() # 소리 재생 중지
            GPIO.output(14, GPIO.LOW) # LED off
            
        was_on = music_on # 현 상태 정보 저장
                    
except KeyboardInterrupt :
    pass

2. 적외선 센서의 원리

  • 적외선 센서가 적외선을 방출하면 적외선이 물체에 반사되어 다시 센서로 돌아온다. 이때 적외선이 반사된 위치에 따라 센서에 입력되는 적외선의 위치가 다르고, 이에 따라 센서가 발신하는 데이터, 즉 전압의 크기가 달라진다. 거리에 따른 전압의 그래프는 위와 같이 나타난다.
  • 이 때, 0~15cm에서 출력하는 값과, 15cm 이후로 출력하는 전압이 일부 겹친다. (실제로 둘 중 더 큰 거리를 측정값으로 보여준다.) 따라서 0~15cm에서 유의미한 값을 출력한다고 보기 어렵다.

3. 시행착오들

  • 센서 측정하는 과정에서 소리가 씹히는 현상이 발생함.

현재 코드에 따르면, 센서의 측정 위치에 사물이 있느냐 없느냐에 따라 소리가 재생되거나 종료한다. SPI 버스의 데이터 전송 주기를 1000,000 hz로 설정하고, main 함수의 while문을 반복적으로 재생하였을 경우, 한 번에 연속적으로 소리가 재생되는 것이 아닌, 불연속적으로 여러번 소리가 났다. 정확한 발생 원인은 모르지만, 같은 값을 너무 자주 SPI bus를 통해 전송함으로써 오류가 생기지 않았나 추측한다.

SPI 버스의 데이터 전송 주기를 낮추고, while문 재생에 간격을 둠으로써 음이 한번에 연속적으로 재생될 수 있게 하였다.

  • 휴대폰으로 오디오를 출력하려는 노력

오디오를 이어폰으로 출력하면 영상을 촬영할 수 없고, HDMI로 출력하면 모니터 간의 간격이 너무 넓어서 라즈베리 파이 두 개를 나란히 두기 어려웠다. 그래서 휴대폰 또는 모니터와 bluetooth pairing을 하여 휴대폰 또는 모니터로 오디오를 출력하고자 하였다.

라즈베리파이와 휴대폰을 bluetooth pairing하는 것까지 성공했으나, 오디오 출력을 휴대폰으로 바꾸는 방법을 찾지 못했다. 블루투스 스피커를 연결할 경우, 라즈베리파이의 상태표시줄 중 볼륨 아이콘을 오른쪽 마우스로 클릭하면 블루투스 스피커가 표시된다고 하는데, 휴대폰은 표시되지 않는 것으로 보아 파이의 오디로를 휴대폰에서 출력하는 기능이 존재하지 않는 것 같다.

결국 HDMI 모니터 두 개를 사용하기로 했고, 그래서 전선의 길이를 길게 할 필요가 있었다.

  • 전선의 길이가 짧음

LED strip에 전선을 연결할 때에는 납땜을 했고, 전선의 반대쪽 끝은 절연테이프를 활용해 이었다. 적외선 센서에 연결된 전선을 길게 하기 위해서 양끝의 모양이 다른 점프선들을 여러 개 연결하였다.

  • 적외선 센서 측정에 오류가 생김

적외선 센서를 바닥에 그대로 부착했을 때 거리 측정값이 엉망으로 출력되었다. 원인은 적외선 센서의 각도가 약간 아래를 향하고 있던 것이었다. 각도를 바닥과 나란하게 조정하자 올바른 출력값을 얻을 수 있었다.


Written by. 하은&혜정

IR 센서 및 mysql 사용, 음악 재생을 위한 준비!!

Sharp IR 센서
1. SPI 라이브러리 설치.
2. cmd에 sudo raspi-config 을 친 다음, 5번, P4, Yes을 차례대로 선택하고, PI을 reboot 한다.
3. etc/modules-load.d에 있는 modules.conf에 spi-bcm2807을 추가한다.

<주의 사항>
– 파이의 방향과 회선의 방향을 확인한다!
– 코드 작성 시, SPI object의 max_speed_hz를 설정하는 것을 잊으시면 안됩니다! 만약 설정하지 않을 경우, max_speed_hz = 0으로 설정되어 측정이 되지 않습니다.

Pygame 라이브러리
1. sudo apt-get install python-pygame // pygame 설치

구체적인 사용 방식은
https://bit.ly/2GCKpCN
https://bit.ly/2Z3r0kN
을 참고하여 코드를 작성해 주세요!

Pymysql
78project는 파이에 있는 센서를 통해 값을 측정하고, 적절한 처리를 하여 해당 정보를 서버에 업로드 합니다. 그리고 서버에서는 값을 저장하기 위해 mysql 데이터베이스를 사용합니다. 그렇다면 과연 파이에 mysql을 다운 받아야할까요? 그렇지 않습니다. 파이에서는 mysql client 역할을 해주는 pymysql 라이브러리를 단순히 import 하여 사용하면 됩니다.

구체적인 사용 방식은
https://bit.ly/2rUUu8i
https://bit.ly/2Z3r0kN
을 참고하여 작성하여 주세요!

mysql에서 원격 접근(pymysql) 허용하기
pymysql이 mysql client 역할을 하긴 하지만, mysql에서 특정 계정에 대해 미리 원격 접근을 허용하지 않는다면 원격으로 접속이 불가능합니다. 우선 root 계정은 원격으로 접속이 불가능합니다. 따라서 새로운 계정을 만들어, 특정 데이터 베이스 또는 모든 데이터 베이스에 대해 원격 접속 권한을 부여해야 합니다.

– ‘1.2.3.4’ 라는 IP에서 fooUser를 통해 fooDatabase에 접속을 허용하는 경우 :
GRANT ALL ON fooDatabase.* TO fooUser@’1.2.3.4′ IDENTIFIED BY ‘my_password’;

– 모든 IP에서 fooUser를 통해 fooDatabase에 접속을 허용하는 경우 :
GRANT ALL ON fooDatabase.* TO fooUser@’%’ IDENTIFIED BY ‘my_password’;

다른 포스트에서도 볼 수 있듯이, 현재는 lee 유저를 통해 서버의 78project 데이터베이스를 모든 IP에서 원격 접속이 가능합니다.

해당 설치 및 사용 방법은 raspberry pi에 해당 library들을 반복적으로 설치하며 보충하겠습니다!

서버 구축 및 DB 다루기

서버는 https://jimnong.tistory.com/612 을 참조하여 구축하였습니다. 78 project의 경우, 웹사이트가 필요하지 않기 때문에 SSL 인증서는 발급 받지 않았습니다.

WIFI는 여러 대의 기기가 연결될 수 있도록 여러 port가 존재합니다. 하지만 port의 수 역시 유한하기 때문에, WIFI에 연결 시 공유기가 유동적으로 port를 할당합니다.

이러한 특성 때문에, WIFI에 새로 연결 될 때마다 새로운 내부 IP 주소가 할당됩니다. 78 project의 경우, 실시간으로 DB에 값을 업로드하고 다운받아야하기 때문에, 서버가 유동적 IP 주소를 가진다는 것은 문제가 됩니다. 이러한 문제를 해결하기 위해, 서버 구축 시 port folding을 통해 서버가 일정한 내부 IP 주소를 가지도록 하였습니다.

포트 포워딩 및 방화벽 해제는 해당 사이트를 참고하여 진행하였습니다.
https://www.lifewire.com/how-to-port-forward-4163829
https://bit.ly/33zjBgz

서버 IP 주소 : 192.168.31.114
Apache Port : 80
mysql Port : 3306

설치 된 mysql을 사용하여 데이터베이스 및 테이블을 구축할 수 있습니다. 해당 mysql 강의를 참고하였습니다.
https://opentutorials.org/course/3161/19532

Piano 음 재생 및 DB 속도 측정하기

Sharp GP2Y0A02YK0F IR 센서를 사용하였습니다. 해당 센서에 관한 정보는 아래 그림을 클릭하면 확인 할 수 있습니다.

import pymysql
import pygame
import spidev
import time

spi = spidev.SpiDev() 
spi.open(0, 0)
spi.max_speed_hz = 1000000

pygame.init()
pygame.mixer.init()

DISTANCE = [20, 30, 40]

sound_channel = pygame.mixer.Channel(0)
sound_list = ["piano-C4.wav", "piano-D4.wav", "piano-E4.wav", "piano-F4.wav"]
for i in range(4) :
    sound_list[i] = pygame.mixer.Sound(sound_list[i]);

cnx = pymysql.connect(host = '192.168.31.114',
                      user = 'lee',
                      password = 'cdt',
                      database = '78project',
                      autocommit = True
                      )

def readChannel(channel) :
  val = spi.xfer2([1, (8 + channel) << 4, 0])
  data = ((val[1] & 3) << 8) + val[2]
  return data

def measure_distance() :
  v = (readChannel(0) / 1023.0) * 3.3
  dist = 16.2537*v**4 - 129.893*v**3 + 382.268*v**2 - 512.611*v + 301.439
  return dist

def which() :
    distance = measure_distance()
    if distance > 130 :
        note = -1
    elif distance < DISTANCE[0] :
        note = 0
    elif distance < DISTANCE[1] :
        note = 1
    elif distance < DISTANCE[2] :
        note = 2
    else :
        note = 3
    return (note, distance)
    
def sound_on(idx) :
    sound_channel.play(sound_list[idx])

def sound_off(idx) :
    sound_channel.stop()

def replay(idx) :
    sound_channel.stop()
    sound_channel.play(sound_list[idx])
    
try:
    playing = -1
    note = -1
    start_time = time.time()
    
    while True:
        playing = note
        data = which()
        note = data[0]
        dist = data[1]
        
        with cnx.cursor() as cursor :
            # PUSH data to database
            sql = 'UPDATE changed_note SET note_id = ' + str(note) + ';'
            cursor.execute(sql)
            sql = 'SELECT * FROM changed_note;'
            cursor.execute(sql)
            rows = cursor.fetchone()

            # PULL data from database
            idx = int(rows[0])
            
            # Error if push & pull value don't match
            if note != idx :
                print("Database Error")
            #else :
                #print(idx)

            # Play & Replay & Stop playing
            if note == -1 :
                sound_off(note)
                
                if playing != note :
                    print("off")
                    print("distance: %dcm" %dist)
                
            elif playing == note :
                if sound_channel.get_busy() :
                    if time.time() - start_time > 0.5 :
                        replay(note)
                        start_time = time.time()

                        print("replay")
                        print("distance: %dcm" %dist)    
                        
                    else :
                        pass
            else :
                sound_off(playing)
                sound_on(note)
                start_time = time.time()

                print("change")
                print("distance: %dcm" %dist)
                    

except KeyboardInterrupt :
    pass

finally :
    cnx.close()
  • 음원은 freesound.org에서 무료로 다운 받았습니다.
  • 음 재생을 시현해 보기 위해 서버에 있는 78project 데이터베이스에 changed_note라는 table을 생성하였습니다. changed_note 데이터베이스에는 현재 재생되어야하는 음의 id를 저장합니다. (C4 : 0, D4 : 1, E4 : 2, F4 : 3, 아무 음 재생 안하는 경우 : -1)
  • 데이터베이스에 정보를 push 하고 pull 하는 유일한 이유는 통신 속도를 측정하기 위함입니다. push와 pull은 전체적인 기능에 다른 영향을 주지 않습니다.
  • 음원의 일부를 반복 재생하기 위해 0.5초를 재생한 뒤, 다시 처음부터 재생하도록 하였습니다. 하지만 stop 한 뒤 play 하는 데까지 약간의 지연 시간이 있기 때문에, 음이 연속적으로 재생되지 않는 문제를 보입니다.

Ultrasonic 및 Sharp IR 센서로 거리 측정하기

Ultrasonic 센서 이용하기

https://bit.ly/2GCKpCN를 참고하여 Ultrasonic 센서로 거리를 측정하였습니다. https://tutorials-raspberrypi.com/raspberry-pi-ultrasonic-sensor-hc-sr04/ 를 참고하시면 됩니다.

import RPi.GPIO as GPIO 
import time
instrument = 0

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

trig = 20
echo = 21
GPIO.setup(trig, GPIO.OUT) # for light shooting
GPIO.setup(echo, GPIO.IN) # for light receiving

DISTANCE = 10 # 10cm
before = 0
now = 0

def measure_distance():
    time.sleep(1)
    GPIO.output(trig, GPIO.HIGH)
    time.sleep(0.001)
    GPIO.output(trig, GPIO.LOW)
    ## shoot for 0.001 second(output)
 
    start = 0
    end = 0
 
    # until there is no input
    while GPIO.input(echo) == GPIO.LOW:
        start = time.time()
 
    # if there is input
    while GPIO.input(echo) == GPIO.HIGH:
        end = time.time()
 
    duration = end - start
    distance = (duration * 340 * 100) / 2
    print("distance: %dcm" %distance)
 
    return distance
 
def state(distance):
    global now
    if distance <= DISTANCE:
        now = 1
    else:
        now = 0
 
def change():
    distance = measure_distance()
    before = now
    state(distance)
    
    if before != now:
        return 1
    else:
        return 0
     
def sense():
    if change() == 0:
        print("stay")
        return 0    #stay
    elif now == 1:
        print("on")
        return 1    #on
    else:
        print("off")
        return -1   #off
    
 #---
     
try:
    while True:
        sns = sense()
        if sns == 0:
            pass
        elif sns == 1:
            print("on")
        else:
            print("off")

except KeyboardInterrupt:
    GPIO.cleanup()

Sharp IR 센서 이용하기

Sharp IR 센서를 사용하기 위해
https://tutorials-raspberrypi.com/infrared-distance-measurement-with-the-raspberry-pi-sharp-gp2y0a02yk0f/
사이트를 참고하였습니다. 주어진 회로와 같이 연결하고, 아래와 같이 코드를 작성하였습니다.

아래 코드는 SPI 객체를 사용하기 위해 spidev library를 import 합니다. SPI는 Serial Peripheral Interface의 약자로, 두 장치간 양방향 통신을 위해 사용되는 프로토콜입니다. Raspberry pi의 경우, 아날로그 신호를 디지털 신호로 변환하는 GPIO 핀이 없기 때문에 별도의 ADC(analog to digital converter)를 사용해야 합니다. spidev library는 adc를 통해 생성된 digital 값을 raspberry pi 내부로 받아오기 위해 사용됩니다. SPI 객체는 raspberry pi에 있는 값과 adc를 통해 변환된 digital 값 3 byte를 교환합니다. raspberry pi에서 전송되는 3 byte는 adc에게 의미 없는 값이지만, adc를 통해 받아온 거리 값은 raspberry pi에게 의미 있는 값이므로 이를 받아 사용합니다.
# 해당 경우, ADC는 MCP8008입니다.

import spidev
import time

spi = spidev.SpiDev() # create spi object
spi.open(0,0) # open spi port 0, device (CS) 0, for the MCP8008
spi.max_speed_hz = 1000000 # set transfer speed

DISTANCE = 10 # 10cm
before = 0
now = 0

def readChannel(channel):
  val = spi.xfer2([1,(8+channel)<<4,0]) 
# 3 바이트의 데이터 1, (8+channel)<<4, 0을 순차적으로 보내고 3바이트를 받습니다.
  data = ((val[1]&3) << 8) + val[2]
  return data

def measure_distance():
  v = (readChannel(0)/1023.0)*3.3
  dist = 16.2537 * v**4 - 129.893 * v**3 + 382.268 * v**2 - 512.611 * v + 301.439
  print("distance: %dcm" %dist)
  return dist
 
def state(distance):
    global now
    if distance <= DISTANCE:
        now = 1
    else:
        now = 0
 
def change():
    global before
    distance = measure_distance()
    before = now
    state(distance)
    
    if before != now:
        return 1
    else:
        return 0
     
def sense():
    if change() == 0:
        print("stay")
        return 0    #stay
    elif now == 1:
        print("on")
        return 1    #on
    else:
        print("off")
        return -1   #off
    
 #---
     
try:
    while True:
        time.sleep(0.5)
        sns = sense()
        if sns == 0:
            pass
        elif sns == 1:
            print("on")
        else:
            print("off")

except KeyboardInterrupt:
    pass

참고 문헌