RaspberryPi 3BからTinker Boardにお引越し

こんにちは。パパドラゴンです。数年前からラズパイでサーバーを動かしています。もちろんこのページがあるサーバーも自作です。しかし最近音声認識とかOpenCVを使ったり、他のIoT機器と連携したりするとなるとちょっとCPUやら通信速度やらの性能不足が気になってきました。最近流行っているスマートホーム的な使い方を考えてみても、音声認識や画像認識、センサ値の取得などは小型のArduinoやラズパイに任せ、メインのコンピュータはそれらのデータを集めて他の機器に指示を出すような構成にしてみようかと思います。

そのため、ネットワーク通信の性能と低消費電力を基準に他のシングルボードコンピュータを調べてみたところ、Tinker Boardというのが良さそうだったので買ってきました。

Tinker Board

ASUS製のシングルボードコンピュータです。ラズパイと比較して、CPUの計算パワー、ネットワーク通信速度がかなり高いです。その代り価格もラズパイの2倍くらいしますが、この価格でこのスペックならばコストパフォーマンスはとても良いと言い切っていいでしょう。



型番にSがついているものはeMMC(内蔵フラッシュメモリ)があるタイプですが、僕の使い方では高速起動とかは必要ないかなと思ったので通常版を買いました。
電源はラズパイよりもさらに多い3.0Aが必要ですので、こちらも専用品を買ったほうが良いと思います。SDカードについては何でもいいですが、現在のラズパイで使っていてまだ一度も壊れていないという理由でSUMSUNG製を選びました。

OSインストール後やったこと

SDカードにTinker OSを焼いて起動した後の設定の備忘録です。

SSH関連のインストール

sudo apt-get install avahi-daemon xrdp xorgxrdp

ユーザー名「linaro」の変更

sambaのインストール

sudo apt-get install samba

geditのインストールとrootで編集する際のエラーの対策

sudo apt-get install gedit
visudoして最後の行にDefaults env_keep=”DISPLAY XAUTHORITY” を追加
このコマンド実行するとエラー無くなる(?)
xhost si:localuser:root
ターミナルを開くたびに実行しないといけないので、~/.bashrcに書いておいた

softetherのインストール

これが大変だった。まずはよくある手順通りにsoftetherのサイトからファイルをダウンロードして展開、makeした後/usr/localに移動。
起動スクリプトを/etc/init.d/vpnserverに書いて、systemctl start vpnserver.service。ところがクライアント側のWindowsのsoftether管理マネージャからログインができない。

原因はよくわからなかった。でもnetstat -ltupnと打ってみて443番や992番ポートをvpnserverが使っていなかった。sudo systemctl restart vpnserver.serviceすると治った。よくわからない。後は手順通りにWindows側から設定。

nginxのインストールとletsencrypt

nginxはsudo apt install nginx
let’s encryptはgit clone

(追加予定…)

ラズパイにOpenCV入れた(ビルドエラー解決)

こんにちは、パパドラゴンです。大晦日に何やってんだって感じですが、今ラズパイ3にOpenCVを入れました。ビルドエラーに嵌ってしまい、3日も無駄にしてしまいましたが、なんとか年越し前に完了しました。

環境

raspbian: 8.0
OpenCV: 3.1.0
Python: 2.7.9

方法

こちらのサイトに書いてあるとおりにコマンドを実行しました。

Raspberry Pi講座 Python3 + OpenCV3(環境構築編)
https://sites.google.com/site/memorandumjavaandalgorithm/raspberry-pi-jiang-zuo-opencv3

失敗① ビルドでerror終了する

通常のapt-get upgradeではなく、sudo apt-get dist-upgradeすると成功しました。

失敗② ビルドで失敗する その2

makeするところでmake -j4 ではなくsudo makeだけで実行すると成功しました。

失敗③ make後、installすると、pkgconfig is not a directoryというerrorが出る

音声解析ライブラリJuliusをインストールしたときに作られたファイルが既にあったのが原因でした。makeファイルの中でpkgconfigで検索かけて保存先を別の名前に変更するか、元々あったファイルをリネームするととりあえず解決しました。

これにてインストール完了。やったね!

【ROS】DS4コントローラを振動させる

やりたいこと

ROSを使うとプレステのコントローラ「DualShock4」を使ってロボットの操作が簡単にできます。しかし、コントローラの操作をロボットに伝える事例はたくさんあっても、コントローラを振動させる(フォースフィードバック)やり方でハマったのでご紹介します。

王道は「ds4drv」と「joy_node」を使う。でも…

DS4コントローラとROSの連携で最も一般的なのはコントローラ用のドライバ「ds4drv」とROSのjoyパッケージのノードである「joy_node」を使うやり方です。(こちらのサイトを参考にしました)
ROSでPS4のコントローラ(DualShock4)を使う方法

ところが、振動機能を試してみると失敗します。
以下は失敗事例です。
振動のテストにはfftestコマンドを使います。まずはds4drvを起動して、コントローラとペアリング

$ sudo ds4drv 
[info][controller 1] Created devices /dev/input/js0 (joystick) /dev/input/event11 (evdev) 
[info][bluetooth] Scanning for devices
[info][bluetooth] Found device DC:0C:2D:5C:86:2B
[info][controller 1] Connected to Bluetooth Controller (DC:0C:2D:5C:86:2B)
[info][bluetooth] Scanning for devices
[info][controller 1] Battery: 37%
[warning][controller 1] Signal strength is low (31 reports/s)

一番上の行にあるevdevのイベント番号を見ます。今回は/dev/input/event11となっています。続いてfftestを実行します。

$ fftest /dev/input/event11
Force feedback test program.
HOLD FIRMLY YOUR WHEEL OR JOYSTICK TO PREVENT DAMAGES

Device /dev/input/event11 opened
…
(中略)
…

