叉焼.log

札幌のこととか、ITの事とか

ラズパイ3内蔵BluetoothでiPhone操作のラジコンカーを作るまで(1)

お久しぶりです。管理人の叉焼です。
さてさて、以前予告したかしてないか忘れましたが、ラズパイ3の新機能である内蔵bluetooth(BLE)を用いてiPhoneから操作できるラジコンカーを作ってみようと思います。私の春休みは全部こいつに溶けて行きましたとさ。めでたし。

目標、仕様

・車部分はraspberry pi3 model B+,OSはraspbian jessie lite、コントローラーはiPhone6s plus ,OSはiOS10.2.1、開発はXcode8.3.3で行う。
・ラジコンの制御にはraspberry piArduino UNOを用い、それらのデバイス間でUSBによるシリアル通信を行う。
・BLEのPeripheralにラズパイ、CentralにiPhone、ラジコンのモータードライバとサーボモーターのPWM制御Arduinoを用いる。
・ラジコンはサーボモーターによるステアリングを実装し、Wi○のマリ○カートのようにiPhoneを傾けるとそれに連動してラジコンの進行方向が変わるようにする。
・ラズパイ側はjavascript(Node.js)、iPhone側はswift3で実装する。(ArduinoはCっぽい専用言語)
・後々はラジコンに速度センサ、加速度センサやカメラをつけてドライブレコーダーが取れるようにしたい。
・後々はカメラに画像認識させて自動運転させたい。
・後々は音声認識で(ry

とりあえず完成したやつ

f:id:cha-shu00:20170425193133g:plain

操縦してるのは私です。カメラはサークルの友達に撮ってもらいました。ぶっちゃけカメラワークひどい。

はじめに

上記の仕様を見て分かる通り、ごちゃごちゃしてます。最初はラズパイだけでいけると思い込んでいたのですが、PWM制御を使えるピンが足りないことに気がつきArduinoを急遽追加しました。BLEの情報がネットに少なく、あっても断片的なものばかりなので真似するのに随分苦労しましたねぇ。ラズパイ初心者ということに加え自分で勝手に推測しているものや解釈を加えているものもあるので、間違ってるよと気付いた方はコメ欄で優しく教えてください。

BLEって何?という方はAppleさんの「CoreBluetoothプログラミングガイド」がとても分かり易かったです。実際iPhone側の実装ではこのCoreBluetoothというフレームワークを使っていきます。ただしこのpdfのサンプルコードはobjective-cで書かれているようなので、BLEの流れを理解するのに使いました。

「CoreBluetoothプログラミングガイド」
https://developer.apple.com/jp/documentation/CoreBluetoothPG.pdf

ラズパイ、Arduino側 〜下準備〜

ラズパイの環境を整えて、プログラムを書き込むところまで行きます。ラズパイのOSにはRaspbian Jessie Liteをインストールし、PCからSSHで接続します。ラズパイからBLEで通信するのにnodeのモジュールを使用するので、まずはnodeをインストールしましょう。nodeは管理用パッケージがたくさんあってどれ使えばいいのかよくわからなくなりましたが、nodebrewを使うと簡単でした。
GitHub - hokaccha/nodebrew: Node.js version manager

ここに書いてあることを実行すればすんなり行くはずです。ちなみに私はnode.jsのv7.6.0を使用しています。nodeをインストールできたらおそらくモジュールが管理できる「npm」というのも同時にインストールされているかと思うので、それを使ってblenoというnodeモジュールをインストールします。基本的にこのモジュールでBLEを操作して行きます。

GitHub - sandeepmistry/bleno: A Node.js module for implementing BLE (Bluetooth Low Energy) peripherals

ここに書いてあることに沿ってインストールしていきます。
bluetoothデーモンが動いていると衝突を起こすことがあるらしいので、ちゃんと無効にしてからsudo hciconfig hci0 upするよう注意してください。
次に、ラズパイとArduino間でシリアル通信するモジュール「serialport」をインストールします。

GitHub - EmergingTechnologyAdvisors/node-serialport: Node.js package to access serial ports for reading and writing. Welcome your robotic JavaScript overlords. Better yet, program them!


ではいよいよBLEを触って行きましょう。と言いたいとこですがその前に便利なソフトを紹介します。

www.jetbrains.com

Android studioでおなじみのIDEAと同じ系統のIDEです。これを使えばPCで開いているIDEから直接ラズパイにSFTP経由でnodeのソースファイルを送ることができます。さらに僕のような学生なら無料でライセンスを入手することができるそうです。若いって素晴らしい。操作方法やラズパイへのアップロード方法は、

PyCharmを使ってRaspberry Pi2上で快適リモートGPIOプログラミング - izm_11's blog

こちらの方が大変丁寧に説明してくださっています。説明に用いているのはpythonIDEですが、JSのIDEでもほとんど同じように操作できます。さて、快適な開発環境を手に入れたところでいよいよ実装に移っていきます。

ラズパイ側 〜実装〜

bleMotor.js
var bleno = require('bleno');
var Characteristic = require('./Characteristic');
var serviceUuid = 'abcd';

/*stateChangeイベントの登録。接続状態が変化するとコールバック関数が呼び出される*/
bleno.on('stateChange',function(state){
    console.log('on ->stateChange:'+state);
    if(state === 'poweredOn'){
        //指定された名前とUUIDでアドバタイズを開始
        bleno.startAdvertising('led',[serviceUuid]);
    }else {
        bleno.stopAdvertising();
    }
});
/*advertisingStartイベントの登録。アドバタイズが始まるとコールバック関数が呼び出される*/
bleno.on('advertisingStart',function(error){
    if(!error){
        bleno.setServices([
                new bleno.PrimaryService({
                    uuid : serviceUuid,
                    characteristics : [new Characteristic()]
                })
            ]
        );
        console.log('on ->advertisingStart');
    }
});
Characteristic.js
var util = require('util');
var bleno = require('bleno');
var characteristicUuid = '12ab';
var SerialPort = require('serialport');
var port = new SerialPort('/dev/ttyACM0');
var flag = false;
/*Arduinoからの応答を受け取るdataイベントを登録*/
port.on('data',function(data){
    /*Arduinoを初期化する時、そのシリアルポートが準備されているか確認*/
    if(data.readInt8(0) == -1){
        flag = true;
        console.log("ready");
    }
    /*不正な値を検出して停止*/
    else if(data.readInt8(0) == -2){
        flag = false;
        console.log("abnormal stop");
    }
});
//Characteristicコンストラクタをオーバーライド
var Characteristic = function(){
    Characteristic.super_.call(this,{
        uuid : characteristicUuid,
        properties : ['write']
    });
};

util.inherits(Characteristic,bleno.Characteristic);

/*iPhoneからの書き込み命令があった時に呼び出される*/
Characteristic.prototype.onWriteRequest = function(data,offset,withoutResponse,callback){
    if(flag){
        port.write(data);
        console.log("sliderData:"+data.readInt8(0));
        console.log("angleData:"+data.readInt8(1));
        callback(this.RESULT_SUCCESS);
    }
};

module.exports=Characteristic;

blenoのreadmeが随分あっさりとしているので割と苦労しました。おまけにnodeも初心者だったのでjavaphpなどのクラス型オブジェクト指向言語との違いにあたふたしてました。春休み中で随分脳トレができたと思ってます。

Arduino側 〜実装〜

#include <Servo.h>
#define FORWARD_PIN 3
#define BACKWARD_PIN 11
#define SERVO_PIN 6
Servo myservo;
byte dataAry[2];
int sliderData;
int angleData;
void setup() {
  //ピンを初期化
  analogWrite(FORWARD_PIN,0);
  analogWrite(BACKWARD_PIN,0);
  //DCモータ、サーボモータをそれぞれ中立の値に初期化。
  sliderData = 4;
  angleData = 40;
  Serial.begin(9600);
  myservo.attach(SERVO_PIN);
  //シリアルポートの準備ができるまで待機
  while(!Serial){}
  //準備完了時に-1を送信
  Serial.write(-1);
}
void loop() {}
//シリアルポートにデータがある時呼び出される。
void serialEvent(){
  while(Serial.available()){
    //dataAryの先頭2バイトにシリアルからのデータが読み込まれる。
    Serial.readBytes(dataAry,2);
    sliderData = dataAry[0];
    angleData = dataAry[1];
    //データが正しく受信できているかを確認
    if(sliderData >= 0 && sliderData <= 8 && angleData >= 0 && angleData <= 80){
      myservo.write(angleData);
      //進行、バック、停止の判断
      if(sliderData > 4){
        analogWrite(BACKWARD_PIN,0);
        analogWrite(FORWARD_PIN,(sliderData-4)*64-1);
      }else if(sliderData < 4){
        analogWrite(FORWARD_PIN,0);  
        analogWrite(BACKWARD_PIN,(4-sliderData)*64-1);
      }else{
        analogWrite(FORWARD_PIN,0);
        analogWrite(BACKWARD_PIN,0);
      }
    }
    //データが不正の時
    else{
      Serial.write(-2);
      Serial.flush();
    }
  }
}

sliderDataはDCモータへの信号、angleDataはサーボモータへの信号が入るようになっています。ラズパイ側からシリアルポートを開く命令が届いてからArduinoの準備ができるまで、モータ制御用のデータが届かないようにしました。

長くなりそうなので制御用iPhoneアプリは次回!