以前、joystickで亀を動かした。今回は亀の代わりにモーターをコントロールする。これは次の投稿を組み合わせたものになる。
●[ROS2]ROS2にjoystickを接続してみた
●DCモータードライバーDRV8871を使ってみる


上図のパッケージjoy_linuxとteleop_twist_joyは、ネットからインストールできる。従ってESP32のアプリケーションだけを作ればよい。

上の画像は、OLEDの表示画面である。上の行から順に
トピック/cmd_velのlinear.xの最高値と最低値
トピック/cmd_velのangular.zの最高値と最低値
トピック/cmd_velからモーター制御用のPWM値に変換した最高値と最低値
モータードライバー端子に出力中の現在値
を示している。linear.xとangular.zは、ステックを回せる範囲で最も大きく回しているので、これが実際に出力される最大値と考えられる。
/cmd_velの型は、geometry_msgs/msg/Twistでこのうち直進成分のlinear.xと回転成分のangular.zだけを使う。これからモーターを回すPWM値を作る。PWM値は分解能8bitに設定するので0~255になる。
int motor_L = msg->linear.x * 255 + msg->angular.z * 255;
今回使用するモータードライバーDRV8871は、正回転時はドライバーの入力ピンIN1を0にしてIN2に255までの正の値を入力する。逆回転の場合は、IN1とIN2の値を逆にする。上記motor_Lの値が正ならば正回転、負ならば逆回転とする。
また、PWMの値がある値より小さい場合は、モーターは回転しない。モーターが回転するぎりぎりの値を下限値とし、それより小さなPWM値は0に見なしている。上限値は255としmotor_Lの絶対値がこれを超える場合は、255に設定しなおしている。
使い方
[端末1] --- PC上でmicro-ROS-Agentを起動する。
sudo docker run -it --rm -v /dev:/dev --privileged --net=host microros/micro-ros-agent:jazzy udp4 --port 8888
[端末2] --- joystickの操作から/joyトピックをパブリッシュする。
ros2 run joy_linux joy_linux_node
[端末3] --- トピックjoy_velをturtlesimのtopicに変更してcmd_velをパブリッシュする。
ros2 launch teleop_twist_joy teleop-launch.py joy_vel:=/cmd_vel
------------------------------------------------------------------------- 以下、必要に応じて
# pubコマンドで/cmd_velをパブリッシュする。一定のスピードで回す場合に使う。
ros2 topic pub --rate 1 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.5, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.3}}"
# キーボードからトピック/cmd_velをパブリッシュする。ジョイステックで想定する最大値を超えるので特別な理由がなければ使わない。
ros2 run turtlesim turtle_teleop_key --ros-args --remap /turtle1/cmd_vel:=/cmd_vel
作成したESP32のアプリ
micro_ROS_Twist2Motor_ESP32
#include <dummy.h>
/* micro_ROS_Twist2Motor_ESP32 2025.01.18
【概要】twist型のトピックをsubscribeして、DC motorを回す。
受信したメッセージとモーター出力をOLEDに表示する。
linear.x, msg->angular.z
subscribe: /cmd_vel (Twist)
*/
#include <Arduino.h>
#include <micro_ros_arduino.h>
#include <stdio.h>
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <geometry_msgs/msg/twist.h>
#include <Wire.h> // SSD1306 — OLED用
#include “SSD1306.h” // SSD1306用ライブラリを読み込み
SSD1306 display(0x3c, 21, 22); //SSD1306インスタンスの作成(I2Cアドレス,SDA,SCL)
const int motor1Pin1 = 12;
const int motor1Pin2 = 13;
const int MAX_SPEED = 254; // 最大スピード
const int MIN_SPEED = 70; // 最小スピード
//—– グローバル変数に行ごとの内容を保持
String line1 = “msg_cnt: 0”; // 1行目
String line2 = “”; // 2行目
String line3 = “”; // 3行目
String line4 = “”; // 4行目
rcl_node_t node;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_init_options_t init_options; //[追加]
rmw_init_options_t* rmw_options; //[追加]
rcl_subscription_t subscriber_tw;
geometry_msgs__msg__Twist msg;
#define STR_SIZE (100) //最大の受信文字数
rclc_executor_t executor_sub;
#define DOMAIN_ID 1 //[追加]
#define LED_PIN 2
#define RCCHECK(fn) \
{ \
rcl_ret_t temp_rc = fn; \
if ((temp_rc != RCL_RET_OK)) \
{ \
error_loop(); \
} \
}
#define RCSOFTCHECK(fn) \
{ \
rcl_ret_t temp_rc = fn; \
if ((temp_rc != RCL_RET_OK)) \
{ \
} \
}
int sub_cnt = 0; // subscription回数
int motor_L_max = 0;
int motor_L_min = 0;
const float FLT_MAX = 0.0;
float linear_x_max = FLT_MAX;
float linear_x_min = FLT_MAX;
float angular_z_max = FLT_MAX;
float angular_z_min = FLT_MAX;
/**
* @brief loop to indicate error with blinking LED
*
*/
void error_loop()
{
while (1)
{
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
//[追加] OLEDにエラーを表示する。
String err_msg = “”;
if (LED_PIN == 0) {
err_msg = ” Error”;
} else {
err_msg = ” “;
}
display.drawString(0, 17, err_msg);
display.display(); // 画面を表示する
}
delay(200);
}
/**
* @brief subscription callback executed at receiving a message
*
* @param msgin
*/
//———————————————————————— cmd_velのコールバック関数
void subscription_callback_twist(const void *msgin) {
const geometry_msgs__msg__Twist *msg = (const geometry_msgs__msg__Twist *)msgin;
//—– 最大値, 最小値取得
if (msg->linear.x > linear_x_max) linear_x_max = msg->linear.x;
if (msg->linear.x < linear_x_min) linear_x_min = msg->linear.x;
if (msg->angular.z > angular_z_max) angular_z_max = msg->angular.z;
if (msg->angular.z < angular_z_min) angular_z_min = msg->angular.z;
char buf_l1[32];
sprintf(buf_l1, “La:%.2f,Li:%.2f”, linear_x_max, linear_x_min);
line1 = buf_l1;
char buf_l2[32];
sprintf(buf_l2, “Aa:%.2f,Ai:%.2f”, angular_z_max, angular_z_min);
line2 = buf_l2;
// cmd_velをPWM出力に変換する。
int motor_L = msg->linear.x * 255 + msg->angular.z * 255;
// 最大値, 最小値取得
if (motor_L > motor_L_max)
motor_L_max = motor_L;
if (motor_L < motor_L_min)
motor_L_min = motor_L;
char buf_l3[32];
sprintf(buf_l3, “mx: %03d, mi: %03d”, motor_L_max, motor_L_min);
line3 = buf_l3;
// PWM出力の上限値チェック
if (abs(motor_L) > MAX_SPEED)
{
if (motor_L > 0)
motor_L = MAX_SPEED;
else
motor_L = -MAX_SPEED;
}
// PWM出力の下限値チェック
else if (abs(motor_L) < MIN_SPEED)
motor_L = 0;
// モーター出力をドライバーへの出力に変換する
int motor_L_IN1, motor_L_IN2;
if (motor_L > 0)
{
motor_L_IN1 = motor_L;
motor_L_IN2 = 0;
}
else
{
motor_L_IN1 = 0;
motor_L_IN2 = abs(motor_L);
}
analogWrite(motor1Pin1, motor_L_IN1);
analogWrite(motor1Pin2, motor_L_IN2);
//—– 4行目の内容を更新 モーター出力値
char buf_l4[32];
sprintf(buf_l4, “L1: %03d, L2: %03d”, motor_L_IN1, motor_L_IN2);
line4 = buf_l4;
//—– OLED表示
display.clear(); // 画面をクリア
display.drawString(0, 0, line1); // 1行目の表示
display.drawString(0, 17, line2); // 2行目の表示
display.drawString(0, 34, line3); // 3行目の表示
display.drawString(0, 49, line4); // 4行目の表示
display.display(); // 画面を更新
}
void setup()
{
//[追加]—– 初期画面表示
display.init(); //ディスプレイを初期化
display.setFont(ArialMT_Plain_16); //フォントを設定
display.drawString(0, 0, “*Program name*”); //(0,0)の位置に表示
display.drawString(0, 17, “micro_ROS_Twis”);
display.drawString(0, 34, “t2motor_ESP32”);
display.display(); //指定された情報を描画
// DC motor
pinMode(motor1Pin1 , OUTPUT);
pinMode(motor1Pin2 , OUTPUT);
analogWrite(motor1Pin1, 0);
analogWrite(motor1Pin2, 0);
set_microros_wifi_transports(“aterm-2b4139-a”, “3e00cfa4ba409”, “192.168.0.63”, 8888);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
delay(3000);
allocator = rcl_get_default_allocator();
// Init options to use domain id —- DOMEIN IDを設定する
init_options = rcl_get_zero_initialized_init_options();
RCCHECK(rcl_init_options_init(&init_options, allocator));
rmw_options = rcl_init_options_get_rmw_init_options(&init_options);
rcl_init_options_set_domain_id(&init_options, (size_t)DOMAIN_ID);
RCCHECK(rclc_support_init_with_options(&support, 0, NULL, &init_options, &allocator));
// create node
RCCHECK(rclc_node_init_default(&node, “micro_ros_xiao_node”, “”, &support));
// create subscriber ————————————————————— cmd_vel 2025.01.11
RCCHECK(rclc_subscription_init_default(
&subscriber_tw,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist),
“cmd_vel”));
// create executor —- subscriber
RCCHECK(rclc_executor_init(&executor_sub, &support.context, 1, &allocator));
RCCHECK(rclc_executor_add_subscription(&executor_sub, &subscriber_tw, &msg, &subscription_callback_twist, ON_NEW_DATA));
}
void loop()
{
delay(10);
RCCHECK(rclc_executor_spin_some(&executor_sub, RCL_MS_TO_NS(100)));
}