Uploading effect #0 (Periodic sinusoidal) ... Error:: Function not implemented
Uploading effect #1 (Constant) ... Error: Function not implemented
Uploading effect #2 (Spring) ... Error: Function not implemented
Uploading effect #3 (Damper) ... Error: Function not implemented
Uploading effect #4 (Strong rumble, with heavy motor) ... Error: Function not implemented
Uploading effect #5 (Weak rumble, with light motor) ... Error: Function not implemented
Enter effect number, -1 to exit

Uploading effectで始まる行ですべてError: Function not implementedが出てしまっています。この状態で0~5の数字キーを押しても全く振動してくれません。

確認すべきこと

カーネルのバージョン

まず第一に、linuxカーネルのバージョンを確認しましょう。linuxカーネルとはOSの中核を担うプログラムのことです。ds4の振動機能はlinuxではカーネル4.10以上でしかサポートされていないので、それ未満の場合は更新する必要があります。

$ uname -r 
4.9.0-43-generic

linuxカーネルはapt-get upgrade等では自動更新されないので、手動で更新する必要があります。といってもapt-getを使って簡単に更新できます。(※バージョンによっては更新後にエラーが起こりLinuxの起動に問題が出ることもあるので注意が必要ですが、最悪CUIでログインできれば元のバージョンに戻すとは可能です。)
カーネルのアップデートは以下コマンドでできます。

sudo apt-get install linux-generic-hwe-16.04

ds4drvを使わない

前述の通りds4drvは振動機能に対応していません。Linuxカーネル4.10以降ではds4drvを使わなくてもBluetoothでPCとペアリングが可能です。ペアリングの方法は、ds4drvを使う場合と同じくPSボタンとshareボタンを5秒くらい長押しし、ds4の青いランプがチカチカッと光るようになったら、PCのBluetoothの設定から新規デバイス追加を行います。

よくあるBluetoothペアリング

ペアリングが完了したら何もしなくてもds4drvで接続したのと同じ状態になっています。ROSを使う際もそのままrosrun joy joy_nodeを実行すればジョイスティックの値を見ることが可能です。


この状態でもう一度fftestによる振動テストを実行してみましょう。

$ fftest /dev/input/event19
Force feedback test program.
HOLD FIRMLY YOUR WHEEL OR JOYSTICK TO PREVENT DAMAGES

Device /dev/input/event19 opened
…
(中略)
…

Setting master gain to 75% ... OK
Uploading effect #0 (Periodic sinusoidal) ... OK (id 0)
Uploading effect #1 (Constant) ... Error: Invalid argument
Uploading effect #2 (Spring) ... Error: Invalid argument
Uploading effect #3 (Damper) ... Error: Invalid argument
Uploading effect #4 (Strong rumble, with heavy motor) ... OK (id 1)
Uploading effect #5 (Weak rumble, with light motor) ... OK (id 2)
Enter effect number, -1 to exit
0
Now Playing: Sine vibration
Enter effect number, -1 to exit

OKとなっている、#0、#4、#5が使えます。数字を入力するとコントローラが振動します。やったね!。

pythonプログラムから振動

kharlashkin氏のpythonプログラムです。いろんな強さ&時間で振動させることができます。python2系で動作します。python3系で動かすときはself.ff_joy = open(file, "r+")の部分をself.ff_joy = open(file, "r+b")に変えるといいみたいです。他のプログラムやROSから使うときはpythonで振動させられるのは便利ですね。

import fcntl, struct, array, time

EVIOCRMFF = 0x40044581
EVIOCSFF = 0x40304580
LOG_CLASS_ON = False
TIME_DELTA = 250

class Vibrate:

    def __init__(self, file):
        self.ff_joy = open(file, "r+")

    def close(self):
        self.ff_joy.close()

    def new_effect(self, strong, weak, length):
        effect = struct.pack('HhHHHHHxHH', 0x50, -1, 0, 0, 0, length, 0, int(strong * 0xFFFF), int(weak * 0xFFFF))
        a = array.array('h', effect)
        fcntl.ioctl(self.ff_joy, EVIOCSFF, a, True)
        return a[1]
        id = a[1]
        return (ev_play, ev_stop)

    def play_efect(self, id):
        if type(id) == tuple or type(id) == list:
            ev_play = ''
            for i in id:
                ev_play = ev_play + struct.pack('LLHHi', 0, 0, 0x15, i, 1)
        else:
            ev_play = struct.pack('LLHHi', 0, 0, 0x15, id, 1)
        self.ff_joy.write(ev_play)
        self.ff_joy.flush()

    def stop_effect(self, id):
        if type(id) == tuple or type(id) == list:
            ev_stop = ''
            for i in id:
                ev_stop = ev_stop + struct.pack('LLHHi', 0, 0, 0x15, i, 0)
        else:
            ev_stop = struct.pack('LLHHi', 0, 0, 0x15, id, 0)
        self.ff_joy.write(ev_stop)
        self.ff_joy.flush()

    def forget_effect(self, id):
        if type(id) == tuple or type(id) == list:
            for i in id:
                fcntl.ioctl(self.ff_joy, EVIOCRMFF, i)
        else:
            fcntl.ioctl(self.ff_joy, EVIOCRMFF, id)

f = Vibrate("/dev/input/event14")
p = f.new_effect(1.0, 1.0, TIME_DELTA )
f.play_efect((p))
time.sleep(TIME_DELTA / 1000.0)
f.stop_effect((p))
f.forget_effect((p))

https://github.com/chrippa/ds4drv/issues/91から引用

今回は以上です。今後はこれを使って危険をユーザに知らせるアラートシステムを作っていきたいと思っています。振動させるROSノードが完成したらそちらも公開しようと思います。論文で大変なので時間があればですが…。

参考

【ROS】hokuyo3dパッケージで3次元LiDARに接続できなかった話

問題

北陽電機の3DLiDAR「YVT-35LX」をROSノードで動かそうとしたら、「connection failed」になって接続できない。

やったこと

環境:(多分ROSが動くLinux環境なら共通)
Ubuntu 16.04
ROS kinetic

まず最初にYVT-35LXに電源12VとLANケーブルを接続。ノートパソコンとLANケーブルでつなぐ。

