はじめに
ROSのノード間通信をシリアル通信越しに行なうためのプロトコルが、rosserialです。このArduino向けの実装がrosserial_arduinoです。(初めて名前見たときは、ROSで直接シリアル通信をするパッケージだと勝手に思い込んでいました。) これを使ってROSからArduino UNOのIOを動かして、ロボに指令を送ったり状態を読んだりするプログラムを書きました。ふと思い立ったので公開しようと思います。
こんな感じのことができるようになります。
rosserialで、PC(Ubuntu)からArduino UNOにtopic通信で数字を送って、それをD2ピン以降を使って二進数表記で出力できた #ROS pic.twitter.com/AitaMS9ZpI
— ふるおらいと (@36kyo) 2019年11月14日
動作を確認した環境は、Ubuntu16.04 ROS 1 Kinetic Kameです。
今更感がすごいのですが、ふと前に書いてたことを思い出したのと、その時にサンプルが少なくて困ったのを思い出して公開しようと思いました。 新しい環境でも動くか試して記事を更新したい。
rosserial
環境構築については最新の情報をこちらから rosserial_arduino/Tutorials - ROS Wiki
rosserial_なんたら という名前のものがたくさんあったりしてややこしくて混乱して調べたメモです。 間違っていたらご指摘お願いいたします。
| 名前 | 内容 |
|---|---|
| rosserial | ROSのノード間通信をシリアル通信越しに行なうためのプロトコル(決めごと)。rosserial - ROS Wiki |
| rosserial_client | rosserialのプロトコルで通信するためのマイコン(クライアント)側のライブラリ。rosserial_client - ROS Wiki |
| rosserial_arduino | rosserial_clientをArduinoで使えるようにするROSパッケージ。その中のArduinoライブラリがros_lib。rosserial_arduino - ROS Wiki |
| rosserial_python | PC(ホスト)側のROSパッケージ。Python実装版。rosserial_python - ROS Wiki |
| serial_node | rosserial_pythonのノード。このノード越しにマイコンと通信する。他のノードとノード間通信をして、マイコンの面倒もみる。pyserialでマイコンとシリアル通信をして、もらったメッセージをマイコンに送ったり、マイコンから受け取ったメッセージを別のノードに送ったりする。これにより、ノード間通信をする感覚で、マイコンと通信できる。 |
| xx.ino | いつものArduinoのコード。rosserialプロトコルを使うためのros_libライブラリから必要なものをインクルードして、どういったROSメッセージをやりとりするのかを追記する。 |
名前がわかりづらいので、rosserial_host_hogehoge、rosserial_client_hogehogeみたいな名前にならないかしら←
rosserialでIO操作するコード
IO操作で動かせるロボがあって、ROSで動かせるようにしたいなと思って書いたものです。
- Arduino Uno 入出力設定
- シリアル通信 ROSとの通信で使用 ( D0,D1 Rx, Tx )
- デジタル出力 13pin ( D2~D13, A0 )
- デジタル入力 5pin ( A1~A5 )
ノード構成
- お膳立てノード
- 「〇〇信号を出して」「××信号を3にして」等といった指示をsubscribe
- Arduinoの出力ピンの上げ下げを表す2進数へ変換してwrite_pinメッセージとしてpublish
- Arduinoノード .ino(serial_node)
- デジタル出力するピンの情報であるwrite_pinメッセージをsubscribe
- デジタル入力するピンの情報であるread_pinメッセージをpublish
- 各ピンの入出力状態のデバッグ出力をするフラグであるio_sendLog_flagメッセージをsubscribe
2つのノードに分けて使っていました。 Arduino側ノードに直接〇〇信号メッセージ、xx信号メッセージを送るのではなく、お膳立てノードを一段挟んでいます。 Arduino側のコードは一発書いたらずっとそのままで、デジタルピンの使い道を変えたかったらお膳立てノード側の処理を変更するだけで対応できます。 ピンの使い方を変えようと思ったら、Arduino側のコードを変えて、通信方法を考えなければいけないしいちいちArduinoに書き込んだりするのが手間だと思ってこのような構成にしました。このArduino側のコードを紹介しようと思います。
(間に何も挟まずに直接やりとりするのが普通だと思います。 お膳立てノードでやっている、複数ピンの状態を表す2進数への変換をするといったぐらいの処理ならそのままマイコンでやってしまえます。 何なら反射みたいに、PC介さずにマイコン内でピン入力から条件分岐してそのまま出力を変化させるぐらいやってしまった方が高速に反応できるしROS側との通信も少なくてよいとか色々あると思います。今回の使用用途的には間に合っていたので、この構成になっています。)
以下Arduinoのコードと、その説明です。
/* rosserial --- $ roscore --- $ rosrun rosserial_python serial_node.py /dev/ttyACM0 --- $ rostopic list --- $ rostopic pub -1 /write_pin std_msgs/UInt16 0 $ rostopic pub -1 /write_pin std_msgs/UInt16 1 ... $ rostopic pub -1 /write_pin std_msgs/UInt16 8191 $ rostopic echo /read_pin $ rostopic pub -1 /io_sendLog_flag std_msgs/Bool "data: true" $ rostopic pub -1 /io_sendLog_flag std_msgs/Bool "data: false" --- Arduino UNO 0 HardwareSerial RX 1 HardwareSerial TX 2 digitalWrite 3 digitalWrite 4 digitalWrite 5 digitalWrite 6 digitalWrite 7 digitalWrite 8 digitalWrite 9 digitalWrite 10 digitalWrite 11 digitalWrite 12 digitalWrite 13 digitalWrite 14 A0 digitalWrite 15 A1 digitalRead 16 A2 digitalRead 17 A3 digitalRead 18 A4 digitalRead 19 A5 digitalRead pin mapping -> #define, output_pin[], input_pin[] */ // ------------------------------------------------- #include <ros.h> #include <std_msgs/UInt8.h> // publish 5 pin input #include <std_msgs/UInt16.h> // subscribe 13 pin output #include <std_msgs/Bool.h> // subscribe sendLog flag ros::NodeHandle nh; std_msgs::UInt16 msg_writePin; std_msgs::UInt8 msg_readPin; std_msgs::Bool msg_sendLogFlag; // settings ----------------------------------------- #define USE_INPUTPULLUP // ifdef use pinMode( pin, INPUT_PULLUP); #define BOARD_PINS_NUM 20 // 0~19 #define OUTPUT_PIN_NUM 13 #define OUTPUT_PIN_USE_BITMASK (0b0001111111111111) // use pin:1 #define INPUT_PIN_NUM 5 #define INPUT_PIN_USE_BITMASK (0b00011111) // pin mapping const int output_pin[ OUTPUT_PIN_NUM ] = { 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 }; const int input_pin[ INPUT_PIN_NUM ] = { 15, 16, 17, 18, 19 }; char pin_state_log[ BOARD_PINS_NUM +1 ] = // +\0 { "RT000000000000000000" // rx, tx, 0, 0, 0,... }; char charBuf[100]; // prototype declaration void digitalOut(int pin, int state); int digitalIn(int pin); void messageCbWritePin( const std_msgs::UInt16 &msg); void messageCbSendLog( const std_msgs::Bool &msg); #define PUB_READPIN_INTERVAL 20 // [ms] 1000[ms] / 50[Hz] #define INFO_SENDLOG_INTERVAL 20 // [ms] 1000[ms] / 50[Hz] unsigned long timeMs = 0; unsigned long timeMsBuf[2] = {0}; ros::Subscriber<std_msgs::UInt16> sub_writePin("write_pin", &messageCbWritePin); ros::Publisher pub_readPin ("read_pin", &msg_readPin); ros::Subscriber<std_msgs::Bool> sub_sendLog ("io_sendLog_flag", &messageCbSendLog); void setup() { // pin init for(int i = 0; i< OUTPUT_PIN_NUM; i++) { pinMode(output_pin[ i ], OUTPUT); digitalOut(output_pin[ i ], LOW); } for(int i = 0; i< INPUT_PIN_NUM; i++) { #ifdef USE_INPUTPULLUP pinMode(input_pin[ i ], INPUT_PULLUP); #else pinMode(input_pin[ i ], INPUT); #endif digitalIn(input_pin[ i ]); } msg_writePin.data = 0; msg_readPin.data = 0; msg_sendLogFlag.data = false; nh.initNode(); nh.advertise(pub_readPin); nh.subscribe(sub_writePin); nh.subscribe(sub_sendLog); } void loop() { timeMs = millis(); // pin read ---------------------------------------------- if( timeMs - timeMsBuf[0] >= PUB_READPIN_INTERVAL ) { msg_readPin.data = readPinState() & INPUT_PIN_USE_BITMASK; pub_readPin.publish(&msg_readPin); timeMsBuf[0] = timeMs; } // ------------------------------------------------------- nh.spinOnce(); // send log ---------------------------------------------- if( msg_sendLogFlag.data ) { if( timeMs - timeMsBuf[1] >= INFO_SENDLOG_INTERVAL ) { nh.loginfo( pin_state_log ); timeMsBuf[1] = timeMs; } } // ------------------------------------------------------- } void messageCbWritePin( const std_msgs::UInt16 &msg) { // update writepin state msg_writePin = msg; msg_writePin.data = msg.data & OUTPUT_PIN_USE_BITMASK; int state = 0; for(int i = 0; i< OUTPUT_PIN_NUM; i++) { state = (msg_writePin.data >> i) & 1; if(state == 1) { digitalOut(output_pin[ i ], HIGH ); }else{ digitalOut(output_pin[ i ], LOW ); } } } // ex. data = 6 = 0b0110 ↓ // i = 0, 6 >> 0 = 0b011|0|, data & 1 = 0 // i = 1, 6 >> 1 = 0b001|1|, data & 1 = 1 // i = 2, 6 >> 2 = 0b000|1|, data & 1 = 1 // i = 3, 6 >> 3 = 0b000|0|, data & 1 = 0 // ... ^ void messageCbSendLog( const std_msgs::Bool &msg) { msg_sendLogFlag = msg; } // write and memory void digitalOut(int pin, int state) { digitalWrite(pin, state); if(state == HIGH) { pin_state_log[ pin ] = '1'; }else{ pin_state_log[ pin ] = '0'; } } // read and memory int digitalIn(int pin) { int state = digitalRead(pin); #ifdef USE_INPUTPULLUP if( state == LOW ) { pin_state_log[ pin ] = '1'; }else{ pin_state_log[ pin ] = '0'; } return 1 - state; #else if( state == HIGH ) { pin_state_log[ pin ] = '1'; }else{ pin_state_log[ pin ] = '0'; } return state; #endif } uint8_t readPinState() { uint8_t readBuf = 0; for(int i = 0; i < INPUT_PIN_NUM; i++) { readBuf += digitalIn(input_pin[ i ]) << i; } return readBuf; }
次のメッセージをやり取りします。
std_msgs::UInt16 msg_writePin; std_msgs::UInt8 msg_readPin; std_msgs::Bool msg_sendLogFlag;
log出力レベル INFO で
"RT000000000000000000" // rx, tx, 0, 0, 0,...
といったような感じでピンの入出力状態のデバッグ出力をします。
ifdef で、入力をINPUT_PULLUPにするかどうか選択できるようにしています。
#define USE_INPUTPULLUP
シリアル通信用の2ピンを飛ばして、D2~D13,A0を出力ピン、A1~A5を入力ピンとしています。 配列にいれて置き換えているので、順番等自由に変えられます。
入力ピンの状態をpublishする周期、ピンの入出力状態をデバッグ出力する周期をmsで設定します。
#define PUB_READPIN_INTERVAL 20 // 1000 / 50 #define INFO_SENDLOG_INTERVAL 20 // 1000 / 50
millis()とタイムスタンプを比較して、ここで指定した周期ごとに通信を出力を行ないます。
やりとりするROSメッセージの設定です。
ros::Subscriber<std_msgs::UInt16> sub_writePin("write_pin", &messageCbWritePin);
ros::Publisher pub_readPin ("read_pin", &msg_readPin);
ros::Subscriber<std_msgs::Bool> sub_sendLog ("io_sendLog_flag", &messageCbSendLog);
ここで指定したコールバック関数が各メッセージ受信、送信時に呼ばれて デジタル入出力等を行ないます。
ピンの入出力はdigitalRead,digitalWriteを直接呼ばずに、digitalIn,digitalOutという関数経由で呼び、 このときに、デバッグ用のピン入出力状態の変数を一緒に更新しています。 INPUT_PULLUPとの読み替えもここでやっています。
動かしてみる
上のinoファイルをArduinoに書き込みます。 書き込んだら、ターミナルから試しにコマンドでメッセージを送って、Arduinoと通信してみます。 以下がその例です。
serial_nodeを動かして、マイコンと通信する。
$ roscore $ rosrun rosserial_python serial_node.py /dev/ttyACM0
Topic通信一覧を見る
$ rostopic list
ピンの入出力状態のデバッグ出力フラグ io_sendLog_flagを送る(publishする)例 trueでデバッグ出力する。falseでデバッグ出力しない。
$ rostopic pub -1 /io_sendLog_flag std_msgs/Bool "data: true" $ rostopic pub -1 /io_sendLog_flag std_msgs/Bool "data: false"
write_pinメッセージを送る(publishする)例 13ピン分で、13bit 0b0~0b1111111111111(0~8191)
$ rostopic pub -1 /write_pin std_msgs/UInt16 0 $ rostopic pub -1 /write_pin std_msgs/UInt16 1 ... $ rostopic pub -1 /write_pin std_msgs/UInt16 8191
read_pinメッセージを受ける(subscribeする)例
$ rostopic echo /read_pin