Created
November 3, 2017 17:41
-
-
Save jjangga0214/9fee7b514f779deff21d680b2c477aa6 to your computer and use it in GitHub Desktop.
창업연계공학입문 components
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import RPi.GPIO as GPIO | |
import time | |
from enum import Enum | |
import functools | |
import threading | |
import getch | |
class Motor: | |
''' | |
모터 클래스이다. RCar 클래스에서 각각 왼쪽 모터와 오른쪽 모터의 객체를 만드는데에 쓰인다. | |
''' | |
class Type(str, Enum): | |
''' | |
모터의 type 을 결정하기 위한 Enum 클래스이다. | |
모터가 왼쪽인지 오른쪽인지를 결정하기 위한 인자로 쓰인다. | |
''' | |
LEFT = "left" | |
RIGHT = "right" | |
def __init__(self, motortype: Type, cali_dir: bool): | |
''' | |
:param motortype: 생성자 내부에서 왼쪽 모터인지 오른쪽 모터인지 분별하는데에 쓰인다. | |
:param cali_dir: forward0 또는 forward1 에 해당하는 값을 받는다. | |
초기화: 인스턴스 변수들 이름의 prefix 로 _ 를 사용하여 관례적 캡슐화했다. | |
''' | |
if motortype is Motor.Type.LEFT: | |
self._a = 12 # MotorLeft_A 에 해당한다. | |
self._b = 11 # MotorLeft_B 에 해당한다. | |
self._pwm_num = 35 # MotorLeft_PWM 에 해당한다. | |
elif motortype is Motor.Type.RIGHT: | |
self._a = 15 # MotorRight_A 에 해당한다. | |
self._b = 13 # MotorRight_B 에 해당한다. | |
self._pwm_num = 37 # MotorRight_PWM 에 해당한다. | |
else: | |
raise ValueError("Type should be either Motor.Type.LEFT or Motor.Type.RIGHT") | |
for el in (self._a, self._b, self._pwm_num): | |
GPIO.setup(el, GPIO.OUT) | |
# self._pwm은 LeftPwm 또는 RightPwm 에 해당한다. | |
self._pwm = GPIO.PWM(self._pwm_num, 100) | |
self._cali = cali_dir | |
self._pwm.start(0) | |
@property | |
def pwm(self): | |
''' | |
pwm 객체는 모터 객체 외부에서 저수준으로 모터를 다루고 싶어하는 경우를 위해 getter를 제공한다. | |
레퍼런스가 바뀌는 것을 방지하기 위해서 setter 는 제공하지 않는다. | |
''' | |
return self._pwm | |
def forward(self, speed: int): | |
''' | |
모터를 앞으로 구동시킨다. | |
''' | |
self._dir(self._cali, speed) | |
def backward(self, speed: int): | |
''' | |
모터를 뒤로 구동시킨다. | |
''' | |
self._dir(not self._cali, speed) | |
def _dir(self, dir: bool, speed: int): | |
''' | |
의도상으로는 private 한 method 이다. | |
:param dir: | |
앞으로 갈지 뒤로 갈지를 결정한다. | |
모터가 왼쪽인지, 오른쪽인지, 앞으로 갈지, 뒤로갈지에 따라 | |
forward0, forward1, backward0, backward0 | |
에 해당하는 값이다. | |
''' | |
GPIO.output(self._a, GPIO.HIGH if dir else GPIO.LOW) | |
GPIO.output(self._b, GPIO.LOW if dir else GPIO.HIGH) | |
GPIO.output(self._pwm_num, GPIO.HIGH) | |
self._pwm.ChangeDutyCycle(speed) | |
def stop(self): | |
GPIO.output(self._pwm_num, GPIO.HIGH) | |
self._pwm.ChangeDutyCycle(0) | |
class UltraSonicSensor: | |
''' | |
초음파 센서 클래스이다 | |
''' | |
def __init__(self, *, trig=33, echo=31): | |
self._trig = trig | |
self._echo = echo | |
GPIO.setup(self._trig, GPIO.OUT) | |
GPIO.setup(self._echo, GPIO.IN) | |
def calc_distance(self): | |
''' | |
:return : 앞에 위치한 장애물과의 거리(단위 : cm) | |
''' | |
self._fire() | |
# wait till signal of echo become HIGH | |
while GPIO.input(self._echo) == 0: | |
started = time.time() | |
# wait till signal of echo become LOW | |
while GPIO.input(self._echo): | |
ended = time.time() | |
return (ended - started) * 17000 | |
def _fire(self, duration=0.00001): | |
''' | |
초음파를 쏜다. | |
''' | |
GPIO.output(self._trig, False) | |
time.sleep(0.5) | |
GPIO.output(self._trig, True) | |
time.sleep(duration) | |
GPIO.output(self._trig, False) | |
""" | |
모터의 주의사항 | |
실험해본 결과 왼쪽 모터와 오른쪽 모터는 서로 다른 속력으로 회전한다. | |
그냥 서로 다른 속력인 것이 아니라, | |
speed 가 낮을때와 높을때 서로 다른 비율로 속도가 달라지는 것을 관찰할 수 있었고, | |
또한, 매번 같은 speed 로 실험하더라도 작동할때마다 조금씩 달라진다. | |
그렇지만, 다행인 것은 비록 속도가 매번 달라지더라도, | |
그 편자가 일정 범위 안에 존재하여 소프트웨어로 '어느 정도는' 조정가능하다는 점이다 | |
(물론 향후 또 꽤나 크게 변화할 확률이 적으나마 있다.) | |
""" | |
class RCar: | |
''' | |
라즈베리파이를 기반으로 좌우에 모터를 하나씩 장착하고 초음파 센서도 장착한 구동체를 모델링한 클래스이다. | |
핵심 클래스이다. | |
''' | |
car = None | |
@classmethod | |
def get_instance(cls): | |
''' | |
RCar 객체는 구동체를 다룬다. 논리적으로 사실 구동체는 하나뿐이므로, | |
RCar 객체가 여러개 생성되지 않고, 하나만 생성되는 것이 바람직하다. | |
즉, 싱글톤이어야 적합하다. | |
이 메서드를 호출하면, 이미 싱글톤이 생성되어 있는 경우, 해당 싱글톤을 반환한다. | |
그렇지 않다면 싱글톤을 생성한 후, 반환한다. | |
:return: 싱글톤 RCar 객체 | |
''' | |
if not cls.car: | |
cls.car = RCar() | |
return cls.car | |
class TurnType(Enum): | |
''' | |
turn의 종류를 결정하기 위한 Enum 클래스이다 | |
swing 턴인지 point 턴인지를 구별하는데에 쓰인다. | |
''' | |
SWING = "swing" | |
POINT = "point" | |
class Helper: | |
@staticmethod | |
def linear(log: callable): | |
''' | |
데코레이터이다. | |
현재로서는 RCar의 forward 또는 backward에 적용되어 공통 로직 수행, 인자의 유효성 확인, 로깅을 담당하고 있다. | |
:param log: | |
움직임 정보를 기룩 또는 출력하는데 사용할 함수 | |
사용예시 : 그냥 콘솔에 출력시 print 를 사용하고, 파일에 입력할 때는 logger를 이용한다. | |
''' | |
def inner_decorate(func): | |
@functools.wraps(func) | |
def wrap(self, speed, seconds=0, safe_distance=None, daemon=True): | |
''' | |
:param self: RCar 객체 | |
:param seconds: 몇초간 움직일지 지정. second 가 명시되면 safe_distance에 상관없이 무조건 그 시간동안 진행한다. | |
:param safe_distance: 거리. 앞에 장애물이 있을 시 어느정도 거리에서 멈출지를 지정. | |
만일 safe_distance에 명시적인 값이 주어지지 않으면 장애물이 있던 없던 계속 움직인다. | |
:param daemon: 무한 루프 안에서 초음파 센서가 거리를 재는 것을 데몬 스레드에서 실행할지 여부 | |
:return: decorating 된 함수의 리턴값 | |
''' | |
if speed < 0: | |
raise ValueError("Speed must be 0 or positive value. Given value : %s" % speed) | |
elif seconds < 0: | |
raise ValueError("Seconds must be 0 or positive value. Given value : %s" % speed) | |
# log 를 찍는다. | |
log("[%s] speed = %s, time = %s" % (func.__name__, speed, seconds)) | |
exec_result = func(self, speed) | |
if 0 < seconds: | |
time.sleep(seconds) | |
self.stop() | |
if safe_distance: | |
if safe_distance < 0: | |
raise ValueError( | |
"safe_distance must be 0 or positive value. Given value : %s" % safe_distance) | |
def chk_distance(): | |
''' | |
무한 루프를 돌면서 장애물과의 거리를 측정하고, | |
만일 safe_distance 보다 적은 값으로 측정되면 stop() 시킨다. | |
''' | |
# DISTANCE_CHK_CYCLE 에 주어진 시간을 주기로 해서 거리를 측정한다. | |
DISTANCE_CHK_CYCLE = 0.05 | |
while True: | |
time.sleep(DISTANCE_CHK_CYCLE) | |
if self.ultrasensor.calc_distance() <= safe_distance: | |
self.stop() | |
break | |
if daemon: | |
# chk_distance 함수에서 무한 루프를 돌기 때문에 데몬 스레드에서 호출하도록 한다. | |
chkthread = threading.Thread(target=chk_distance) | |
chkthread.daemon = True | |
chkthread.start() | |
else: | |
chk_distance() | |
return exec_result | |
return wrap | |
return inner_decorate | |
@staticmethod | |
def turn(*, speed90deg: int, seconds90deg: float, turntype): | |
''' | |
:param speed90deg: deg 를 90 으로 호출했을 때 지정할 실험적으로 최적화된 speed | |
:param seconds90deg: deg 를 90 으로 호출했을 때 지정할 실험적으로 최적화된 seconds | |
:param turntype: swing 턴인지 point 턴인지를 명시 | |
값은 RCar.TurnType.SWING 또는 RCar.TurnType.POINT 이면 된다. | |
''' | |
def inner_decorate(func): | |
@functools.wraps(func) | |
def wrap(self, *, deg: int = 90, speed: float = 0, seconds=0, stop: bool = False): | |
''' | |
:param deg: 턴할 각도. 90 로 주어질 시 speed는 speed90deg로, seconds는 seconds90deg로 초기화한다. | |
만일 90이 아니면, RCar._time_speed에 deg 를 넘겨서 턴할 speed와 seconds 를 추정해 계산한다. | |
:param speed: 턴할 속도. 이 값을 명시하면 deg 를 통해 추정 계산된 값을 덮어쓴다. | |
:param seconds: 턴할 시간. 이 값을 명시하면 deg 를 통해 추정 계산된 값을 덮어쓴다. | |
:param stop: 턴 후에 stop 할지 여부 | |
''' | |
if deg is 90: | |
speed = speed90deg | |
seconds = seconds90deg | |
else: | |
t_s = RCar._time_speed(deg, turntype) | |
if not speed: | |
speed = t_s[1] | |
if not seconds: | |
seconds = t_s[0] | |
exec_result = func(self, speed) | |
if 0 < seconds: | |
time.sleep(seconds) | |
if stop: | |
self.stop() | |
return exec_result | |
return wrap | |
return inner_decorate | |
def __init__(self): | |
GPIO.setmode(GPIO.BOARD) | |
GPIO.setwarnings(False) | |
# 왼쪽 모터를 초기화한다. | |
self._lmotor = Motor(Motor.Type.LEFT, True) | |
# 오른쪽 모터를 초기화한다. | |
self._rmotor = Motor(Motor.Type.RIGHT, False) | |
# 초음파 센서를 초기화한다. | |
self._ultrasensor = UltraSonicSensor() | |
# 무선 조종시 사용할 단축키들을 초기화한다. | |
self.update_shortcuts(100, 30, stop=True) | |
def update_shortcuts(self, speed, safe_distance, stop=False): | |
''' | |
무선 조종시 사용할 단축키들을 초기화한다. | |
''' | |
self.actions = { | |
"1": lambda: self.forward(speed, 0, safe_distance), # 장애물을 만나기 전까지 앞으로 감 [이유: 두번째 인자인 seconds에 0을 주었기 때문] | |
"w": lambda: self.sforward(), # 조금만 앞으로 간다. | |
"s": lambda: self.sbackward(), # 조금만 뒤로 간다. | |
"h": lambda: self.slswing(stop=stop), # 조금만 왼쪽으로 swing턴. | |
"j": lambda: self.srswing(stop=stop), # 조금만 오른쪽으로 swing 턴. | |
"k": lambda: self.slpoint(stop=stop), # 조금만 왼쪽으로 point 턴. | |
"l": lambda: self.srpoint(stop=stop), # 조금만 오른쪽으로 point 턴. | |
"v": lambda: self.stop(), # stop. | |
} | |
@property | |
def ultrasensor(self): | |
return self._ultrasensor | |
@staticmethod | |
def _adjust_speed(speed: float, adjustments: dict): | |
''' | |
왼쪽[l]과 오른쪽[r] 모터에 전달할 속력을 조정한다. | |
인자로 받은 가중치와 스피드가 가질수 있는 속력의 상한(100)을 모두 고려해 | |
왼쪽과 오른쪽 모터에 전달할 최종 속력을 각각 계산해 리턴한다. | |
:param speed: 주어진 스피드 | |
:param adjustments: 스피드를 조정할 가중치를 가진 딕셔너리 | |
:return: 가중치와 스피드의 상하 | |
''' | |
l_adjusted_speed = adjustments[Motor.Type.LEFT] * speed | |
r_adjusted_speed = adjustments[Motor.Type.RIGHT] * speed | |
# 만일 조정한 속력이 100을 넘으면 100으로 세팅한다. | |
l_adjusted_speed = l_adjusted_speed if l_adjusted_speed <= 100 else 100 | |
r_adjusted_speed = r_adjusted_speed if l_adjusted_speed <= 100 else 100 | |
return {Motor.Type.LEFT: l_adjusted_speed, Motor.Type.RIGHT: r_adjusted_speed} | |
@Helper.linear(log=print) | |
def forward(self, speed: float): | |
def adjust(): | |
''' | |
lc : 왼쪽 모터의 속도에 곱해질 계수 | |
rc : 오른쪽 모터의 속도에 곱해질 계수 | |
''' | |
lc = 1 | |
rc = 1 | |
if 80 <= speed: | |
lc = 1.0 | |
rc = 1.0 | |
elif 60 <= speed: | |
lc = 1.0 | |
rc = 1.1 | |
elif 40 <= speed: | |
lc = 1.0 | |
rc = 1.1 | |
return {Motor.Type.LEFT: lc, Motor.Type.RIGHT: rc} | |
adjusted = RCar._adjust_speed(speed, adjust()) | |
self._lmotor.forward(adjusted[Motor.Type.LEFT]) | |
self._rmotor.forward(adjusted[Motor.Type.RIGHT]) | |
@Helper.linear(log=print) | |
def backward(self, speed: float): | |
def adjust(): | |
''' | |
lc : 왼쪽 모터의 속도에 곱해질 계수 | |
rc : 오른쪽 모터의 속도에 곱해질 계수 | |
''' | |
lc = 1 | |
rc = 1 | |
if 80 <= speed: | |
lc = 1.1 | |
rc = 1.0 | |
elif 60 <= speed: | |
lc = 1.1 | |
rc = 1.0 | |
elif 40 <= speed: | |
lc = 1.3 | |
rc = 1.2 | |
return {Motor.Type.LEFT: lc, Motor.Type.RIGHT: rc} | |
adjusted = RCar._adjust_speed(speed, adjust()) | |
self._lmotor.backward(adjusted[Motor.Type.LEFT]) | |
self._rmotor.backward(adjusted[Motor.Type.RIGHT]) | |
def sforward(self): | |
''' | |
slightly forward 의 줄임말 | |
짧은 시간동안 아주 살짝 앞으로 간다. | |
''' | |
self.forward(speed=100, seconds=0.05) | |
def sbackward(self): | |
self.backward(speed=100, seconds=0.05) | |
def repeat(self, speed: float, seconds: float): | |
''' | |
먼저 앞으로 이동 후, 뒤로 이동한다. | |
''' | |
self.forward(speed, seconds) | |
self.backward(speed, seconds) | |
def stop(self): | |
self._lmotor.stop() | |
self._rmotor.stop() | |
@Helper.turn(speed90deg=80, seconds90deg=0.9, turntype=TurnType.SWING) | |
def lswing(self, speed): | |
''' | |
left swing turn 의 줄임말. | |
왼쪽으로 swing 턴 한다. | |
:param speed: 턴할 속도 | |
''' | |
self._rmotor.forward(speed) | |
self._lmotor.stop() | |
@Helper.turn(speed90deg=80, seconds90deg=0.8, turntype=TurnType.SWING) | |
def rswing(self, speed): | |
self._rmotor.stop() | |
self._lmotor.forward(speed) | |
@Helper.turn(speed90deg=80, seconds90deg=0.6, turntype=TurnType.POINT) | |
def lpoint(self, speed): | |
self._rmotor.forward(speed) | |
self._lmotor.backward(speed) | |
@Helper.turn(speed90deg=80, seconds90deg=0.5, turntype=TurnType.POINT) | |
def rpoint(self, speed): | |
self._rmotor.backward(speed) | |
self._lmotor.forward(speed) | |
def slswing(self, stop=False): | |
''' | |
slight left swing turn 의 줄임말. | |
왼쪽으로 아주 살짝 swing턴 한다. | |
:param stop: 회전 후 stop 할지 여부 | |
''' | |
self.lswing(deg=0, speed=100, seconds=0.05, stop=stop) | |
def srswing(self, stop=False): | |
self.rswing(deg=0, speed=100, seconds=0.05, stop=stop) | |
def slpoint(self, stop=False): | |
self.lpoint(deg=0, speed=100, seconds=0.05, stop=stop) | |
def srpoint(self, stop=False): | |
self.rpoint(deg=0, speed=100, seconds=0.05, stop=stop) | |
def handle(self, cmd): | |
''' | |
무선 조작을 위한 단축키를 받아 적합한 동작을 수행한다. | |
예시) cmd가 'w'일때 self.sforward() 가 호출된다. | |
:param cmd: 무선 조작을 위한 단축키 | |
''' | |
action = self.actions.get(cmd, lambda: print("Invalid cmd")) | |
action() | |
@staticmethod | |
def _time_speed(deg=90, turntype: TurnType = TurnType.SWING): | |
''' | |
각도와 턴 타입을 받아 어느정도의 속력과 시간으로 회전해야 해 | |
해당 각도만큼 회전할수 있을지를 대략적으로 추정해 계산한다. | |
:param deg: 턴할 각도 | |
:param turntype: swing 턴인지 point 턴인지를 구별 | |
:return : 리스트. | |
0번째 인덱스 : 회전할 시간, | |
1번째 인덱스 : 회전할 속력 | |
''' | |
if not isinstance(turntype, RCar.TurnType): | |
raise ValueError("type should be instance of RCar.TurnType") | |
seconds = deg * 0.02 * (0.95 if turntype is RCar.TurnType.SWING else 0.3) | |
speed = deg * 0.6 | |
return [seconds, speed] | |
class SpecialActs: | |
''' | |
특수 모드로 구동체를 조작한다. | |
지금은 handling 단 하나뿐이다. | |
''' | |
@staticmethod | |
def handling(): | |
''' | |
무한 루프를 돌면서 사용자에게 명령어를 입력받아 해당 명령어에 알맞은 조작을 실행한다. | |
''' | |
# 무한 루프를 빠져나가지 위해 입력하면 될 명령어. 단 하나의 문자로 표현 가능해야 한다. | |
EXIT_CMD = "b" | |
while True: | |
''' | |
y 입력시 : 매번의 원격 조작 후 stop() 한다. | |
예를 들어, 턴하는 명령어 입력시 살짝 턴하고 stop | |
n 입력시 : 매번의 원격 조작 후 stop() 하지 않는다. | |
예를 들어, 턴하는 명령어 입력시 다른 명령어가 주어지기 전까지 계속 턴한다. | |
''' | |
option = input("단위 행동입니까? (y/n)") | |
try: | |
if option not in ("y", "n"): | |
raise ValueError | |
BASE_SPEED = int(input("기본 속력을 입력해주세요:")) # 기본 주행 스피드 | |
SAFE_DISTANCE = int(input("안전 거리를 입력해주세요:")) # 기본 주행 스피드 | |
except ValueError: | |
print("적절한 값을 입력해주세요.") | |
continue | |
break | |
RCar.get_instance().update_shortcuts(BASE_SPEED, SAFE_DISTANCE, stop=option is "y") | |
try: | |
while True: | |
''' | |
getch 모듈의 getch() 메서드는 기본적으로 input() 과 같지만, 다음과 같은 결정적인 차이점들을 가진다. | |
* 엔터 키를 누르지 않고도 키를 누르는 대로 바로 실시간으로 입력된다. | |
* 한번에 하나의 글자만 받을 수 있다. | |
따라서 getch() 메서드를 통해 명령어를 입력받는다. 예를 들어 사용자가 w를 입력시 앞으로 간다. | |
''' | |
cmd = getch.getch() | |
if cmd is EXIT_CMD: | |
return | |
print(cmd) | |
RCar.get_instance().handle(cmd) | |
except KeyboardInterrupt: | |
pass | |
print("bye") | |
if __name__ == '__main__': | |
# RCar 객체를 싱글톤으로 받아온다. | |
rcar = RCar.get_instance() | |
''' | |
예시 1 | |
이 부분은 구동체를 40, 60, 80 의 속도로 각각 3초간 앞뒤로 움직이는 단순한 예시입니다. | |
2차 과제에서 사용될 수 있습니다. | |
''' | |
try: | |
seconds = 3 | |
# 40, 60, 80 의 속도로 각각 3초간 forward, backward 를 실행한다. | |
for speed in (40, 60, 80): | |
rcar.repeat(speed, seconds) | |
except KeyboardInterrupt: | |
rcar.stop() | |
''' | |
예시 2 | |
장애물을 4번 마주치고, 마주칠때마다 순서대로 턴을 해 'ㄷ'자 모양의 주행을 2번 반복하는 예시입니다. | |
오른쪽 스윙턴, 오른쪽 포인트턴, 왼쪽 포인트턴, 왼쪽 스윙턴을 순서대로 수행합니다. | |
forward_any 를 호출해 40의 속력으로 전진하고 앞의 장애물과 거리가 30cm 이하가 되었을때 stop 합니다. | |
3차 과제에서 사용될 수 있습니다. | |
''' | |
forward_any = lambda: rcar.forward(speed=40, seconds=0, safe_distance=30, daemon=False) | |
for turn in (rcar.rswing, rcar.rpoint, rcar.lpoint, rcar.lswing): | |
forward_any() | |
turn(deg=90) | |
forward_any() | |
''' | |
예시 3 | |
이 부분은 원격에서 키보드로 부터 명령어를 입력받아 자유롭게 조종가능하게 하는 예시입니다. | |
장애물에 부딪힐 때까지 계속 전진하는 기능과 조금씩 턴하는 기능 등이 있으므로 | |
3차 과제에도 사용될 수 있습니다. | |
''' | |
RCar.SpecialActs.handling() | |
GPIO.cleanup() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
주의사항
본 코드는 서드파티 라이브러리인 py-getch의 모듈인
getch
를 사용합니다.때문에 실행 전, 라즈베리파이에 해당 모듈을 설치해야합니다.
라즈베리 파이의 터미널에서 다음 두 명령어를 입력합니다.
py-getch 에 대한 자세한 정보는 다음에서 찾아볼 수 있습니다.