UbuntuのWiFiマーク->接続を編集する…->Ethernet->IPv4
の項目を設定して、固定IPアドレスにした。←これがまずかった

[追加]→[IPv4]から固定IPアドレスを設定
IPアドレスを192.168.0.10に設定

一度WiFiマークをクリックして切断→接続で設定を反映させる。


hokuyo3dパッケージをインストール
ノードを起動させると…

sudo apt-get install ros-kinetic-hokuyo3d

rosrun hokuyo3d hokuyo3d
[ INFO] [1544185161.008533462]: Connecting to 192.168.0.10
[ERROR] [1544185161.010972849]: Connection failed
[ INFO] [1544185161.111372013]: Connection closed
[ INFO] [1544185161.111556411]: Communication stoped

接続できない…。

正しい方法

いろいろ試行錯誤して最終的には接続できるようになった。原因を検証してみた結果次のポイントがあった。

まず、Ubuntu側の設定でLANケーブル接続時の固定IPアドレスの設定は、192.168.0.10以外192.168.0.xxにしなければならない。
ここが192.168.1.20とかだと接続できない。

(2020/08/12 追記)
当時は理解が不十分でしたが、「192.168.0.xxにしなければならない」は間違いで、固定IPとセンサIPが同一サブネットに属すということなので、ネットマスクの値によります。上記はネットマスクが24のときです。

正しい固定IPは192.168.0.xx(ただし、xx≠10)

また、hokuyo3dノードのパラメータ[~/ip]と
[~/port]はセンサ自体の設定を専用ソフトでいじったりしない限りはデフォルトで良い

# IPアドレスを192.168.0.20に変更
rosrun hokuyo3d hokuyo3d _ip:=192.168.0.20

これは接続できない。


なお、ノードのパラメータは一度設定すると、明示的に変更されるかroscoreが一度終了するまで残る。ノードを止めただけではパラメータはリセットされないので心配な人はroscoreやroslaunchを一度すべて止めよう。

解決するまでに一日使った…
成功するとちゃんとデータ出てる

ppdr@Ubuntu:~$ rosrun hokuyo3d hokuyo3d
[ INFO] [1544185571.860202705]: Connecting to 192.168.0.10
[ INFO] [1544185571.863357229]: Connection established
[ INFO] [1544185571.863433355]: Communication started

ESP32のタッチセンサの使い方と外れ値処理

はじめに

Arduinoの無線版であるESP32(正式名称はESP-WROOM-32)モジュールにはデフォルトでタッチセンサが10個もついています。ライブラリもあって使うのは非常に簡単です。今回はタッチセンサの使い方と、実際に自作デバイスを作るときのノウハウについて書きます。

使い方

無線チップが乗っていてArduinoとしてプログラミングもできるモジュールESP32を使用します。


ちなみに20円安くwavesという所からも出てますが、ピンヘッダの半田付けが悪くブレッドボードに刺さりにくいというレビューがあったので、こちらのHiletgoの方をおすすめします。ピンヘッダも問題ありませんでした。

ArduinoIDEにライブラリをインストールするのはこちらを参考にしました。
ESP32-DevKitCを使ってみた〜環境構築からタッチセンサによるLチカまで〜

タッチセンサを使うプログラムはめちゃくちゃ簡単で、次のコードだけです。(サンプルコードそのまま)

// ESP32 Touch Test
// Just test touch pin - Touch0 is T0 which is on GPIO 4.

void setup()
{
  Serial.begin(115200);
  delay(1000); // give me time to bring up serial monitor
  Serial.println("ESP32 Touch Test");
}

void loop()
{
  Serial.println(touchRead(T0));  // get value using T0
  delay(1000);
}

何も宣言しなくても、「touchRead(T0)」を書くだけで値の取得ができます。T0ピンに触れていないときは40~70の値、触れてるときは10~30の値になります。

タッチ判定

このタッチセンサを使うシチュエーションとしては、センサに触ったら何かの動作をさせることが多いと思います。タッチの判定条件として一番簡単でよく使われているのは、「センサ値がt以下になった時、処理を実行する」です。

void setup() {
  Serial.begin(115200);
}

void loop() {

  int t0 = touchRead(T0);
  Serial.println(t0);
  
  if(t0 < 40){
    Serial.println("なんらかの処理");
    /*
     * なんらかの処理
     */

  }

  delay(10);

}

しかし、これだとセンサに触れている間はずっとt0<40になってしまい、処理が連続で実行されてしまいます。そこで、一度タッチしたらセンサから指を離すまで待つことが必要です。

void setup() {
  Serial.begin(115200);
}

void loop() {

  int t0 = touchRead(T0);
  Serial.println(t0);
  
  if(t0 < 40){
    Serial.println("なんらかの処理");
    /*
     * なんらかの処理
     */

    //継続して触れている間は何もしない
    while(t0 < 40){
      t0 = touchRead(T0);
      Serial.println(t0);
      delay(10);
    }
  }

  delay(10);

}

たまに外れ値が出る。。。

はじめは上のプログラムで動かしていたのですが、このタッチセンサはたまに触れていないのに0を出すことがあります。

センサ値をプロットしたところ、触れていないのに変な値が出ることがある

この外れ値は連続で出るわけではなく、一瞬出てすぐに普通の値に戻ります。そのような外れ値に対応するための処理として急激な変化の影響を少なくする、ローパスフィルタというものがあります。ローパスフィルタのプログラムは簡単で、前の値を覚えておいて今回の値の方にちょっとずらすことを繰り返します。

サンプルコードはこんな感じです。

int t0_old;

void setup() {
  Serial.begin(115200);
}

void loop() {

  int t0 = touchRead(T8)*0.1 + t0_old*0.9;
  t0_old = t0;
  
  Serial.println(t0);

  
  if(t0 < 40){
    Serial.println("なんらかの処理");
    
    while(t0 < 40){
      t0 = touchRead(T8)*0.1 + t0_old*0.9;
      t0_old = t0;
      Serial.println(t0);
      delay(10);
    }
  }

  delay(10);

}

