OSOYOO PI CARキット ROS2化 #1 – ROS2でPI CARの駆動制御を行う

OSOYOO PI CAR

前回ROS2のインストールが完了しましたので、続いて、OSOYOO PI CARキットをROS2を使って走らせようと思います。今回は、下記の順番で進めようと思います。
各ノードの設計については、できるだけ簡潔に説明しようと思いますが、ROS2のお作法的なところは省略させていただきます。

  1. OSOYOO PI CARを動作させる前準備
    OSOYOO PI CARはPCA9685 PWMライブラリを使用して駆動モーターを制御します。
    PCA9685を使用するためにI2C、GPIOを有効化します。
  2. パッケージの準備
    まずは、駆動系、テストノード設計に必要なパッケージを用意します。
    OSOYOO PI CARのリソースがPythonですので、まずは、python用を用意します。
  3. 駆動系ノード
    車輪を駆動して車を移動制御するノードです。
  4. テストノード
    駆動系ノードのテストドライバです。

OSOYOO PI CARはPCA9685 PWMライブラリを使用して駆動モーターを制御します。
PCA9685を使用するために必要なI2C、GPIOを有効化します。

〇 raspi-configの有効化

ラズパイのI2Cバスを有効にするためと、今後のデバイスの有効化/無効化を行う際にraspi-configを入れておくと楽なので、下記手順でUnbuntuに導入しておきます。
なお、この時シェルはbashを使用してください。

% bash
$ sudo echo "deb http://archive.raspberrypi.org/debian/ buster main" >> /etc/apt/sources.list
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 7FA3303E
$ sudo apt-get update
$ sudo apt-get install raspi-config
$ sudo mount /dev/mmcblk0p1 /boot

〇 I2Cの有効化

I2Cを有効化するためにrspi-configを起動します。

$ sudo rspi-config

起動後に下記の手順でメニューを選択してI2Cバスを有効化します。

「Interfacing Options」→「I2C」→「Yes」→「Ok」→「Fiinish」

〇 gpio/pca9685 pythonライブラリ導入

車輪モーターの駆動に必要なライブラリ:GPIO/PCA9685を導入します。

$ sudo pip3 install adafruit-gpio
$ sudo pip3 install adafruit-pca9685

いきなりパッケージという用語が出てきてわかりにくいと思いますが、ROS2はビルド/実行単位をパッケージ毎に行います。プログラムの実行単位(ノード)は、パッケージ内に複数作成することが可能です。このパッケージはプログラム言語毎に作成することになっています。(python用、C++用等々)
今回は、下記のようなパッケージとすることにしました。

パッケージ名言語内容
picar_py_pkgpythonpython用 pi car パッケージ
picar_cpp_pkgC++C++用 pi car パッケージ
picar_interfacespicar制御に必要なメッセージ、サービス定義
picar_bringuppythonlaunch設定
パッケージ一覧

一応、パッケージは決めましたが、ルールを決めただけでパッケージが必要になった時点で順次作成していこうと思いますが、作成手順を記載します。
今回は、下記のような構成でROS2ワークスペースを構成します。

.
├── build                  ← ROS2環境生成時に自動生成される
├── install                ← 同上
├── log                    ← 同上
└── src                    ← ソースディレクトリ(緑字は、これから生成するパッケージ)
    ├── picar_py_pkg
    ├── picar_cpp_pkg
    ├── picar_interfaces
    └── picar_brinngup

ここでは、ros2_wsをROS2ワークスペースとして取り扱います。
パッケージ生成後に各言語毎におまじないをしなと使えません。生成時に説明しようと思います。

$ cd ros_ws
$ colcon build
$ mkdir src
$ ros2 pkg create --build-type ament_python picar_py_pkg
$ ros2 pkg create --build-type ament_cmake picar_cpp_pkg
$ ros2 pkg create picar_interfaces
$ ros2 pkg create bringup

駆動系ノードは、pythonで作成することにしました。
強い理由はないですが、osoyooのリソースがpythonだったくらいの理由です。
picarの駆動制御IFは、外部ノードより「前進」「後退」「左回転」「右回転」の指示に従い、動作するように設計します。今回は、ROS2で標準的に用意されているメッセージgeometry_msgs/twistを使用します。このメッセージは下記のように定義されていますが、便宜上、picar制御用に読み替えて使用します。

Twist メンバーlinear/angularメンバーpicar 制御
linearx+方向:前進(0.0 – 1.0)
-方向:後進(-1.0 – 0.0)
0:停止

