21. [ROS RC์นด] ROS ์ฐ๋ RC์นด ์ฃผํ 2

21. [ROS RC์นด] ROS ์ฐ๋ RC์นด ์ฃผํ 2
์ง๋ ์๊ฐ์ ์ด์ด์ ์ฝ๋ ์ค๋ช ์ ๊ณ์ํด ๋ณด๊ฒ ์ต๋๋ค.

- joy node โ joy teleop axes :
publish : sensor_msgs/Joy
- joy teleop axes โ donkey_control :
subscribe : sensor_msgs/Joy
publish : ackermann_msgs/AckermannDriveStamped
- donkey_control :
subscribe : ackermann_msgs/AckermannDriveStamped
โ
PWM (PCA9685)
๋๋จ ๋ ธ๋์์๋ถํฐ ์ญ์์ผ๋ก ์ฌ๋ผ์ค๋ฉด์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
donkey_control
- joy_control.py
์ฝ๋๋ ๋ค์ ์์น์ ์์ต๋๋ค. โ
donkey_ros/donkey_control/src
#!/usr/bin/env python
"""
Node for control PCA9685 using AckermannDriveStamped msg
referenced from donekycar
url : https://github.com/autorope/donkeycar/blob/dev/donkeycar/parts/actuator.py
"""
import time
import rospy
from threading import Thread
from ackermann_msgs.msg import AckermannDriveStamped
class PCA9685:
"""
PWM motor controler using PCA9685 boards.
This is used for most RC Cars
"""
def __init__(
self, channel, address=0x40, frequency=60, busnum=None, init_delay=0.1
):
self.default_freq = 60
self.pwm_scale = frequency / self.default_freq
import Adafruit_PCA9685
# Initialise the PCA9685 using the default address (0x40).
if busnum is not None:
from Adafruit_GPIO import I2C
# replace the get_bus function with our own
def get_bus():
return busnum
I2C.get_default_bus = get_bus
self.pwm = Adafruit_PCA9685.PCA9685(address=address)
self.pwm.set_pwm_freq(frequency)
self.channel = channel
time.sleep(init_delay) # "Tamiya TBLE-02" makes a little leap otherwise
self.pulse = 340
self.prev_pulse = 340
self.running = True
def set_pwm(self, pulse):
try:
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))
except:
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))
def run(self, pulse):
pulse_diff = pulse - self.prev_pulse
if abs(pulse_diff) > 40:
if pulse_diff > 0:
pulse += 0.7 * pulse_diff
else:
pulse -= 0.7 * pulse_diff
self.set_pwm(pulse)
self.prev_pulse = pulse
def set_pulse(self, pulse):
self.pulse = pulse
def update(self):
while self.running:
self.set_pulse(self.pulse)
class Vehicle(object):
def __init__(self, name="donkey_ros"):
self._throttle = PCA9685(channel=0, busnum=1)
rospy.loginfo("Throttle Controller Awaked!!")
self._steering_servo = PCA9685(channel=1, busnum=1)
rospy.loginfo("Steering Controller Awaked!!")
self._name = name
self._teleop_sub = rospy.Subscriber(
"/donkey_teleop",
AckermannDriveStamped,
self.joy_callback,
queue_size=1,
buff_size=2 ** 24,
)
rospy.loginfo("Teleop Subscriber Awaked!! Waiting for joystick...")
def joy_callback(self, msg):
speed_pulse = msg.drive.speed
steering_pulse = msg.drive.steering_angle
print(
"speed_pulse : "
+ str(speed_pulse)
+ " / "
+ "steering_pulse : "
+ str(steering_pulse)
)
self._throttle.run(speed_pulse)
self._steering_servo.run(steering_pulse)
if __name__ == "__main__":
rospy.init_node("donkey_control")
myCar = Vehicle("donkey_ros")
rate = rospy.Rate(10)
while not rospy.is_shutdown():
rate.sleep()
ํญ์ ๊ทธ๋ฌ๋ ๊ฒ๊ณผ ๊ฐ์ด ์์์๋ถํฐ ์ฐจ๊ทผ์ฐจ๊ทผ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ๐๐
- import
์์ ์ดํด๋ณธ ackermann_msgs์ AckermannDriveStamped๊ฐ ๋ณด์ ๋๋ค.
import time
import rospy
from threading import Thread
from ackermann_msgs.msg import AckermannDriveStamped
์ด๋ ํ ๋ชจ์ต์ด์๋์ง ๋ค์ ํ ๋ฒ ์ง๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
$ rosmsg info ackermann_msgs/AckermannDriveStamped
std_msgs/Header header
uint32 seq
time stamp
string frame_id
ackermann_msgs/AckermannDrive drive
float32 steering_angle
float32 steering_angle_velocity
float32 speed
float32 acceleration
float32 jerk
- main
if __name__ == "__main__":
rospy.init_node("donkey_control")
myCar = Vehicle("donkey_ros")
rate = rospy.Rate(10)
while not rospy.is_shutdown():
rate.sleep()
Vehicle์ด๋ผ๋ ํด๋์ค๋ฅผ ์์ฑํ๊ณ , 10Hz์ ์ฃผ๊ธฐ๋ฅผ ๊ฐ๊ณ ๋ฐ๋ณต์ํค๊ณ ์์ต๋๋ค.
- Vehicle Class
์ค์ง์ ์ธ subscriber์
๋๋ค.
PCA9685
ํด๋์ค 2๊ฐ๋ฅผ ์ ์ ํ๊ณ ์์ผ๋ฉฐ, ์ ์ํ์
์ผ ํ ์ ์ผ๋ก, ๋ฒ๋ฒ
์์ ์ค์ด๊ธฐ ์ํด buff_size๋ฅผ ์ถ๊ฐ ์ค์ ํ ๊ฒ์ด ๋ณด์
๋๋ค.
๋ก๊ทธ๋ฅผ ์ฐ๊ณ ์๋ ๋ชจ์ต๋ ๋ณด์ด๋ค์.
class Vehicle(object):
def __init__(self, name="donkey_ros"):
self._throttle = PCA9685(channel=0, busnum=1)
rospy.loginfo("Throttle Controller Awaked!!")
self._steering_servo = PCA9685(channel=1, busnum=1)
rospy.loginfo("Steering Controller Awaked!!")
self._name = name
self._teleop_sub = rospy.Subscriber(
"/donkey_teleop",
AckermannDriveStamped,
self.joy_callback,
queue_size=1,
buff_size=2 ** 24,
)
rospy.loginfo("Teleop Subscriber Awaked!! Waiting for joystick...")
def joy_callback(self, msg):
speed_pulse = msg.drive.speed
steering_pulse = msg.drive.steering_angle
print(
"speed_pulse : "
+ str(speed_pulse)
+ " / "
+ "steering_pulse : "
+ str(steering_pulse)
)
self._throttle.run(speed_pulse)
self._steering_servo.run(steering_pulse)
- PCA9685 Class
์ด class๋ donkeycar ํ๋ก์ ํธ์์ ๊ฐ์ ธ์จ ๊ฒ์ ๋๋ค. donekycar๋ ์ค๋ก์ง ํ์ด์ฌ์ผ๋ก ์ง์ฌ์๊ธฐ ๋๋ฌธ์ ๋๋ถ๋ถ์ ์์ ๋ค์ด threading์ ํตํด ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฉ๋๋ค. ROS๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด, ์ด๋ฌํ ์์ ์ ํฌ๊ฒ ์ ๊ฒฝ์ฐ์ง ์์๋ ๋๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
class PCA9685:
"""
PWM motor controler using PCA9685 boards.
This is used for most RC Cars
"""
def __init__(
self, channel, address=0x40, frequency=60, busnum=None, init_delay=0.1
):
self.default_freq = 60
self.pwm_scale = frequency / self.default_freq
import Adafruit_PCA9685
# Initialise the PCA9685 using the default address (0x40).
if busnum is not None:
from Adafruit_GPIO import I2C
# replace the get_bus function with our own
def get_bus():
return busnum
I2C.get_default_bus = get_bus
self.pwm = Adafruit_PCA9685.PCA9685(address=address)
self.pwm.set_pwm_freq(frequency)
self.channel = channel
time.sleep(init_delay) # "Tamiya TBLE-02" makes a little leap otherwise
self.pulse = 340
self.prev_pulse = 340
self.running = True
def set_pwm(self, pulse):
try:
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))
except:
self.pwm.set_pwm(self.channel, 0, int(pulse * self.pwm_scale))
def run(self, pulse):
pulse_diff = pulse - self.prev_pulse
if abs(pulse_diff) > 40:
if pulse_diff > 0:
pulse += 0.7 * pulse_diff
else:
pulse -= 0.7 * pulse_diff
self.set_pwm(pulse)
self.prev_pulse = pulse
def set_pulse(self, pulse):
self.pulse = pulse
def update(self):
while self.running:
self.set_pulse(self.pulse)
Adafruit_PCA9685 ๋ชจ๋์ ๊ฐ์ ธ์ค๋ ๋ชจ์ต์ด ๋ณด์ด๋ฉฐ, PWM Pulse๋ฅผ ์กฐ์ ํ์ฌ ๋ชจํฐ๋ฅผ ๊ตฌ๋์ํฌ ์ ์์ต๋๋ค.
- ์ด์ calibration ๊ฐ์์์ ๋ณด์๋ฏ, ๊ฐ์๊ธฐ ํฐ ์กฐํฅ์ ์ฃผ๋ฉด, ๋ฉ์ถฐ๋ฒ๋ฆด ์ ์๊ธฐ ๋๋ฌธ์ ๋ค์๊ณผ ๊ฐ์ ์ฒ๋ฆฌ๊ฐ ๋ค์ด๊ฐ์ต๋๋ค. (maze ์์ ์์ ๋ณด์๋ ๋ฐฉ์์ด์ง์?)
if abs(pulse_diff) > 40:
if pulse_diff > 0:
pulse += 0.7 * pulse_diff
else:
pulse -= 0.7 * pulse_diff
๋ค์๊ธ PCA9685
ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ ๋ถ๋ถ์ ๊ฐ์ ธ์์ต๋๋ค.
throttle, steering์ด ๊ฐ๊ฐ 0๋ฒ 1๋ฒ ์ฑ๋์ ์ฌ์ฉํ๊ณ ์๋ค๋ ์ ์๊ธฐ์์ผ๋๋ฆฝ๋๋ค.
class Vehicle(object):
def __init__(self, name="donkey_ros"):
self._throttle = PCA9685(channel=0, busnum=1)
rospy.loginfo("Throttle Controller Awaked!!")
self._steering_servo = PCA9685(channel=1, busnum=1)
rospy.loginfo("Steering Controller Awaked!!")
joy teleop axes
์ด ๋
ธ๋๋, mit-racecar
๋ผ๋ ํ๋ก์ ํธ์์
ํ์ํ ๋ถ๋ถ์ ๊ฐ์ ธ์ ์์ ํ ๊ฒ์
๋๋ค. ํ๋ฅญํ ํ๋ก์ ํธ์ด๋ ์๊ฐ์ด ๋์ค ๋ ํจ๊ป ์ดํด๋ณด์๋ฉด ์ข์ต๋๋ค. ๐๐
Originally from https://github.com/ros-teleop/teleop_tools Pulled on April 28, 2017. Edited by Winter Guerra on April 28, 2017 to allow for default actions.
์ฝ๋๋ฅผ ๋ณผ ํ์์์ด, ์ฌ์ฉํ๊ธฐ ํธ๋ฆฌํ๊ฒ ์ค์ ๋ถ๋ถ์ ๋ฐ๋ก yaml ํ์ผ๋ก ๋นผ๋ ํ๋ก์ ํธ์ธ๋ฐ์. ๋ค์๊ณผ ๊ฐ์ ์์น์ ์์ต๋๋ค.
- donkey_joy/config/joy_teleop.yaml
joy_node:
deadzone: 0.01
autorepeat_rate: 20
coalesce_interval: 0.01
teleop:
# Default mode - Stop for safety
default:
type: topic
is_default: true
message_type: ackermann_msgs/AckermannDriveStamped
topic_name: donkey_teleop
message_value:
-
target: drive.speed
value: 370.0
-
target: drive.steering_angle
value: 375.0
# Enable Human control by holding Left Bumper
human_control:
type: topic
message_type: ackermann_msgs/AckermannDriveStamped
topic_name: donkey_teleop
deadman_buttons: [4]
axis_mappings:
-
axis: 1
target: drive.speed
scale: 40.0 # joystick will command plus or minus 2 meters / second
offset: 380.0
-
axis: 2
target: drive.steering_angle
scale: 78.0 # joystick will command plus or minus ~20 degrees steering angle
offset: 375.0
# Enable autonomous control by pressing right bumper
# This switch causes the joy_teleop to stop sending messages to input/teleop
# And send messages to /dev/null (an unused ROS topic)
autonomous_control:
type: topic
message_type: std_msgs/Int8
topic_name: /dev/null
deadman_buttons: [5]
message_value:
-
target: data
value: 0
message_type
: ์ฌ์ฉํ๋ topic msg ํ์ ์ ๋๋ค.
topic_name
: publishํ topic ์ด๋ฆ์ ๋๋ค.
deadman_buttons
: ํธ๋ฆฌ๊ฑฐ ๋ฒํผ์ ๋๋ค. (์ปจํธ๋กค๋ฌ ๋ค์ LB๋ฒํผ์ ๋๋ฌ์ค์ผ ํ๋ค๊ณ ๋งํ์์ง์?)
axis_mappings/offset
: ์กฐ์ข ์ ํธ์ ์์ ์ผ๋ก, ์์ donkey car calibration ๋น์ ์์๋ธ ๊ฐ์ ์ฌ์ฉํ์์ต๋๋ค.
axis_mappings/scale
: offset ์ ๊ธฐ์ ์ผ๋ก ์ด๋ ์ ๋์ ๋ฒ์๋ฅผ ๊ฐ์ง๊ณ ๋ณํํ ๊ฒ์ธ์ง ๋ํ๋ด๋ ๊ฐ์ ๋๋ค.
joy node
์ด ๋
ธ๋๋, ros์์ ๊ธฐ๋ณธ ์ ๊ณตํ๋ ํจํค์ง์
๋๋ค. ๊ทธ๋์
sudo apt-get
์ผ๋ก ์ค์น๊ฐ ๊ฐ๋ฅํ์์ง์.
์ฌ์ฉํ๋ message type์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
$ rosmsg info sensor_msgs/Joy
std_msgs/Header header
uint32 seq
time stamp
string frame_id
float32[] axes
int32[] buttons
axes
: ์์ชฝ ์กฐ์ดํจ๋ ์ํ์ข์ฐ ๋ฒํผ์ ๋งคํ๋๋ฉฐ, ๋ชจ๋ ์ตํฉํ์ฌ ๋ฐฐ์ด๋ก publish๋ฉ๋๋ค.
buttons
: X Y A B ๊ทธ๋ฆฌ๊ณ ์์ ํ์ํ ๋ฒํผ, ํ๋ฉด L/R ๋ฒํผ๊น์ง ๋ชจ๋๋ฅผ mappingํด์ ๋ฐฐ์ด๋ก ์ ๋ฌํ๊ฒ ๋ฉ๋๋ค.
ํฐ๋ฏธ๋์ ํตํด ํ์ธํด ๋ณผ๊น์?
# connection check!!
$ sudo jstest /dev/input/js0
$ roscore
$ rosrun joy joy_node
$ rostopic echo /joy


Reference



์ฒซ๋ฒ์งธ ํ๋ก์ ํธ๋ฅผ ๋ง์ณค์ต๋๋ค. ์ด ์ฝ๋๋ค์ ์ฌ์ฉํด์ ์์ฉํ ์ ์๋ ๊ฒ๋ค์ด ๋ง๊ตฌ๋ง๊ตฌ ์๊ฐ๋์ง ์๋์? ๐ ์ ๋ ๋ค์ ๊ฐ์๋ก ๋์ด๊ฐ์ ์นด๋ฉ๋ผ๋ฅผ ์ฌ์ฉํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ๐ธ