このときのセンサ値をプロットしたものがこちらです。

ローパスフィルタによって外れ値が除去できました!

ローパスフィルタによって外れ値が除去できました。これでタッチ判定が誤作動することも無くなります。あとは何らかの処理を書けば、簡易スイッチの完成です!

【ROS】PointCloudで任意の(x,y)でのz座標を得る

はじめに

LiDARもデプスカメラもPointCloudでデータを扱いますが、コストマップや平面検出や法線推定は多くのライブラリがあるのに対して、あるxy座標でのz座標の値をプロットしたいと思った時にちょっと困ったのでその方法をご紹介します。

やりたいこと

z座標を求めるにあたり次のパターンがあると思います。

  • (x,y,0)に最も近い点を求め、その点のz座標を使う
  • 全ての点をxy平面上に投影したときの最も近い点を求め、その点のz座標を使う
  • 周辺の点の平均値をとる
  • 周辺の点の中央値をとる

最近傍点を見つける際に3次元で考えるか2次元で考えるかで少し変わってくるケースもあるかと思いますので、みなさんの環境に合わせてどちらが良いか選んでください。

①3次元での探索と②2次元の探索で異なる場合もあります

CMakeList.txt

今回の内容にはpcl_rosの追加が必要です。

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  tf
  pcl_ros
)

 

3次元近傍点探索

近傍点の探索にはPCLライブラリを使います。サンプルコードはこちらです。

3次元で半径rで探索して一番近い点を返すやつ

#include <ros/ros.h>
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/filters/filter.h>
#include <pcl/kdtree/kdtree_flann.h>


void callback(sensor_msgs::PointCloud2 pc2){
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_nan (new pcl::PointCloud<pcl::PointXYZ>); // NaN値あり
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>); // NaN値なし
  
  //sensor_msgs::PointCloud2からpcl::PointXYZに変換
  pcl::fromROSMsg(pc2, *cloud_nan);

  // NaN値が入ってるといろいろ面倒なので除去
  std::vector<int> nan_index;
  pcl::removeNaNFromPointCloud(*cloud_nan, *cloud, nan_index);

  // KD木を作っておく。近傍点探索とかが早くなる。
  pcl::KdTreeFLANN<pcl::PointXYZ>::Ptr tree (new pcl::KdTreeFLANN<pcl::PointXYZ>);
  tree->setInputCloud(cloud);

  //近傍点探索に使うパラメータと結果が入る変数
  double radius = 0.1;  //半径r
  std::vector<int> k_indices;  //範囲内の点のインデックスが入る
  std::vector<float> k_sqr_distances;  //範囲内の点の距離が入る
  unsigned int max_nn = 1;  //何点見つかったら探索を打ち切るか。0にすると打ち切らない
  
  pcl::PointXYZ p;  //中心座標
  p.x = 0.5;
  p.y = 0.5;
  p.z = 0.0;

  //半径r以内にある点を探索
  tree->radiusSearch(p, radius, k_indices, k_sqr_distances, max_nn);
  
  if(k_indices.size() == 0) return;
  
  pcl::PointXYZ result = cloud->points[k_indices[0]];

  ROS_INFO("A nearest point of (0.5, 0.5) is...\nx: %lf, y:%lf, z:%lf", result.x, result.y, result.z);
  
}

int main(int argc, char** argv){
	ros::init(argc,argv,"z_at_xy");
	ros::NodeHandle nh;
	
	ros::Subscriber sub = nh.subscribe<sensor_msgs::PointCloud2>("in", 1, callback);
	
	ros::spin();
	
	return 0;
}

このコードは最も近い1点を見つけます。k_indicesに見つかった点のインデックスが入ります。周辺の点の平均値や中央値をとるなどの処理をしたい場合はmax_nnを2以上か0にしておくと指定の個数になるまで探します。

xy平面上での距離で近傍点探索

2次元バージョンはこんな感じで

void callback_2d(sensor_msgs::PointCloud2 pc2){
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_nan (new pcl::PointCloud<pcl::PointXYZ>); // NaN値あり
  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>); // NaN値なし
  
  pcl::fromROSMsg(pc2, *cloud_nan);

  // NaN値が入ってるといろいろ面倒なので除去
  std::vector<int> nan_index;
  pcl::removeNaNFromPointCloud(*cloud_nan, *cloud, nan_index);

  //x座標とy座標だけをコピーしたPointCloudを作る
  pcl::PointCloud<pcl::PointXY>::Ptr cloud2d (new pcl::PointCloud<pcl::PointXY>); // NaN値なし
  cloud2d->points.resize(cloud->size());
  for(int i=0; i<cloud->points.size(); i++){
    cloud2d->points[i].x = cloud->points[i].x;
    cloud2d->points[i].y = cloud->points[i].y;
  }

  //treeも2Dで作る
  pcl::KdTreeFLANN<pcl::PointXY>::Ptr tree2d (new pcl::KdTreeFLANN<pcl::PointXY>);
  tree2d->setInputCloud(cloud2d);

  //近傍点探索に使うパラメータと結果が入る変数
  double radius = 0.1; //半径r
  std::vector<int> k_indices; //範囲内の点のインデックスが入る
  std::vector<float> k_sqr_distances; //範囲内の点の距離が入る 
  unsigned int max_nn = 1; //何点見つかったら探索を打ち切るか。0にすると打ち切らない
  
  //中心座標
  pcl::PointXY p;
  p.x = 0.5;
  p.y = 0.5;

  //2Dで近傍点探索
  tree2d->radiusSearch(p, radius, k_indices, k_sqr_distances, max_nn);
  
  if(k_indices.size() == 0) return;
  
  //求めたインデックスでもとのPointCloudの点を見る
  ROS_INFO("A nearest point of (0.5, 0.5) is...\nx: %lf, y:%lf, z:%lf", cloud->points[k_indices[0]].x, cloud->points[k_indices[0]].y, cloud->points[k_indices[0]].z);
  
}