数値は、前進/後退スピードを示す
y未使用
z未使用
angularx未使用
y未使用
zz軸の回転時間を示す。
-方向:右方向の回転。(0.0 – 1.0)
+方向:左方向の回転。(-1.0 – 0.0)

数値は、回転時間(秒)を示す。大体1秒で360度回転程度。
舵角制御したいので、指定時間が来たら停止。
geometry/twist(“/picar_vehicle/cmd_vel”)

具体的には、外部ノードが”/picar_vehicle/cmd_vel”という名称で発信するメッセージを駆動系ノードでキャッチして指示に従い動作するように設計します。

picar_vehicle:駆動系ノード(PicarVehicleNode)
#!/usr/bin/env python3
import rclpy
import time
from rclpy.node import Node
from Adafruit_PCA9685 import PCA9685
import RPi.GPIO as GPIO

from geometry_msgs.msg import Twist

class PicarVehicleNode(Node):
    def __init__(self):
        super().__init__("picar_vehicle")
        # this get the paramter to set running mode.
        self.declare_parameter("debug", False)
        self.is_debug_ = self.get_parameter("debug").value
        if self.is_debug_ == True:
            self.get_logger().info("debug mode has been started")
        else:
            self.get_logger().info("native mode has been started")

        # this configuration device
        self.pwm_ = PCA9685(address=0x40)   # PCA9685:address = 0x40
        self.pwm_.set_pwm_freq(60)          # 60hz for motor.
        GPIO.setmode(GPIO.BCM)              # is to specify GPIO No.
        GPIO.setwarnings(False)             # without warning.
        self.left_backward_ = 23            # dir = backward.(GPIO23)
        self.left_forward_ = 24             # dir = foward.(GPIO24)
        self.right_backward_ = 27           # dir = backward.(GPIO27)
        self.right_forward_ = 22            # dir = forward.(GPIO22)
        self.en_left_ = 0                   # PCA9685 left channel.
        self.en_right_ = 1                  # PCA9685 right channle.


        # GPIOをOUTPUTモードに設定
        GPIO.setup(self.left_backward_, GPIO.OUT)
        GPIO.setup(self.left_forward_, GPIO.OUT)
        GPIO.setup(self.right_backward_, GPIO.OUT)
        GPIO.setup(self.right_forward_, GPIO.OUT)

        # this starts cmd_vel subscription.
        self.subscriber_ = self.create_subscription(
            Twist, "picar_vehicle/cmd_vel", self.callback_cmd_vel, 10)

        # this starts vehicle controller.
        self.action_unit_ = 0.01
        self.life_time_ = 2.0
        self.x_val_ = 0.0
        self.theta_val_ = 0.0
        self.heart_beat_ = 0.0
        self.vehicle_timer_ = self.create_timer(
            self.action_unit_, self.vehicle_controller)

    def callback_cmd_vel(self, msg):
        self.get_logger().info("linear(" +
            str(msg.linear.x) + "," +
            str(msg.linear.y) + "," +
            str(msg.linear.z) + ") angular(" +
            str(msg.angular.x) + "," +
            str(msg.angular.y) + "," +
            str(msg.angular.z) + ")")

        self.heart_beat_ = self.life_time_
        self.theta_val_ = msg.angular.z
        self.x_val_ = msg.linear.x

    def vehicle_controller(self):
        if self.heart_beat_ <= 0.0:
            self.action_stop()
            return;
        self.heart_beat_ = self.heart_beat_ - self.action_unit_

        if self.theta_val_ < -self.action_unit_:
            self.theta_val_ = self.theta_val_ + self.action_unit_
            if self.is_debug_ == True:
                self.get_logger().info("move right:" + str(self.theta_val_));
            self.action_left(self.theta_val_)
            return
        elif self.theta_val_ > self.action_unit_:
            self.theta_val_ = self.theta_val_ - self.action_unit_
            if self.is_debug_ == True:
                self.get_logger().info("move left:" + str(self.theta_val_));
            self.action_right(self.theta_val_)
            return

        if self.x_val_ > self.action_unit_:
            if self.is_debug_ == True:
                self.get_logger().info("move forward");
            self.action_forward(self.x_val_)
        elif self.x_val_ < -self.action_unit_:
            if self.is_debug_ == True:
                self.get_logger().info("move backward");
            self.action_backward(self.x_val_)
        else:
            self.action_stop()

    def action_forward(self, speed):
        if speed > 1.0:
            speed = 1.0
        GPIO.output(self.left_backward_, GPIO.LOW)
        GPIO.output(self.left_forward_, GPIO.HIGH)
        GPIO.output(self.right_backward_, GPIO.LOW)
        GPIO.output(self.right_forward_, GPIO.HIGH)
        self.pwm_.set_pwm(self.en_left_, 0, int(speed * 4095))
        self.pwm_.set_pwm(self.en_right_, 0, int(speed * 4095))

    def action_backward(self, speed):
        if speed < -1.0:
            speed = -1.0
        GPIO.output(self.left_backward_, GPIO.HIGH)
        GPIO.output(self.left_forward_, GPIO.LOW)
        GPIO.output(self.right_backward_, GPIO.HIGH)
        GPIO.output(self.right_forward_, GPIO.LOW)
        self.pwm_.set_pwm(self.en_left_, 0, int(-speed * 4095))
        self.pwm_.set_pwm(self.en_right_, 0, int(-speed * 4095))

    def action_stop(self):
        GPIO.output(self.left_backward_, GPIO.LOW)
        GPIO.output(self.left_forward_, GPIO.LOW)
        GPIO.output(self.right_backward_, GPIO.LOW)
        GPIO.output(self.right_forward_, GPIO.LOW)
        self.pwm_.set_pwm(self.en_left_, 0, 0)
        self.pwm_.set_pwm(self.en_right_, 0, 0)

    def action_right(self, speed):
        if speed > 1.0:
            speed = 1.0
        GPIO.output(self.left_backward_, GPIO.LOW)
        GPIO.output(self.left_forward_, GPIO.HIGH)
        GPIO.output(self.right_backward_, GPIO.HIGH)
        GPIO.output(self.right_forward_, GPIO.LOW)
        self.pwm_.set_pwm(self.en_left_, 0, int(speed * 4095))
        self.pwm_.set_pwm(self.en_right_, 0, int(speed * 4095))

    def action_left(self, speed):
        if speed < -1.0:
            speed = -1.0
        GPIO.output(self.left_backward_, GPIO.HIGH)
        GPIO.output(self.left_forward_, GPIO.LOW)
        GPIO.output(self.right_backward_, GPIO.LOW)
        GPIO.output(self.right_forward_, GPIO.HIGH)
        self.pwm_.set_pwm(self.en_left_, 0, int(-speed * 4095))
        self.pwm_.set_pwm(self.en_right_, 0, int(-speed * 4095))

