雑食エンジニアの気まぐれレシピ

日ごろ身に着けた技術や見知った知識などの備忘録的なまとめ.主にRaspberry Piやマイコンを使った電子工作について綴っていく予定.機械学習についても書けるといいな.

レオリモコンと自前ルータを共存させるために奮闘した話

今回の要旨

  • スイッチングハブ(有線)越しだとレオリモコンが動作しない
  • 有線ハブでも2重ルータになっている可能性がある?
  • アクセスポイントモードにすることでレオリモコンと共存可能

先日レオリモコンなる機器が設置されたレオパレス物件に引っ越しを行いました.このレオリモコンはスマートリモコンで有名なiRemoconの亜種で,家電によくある赤外線のリモコンを一つに集約することができる優れものです.レオリモコンはwi-fi経由でアクセスできるため,これを使うと家電をすべてネットワーク越しに操作できるようになります.素晴らしい.

レオパレス21、グラモと提携し住戸のIoT 化を加速|2016年一覧|ニュースリリース|株式会社レオパレス21

ただひとつ欠点?がありまして,なぜかこのレオリモコン,レオネットに直接繋がないと動かないんですよね.

レオネットというのはレオパレスの用意したネット回線で,安い代わりに糞遅いことで有名です.この糞遅い回線を少しでもマシに使うために,自前のルータ(スイッチングハブ)を使っていました.TP-Linkのやつで,2xの11ac回線とギガビットポートが4つついています.

しかし,いざレオリモコンを使おうとすると,ルータを有線のスイッチングハブとしてつないだ際に動かないんですよね.これが動かないとなると,壁のLANポートに直接レオリモコンをつなぐ必要があるので有線LANの分岐が不可能となります.それは困る.

f:id:shikky_lab:20171214232324p:plain
レオリモコン接続方式の理想と現実

なぜ動かないのか※推測になります.

そもそもなぜインターネットにつながないとレオリモコンが動かないのかという話ですが,レオリモコンにはスマホGPSと連動して,家から離れるor近づくをトリガーとした家電の操作ができます.厄介なことに.

これはつまり室内のレオリモコンに外部からアクセスできているということなので,ポートを開放しているということでしょう.しかし,レオネットはプライベートIPを割り当てる形式の回線なので,ポート開放を行うのはそれらを集約している管理ルータということになります.

つまり,レオネットの管理ルータはレオリモコン越しのポート開放を許可している

ということなんじゃないかと考えられます.

ハブ越しで動かないのはなぜ?

そのうえでハブ越しだと動かない理由を考えると,2重ルータになっている可能性が考えられます.ハブがルータ機能を果たしてしまうと,レオリモコンはこのハブのポートを開放しようとするわけですが,ここを開放したところで外部からは見えないので無意味です.でも有線のスイッチングハブってブリッジと同義なんじゃなかったっけ.

物の試しにルータ設定切り替え.そして成功

そうは思いつつも念のため試してみます.TP-Linkのルータをアクセスポイントモードに切り替えるのはなかなか厄介で,今までWAN側に刺していたケーブルをLAN側に刺したうえで,下記リンクのようにいろいろ設定をいじる必要があります.

www.tp-link.jp

やや面倒ではありますが,こうすることでレオリモコンを操作することができるようになりました.有線でも2重ルータになっていたとは.

何はともあれこれでやや快適な生活を送ることができます.それにしても,せっかくスマートリモコンがあるわけなので,ゆくゆくはGoogleHomeかAmazonEchoでも買って連携させていきたいところですね.それらに手を出せたらまた記事にしていきたいと思います.

Androidで画像のUDP受信とヘッドトラッキング

「GoogleCardboardとRaspberryPiを使って視界を共有できるロボットを作る」解説
第3回はAndroid側のプログラムについてです.一応これで最終回予定です.

なおAndroid開発はほぼ初めてのため,誤りを多分に含む可能性があります.ご了承ください.

 

Android側の主な仕事はUDP通信とヘッドトラッキングです.

はじめにUDP通信についてです.前回のC++Javaになっただけかと思いきや,存外面倒くさかった.

 

Androidで通信を行うためには非同期処理が必須のようです(Android3.0以降).

非同期処理の方法について調べると,概ね次の3つがあるそうで.

・AsyncTask

・AsyncTaskLoader

・Thread

(一応Service使ってもできないことはない気も...)

とはいえ,実際にはThreadがベースになっていて,AsynctaskもAsyncTaskLoaderも,Threadを使いやすくしただけみたいです.

 

※なお,非同期処理については