int main(int argc, char** argv){
	ros::init(argc,argv,"z_at_xy");
	ros::NodeHandle nh;
	
	ros::Subscriber sub = nh.subscribe<sensor_msgs::PointCloud2>("in", 1, callback_2d);
	
	ros::spin();
	
	return 0;
}

pcl::PointCloudXYZのxy座標だけをコピーしたpcl::PointXYを作っておいてそちらで近傍点探索した後、得られたインデックスk_indicesでもとのpcl::PointCloudXYZを参照します。

求めた点の中央値を取る

平均値は求めた点のz座標を合計して個数で割るだけなのでたぶん簡単なので書く必要は無いと思いますが、中央値を取る方法は知らなかったので載せておきます。

ちなみに周辺の点の中央値をその点の値とする手法はメディアンフィルタ(中央値フィルタ)と呼ばれ、画像の境界線を残したままノイズを除去する方法として画像処理などでよく用いられるようです。このフィルタは計算コストがちょっと高いらしいので、多用はしないほうが良いかも知れません。

std::vector<float> vals;  //近傍点のz座標
for(int j=0; j<k_indices.size(); j++){
  vals.push_back(depth_cloud->points[k_indices[j]].z);
}

//上位半分までソートする
partial_sort(vals.begin(), vals.begin() + vals.size()/2 + 1, vals.end());

//中央値をとる
float filtered_z = vals[vals.size()/2];

 

ROSを最初から勉強したい方はこちら。

帰宅したら自動で解錠するスマートロックを自作する①~オートロック編~

はじめに

ホテルとかオフィスのドアみたいに閉まると自動で施錠してくれるやつ、あれすごく便利ですよね。家を出るときにわざわざポケットから鍵を出して閉める動作が不要になるので、意外にも快適です。今回はこのオートロックを自作して自宅の玄関に取り付けた話です。

作りたいもの

まず、作りたいオートロックに必要な機能を決めます。

  1. ドアが閉まったら自動で施錠
  2. 暗証番号で解錠
  3. ICカードで解錠
  4. スマホを持って近づくだけで解錠

1→4になるほど難易度が高いと思います。この記事では最初の「ドアが閉まったら自動で施錠」をご紹介します。 まだ作ってる途中ですので、随時更新していきます。

必要なもの

  • ArduinoまたはESP8266 400円~2000円くらい
  • サーボモータSG92R  830円くらい
  • 100均の下駄箱ライト  108円

ArduinoまたはESP8266

Arduinoの種類は何でも大丈夫ですが、後々解錠にFelica(ICカード)を使う予定ならシリアル通信で152000bpsが使えるボードが必要です。一番良く使われているArduinoUnoは対応していません。下記の安いArduinoNano互換品とかでいいと思います。また今後スマホを使って解錠するなど、WiFiを使って何かすることを考えるならESP8266をおすすめします。ちなみに僕はESP8266で作ってます。

サーボモータ

サーボモータとは、指定した角度まで回転してくれるモーターです。室内側のサムターンを回すので、それなりにパワーのあるものが必要です。僕の家のサムターンはちょっと固めだったのか、持っていたSG90では回せませんでした。
不安な方はよりトルクの大きいSG92Rをおすすめします。こちらはSG90よりも動きが遅いらしいですが、楽々回してくれてかっこ良いです。

100均の開閉センサ付きライト

100均の下駄箱ライトは写真を撮るの忘れましたが、扉に磁石とセンサーを取り付けて双方が離れるとスイッチON、近づくとスイッチOFFするライトです。今回はこれの磁石とセンサー部分を切り取って使っています。100円で開閉センサが手に入るので便利です。
ここで紹介されているようなやつです。)

サムターン回し装置

まずモーターでサムターンを回す装置を作っていきます。3Dプリンターで部品を作っている人も見かけますが、CAD書いて出力するのも面倒なので割り箸とかで作っていきます。

どんな機構にするか

まずサムターンを回すための機構ですが、こんな感じになるように作ります。

サムターン回し機構。木のブロックがついた部分を左右に90度回すことによって鍵が開く。

木のブロックは余ってる木材の端材とかで全然OKです。それをサーボモーターに付属しているプラスチックのパーツに取り付けます。

木の端材をサーボモータについてるパーツにネジ止めしたところ

こうすることによって、ニュートラルにしておけば鍵を使って開けたり手で開けたりできます。モータで開閉するときは左右に90度回転させて、またニュートラルに戻すことで、手動とモータの動作を両立させることができます。

ドアノブに取り付け

割り箸とマスキングテープを使ってモータをドアノブに固定します。ここはドアノブの形状によって色々やり方があると思うので、ご自宅のドアにあったやり方で固定してください。

割り箸でモータを固定
ドアノブにつける。ここのやり方はお好みで。

開閉センサ

ドアが開いているか閉まっているかを検知するセンサを作ります。100均の下駄箱ライトを使えば簡単に作れます。100均のライトは片方が磁石、もう片方が金属板になっていて、磁石が近づくことで金属板が接触して電気が流れる仕組みになっています。今回はこの部分だけを切ってセンサ代わりに使います。

導線を切ってコネクタをはんだ付けする

2つの箱が近づくと導通、離れると絶縁です。

配線

サーボモータと開閉センサが用意できたら配線してみます。サーボモータは接続を間違えないようにESP8266のGND、Vin、D4(PWMピン)につなぎます。サーボモータから出ている線は茶色がGND、赤が5V、オレンジがPWM信号です。

開閉センサはGND、D0につなぎます。2本ありますがどちらの線をどちらにつないでもOKです。

配線イメージ

プログラム

ESP8266にこちらのプログラムを書き込みます。動作としてはすごく簡単で、開閉センサが「開」から「閉」になったタイミングでサムターンを回すだけです。せっかくWiFiが使えるESP8266を使ったので、WiFi経由で開け締めする機能も付けました!

#include <Servo.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

// ルータ設定
IPAddress ip(192,168,1,102);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);

// ESP8266のピン番号
const int servo_pin = 2;
const int door_pin = 12;

Servo myservo;             // サーボオブジェクトを生成
int door_sensor = LOW;
int door_sensor_last = LOW;
ESP8266WebServer server(80);  //サーバーオブジェクト


void door_open(){
  myservo.write(170);
  delay(1000);
  myservo.write(90);
}

void door_close(){
  myservo.write(10);
  delay(1000);
  myservo.write(90);
}

void handle_open(){
  door_open();
  server.send(200, "text/html", "opened");
}

void handle_close(){
  door_close();
  server.send(200, "text/html", "closed");
}

void setup() 
{ 
  
  Serial.begin(115200);
  delay(200);

  //固定IPで運用するときの設定
  WiFi.config(ip, gateway, subnet);
  WiFi.begin("WIFIのSSID", "WIFIのパスワード");

  // WiFiに接続するまで待つ
  Serial.println("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Webサーバを設定
  server.on("/open", handle_open);
  server.on("/close", handle_close);
  server.begin();

  // 開閉センサの入力を内部プルアップにする
  pinMode(door_pin, INPUT_PULLUP);

  // サーボ変数をピンに割り当て
  myservo.attach(servo_pin);

  door_close();
} 

void loop() 
{ 
  // サーバとして待ち受ける
  server.handleClient();

  // 現在のドアの開閉を検知
  // LOW=閉
  // HIGH=開
  door_sensor = digitalRead(door_pin);

  // 「開」→「閉」になったタイミングでサーボモータを回す
  if(door_sensor == LOW && door_sensor_last == HIGH){
    delay(1000);
    door_close();
  }
  
  door_sensor_last = door_sensor;
  
}

ルータ設定やWiFiパスワードの部分はご自身の環境に合わせて変えてください。

Arduinoを使う人はヘッダーファイルとか適宜変更して、それ以外はほとんどそのままで行けると思います。

完成!

プログラムができたら開閉センサを近づけたり離したりしてみてください。近づけたときにサーボモータが回るはずです。

ESP8266を使っている人はスマホやパソコンのURLのところに192.168.xxx.xxx/closeと入力しても動きます。

確認ができたらドアに取り付けてみます。

※注意 オートロックなので閉め出されないように鍵を持っておくこと!動作テスト中に閉め出されたら恥ずかしいです。

ドアに取り付けてマスキングテープでいい感じにしました

最大の問題はUSBケーブルが玄関まで届くかですね。僕は延長コードを3個経由しています。あと家から出るときに室内側からサムターンを回すのが少し大変です。慣れれば簡単に開けられますがちょっとモータが邪魔なので、今度は家の中からスイッチとかで開けられるようにしておきます。

今回は以上です。次回は帰ってきたときに暗証番号的なもので解錠する方法をご紹介します。

次回→

PS4のコントローラの値をLinuxで表示

はじめに

プログラミングの勉強を始めた頃は、プログラム書いたは良いけどターミナルから値を入力するのダサすぎ…って思ってたものです。
電気回路の知識がちょっと増えるにつれて、押しボタンスイッチとかジョイスティックを押したのを検知してプログラムを起動できるようになりましたが、今回はUbuntuやラズパイ上で動いているプログラムにPS4のコントローラから数値入力を渡す方法を模索したいと思います。

「PS4 Linux」で検索するとPCゲームのコントローラにPS4コントローラを使う方法が出てきますが、今回は自分で作ったプログラムにコントローラの値を受け渡す方法です。

意外と情報が少ない

ROSを使っていれば、PS4コントローラでコントロールする方法は簡単に見つかります。pythonの「ds4drv」でコントローラとPCを接続して、ROSのjoy_nodeで値を読み込んでjoyトピックで出してくれるのでそれを使うだけです。

しかし、ROSを使ってないシステムでコントローラの値を使いたいときはどうすれば良いんじゃ…ってことで探してみました。

やり方

環境:
Ubuntu 16.04、ラズパイ(Jessie)

コントローラをPCと接続するところまでは「ds4drv」を使います。次のコマンドをターミナルに入力してインストール、起動します。

sudo pip install ds4drv

sudo ds4drv

起動したらPS4コントローラの「PSボタン」と「SHAREボタン」を青のランプがカチカチッと早い点滅をするまで同時に押します。成功すると「connected」的なのがターミナルに出ると思います。

続いて値の表示ですが、こちらのページにサンプルプログラムと解説がありましたがヘッダ等がありませんでしたので、色々調べた結果コンパイルできるようになったものを載せます。

#include <linux/joystick.h>
#include <linux/input.h>
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
#define BUTTON_DATA_MAX 100
#define STICK_DATA_MAX 100

int fd= open( "/dev/input/js0", O_RDONLY );

unsigned char  ButtonData[BUTTON_DATA_MAX];
signed int     StickData[STICK_DATA_MAX];

int main(int argc, char** argv){

	for(;;){
		  struct js_event  event;
		  if( read( fd, &event, sizeof(struct js_event) ) >= sizeof(struct js_event) ){
		      switch( event.type & 0x7f ){
		      case JS_EVENT_BUTTON:
		          if( event.number < BUTTON_DATA_MAX ){
		              ButtonData[ event.number ]= event.value != 0;
		              std::cout << event.value << std::endl;

		          }
		          break;
		      case JS_EVENT_AXIS:
		          if( event.number < STICK_DATA_MAX ){
		              StickData[ event.number ]= event.value;
		          }
		          break;
		      }
		  }
	}
	close( fd );
}

これをds4.cppに保存してg++でコンパイル

g++ ds4.cpp

すればいけるはずです。
ちなみにjoystick.hの場所は僕の環境では/usr/include/linux/joystick.hでした。

出力ファイルa.outができていると思うので実行すると、ボタンを押したタイミングで1が出力されるはずです。

ppdr@raspberrypi:~ $ ./a.out
0
0
0
0
1
0

あとは値を表示するなり他のプログラムにパイプで渡したり自由にどうぞ。

./a.out | ./my_program.exe

 

プログラムの内容は全然追えていませんが、event.numberがボタンの種類、event.valueがボタンの値を示すみたいです。この辺はどうなってるかまだ把握していないので、std::coutしてみてご確認ください。

この辺の情報をご存知の方は教えていただけると幸いですm(_ _)m

【ROS】sensor_msgs::PointCloud2を座標変換する

はじめに

こんにちは。最近それこそVelodyneに近い性能を持って低価格なLRFが出てるみたいですね。自動車にもロボットにもLRFが大活躍してる現状を見ると、LRFの低価格化は嬉しいニュースです。

さてVelodyne等で取ったポイントクラウドを処理したい時に、各点をxyz座標で表したい時ってありますよね。LRFから出るデータはセンサ座標系でのxyz座標なので、他の基準座標系(例えばロボットの中心)からみたxyz座標に変換する必要があります。

ポイントクラウドの変換方法はpclを使う方法やsensor_msgsのライブラリを使う方法等、色々なやり方があって非常にわかりづらいです。今回は僕が使ってみて一番実装が簡単だと思ったやり方をご紹介します。

やり方

とりあえずサンプルコードはこちらです。

#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <pcl_ros/transforms.h>
#include <sensor_msgs/PointCloud.h>
#include <sensor_msgs/PointCloud2.h>
#include <sensor_msgs/point_cloud_conversion.h>

tf::StampedTransform transform;  // base_linkとbase_laserの座標変換を保存する用

void pc_callback(sensor_msgs::PointCloud2 pc2){
	sensor_msgs::PointCloud2 pc2_transformed;

	// 座標変換はこの一行だけ!
	pcl_ros::transformPointCloud("base_link", transform, pc2, pc2_transformed);

	// 確認用にxyz座標のsensor_msgs::PointCloudに変換
	sensor_msgs::PointCloud pc;
	sensor_msgs::convertPointCloud2ToPointCloud(pc2_transformed, pc);

	// xyzの値を表示
	for(int i=0; i<pc.points.size(); i++){
		ROS_INFO("x:%f y:%f z:%f", pc.points[i].x, pc.points[i].y, pc.points[i].z);
	}
}

int main(int argc, char** argv){
	ros::init(argc,argv,"transform_test");
	ros::NodeHandle nh;
	ros::Rate loop_rate(10);

	ros::Subscriber sub = nh.subscribe("in", 1, pc_callback);
	ros::Publisher pub = nh.advertise("out", 1);

	// TFを受信するやつ
	tf::TransformListener listener;

	// base_linkとbase_laserの間のTFが取得できるまで繰り返す
	while(true){
		try{
			//取得できたらtransformに保存しておく
			listener.lookupTransform("base_link", "base_laser", ros::Time(0), transform);
			ROS_INFO("I got a transform!");
			break;
		}
		catch(tf::TransformException ex){
			ROS_ERROR("%s",ex.what());
		  ros::Duration(1.0).sleep();
		}
	}

	//メインループでは特に何もしない
  while(ros::ok()){
  	ros::spinOnce();

  	loop_rate.sleep();
  }

	return 0;
}

まずは最初のincludeの部分ですが、

#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <pcl_ros/transforms.h>
#include <sensor_msgs/PointCloud.h>
#include <sensor_msgs/PointCloud2.h>
#include <sensor_msgs/point_cloud_conversion.h>

pcl_ros/transforms.hをインクルードします。pcl_rosのライブラリはROSのインストールに自動で入ってると思いますが、CMakeList.txtに書いておかないと見つけられないので注意してください。

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  tf
  pcl_ros
)

続いてグローバル変数でtransformを保存しておく用の変数を定義しています。

 tf::StampedTransform transform;  // base_linkとbase_laserの座標変換を保存する用

続いてトピックをsubscribeした時に呼ばれるコールバック関数です。ここで座標変換を行います。

void pc_callback(sensor_msgs::PointCloud2 pc2){
	sensor_msgs::PointCloud2 pc2_transformed;

	// 座標変換はこの一行だけ!
	pcl_ros::transformPointCloud("base_link", transform, pc2, pc2_transformed);

pcl_rosのリファレンスを見ると他にも色々な型or変換方法があるみたいです。今回はTFは一回取得するだけであとはずっと同じものを使うので、処理が軽そうなtransformを使う方法を取りました。この一行でポイントクラウドがbase_link座標系から見た値に変換されます。

その下は値の確認用です。現在のROSポイントクラウドの型の主流はsensor_msgs::PointCloud2型で、sensor_msgs::PointCloud型はあまり使われてない印象ですが、sensor_msgs::PointCloudのほうがxyz座標での表示で直感的にはわかりやすいのでよく使うんですよね。
他の方によるとsensor_msgs::PointCloud2型からsensor_msgs::PointCloud型への変換処理は重いらしいのでリアルタイムな処理をするときは注意が必要です。

// 確認用にxyz座標のsensor_msgs::PointCloudに変換
	sensor_msgs::PointCloud pc;
	sensor_msgs::convertPointCloud2ToPointCloud(pc2_transformed, pc);

	// xyzの値を表示
	for(int i=0; i<pc.points.size(); i++){
		ROS_INFO("x:%f y:%f z:%f", pc.points[i].x, pc.points[i].y, pc.points[i].z);
	}

続いてメイン関数です。

int main(int argc, char** argv){
	ros::init(argc,argv,"transform_test");
	ros::NodeHandle nh;
	ros::Rate loop_rate(10);

	ros::Subscriber sub = nh.subscribe<sensor_msgs::PointCloud2>("in", 1, pc_callback);
	ros::Publisher pub = nh.advertise<sensor_msgs::PointCloud2>("out", 1);

ここは通常のノードハンドルの宣言やSubscriberとPublisherの宣言です。公式ドキュメントそのまんまなので説明は割愛します。

続いてTFの受信です。

// TFを受信するやつ
	tf::TransformListener listener;

	// base_linkとbase_laserの間のTFが取得できるまで繰り返す
	while(true){
		try{
			//取得できたらtransformに保存しておく
			listener.lookupTransform("base_link", "base_laser", ros::Time(0), transform);
			ROS_INFO("I got a transform!");
			break;
		}
		catch(tf::TransformException ex){
			ROS_ERROR("%s",ex.what());
		  ros::Duration(1.0).sleep();
		}
	}

今回はTFは最初の一回だけ受信して、以降は同じ値をずっと使って座標変換します。TFの取得は結構注意が必要で、try-catchを書いておかないとすぐエラーで止まったりします。特にノード起動直後はエラーが起こりやすいので、ちゃんと例外処理しましょう。
lookupTransformは”base_link”から”base_laser”までのTFを取得していますが、順序が逆でも座標変換はできます。

//メインループでは特に何もしない
  while(ros::ok()){
  	ros::spinOnce();

  	loop_rate.sleep();
  }

最後のmainループでは特に何もしていません。トピックが来たらコールバック関数が実行されます。

結果

テスト用にダミーのポイントクラウドを生成してみました。base_linkの上1.0mにbase_laser座標系を置き、センサに水平に点を置いてみました。上のノードを動かした結果、ちゃんとbase_link座標系から見た値(z座標が1.0)になっています。

センサに水平にポイントクラウドを並べる
[ INFO] [1539448429.565021857]: x:0.808028 y:0.589145 z:1.000000
[ INFO] [1539448429.565045002]: x:0.777573 y:0.628793 z:1.000000
[ INFO] [1539448429.565095699]: x:0.745174 y:0.666870 z:1.000000
[ INFO] [1539448429.565120954]: x:0.710914 y:0.703279 z:1.000000
[ INFO] [1539448429.565142811]: x:0.674876 y:0.737931 z:1.000000
[ INFO] [1539448429.565163881]: x:0.637151 y:0.770739 z:1.000000
[ INFO] [1539448429.565184455]: x:0.597834 y:0.801620 z:1.000000
[ INFO] [1539448429.565205922]: x:0.557023 y:0.830497 z:1.000000
[ INFO] [1539448429.565231879]: x:0.514819 y:0.857299 z:1.000000
[ INFO] [1539448429.565282451]: x:0.471328 y:0.881958 z:1.000000
[ INFO] [1539448429.565307669]: x:0.426660 y:0.904412 z:1.000000
[ INFO] [1539448429.565329580]: x:0.380925 y:0.924606 z:1.000000
[ INFO] [1539448429.565350394]: x:0.334238 y:0.942489 z:1.000000
[ INFO] [1539448429.565372862]: x:0.286715 y:0.958016 z:1.000000
[ INFO] [1539448429.565394970]: x:0.238476 y:0.971148 z:1.000000
[ INFO] [1539448429.565470539]: x:0.189641 y:0.981854 z:1.000000
[ INFO] [1539448429.565500795]: x:0.140332 y:0.990105 z:1.000000
[ INFO] [1539448429.565522796]: x:0.090672 y:0.995881 z:1.000000
[ INFO] [1539448429.565544612]: x:0.040785 y:0.999168 z:1.000000

今回は以上です。

質問・コメント・間違っている箇所があれば教えてください。
よろしくおねがいしますm(_ _)m。

Volumio2に再生コマンドを送る!

はじめに

こんにちは。何も考えずにラズパイzeroを購入してしまったせいで使い道が無くて持て余していたので、volumioをインストールしてミュージックサーバーにしてみたパパドラゴンです。

volumioのインストールはこちらのページで紹介されているDACをそのまま買って組んでSDカード焼くだけでめっちゃ簡単でした!
でもvolumioはブラウザから操作することを考えて作られているらしく、別のラズパイから再生コマンドを送る方法がいまいち分かりにくかったので紹介します。

やりたいこと

  • Volumio2を「他のラズパイ」から「コマンドラインで」操作したい
  • Google Homeを使って操作したい
  • WiFi経由でArduinoから操作したい

方法

結論から言いますと、Volumioの公式ドキュメントにAPI一覧が書いてあります。
ネットワーク関係は全く素人なので理解できたとこだけ読んだところ(よくない)、GETリクエストを送る方法が一番カンタンで早い気がします。

仕組みはともかく使い方だけなら、ブラウザでもラズパイのターミナル上でもpython上でもとにかく「http://volumio.local/api/v1/commands?cmd=〇〇」のアドレスにGETリクエストを送れば、結果(成功すれば現在の状態、失敗すればエラー)が辞書型で帰ってきます。
「volumio.local」の部分は「192.168.0.5」のようなアドレスに変える必要があるかも知れません。

例えばchromeだとアドレスバーに上記を打ち込めばこんな感じに帰ってきて再生が始まります。

「192.168.0.5」の部分は自分のvolumioのアドレスに変えてください

ラズパイのターミナルだとcurlコマンドを使ってこんな感じ。

ppdr@raspberrypi:~ $ curl http://volumio.local/api/v1/commands?cmd=stop
{"time":1538585873376,"response":"stop Success"}

pythonならこんな感じです。

import urllib2

res = urllib2.urlopen("http://volumio.local/api/v1/commands?cmd=play",timeout=5)
print( res.read() )

 

どんなコマンドが送れるの?っていうとアドレスの「?」以降(クエリ)の部分に色々指定して送れば良さそうです。

 

クエリ動作
cmd=play再生
cmd=stop停止
cmd=pause一時停止
cmd=prev前の曲へ
cmd=next次の曲へ
cmd=volume&volume=80音量を80に
cmd=playplaylist&name=Rockプレイリスト「Rock」を再生
cmd=repeatリピートをON&OFF
cmd=randomランダム再生をON&OFF

他のコマンドについては公式ドキュメントを参照してくださいm(_ _)m。

これが出来ればとりあえず汎用性は高そうです。プレイリストやキューの一覧を取得するコマンドもあるので帰ってきた辞書型を判断して処理を分けるとかもできそうです。
時間があればGoogle Homeから起動とかも試してみたいです。お部屋BGMライフが始まりそう。