한이음/GPS 좌표 트래킹을 통한 자율주행 로봇

ROS 키 입력 받기 (teleop_twist) + 모터 구동

MZ 아이종 2022. 3. 9. 11:47

자율주행 로봇을 제작하는 것을 목표로 하지만, 제작 과정에서 실험을 위해 직접 조작이 필요한 경우가 있다.

그 때 사용하는 패키지를 다운받아 수정하여 사용해 보자.

 

사용버전. 노트북. 모터

˙ Samsung Ultrabook i7

˙ Ubuntu 18.04.6 LTS

˙ Ros Melodic

˙ IG-52GM+Encoder 04TYPE (24V)

 

단계

1. 패키지 다운

2. 패키지 실행

3. subscriber 파일 생성 및 코드 작성

4. package.xml 수정

5. 입력내용 확인

6. 패키지 publish 코드 수정

7. Subscriber 수정

8. 수정된 코드 동작 확인

9. OpenCR 이용해 모터 구동

 

1. 패키지 다운

먼저 catkin_ws/src 로 이동 한다. 이후 teleop_twist를  설치해 준다.

$ cw
$ cd src
$ git clone https://github.com/methylDragon/teleop_twist_keyboard_cpp.git
$ cd ..
$ cm
$ source devel/setup.bash

 

패키지 복사
생성된 패키지를 확인할 수 있다.

 

 

2. 패키지 실행

패키지가 잘 설치되었는지 확인하기 위해 패키지를 실행시켜 본다. 터미널 2개를 띄워 실행한다.

#1st Terminal
$ roscore

#2nd Terminal
$ rosrun teleop_twist_keyboard_cpp teleop_twist_keyboard

 

이상없이 실행됨을 알 수 있다.

 

 

3. subscriber 파일 생성 및 코드 작성

키보드로 입력이 됨은 확인이 되었으니, 입력받은 내용을 확인해 보자.

 teleop_twist_keyboard_cpp 패키지 안에 key_sub라는 이름으로 subcriber를 만들어 보자.

$ cw
$ cd src/teleop_twist_keyboard_cpp/src
$ gedit key_sub.cpp

 

teleop_twist가 geometry_msgs/Twist를 사용하므로 각 변수들의 값을 출력하는 코드를 작성해 본다.

#include "ros/ros.h"
#include <geometry_msgs/Twist.h>

void response(const geometry_msgs::Twist::ConstPtr& msg)
{
	ROS_INFO("linear.x = %f",msg->linear.x);
	ROS_INFO("linear.y = %f",msg->linear.y);
	ROS_INFO("linear.z = %f",msg->linear.z);
	ROS_INFO("angular.x = %f",msg->angular.x);
	ROS_INFO("angular.y = %f",msg->angular.y);
	ROS_INFO("angular.z = %f",msg->angular.z);
	
	ROS_INFO("=======================");
}

int main(int argc, char **argv)
{
	ros::init(argc, argv, "key_sub");
	ros::NodeHandle n;
	ros::Subscriber sub = n.subscribe("/cmd_vel",100,response);
	ros::spin();
	return 0;
}

이후 catkin_make를 통해 오류를 확인하고 수정한다. 오류가 발생하지 않으면, 넘어간다.

$ cm

 

 

4. CMakeList.txt 수정

패키지에 subscriber를 추가했기 때문에 해당 내용을 추가해 주도록 한다. 

package.xml은 수정하지 않아도 된다.

 

아래는 원본 코드이다.

cmake_minimum_required(VERSION 2.8.3)
project(teleop_twist_keyboard_cpp)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  geometry_msgs
)

catkin_package(
  INCLUDE_DIRS src
  CATKIN_DEPENDS roscpp geometry_msgs
)

include_directories(${catkin_INCLUDE_DIRS})

add_executable(teleop_twist_keyboard src/teleop_twist_keyboard.cpp)

target_link_libraries(teleop_twist_keyboard ${catkin_LIBRARIES})

set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")


cmake_minimum_required(VERSION 2.8.3)
project(teleop_twist_keyboard_cpp)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  geometry_msgs
)