def main(args=None):
    rclpy.init(args=args)
    node = PicarVehicleNode()
    rclpy.spin(node)
    rclpy.shutdown()


if __name__ == "__main__":
    main()
PicarVehicleNode メソッド機能概要
__init__PicarVehicleNodeの初期化を行う。
PCA9685(PWMコントローラ)、GPIOのセットアップ後サブスクリプション(callback_cmd_vel)、駆動制御(vehicle_controller)を起動します。
callback_cmd_vel“/picar_vehicle/cmd_vel”サブスクリプションを行います。
受け取ったメッセージを駆動制御値に変換します。
vehicle_controller駆動制御値に従い、picarを前進、後退、左回転、右回転、停止します。
それぞれの制御には以下のメソッドを呼び出して制御します。
action_forward指定されたspeedで前進します。
action_backward指定されたspeedで後退します。
action_stoppicarを停止します。
action_rightpicarを右回転します。
action_leftpicarを左回転します。

駆動系ノードIFにgeometry/twistを使用しましたので、テストノードは手を抜いてROS2標準搭載のノード(teleop_twist_keyboard)を下記のように実行します。
ros2 runはノードの実行を行います。
下記は、「teleop_twist_keyboardパッケージのteleop_twist_keyboardノードを実行し、twistメッセージは、”/picar_vehicle/cmd_vel”で送信する。」ということなります。
あとは、キー操作でpicarが意図通りに動けば、駆動系ノードの動作確認完了です。

$ ros2 run teleop_twist_keyboard teleop_twist_keyboard cmd_vel:="/picar_vehicle/cmd_vel"
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

currently:	speed 0.5	turn 1.0 

駆動系ノードの動作確認を一通った様子です。
ほぼ、期待通りに動作することを確認できました。

動作確認の様子

次回は、動画配信のノードを試行していこうと思います。
また、よろしければ見ていただけると嬉しいです。

タイトルとURLをコピーしました