アプリ開発者を育てるプログラミングスクール Tech Institute(テックインスティチュート)

のVol12がかなり参考になりました.

 

今回のように何度も送受信が必要な処理の場合はAsyncTaskLoaderが適しているようなのですが,一つ問題が.

AsyncTaskもAsyncTaskLoaderも,原則Fragmentにしか使えません(もしくはFragmentを継承したActivity).

後ほど触れますが,GoogleCardboardのAPIを使うためにはメインアクティビティをGvrActivityから継承しなければなりません.

そしてGvrActivityはFragmentを継承していないため,AsyncTaskLoaderは使えないということになります.(ホントかなぁ...)

 

正直この部分については自信がありませんが,どうにも方法を見つけられなかったので今回はThreadを使って実装しました.

UDP通信部分については割愛しますが,受信したデータを変数bufに格納した場合,

Bitmap bmp= BitmapFactory.decodeByteArray(buf,0,buf.length);

でBitmapに変換できます.

あとはこれをUIスレッドにpostすれば受信した画像を表示できます.

......実際この実装でうまく動いているのですが,jpegで送ってbitmapで読み込みって普通に考えて奇妙ですね.decodeByteArrayがうまいことやってくれてるってことなんでしょうけど,どののフォーマットに対応しているのかリファレンス見てもよくわからなかったり.

 

ちなみにAndroidはbounjorに対応していないため,ホスト名のみでIPアドレスを解決することはできません(正確には方法はあるようですが,私にはできませんでした).ですが今回はテザリングで機器間をつなぐため,androidの持っているarpテーブルを使うことで,macアドレスからIPアドレスを解決することができます.取得方法は次のリンクを参考にしました.

java - How to get the IP address of a system using Android phone? - Stack Overflow

 

続いてヘッドトラッキングについて.

これはGoogleCardboardのAPIを使います.

Google VR SDK for Android  |  Google VR  |  Google Developers

本当は画像の表示にもこのAPIを使いたかったのですが,どうにも方法を見つけられなかったので画像は別途貼り付けています.

Google Cardboard Tutorial: How to render images using Cardboard App

(こことほぼ同じやり方です.)

 

というわけで,CardboardのAPIは本当にヘッドトラッキングのためだけに導入してます.

※補足

ゴーグル越しに画面を見る場合,ゴーグルの凸レンズで画面が拡大されてみえるため,樽型のひずみのようなものを予めつけておく必要があります.CardboadのAPIではこのひずみを付けたうえで描画してくれる機能があるのですが,これは3D空間を投影する部分とセットになっているようで,今回のようにもともと2次元の画像を描画する機能は見当たりませんでした.

タオバイザー様の公式アプリでもAndroidのカメラ画像をVR画面として出す双眼鏡モードでは樽型のひずみがついていないので,今のところ方法はないんじゃないかと思います.

※補足終わり

 

GvrActivityを継承したうえでGvrView.Rendererをimplementしたアクティビティを用意すると,オーバーライドが必要なメソッドの一つにonDrawFrameがあります.

このメソッドはフレームを描画する際に呼ばれるものですが,これの第一引数に頭の向きを表すHeadTransformが格納されます.

headtransformはgetterで様々な角度情報に変換して取得することができます.

HeadTransform  |  Google VR  |  Google Developers

オイラー角やクォータニオンなど大体のものが揃っているので,使いやすい形で取り出しましょう.今回の実装ではgetForwardVectorで取り出しました(クォータニオンには自信がないので......).

ForwardVectorは現在向いている方向をxyzの単位ベクトルとして返します.ベクトルをparamという配列に格納したとすると,座標系は次の通りです.

f:id:shikky_lab:20161114125322p:plain

あとはこれをarctanで角度に変えて使っています.

pan=arctan(x/z)

tilt=arctan(y/z)

という感じですね.なおarctanは分母が0のときにエラーを吐くので,arctan2を使うことをお勧めします.

RaspberryPiCamera+OpenCVでカメラ画像のUDP送信

「GoogleCardboardとRaspberryPiを使って視界を共有できるロボットを作る」
第二回はラズパイ側のUDP通信についてです.

C言語でのUDP通信については以下の内容が非常に参考になりました.
http://www.sbcr.jp/books/img/Linuxnet_03.pdf

今回のロボットにおけるラズパイのUDP通信部分は
・カメラ画像の送信
Androidの傾き情報の受信
をそれぞれ行います.なお,簡単のため送信と受信でポートを分けて実装しました.

画像の送信については
OpenCVでWebカメラのストリーミング - kivantium活動日記
様のコードをベースに使わせていただきました.

RasPiカメラモジュールの画像をOpenCVで取得する方法については過去記事参照
shikky-lab.hatenablog.com

#include <iostream>
#include <vector>

#include <sys/socket.h>
#include <netdb.h>//gethostbyname
#include <arpa/inet.h>//inet_ntop

#include <raspicam/raspicam_cv.h>

//ipアドレスの確認用
void print_host_info(struct hostent *host, struct sockaddr_in &send_addr) {
    unsigned int **addrptr;
    addrptr = (unsigned int **) host->h_addr_list;
    send_addr.sin_addr.s_addr = *(*addrptr); //送信先IPアドレス
    /*ipアドレスの確認*/
    char sender_str[256] = {0};
    inet_ntop(AF_INET, &send_addr.sin_addr, sender_str, sizeof (sender_str));
    std::cout << "sendto:" << sender_str << ",Port=" << ntohs(send_addr.sin_port) << std::endl;
}

int main(int argc, char *argv[]) {
    //送信ソケットの設定
    int send_sock;
    struct sockaddr_in send_addr;
    send_sock = socket(AF_INET, SOCK_DGRAM, 0);

    send_addr.sin_family = AF_INET;
    send_addr.sin_port = htons(8765); //送信用のポート番号.好きな値を設定.
    struct hostent *host;
    host = gethostbyname("192.168.43.1");//各自の環境に合わせて.bonjourに対応していれば"ホスト名.local"でも可
    if (host == NULL) {
        return 1;
    }
    print_host_info(host, send_addr);//IPアドレスの確認

    //カメラの設定
    raspicam::RaspiCam_Cv Camera;
    //set camera params
    Camera.set(CV_CAP_PROP_FORMAT, CV_8UC3);
    Camera.set(CV_CAP_PROP_FRAME_WIDTH, 640);//画像サイズの設定.
    Camera.set(CV_CAP_PROP_FRAME_HEIGHT, 720);//後々Android上で分割表示するため,スマホ画面サイズの半分に設定
    std::cout << "Opening Camera..." << std::endl;
    if (!Camera.open()) {
        std::cerr << "Error opening the camera" << std::endl;
        return -1;
    }

    static const int sendSize = 65000; //UDPの仕様により上限は65kB

    cv::Mat image;
    std::vector<unsigned char> ibuff;
    std::vector<int> param = std::vector<int>(2);//jpeg変換時のパラメータ設定
    param[0] = CV_IMWRITE_JPEG_QUALITY;
    param[1] = 80;

    while (cvWaitKey(10) == -1) {//sleepの代用.基本的にはsleepなしでも構わないが,joystickをつなぐと不安定になったりするっぽい?
        Camera.grab();//カメラ画像の取得
        Camera.retrieve(image);
        imencode(".jpg", image, ibuff, param);
        if (ibuff.size() < sendSize) {//上限オーバーの場合は送らない
            sendto(send_sock, ibuff.data(), ibuff.size(), 0, (struct sockaddr *) &send_addr, sizeof (send_addr));
        }
        ibuff.clear();
    }
}

コンパイルの際にはリンカに-lraspicam -lraspicam_cv -lopencv_highgui -lopencv_core -lopencv_imgcodecs のオプションが必要です.

UDPでは1パケットの最大サイズがヘッダ込みで65535byteとなるため送信できる画像は最大で約65kBとなります.ラズパイのカメラモジュールは無駄に?解像度がすごいので注意が必要です.今回は画像の幅と高さをスマホ画面サイズの半分にしたうえで,jpegのクオリティを変えることで調整しています.リアルタイム性を追求するならjpegクオリティを更に落として送信サイズを小さくしたほうがいいです(正確にはAndroid側の受信サイズを小さく).
※補足
jpeg画像は内部でハフマン符号化を行っているため画像サイズは毎回変わります(カラフルな画像ほど大きくなる).そのため効率的な通信のためには送受信のサイズを可変にする必要があります.送信サイズを可変にするのは簡単ですが,遅延に直接影響するのは受信サイズのようなのであまり意味ないようです(少なくとも実感はありませんでした).結局最終的な実装では各画像を10kBごとに分割して送信し,受信側で再構成するという処理を行いました.
※補足終わり

受信についてはマルチスレッドやマルチプロセスで並列処理する方法が理想的なのだと思いますが,いまいち扱いづらかったので
ノンブロッキングソケット:Geekなぺーじ
様の記事を参考にノンブロッキング処理によって実装しました.

GoogleCardboardとRaspberryPiを使って視界を共有できるロボットを作る

私の子供のころからの夢の一つに,ロボットに乗って操縦したいというものがありました.人が乗れるロボットを作るのはあまりに大変ですが,視覚が共有できるロボットなら作れそうな気がしたのでやってみました.

 

カギになるのはGoogleCardbordというGoogle謹製のお手軽VR体験装置.

Google Cardboard – Google VR

段ボールだけでそこそこVRを体験できる非常に素晴らしい装置なのですが,今回はこれとRaspberryPiおよびカメラジンバルを組み合わせて,頭の動きとカメラの向きが同期する視覚共有装置の製作を試みました.そのカメラを台車に乗せてPS3コントローラで遠隔操作できるようにすることで疑似的にロボットを操縦している感覚を味わおうという魂胆です.

まだ一次完成版ですが一応形になったので整理を兼ねてまとめたいと思います.そこそこ長くなりそうなので分割で.

f:id:shikky_lab:20160722104559p:plain

外観はこんな感じ.右にあるのがGoogleCardbordのケースです.

動画

視覚共有ロボットの動作確認 - YouTube

 

今回は装置概要をまとめます.接続関係はこんな感じ.

f:id:shikky_lab:20160721062105p:plain

以下詳細

・RaspberryPi3+カメラモジュール

今回の主役その1.周辺機器については過去記事参照のこと(RaspberryPi3の下準備 - 徒然創作記)

カメラ画像の取得と同時にAndroidとはWi-Fi(UDP)通信,コントローラとはBluetooth通信,マイコンとはUART通信と大忙し.マイコンとの通信にはUSB接続のシリアル変換モジュールを使用しました.

・サーボジンバル

フリスクでraspberry piカメラのケースを作る - 徒然創作記 でカメラ雲台といっていたやつです.

・サーボ駆動用マイコン(AVR ATmega88P)

RaspberryPiから直接サーボモータを動かすこともできますが,念のためモータ駆動用には別のマイコンを置きました.

Android端末(Xperia Z3C)

今回の主役その2.ちゃんとGoogleがCardboad用のAPIを用意してくれているのでそれをベースに実装します.ちなみにGoogleiPhone用のAPIもちゃんと出してくれてます.

今回はテザリングを使うことでAndroidとRspberryPiを疑似的に直接接続しました.

・Cardboard対応ケース

ebayで買ったプラ製のケースを使用.ヘッドバンド付きで900円という素晴らしさでした.

・台車

テキトーな板にタミヤさんのギヤボックスをテキトーに取り付けて,テキトーなモータドライバで動かします.モータドライバについては過去に設計したものを流用しましたが,今回の負荷ならモータドライバICで十分そう.

PS3コントローラ

主に台車操作用に使用.

 

概ねこのような構成で作成しました.ここまで書いて思いましたが,ただの台車をロボットというのはかなり無理がありますね.でもまあこれだけで結構楽しめます.

何部構成になるかわかりませんが,次回はRaspberryPiのUDP通信部分について書きたいと思います.

 

 

 

 

 

getter関数のvolatile宣言

AVRの割り込みでクラス内変数を書き替える場合,その変数にgetterでアクセスして値の変更を待つ次のような文がうまく動かなかった.

while(!hoge.getval());

変数自体にvolatile宣言しても変わりがない.どうやら関数自体にvolatile宣言が必要らしい.

int class::getval(void) volatile
{
}

のように宣言すると一応動いた.しかし想定通りの動きとは少し異なってしまい,結局

volatile int temp;
do{
    temp=hoge.getval();
}while(!temp);

と書いたらうまくいった.とはいえただのポーリングに4行も書くのは何かばからしい.何とかならないものか.

RaspberryPi3にopencv3.1を導入してRaspiカメラモジュールを使う

RaspberryPi3で画像処理をするにあたり,とりあえずopencvを導入することに.

せっかくなので最新版のopencv3.1を自前でbuildして入れる.(opencv2.xで構わないという人はapt-getで入れられるのでそっちで入れた方がずっと楽)

 

opencvをbuildするための参考ページはいろいろ探してみたけど

Install guide: Raspberry Pi 3 + Raspbian Jessie + OpenCV 3 - PyImageSearch

ここが環境が完全に一致しているうえ,説明も丁寧で非常に分かりやすかった.結構長い手順だけど途中でpythonとvirtualenvの話をかなりがっつりやっているため,すでに入れている人やPythonを使う気がない人は無視してしまっていいかも.

opencvのmakeは結構重い作業で,二回ほど途中で止まっていた(再開したら何事もなく進むという謎).結局4コアすべて使って合計80分程度かかった.

 

opencvの準備はこれで完了だが,Raspiのカメラモジュールをopencvで使うためにはRaspicam_cvを導入する必要がある.

GitHub - cedricve/raspicam: AVA RaspiCam: C++ API for using Raspberry camera with/without OpenCv

READMEが結構詳しいため導入は割と順調に進んだ.動作確認のためにREADME下部にあるOpenCV Interfaceのサンプルコードをコンパイルしてみる.NetBeansの場合はプロジェクトプロパティのリンカ―タブの追加のオプションの項に

-lraspicam -lraspicam_cv -lopencv_core -lopencv_highgui

を書いてライブラリの指定を行ってからビルド.

f:id:shikky_lab:20160609230159p:plain

(追加のオプションの指定)

 

ところがDSO missingが出て怒られた.どうもimwriteで怒られているらしい.

原因はimwriteの含まれるライブラリで,opencv3からはhighguiではなくopencv_imcodecsが必要らしい.

というわけで先ほどのオプションに-lopencv_imgcodecsを追加してビルドしてみると無事に通った.

RaspberryPiの開発環境の構築

RaspberryPi3の開発を始めるにあたり,ひとまず以前A+の開発に使っていたときの環境をベースに環境構築.

 

基本的に母艦のWindowsベースで開発するので基本操作はVNCSSH経由.

 

SSH

SSHに関してはraspi-configからSSHをenableにするだけで準備は完了するので楽ちん.

windowsからはTeraTermでアクセスしている.

 

VNC

こじ研(Raspberry Pi)

の記事を参考に導入していく.VNCサーバは自動起動させておきたいので

Raspberry Piで遊ぼう [No.5:VNC接続をしよう]:アシマネくんのほんわか日記:So-netブログ

に従って設定.

Windows側はRealVNCを使ってアクセスしている.

あと,VNC接続中にクリップボードの内容を同期できるautocutselは非常に便利なので一緒に導入

raspbian - TightVNC copy/paste between local OS and Raspberry Pi? - Raspberry Pi Stack Exchange

 

ホスト名参照

ラズパイと接続するときにいちいちIPアドレスを調べてられないのでホスト名による参照を導入.

Raspberry Piにホスト名の設定をしたメモ – 1ft-seabass.jp.MEMO

Windows側の準備としてはBonjourを導入する必要があったのだが,Windows10からはデフォルトでホスト名参照できるらしい.なおどちらにせよファイアウォールの登録が必要.受信と送信のどちらにも必要っぽい.

登録の手順

コントロールパネル→システムとセキリュティ→Windowsファイアウォールから詳細設定をクリック.受信or送信の規則→新しい規則 を選び,

規則の種類:ポート

プロトコルおよびポート:UDP,特定のリモートポート:5353

後は適用範囲や名前を適当に決めて完了.

 

以降ホスト名.localでアクセスできるようになる.

 

フォルダ共有

VNCベースで開発を進めていると共有フォルダがほしくなることが多いので,

Sambaのインストールと設定と接続

を参考にSambaでホームディレクトリ全体を共有している.

 

NetBeans

プログラムの開発についてはNetBeansというIDEを使用.NetBeansWindows上で開発しながらコンパイルのみラズパイ上で行うという器用なことができる.

https://netbeans.org/downloads/

から対象のインストーラを入手してインストール.

以降ラズパイとの接続設定.C/C++の例だがたぶん他も同じ.これらはネットワーク上にラズパイが繋がっていることを確認してから行う.

1.ひとまず普通にプロジェクトを作成

2.ツール→オプション→C/C++のビルド・ツールタブをクリック

3.右上の編集ボタンをクリックしてRaspberryPiの情報を追加

4.プロジェクトのプロパティのビルドの項でビルド・ホストを選択

これでラズパイ上でのコンパイルが可能に.

なおデフォルト設定でコンパイルを行った場合,実行ファイルは

~/.netbeans/remote/ホスト名/ユーザー名-Windows-x86_64/Windows上でのプロジェクトのパス/dist/Debug/GNU-Linux-x86

というめちゃくちゃ深いところに作られる.プロジェクトのプロパティのリンカの項目から,出力ディレクトリを

/home/pi/ファイル名

のように指定すれば変更できるが,IDE上での実行やデバッグはできなくなる?

実行やデバッグのパスを合わせればできそうなものだけど,どうもうまくいかないのでSSHVNCで直接起動している.いずれなんとかしたい.

 

今のところの開発環境はこんな感じ.また便利なものを見つけたら紹介する予定.