Skip to content

Instantly share code, notes, and snippets.

@jjangga0214
Created November 3, 2017 17:41
Show Gist options
  • Save jjangga0214/9fee7b514f779deff21d680b2c477aa6 to your computer and use it in GitHub Desktop.
Save jjangga0214/9fee7b514f779deff21d680b2c477aa6 to your computer and use it in GitHub Desktop.
창업연계공학입문 components
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()
@jjangga0214
Copy link
Author

jjangga0214 commented Nov 4, 2017

주의사항

본 코드는 서드파티 라이브러리인 py-getch의 모듈인 getch 를 사용합니다.
때문에 실행 전, 라즈베리파이에 해당 모듈을 설치해야합니다.
라즈베리 파이의 터미널에서 다음 두 명령어를 입력합니다.

pip install py-getch
pip3 install py-getch

py-getch 에 대한 자세한 정보는 다음에서 찾아볼 수 있습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment