정밀 핸들링을 위한 고급 원격 제어 그리퍼 로봇
부품 및 소모품
| Runt Rover Half-pint 섀시 |
× |
1 |
| 아두이노 UNO |
× |
1 |
| V5 쉴드 |
× |
1 |
| Female/Female 점퍼 와이어 |
× |
1 |
| L9110S 모터 드라이버 |
× |
1 |
| 90° 싱글 앵글 채널 브라켓 (585424) |
× |
1 |
| 경량 서보 허브, Futaba |
× |
1 |
| 표준 그리퍼 키트 A (637094) |
× |
1 |
| MG995 서보 |
× |
1 |
| 표준 서보 플레이트 |
× |
1 |
| 90° 싱글 앵글 채널 브라켓 |
× |
1 |
| 5슬롯 AA 홀더 |
× |
1 |
| AA 충전식 배터리 |
× |
5 |
| .750" 8-32 나일론 스페이서 |
× |
2 |
| .500" 6-32 나사 및 너트 |
× |
8 |
| .3125" 6-32 나사 |
× |
8 |
앱 및 온라인 서비스
프로젝트 소개
몇 달 전 아들이 원격 조종 로봇을 만들어달라고 했습니다. 원하는 목록을 줬는데, 관리 가능한 수준으로 좁혔죠 :) 결국 이렇게 만들었습니다...
- UNO와 V5 센서 쉴드를 결합
- 결합된 UNO/센서 보드를 봇 중앙의 클립 홀더에 장착
- 양면 테이프로 모터 드라이버를 왼쪽 내부 벽에, 블루투스 어댑터를 오른쪽 벽에 부착
이 프로젝트에서는 직접 섀시를 만들거나 다른 4WD 섀시를 써도 됩니다.
1단계: 필요한 소프트웨어
다운로드할 것들:
- 아두이노 IDE
- Python 2.7 (tkinter 포함 풀 설치)
- pySerial 라이브러리
- L9110S 모터 컨트롤러 라이브러리 첨부 .zip (이 단계의 파일 참조)
- 아래 로봇 코드 또는 첨부 .zip (이 단계의 파일 참조)
- 파이썬 원격 제어 애플리케이션 (이 단계의 파일 참조)
2단계: 하드웨어 자재 리스트
필요한 부품:
- Runt Rover Half-pint 섀시 또는 적절한 4WD 밀폐형 대체품
- 아두이노 Uno 또는 USB 케이블 포함 유사 보드
- V5 센서 쉴드
- L9110S 모터 드라이버 (저렴하니 여러 개 구매 권장)
- MG995 서보 또는 적절한 대체품 x2
- HC-05 또는 HC-06 블루투스 어댑터 x1
- 표준 서보 플레이트 B x1
- 90도 브라켓 x1
- 싱글 브라켓 x2
- 경량 서보 허브 (Futaba용 525125) x1
- 표준 그리퍼 키트 A x1
- Female-to-Female 점퍼 와이어
- 충전식 AA 배터리 5개 (NiMH) 및 충전기
- 양면 테이프와 작은 케이블 타이
- AA 5슬롯 배터리 팩 (AA 충전식 옵션 선택 시)
- 6V NiMH 또는 7.4V Li-ion 배터리 팩 및 충전기
- 1.250" 6-32 나사 x2
- .750" 8-32 나일론 스페이서 x2
- .500" 6-32 나사 및 너트 x8
- .3125" 6-32 나사 x8
부품 다수는 이베이나 Sparkfun, Adafruit, ServoCity 같은 공급처에서 구매 가능합니다.
구매, 수집, 다운로드가 모두 끝났다면 빌드 시작!
3단계: 조립 - 섀시
섀시 동봉 설명서나 영상 따라 조립. 완료 후 이미지 같은 모습이어야 합니다. 컨트롤러 홀더 플레이트를 봇에 올릴 때 중간 구멍에 장착해 앞뒤 공간을 확보하세요.
주의: 더 이상 진행하기 전, 모터 손상 방지를 위해 4개 바퀴를 모두 분리하세요.
4단계: 그리퍼 조립
이 영상 지침 따라 그리퍼 조립. 완료 후 이미지 같은 모습.
5단계: 그리퍼 장착 - 1단계
90도 브라켓, 경량 서보 허브, .3125" 나사 4개 준비:
- 서보 허브를 브라켓 한쪽에 놓고 .3125" 나사로 고정, 따로 둠
- 두 싱글 브라켓, 1.250" 나사, .500" 나사+너트, 나일론 스페이서 2개, 서보 플레이트 B 준비
- 싱글 브라켓 하나를 Half-pint 봇 내부에 놓고 1.250" 나사 2개는 왼쪽 구멍(내부에서), .500" 나사 2개는 오른쪽 구멍에 체결. 모든 나사 머리는 봇 내부 쪽
- 두 번째 싱글 브라켓을 봇 외부의 나사들에 끼움
- 나일론 스페이서를 긴 나사(1.250")에 끼우고 서보 플레이트를 긴 나사에 조임
- 서보 플레이트로 가는 나사들을 육각 렌치나 드라이버로 조임
필요시 사진 참조.
6단계: 그리퍼 장착 - 2단계
- 남은 서보를 서보 브라켓에 척추가 위로 오게 장착, .3125" 나사 4개로 고정
- 서보 허브가 달린 90도 브라켓에 그리퍼를 .500" 나사 4개로 장착, 브라켓과 그리퍼 사이에 8-32 너트 2개를 스페이서로 끼움. 그리퍼 위(또는 브라켓 아래)에 너트로 고정
- 그리퍼 어셈블리를 서보에 올리고 서보 나사(서보 패키지 동봉)로 단단히 고정
완료 후 두 서보 와이어를 평평한 브라켓 구멍으로 통과시켜 봇 내부로 뺌.
자세한 건 사진 참조.
7단계: 전자장치 장착
- UNO와 V5 센서 쉴드 결합
- 결합된 보드를 봇 중앙 클립 홀더에 장착
- 양면 테이프로 모터 드라이버를 왼쪽 내부 벽에, 블루투스 어댑터를 오른쪽 벽에 부착
8단계: 배선 - 모터
- 모터 드라이버 A-1A 핀 → 센서 쉴드 5번 핀
- 모터 드라이버 A-1B 핀 → 센서 쉴드 6번 핀
- 모터 드라이버 B-1A 핀 → 센서 쉴드 3번 핀
- 모터 드라이버 B-1B 핀 → 센서 쉴드 11번 핀
로봇 앞쪽이 자신을 향하지 않게 두고, 왼쪽 측 와이어들:
- 왼쪽 두 검정 와이어 → 모터 A 첫 번째 출력 스크류 터미널
- 왼쪽 두 빨강 와이어 → 모터 A 두 번째 출력 스크류 터미널
오른쪽 측 와이어들:
- 오른쪽 두 검정 와이어 → 모터 B 첫 번째 출력 스크류 터미널
- 오른쪽 두 빨강 와이어 → 모터 B 두 번째 출력 스크류 터미널
서보:
- Y 서보 와이어 → 센서 쉴드 2번 핀 라인 (흰색/주황색이 신호선)
- X (그리퍼) 서보 와이어 → 센서 쉴드 7번 핀 라인 (흰색/주황색이 신호선)
9단계: 블루투스 모듈 배선
- 블루투스 어댑터 Tx → 센서 쉴드 0번 핀
- 블루투스 어댑터 Rx → 센서 쉴드 1번 핀
10단계: 전원 연결
5 'AA' (또는 다른 6V 팩) 배터리 팩 전원선을 센서 쉴드 전원 스크류 터미널에 연결:
- 빨강 → Vcc
- 검정 → GND
- V5 센서 쉴드의 점퍼가 점퍼 핀에 꽂혀 있는지 확인
- L9110S 드라이버 GND 핀 → 센서 쉴드 GND 핀
- 모터용 6V 또는 7.2V 배터리 양극 → L9110S 모터 드라이버 Vcc 핀
- 모터 배터리 음극(GND) → 모터 쉴드 GND 핀
11단계: HC05/HC06 블루투스 모듈 설정
PC의 OS "제어판" 또는 "시스템" 패널에서 블루투스 장치 설정으로 들어가거나 구글 검색.
가능한 OS별 블루투스 설정 정보:
- Windows
- Linux (리눅스 민트에서 blueman 사용, 배포판별 안내 참조)
- Mac
할당된 시리얼 포트 장치 이름 또는 번호를 메모해두세요.
12단계: 코드 업로드
배선 재확인 후, 코드 업로드. 아두이노 사이트의 IDE 설치 지침 참조.
IDE 설치 후 L9110 모터 드라이버 라이브러리 설치. STEP 1의 L9110Driver.zip 다운로드 후 아두이노 라이브러리 폴더에 압축 해제하거나 라이브러리 추가 방법 참조.
이제 IDE와 모터 드라이버 라이브러리 설치됐으니, STEP 1의 blue_t_slave.zip에서 로봇 코드를 아두이노 IDE로 로드. 이 단계에도 파일 포함돼 있습니다.
PC와 아두이노 USB 연결. Tools→Board 메뉴에서 보드 선택 (Uno). Tools→Port에서 COM 포트 선택. 업로드 버튼 클릭. 잘 됐다면 코드 업로드 완료, 안 되면 IDE 문제 해결 참조.
13단계: 파이썬 제어 애플리케이션 실행
이 단계의 zip 파일을 다운로드해 원하는 위치에 압축 해제. 명령 셸(터미널, cmd.exe 등)을 열고 압축 푼 디렉토리로 이동. python rover.py 입력하면 이미지 같은 창이 뜹니다. 안 뜨면 파이썬 에러 확인(라이브러리 누락 등). 파이썬 사이트 참고.
앱 실행되면 준비 완료.
봇 전원 켜기. 어댑터의 COM 포트 입력 후 "Connect" 버튼 클릭. 몇 초 후 다른 컨트롤들이 활성화되고 Connect 버튼이 비활성화되면 연결 성공. 블루투스 어댑터 연결 안 되면 구글 신의 도움 받으세요!
제어 앱은 마우스/키보드 모두 지원하며 사용법 간단.
키보드 컨트롤:
- 방향키: 전진, 후진, 좌회전 90°, 우회전 90°
- a: 좌회전 45°
- s: 우회전 45°
- h: 정지
- y: 그리퍼 Y축 설정
- u: 그리퍼 열기/닫기
- c: 그리퍼 홈
전진/후진 명령은 지속적(새 방향 명령이나 정지 명령까지 계속 이동). 90°/45° 회전은 일시적(딜레이 후 정지).
그리퍼 슬라이더는 자동 실행 안 됨. 해당 "Set" 버튼이나 키를 눌러야 실제 실행. 슬라이더 범위 0-180.
- 그리퍼 Y축: 0=최상단, 180=최하단
- 그리퍼 열기/닫기: 0=완전 닫힘, 180=완전 열림
"Disconnect" 버튼으로 프로그램 중단. 봇 정지 및 그리퍼 홈 명령 전송.
코드
파이썬 드라이버 GUI Python
#!/usr/bin/env python
# Linux, BSD, Mac OSX에서는 chmod +x로 실행 가능하게 설정
###########
# Rover control app
# Written by Scott Beasley - 2015
# Free to use or modify. Enjoy.
###########
import sys
import serial
import time
from Tkinter import *
import tkFont
import tkMessageBox
class App(Frame):
def createWidgets(self):
self.connected = False
self.message = StringVar()
helv6 = tkFont.Font(family='Helvetica', size=6, weight='normal')
self.frame = Frame(self.master)
self.frame.pack()
self.f1 = Frame(self.frame)
self.l1 = Label(self.f1, text="Comm Port: ")
self.l1.pack(side=LEFT)
self.comm_entry = Entry(self.f1, bd=5, name="comm_entry")
self.comm_entry.pack(side=LEFT)
self.connectButton = Button(self.f1, text="Connect", command=self.SerialConnect, name="b_connect")
self.connectButton.pack(side=LEFT)
self.disconnectButton = Button(self.f1, text="Disconnect", command=self.SerialDisconnect, name="b_disconnect")
self.disconnectButton.pack(side=RIGHT)
self.f1.grid(row=0, column=0)
self.f2 = LabelFrame(self.frame, bd=3, relief="groove", text="Ground Control")
self.g_vert_fm = Frame(self.f2)
self.grip_vert = Scale(self.g_vert_fm, from_=0, to=180)
self.grip_vert.grid(row=0, column=0, rowspan=4, sticky=W)
self.grip_vert_set = Button(self.g_vert_fm, text="Set", command=self.GripperY, name="b_grip_vert_set", width=1, height=2, font=helv6)
self.grip_vert_set.grid(row=5, column=0, sticky=W)
self.master.bind("y", self.GripperY)
self.g_vert_fm.grid(row=0, column=0, rowspan=4, sticky=W)
self.leftforwardButton = Button(self.f2, text="\\", command=self.TurnLeft45, name="b_left_forward")
self.leftforwardButton.grid(row=0, column=1)
self.master.bind("a", self.TurnLeft45)
self.leftButton = Button(self.f2, text="<", command=self.TurnLeft, name="b_left")
self.leftButton.grid(row=1, column=1)
self.master.bind("", self.TurnLeft)
self.rightforwardButton = Button(self.f2, text="/", command=self.TurnRight45, name="b_right_forward")
self.rightforwardButton.grid(row=0, column=3)
self.master.bind("s", self.TurnRight45)
self.haltButton = Button(self.f2, text="Halt!", command=self.Halt, name="b_halt")
self.haltButton.grid(row=1, column=2)
self.master.bind("h", self.Halt)
self.rightButton = Button(self.f2, text=">", command=self.TurnRight, name="b_right")
self.rightButton.grid(row=1, column=3)
self.master.bind("", self.TurnRight)
self.upButton = Button(self.f2, text="^", command=self.Forward, name="b_forward")
self.upButton.grid(row=0, column=2)
self.master.bind("", self.Forward)
self.leftdownButton = Button(self.f2, text="/", command=self.TurnRight45, name="b_left_down")
self.leftdownButton.grid(row=2, column=1)
self.downButton = Button(self.f2, text="V", command=self.Reverse, name="b_reverse")
self.downButton.grid(row=2, column=2)
self.master.bind("", self.Reverse)
self.f2.grid(row=1, column=0, pady=25)
self.rightdownButton = Button(self.f2, text="\\", command=self.TurnLeft45, name="b_right_down")
self.rightdownButton.grid(row=2, column=3)
self.g_horz_fm = Frame(self.f2)
self.grip_horz = Scale(self.g_horz_fm, from_=0, to=180, orient=HORIZONTAL)
self.grip_horz.grid(row=0, column=0, columnspan=7, sticky=E)
self.grip_horz_set = Button(self.g_horz_fm, text="Set", command=self.GripperX, name="b_grip_horz_set", width=1, height=2, font=helv6)
self.grip_horz_set.grid(row=0, column=8)
self.master.bind("u", self.GripperX)
self.g_horz_fm.grid(row=4, column=0, columnspan=7, sticky=E)
self.master.bind("c", self.GripperHome)
self.f3 = Frame(self.frame)
self.l2 = Label(self.f3, text="Last action: ")
self.l2.pack(side=LEFT)
self.l3 = Label(self.f3, text=" ", textvariable=self.message)
self.l3.pack(side=RIGHT)
self.f3.grid(row=3, column=0, pady=8)
def CtrlButtonsState(self, bstate):
self.leftforwardButton.config(state=bstate)
self.leftButton.config(state=bstate)
self.rightforwardButton.config(state=bstate)
self.rightButton.config(state=bstate)
self.upButton.config(state=bstate)
self.leftdownButton.config(state=bstate)
self.downButton.config(state=bstate)
self.rightdownButton.config(state=bstate)
self.haltButton.config(state=bstate)
self.disconnectButton.config(state=bstate)
self.grip_horz.config(state=bstate)
self.grip_vert.config(state=bstate)
self.grip_horz_set.config(state=bstate)
self.grip_vert_set.config(state=bstate)
def ConnCtrlsState(self, bstate):
self.connectButton.config(state=bstate)
self.comm_entry.config(state=bstate)
def SerialConnect(self):
try:
self.ser = serial.Serial(self.comm_entry.get(), 9600)
except IOError:
tkMessageBox.showerror("Invalid comm port", "Comm port not found.")
return
self.ConnCtrlsState(DISABLED)
self.CtrlButtonsState(NORMAL)
self.message.set("SerialConnect")
self.connected = True
time.sleep(3)
def SerialDisconnect(self):
try:
self.send_cmd('h', "Halt!")
time.sleep(1)
self.ser.close()
except IOError:
print("Could not close port...")
self.message.set("SerialDisconnect")
self.ConnCtrlsState(NORMAL)
self.CtrlButtonsState(DISABLED)
self.connected = False
time.sleep(2)
def send_cmd(self, action, msg):
if self.connected == True:
for val in action:
self.ser.write(val)
self.ser.flush()
self.message.set(msg)
def TurnLeft(self, event=None):
self.send_cmd('a', "Left")
def TurnLeft45(self, event=None):
self.send_cmd('q', "Left45")
def TurnRight(self, event=None):
self.send_cmd('s', "Right")
def TurnRight45(self, event=None):
self.send_cmd('e', "Right45")
def Forward(self, event=None):
self.send_cmd('w', "Up")
def Reverse(self, event=None):
self.send_cmd('z', "Down")
def Halt(self, event=None):
self.send_cmd('h', "Halt!")
def GripperX(self, event=None):
grp_change = ('>', chr(self.grip_horz.get()), chr(255))
self.send_cmd(grp_change, "Gripper X")
def GripperY(self, event=None):
grp_change = ('^', chr(self.grip_vert.get()), chr(255))
self.send_cmd(grp_change, "Gripper Y")
def GripperHome(self, event=None):
self.send_cmd(('c', chr(255)), "Gripper Home")
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.createWidgets()
self.CtrlButtonsState(DISABLED)
def main():
root = Tk()
root.geometry("450x350")
root.wm_title("Rover Control Center (RCC)")
app = App(master=root)
app.mainloop()
if __name__ == '__main__':
main()
로버 코드 Arduino
/* Bluetooth Rover. 목표: 공중(또는 USB)을 통해 마법처럼 전달된 명령 따르기!
Written by Scott Beasley - 2015
Free to use or modify. Enjoy.
*/
/* L9110S 라이브러리 사용. L9110S H-브릿지와 호환.
https://github.com/jscottb/L9110Driver 에서 다운로드하거나
https://github.com에서 zip 클론 후 아카이브 파일명에서 '-master' 제거해 라이브러리 추가
*/
#include <L9110Driver.h>
#include <Servo.h>
#define SERVO_Y 2 // 그리퍼 Y 서보 핀
#define SERVO_CLAW 7 // 그리퍼 클로 서보 핀
#define pinAIN1 5 // I1 인터페이스
#define pinAIN2 6 // I2 인터페이스
#define pinBIN1 3 // I3 인터페이스
#define pinBIN2 11 // I4 인터페이스
// 속도 정의
#define MAXFORWARDSPEED 225
#define MAXBACKWARDSPEED 225
#define TOPSPEED 255 // 카펫/거친 표면에서 회전 보조용
// 각종 시간 지연
#define TURNDELAY 475
#define TURNDELAY45 235
#define BACKUPDELAY 400
#define SERVOMOVEDELAY 200
#define SERVOSEARCHDELAY 85
/* 전역 영역 */
L9110_Motor motor_left(pinAIN1, pinAIN2);
L9110_Motor motor_right(pinBIN1, pinBIN2);
Servo grip_y_servo;
Servo grip_servo;
void setup() {
Serial.begin(9600);
grip_y_servo.attach(SERVO_Y);
grip_y_servo.write(90);
grip_servo.attach(SERVO_CLAW);
grip_servo.write(90);
delay(500);
}
void loop() {
byte command = 0, val = 0;
if (Serial.available() > 0) {
command = Serial.read();
}
switch (command) {
case 'w': go_forward(); break;
case 'z': go_backward(); break;
case 'a': go_left(); delay(TURNDELAY); halt(); break;
case 's': go_right(); delay(TURNDELAY); halt(); break;
case 'q': go_left(); delay(TURNDELAY45); halt(); break;
case 'e': go_right(); delay(TURNDELAY45); halt(); break;
case 'h': halt(); break;
case '>':
val = Serial.read();
grip_servo.write(constrain(val, 64, 179));
break;
case '^':
val = Serial.read();
grip_y_servo.write(constrain(val, 53, 179));
break;
case 'c':
grip_y_servo.write(90);
grip_servo.write(90);
break;
case 255:
Serial.flush();
break;
}
Serial.flush();
delay(125);
}
void go_forward() {
ramp_it(MAXFORWARDSPEED, FORWARD, FORWARD);
motor_left.setSpeed(MAXFORWARDSPEED);
motor_right.setSpeed(MAXFORWARDSPEED);
motor_left.run(FORWARD|RELEASE);
motor_right.run(FORWARD|RELEASE);
}
void go_backward() {
ramp_it(MAXBACKWARDSPEED, BACKWARD, BACKWARD);
motor_left.setSpeed(MAXBACKWARDSPEED);
motor_right.setSpeed(MAXBACKWARDSPEED);
motor_left.run(BACKWARD|RELEASE);
motor_right.run(BACKWARD|RELEASE);
}
void go_left() {
ramp_it(TOPSPEED, BACKWARD, FORWARD);
motor_left.setSpeed(TOPSPEED);
motor_right.setSpeed(TOPSPEED);
motor_left.run(BACKWARD|RELEASE);
motor_right.run(FORWARD|RELEASE);
}
void go_right() {
ramp_it(TOPSPEED, FORWARD, BACKWARD);
motor_left.setSpeed(TOPSPEED);
motor_right.setSpeed(TOPSPEED);
motor_left.run(FORWARD|RELEASE);
motor_right.run(BACKWARD|RELEASE);
}
void halt() {
motor_left.setSpeed(0);
motor_right.setSpeed(0);
motor_left.run(BRAKE);
motor_right.run(BRAKE);
}
void ramp_it(uint8_t speed, uint8_t lf_dir, uint8_t rt_dir) {
uint8_t ramp_val = 0, step_val = 0;
step_val = abs(speed / 4);
if (!speed) step_val = -step_val;
for (uint8_t i = 0; i < 4; i++) {
ramp_val += step_val;
motor_left.setSpeed(ramp_val);
motor_right.setSpeed(ramp_val);
motor_left.run(lf_dir|RELEASE);
motor_right.run(rt_dir|RELEASE);
delay(25);
}
}
L9110 드라이버
아두이노용 L9110 모터 드라이버: GitHub
커스텀 파트 및 인클로저
4wdgripperbt2.fzz
회로도
4wdgripperbt2.fzz