catkin_package(
  INCLUDE_DIRS src
  CATKIN_DEPENDS roscpp geometry_msgs
)

include_directories(${catkin_INCLUDE_DIRS})

add_executable(teleop_twist_keyboard src/teleop_twist_keyboard.cpp)
add_executable(key_sub src/key_sub.cpp)

add_dependencies(teleop_twist_keyboard ${teleop_twist_keyboard_cpp_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(key_sub ${teleop_twist_keyboard_cpp_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(teleop_twist_keyboard ${catkin_LIBRARIES})
target_link_libraries(key_sub ${catkin_LIBRARIES})

set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")

아래는 수정된 코드이다.

cmake_minimum_required(VERSION 2.8.3)
project(teleop_twist_keyboard_cpp)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  geometry_msgs
)

catkin_package(
  INCLUDE_DIRS src
  CATKIN_DEPENDS roscpp geometry_msgs
)

include_directories(${catkin_INCLUDE_DIRS})

add_executable(teleop_twist_keyboard src/teleop_twist_keyboard.cpp)
add_executable(key_sub src/key_sub.cpp)

add_dependencies(teleop_twist_keyboard ${teleop_twist_keyboard_cpp_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(key_sub ${teleop_twist_keyboard_cpp_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(teleop_twist_keyboard ${catkin_LIBRARIES})
target_link_libraries(key_sub ${catkin_LIBRARIES})

set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")

코드는 수정 이후에 catkin_make 해준다.

 

 

5. 입력내용 확인

터미널 창 3개를 띄워서 입력한 값이 출력되는걸 확인한다.

#1st Terminal
$ roscore

#2nd Terminal
$ rosrun teleop_twist_keyboard_cpp teleop_twist_keyboard

#3rd Terminal
$ rosrun teleop_twist_keyboard_cpp key_sub

 

 

입력한 값이 출력됨을 확인할 수 있었다.

 

 

6. 패키지 publish 코드 수정

각각의 키 입력에 대한 작성된 값이 원하는 바와 다르므로 수정하여 사용하도록 한다.

w/a/s/d 를 통해 움직이고 i/o를 통해 속력을, k/l을 통해 회전 정도를 조절한다. 

w/a/s/d 의 경우는 1/0 으로 신호만 준다.

 

아래는 원본 코드이다.

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>

#include <stdio.h>
#include <unistd.h>
#include <termios.h>

#include <map>

// Map for movement keys
std::map<char, std::vector<float>> moveBindings
{
  {'i', {1, 0, 0, 0}},
  {'o', {1, 0, 0, -1}},
  {'j', {0, 0, 0, 1}},
  {'l', {0, 0, 0, -1}},
  {'u', {1, 0, 0, 1}},
  {',', {-1, 0, 0, 0}},
  {'.', {-1, 0, 0, 1}},
  {'m', {-1, 0, 0, -1}},
  {'O', {1, -1, 0, 0}},
  {'I', {1, 0, 0, 0}},
  {'J', {0, 1, 0, 0}},
  {'L', {0, -1, 0, 0}},
  {'U', {1, 1, 0, 0}},
  {'<', {-1, 0, 0, 0}},
  {'>', {-1, -1, 0, 0}},
  {'M', {-1, 1, 0, 0}},
  {'t', {0, 0, 1, 0}},
  {'b', {0, 0, -1, 0}},
  {'k', {0, 0, 0, 0}},
  {'K', {0, 0, 0, 0}}
};

// Map for speed keys
std::map<char, std::vector<float>> speedBindings
{
  {'q', {1.1, 1.1}},
  {'z', {0.9, 0.9}},
  {'w', {1.1, 1}},
  {'x', {0.9, 1}},
  {'e', {1, 1.1}},
  {'c', {1, 0.9}}
};

// Reminder message
const char* msg = R"(

Reading from the keyboard and Publishing to Twist!
---------------------------
Moving around:
   u    i    o
   j    k    l
   m    ,    .

For Holonomic mode (strafing), hold down the shift key:
---------------------------
   U    I    O
   J    K    L
   M    <    >

t : up (+z)
b : down (-z)

anything else : stop

q/z : increase/decrease max speeds by 10%
w/x : increase/decrease only linear speed by 10%
e/c : increase/decrease only angular speed by 10%

CTRL-C to quit

)";

// Init variables
float speed(0.5); // Linear velocity (m/s)
float turn(1.0); // Angular velocity (rad/s)
float x(0), y(0), z(0), th(0); // Forward/backward/neutral direction vars
char key(' ');

// For non-blocking keyboard inputs
int getch(void)
{
  int ch;
  struct termios oldt;
  struct termios newt;

  // Store old settings, and copy to new settings
  tcgetattr(STDIN_FILENO, &oldt);
  newt = oldt;

  // Make required changes and apply the settings
  newt.c_lflag &= ~(ICANON | ECHO);
  newt.c_iflag |= IGNBRK;
  newt.c_iflag &= ~(INLCR | ICRNL | IXON | IXOFF);
  newt.c_lflag &= ~(ICANON | ECHO | ECHOK | ECHOE | ECHONL | ISIG | IEXTEN);
  newt.c_cc[VMIN] = 1;
  newt.c_cc[VTIME] = 0;
  tcsetattr(fileno(stdin), TCSANOW, &newt);

  // Get the current character
  ch = getchar();

  // Reapply old settings
  tcsetattr(STDIN_FILENO, TCSANOW, &oldt);

  return ch;
}

int main(int argc, char** argv)
{
  // Init ROS node
  ros::init(argc, argv, "teleop_twist_keyboard");
  ros::NodeHandle nh;

  // Init cmd_vel publisher
  ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("cmd_vel", 1);

  // Create Twist message
  geometry_msgs::Twist twist;

  printf("%s", msg);
  printf("\rCurrent: speed %f\tturn %f | Awaiting command...\r", speed, turn);

  while(true){

    // Get the pressed key
    key = getch();

    // If the key corresponds to a key in moveBindings
    if (moveBindings.count(key) == 1)
    {
      // Grab the direction data
      x = moveBindings[key][0];
      y = moveBindings[key][1];
      z = moveBindings[key][2];
      th = moveBindings[key][3];

      printf("\rCurrent: speed %f\tturn %f | Last command: %c   ", speed, turn, key);
    }

    // Otherwise if it corresponds to a key in speedBindings
    else if (speedBindings.count(key) == 1)
    {
      // Grab the speed data
      speed = speed * speedBindings[key][0];
      turn = turn * speedBindings[key][1];

      printf("\rCurrent: speed %f\tturn %f | Last command: %c   ", speed, turn, key);
    }

    // Otherwise, set the robot to stop
    else
    {
      x = 0;
      y = 0;
      z = 0;
      th = 0;

      // If ctrl-C (^C) was pressed, terminate the program
      if (key == '\x03')
      {
        printf("\n\n                 .     .\n              .  |\\-^-/|  .    \n             /| } O.=.O { |\\\n\n                 CH3EERS\n\n");
        break;
      }

      printf("\rCurrent: speed %f\tturn %f | Invalid command! %c", speed, turn, key);
    }

    // Update the Twist message
    twist.linear.x = x * speed;
    twist.linear.y = y * speed;
    twist.linear.z = z * speed;

    twist.angular.x = 0;
    twist.angular.y = 0;
    twist.angular.z = th * turn;

    // Publish it and resolve any remaining callbacks
    pub.publish(twist);
    ros::spinOnce();
  }

  return 0;
}

 

다음은 수정한 코드이다.

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>

#include <stdio.h>
#include <unistd.h>
#include <termios.h>

#include <map>

// Map for movement keys
std::map<char, std::vector<float>> moveBindings
{
  {'w', { 1,  0,  0}},
  {'a', { 0, -1,  0}},
  {'d', { 0,  1,  0}},
  {'s', {-1,  0,  0}}	
};

// Map for speed keys
std::map<char, std::vector<float>> speedBindings
{
  {'i', { 0.5,  0}},
  {'o', {-0.5,  0}},
  {'k', { 0,  1.0}},
  {'l', { 0, -1.0}}
};

// Reminder message
const char* msg = R"(

Reading from the keyboard and Publishing to Twist!
---------------------------
Moving around:
        w    
   a    s    d
        x

velocity
i : up   (+0.5) m/s
o : down (-0.5) m/s

turn weight
k : up   (+1,0) rad/s
l : down (-1,0) rad/s

anything else : stop

CTRL-C to quit

)";

// Init variables
float velocity(0.5); // Linear velocity (m/s)
float turn(1.0); // Angular velocity (rad/s)
float x(0), y(0), z(0), th(0); // Forward/backward/neutral direction vars
char key(' ');

// For non-blocking keyboard inputs
int getch(void)
{
  int ch;
  struct termios oldt;
  struct termios newt;

  // Store old settings, and copy to new settings
  tcgetattr(STDIN_FILENO, &oldt);
  newt = oldt;

  // Make required changes and apply the settings
  newt.c_lflag &= ~(ICANON | ECHO);
  newt.c_iflag |= IGNBRK;
  newt.c_iflag &= ~(INLCR | ICRNL | IXON | IXOFF);
  newt.c_lflag &= ~(ICANON | ECHO | ECHOK | ECHOE | ECHONL | ISIG | IEXTEN);
  newt.c_cc[VMIN] = 1;
  newt.c_cc[VTIME] = 0;
  tcsetattr(fileno(stdin), TCSANOW, &newt);

  // Get the current character
  ch = getchar();

  // Reapply old settings
  tcsetattr(STDIN_FILENO, TCSANOW, &oldt);

  return ch;
}

int main(int argc, char** argv)
{
  // Init ROS node
  ros::init(argc, argv, "teleop_twist_keyboard");
  ros::NodeHandle nh;

  // Init cmd_vel publisher
  ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("cmd_vel", 1);

  // Create Twist message
  geometry_msgs::Twist twist;

  printf("%s", msg);
  printf("\rCurrent: velocity %f\tturn %f | Awaiting command...\r", velocity, turn);

  while(true)
{
    // Get the pressed key
    key = getch();

    // If the key corresponds to a key in moveBindings
    if (moveBindings.count(key) == 1)
    {
      // Grab the direction data
      x = moveBindings[key][0];
      y = moveBindings[key][1];
      z = moveBindings[key][2];
    }

    // Otherwise if it corresponds to a key in speedBindings
    else if (speedBindings.count(key) == 1)
    {
      // Grab the speed data
      velocity = velocity + speedBindings[key][0];
      turn = turn + speedBindings[key][1];
    }

    // Otherwise, set the robot to stop
    else
    {
      velocity = 0;
      turn = 0;
      
      // If ctrl-C (^C) was pressed, terminate the program
      if (key == '\x03')
        break;
    }
    
    printf("\rCurrent: velocity %f m/s\tturn %f rad/s\tLast command: %c   ", velocity, turn, key);
    
    // Update the Twist message
    twist.linear.x = x;
    twist.linear.y = y;
    twist.linear.z = 0;

    twist.angular.x = velocity;
    twist.angular.y = turn;
    twist.angular.z = 0;

    // Publish it and resolve any remaining callbacks
    pub.publish(twist);
    ros::spinOnce();
  }

  return 0;
}

수정이후 catkin_make 해준다.

 

 

7. Subscriber 수정

6개의 값중 4개의 값만 사용하므로 코드를 수정해 준다.

#include "ros/ros.h"
#include <geometry_msgs/Twist.h>

void response(const geometry_msgs::Twist::ConstPtr& msg)
{
	ROS_INFO("x = %f",msg->linear.x);
	ROS_INFO("y = %f",msg->linear.y);
	ROS_INFO("speed = %f",msg->angular.x);
	ROS_INFO("turn = %f",msg->angular.y);

	ROS_INFO("=======================");
}

int main(int argc, char **argv)
{
	ros::init(argc, argv, "key_sub");
	ros::NodeHandle n;
	ros::Subscriber sub = n.subscribe("cmd_vel",100,response);
	ros::spin();
	return 0;
}

수정이후 catkin_make 해준다.

 

 

8. 수정된 코드 동작 확인 

위와 동일하게 3개의 터미널을 이용해 동작을 확인한다.

#1st Terminal
$ roscore

#2nd Terminal
$ rosrun teleop_twist_keyboard_cpp teleop_twist_keyboard

#3rd Terminal
$ rosrun teleop_twist_keyboard_cpp key_sub

 

 

 

9. OpenCR 이용해 모터 구동

- 9.1 IG-52GM+Encoder 04TYPE (24V)

6개 색의 선이 존재한다. 

빨간색 : Motor VCC

검은색 : Motor GND

갈색 : Encoder VCC

초록색 : Encoder GND

파란색 : Encoder B

보라색 : Encoder A

 


- 9.2 L298N

2개의 모터를 동시에 제어할 수 있는 모터 드라이버이다.

모터에서 Motor VCC, GND는 드라이버 좌우에 하나씩 있는 핀에 연결하는데, 연결하는 방법에 따라 모터 회전 방향이 바뀐다. 

최대 24V까지 입력이 가능하며, 정면 왼쪽에 있는 3개의 핀 중 VCC, GND에 각각 연결한다.

정면 오른쪽에 10개의 핀 중 ENA, IN1, IN2를 통해 소프트웨어적으로 모터의 회전방향을 결정한다. ENB, IN3, IN4를 통해 오른쪽에 연결된 모터를 제어할 수 있다.

 

 

 

 

- 9.3 연결

Ooen CR <-> MOTOR / DRIVER

Open CR 5V GND 3 4 8 9 10
MOTOR ENCODER
VCC
ENCODER
GND
ENCODER A ENCODER B      
DRIVER         IN2 IN1 ENA

ENA 핀은 ~표시가 있는 PWM 핀에 연결해야 한다.

 

 

POWER SUPPLY <-> DRIVER <-> MOTOR

POWER SUPPLY VCC GND    
MOTOR     MOTOR VCC MOTOR GND
DRIVER VCC GND OUT 1 OUT 2

 

- 9.4 코드 작성

아두이노에 맞는 코드를 작성한다. 테스트 용으로 작성하였다.

//ros

#include <Arduino.h>
#include <ros.h>
#include <geometry_msgs/Twist.h>

//motor
#define ENABLE_A  10  
#define IN1_A  9   
#define IN2_A  8   
 
ros::NodeHandle  nh;

float velocity = 0;

void MOTOR( const geometry_msgs::Twist& msg)
{  
  if(msg.linear.x == 1) //w
  {
    digitalWrite(IN1_A, HIGH);  
    digitalWrite(IN2_A, LOW);
    velocity = msg.angular.x;
  }

  else if(msg.linear.x == -1) //s
  {
    digitalWrite(IN1_A, LOW);  
    digitalWrite(IN2_A, HIGH);
    velocity = msg.angular.x;
  }
  
  else if(msg.linear.y == -1) //a
  {
    digitalWrite(IN1_A, HIGH);  
    digitalWrite(IN2_A, LOW);
    velocity = msg.angular.y;
  }
  
  else if(msg.linear.y == 1) //d
  {
    digitalWrite(IN1_A, LOW);  
    digitalWrite(IN2_A, HIGH);
    velocity = msg.angular.y;
  }
  
  else if(msg.linear.x == msg.linear.y)
    velocity = 0;
  
  else;
  
  analogWrite(ENABLE_A, velocity);
  
}

ros::Subscriber<geometry_msgs::Twist> sub("cmd_vel", &MOTOR);

void setup()
{
  pinMode(IN1_A, OUTPUT); // 방향1
  pinMode(IN2_A, OUTPUT); // 방향2
  pinMode(ENABLE_A, OUTPUT); // 속도

  nh.initNode();
  nh.subscribe(sub);
}

void loop(){
  nh.spinOnce();
  delay(1);
}

 

이후 3개의 터미널에 각각의 명령어를 입력하여 실행한다.

#1st Terminal
$ roscore

#2nd Terminal
$ rosrun teleop_twist_keyboard_cpp teleop_twist_keyboard

#3rd Terminal
$ rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0 _baud:=57600