LoginSignup
13

More than 5 years have passed since last update.

ラズパイ3内蔵Bluetooth経由でiPhoneから操作できるラジコンカーを作る

Posted at

はじめに

ラズパイ3にはBLE(Bluetooth Low Energy)が標準で搭載されました。という訳でiPhoneから操作できるラジコンを作ってみようと思います。この記事は以前自分のブログに乗せたものとほぼ同じですが、ここでは環境設定などの詳しい話は端折って書くことにします。それらが気になる方はぜひブログ(ラズパイ3内蔵BluetoothでiPhone操作のラジコンカーを作るまで(1))に遊びに来てください。

目標

radio_con.gif

春なのに落ち葉のような服で操縦してるのが私です。カメラワークが酷くてすみません。

仕様など

・ラジコンの制御にはraspberry pi3 modelB+とArduino UNOを用い、それらのデバイス間でUSBによるシリアル通信を行う。
・BLEのPeripheralにラズパイ、CentralにiPhone、ラジコンカーのモータードライバとサーボモーターのPWM制御にArduinoを用いる。
・iPhoneのUIコンポーネントであるSliderでラジコンの前進、後退、速度を制御する。
・ラジコンはサーボモーターによるステアリングを実装し、Wi○のマリ○カートのようにiPhoneを傾けるとそれに連動してラジコンの進行方向が変わるようにする。
・ラズパイ側はNode.js、iPhone側はswift3、ArduinoはCっぽい専用言語で実装する。
・前輪のサーボはSG90、後輪モーター制御用のモータードライバにはL298を使用する。
・後々はカメラに画像認識させて自動運転させたり、ラズパイ側にセンサつけたりして色々したい。

モジュール、デバイスのバージョン

iPhone側:iOS 10.3, Xcode8.3.2, Swift3.1
ラズパイ側:Raspberry Pi3 modelB+, Raspbian Jessie Lite, node.js 7.6.0, serialport 4.0.7
Arduino側:Arduino UNO

作り方

ラズパイ側

まずは必要なnodeモジュールをインストールしましょう。今回使うのはblenoとserialportです。
bleno : https://github.com/sandeepmistry/bleno
serialport : https://github.com/EmergingTechnologyAdvisors/node-serialport

blenoはBLEのPeripheral用モジュールです。基本的にREADMEに従って行きますが、bluetoothデーモンが動いていると衝突を起こすことがあるらしいので、そいつを無効にしてからsudo hciconfig hci0 upするよう注意してください。

ラズパイ側ソースコード

bleMain.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があっさりしているので大変でした。コードの隙間にある程度解説を入れてますが間違えている可能性があるので見つけた方はコメントで教えてください。

Arduino側

そもそもなぜArduinoを使ったかというと、ラズパイではPWM制御をハードウェアレベルで扱えるピンがGPIO18番の1つしかないからです。ラジコンの後輪2つは同じ電圧で動作させるにしても、前輪のサーボを扱えないので仕方なくArduinoを追加したといった感じです。ラズパイでプログラムからパルスを発生させることも一応できるらしいですが、今後の拡張性を考えてArduinoを乗せてやりました。USBシリアルを用いて通信していますが、ラズパイ側からシリアルポートを開く命令が届いてからArduinoの準備ができるまで、モータ制御用のデータが届かないようにしました。

Arduino側ソースコード

BLEMotor.ino
#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();
    }
  }
}

iPhone側

続いてiPhone側です。なかなかに汚いコードですがこちらに上げておきます。
https://github.com/teru01/BLE-radio-controller
わかり辛い点があるのでいくつか解説させてください。

SecondViewController.swift
/*前略*/
func motionAnimation(_ motionData:CMDeviceMotion?,_ error:Error?){
        if let motion = motionData{
            //pitchはradで渡されるので度に変換
            var pitch = motion.attitude.pitch/Double.pi*180
            //pitchを-40から40に抑える
            pitch = (pitch < -40) ? -40 : pitch
            pitch = (pitch > 40) ? 40 : pitch
            var predif = 1000
            for i in 0..<40{
                let dif = abs((i*2)-Int(pitch+40))
                if predif-dif > 0{
                    predif = dif
                    tempPitch = i*2
                }
            }
            if (tempPitch != prevPitch){
                //データを送信
                pitchLabel.text = String(tempPitch-40)
                print(tempPitch)
                angleValue = UInt8(tempPitch)
                sendData(sliderValue,angleValue)
            }
            prevPitch = tempPitch
        }
    }
/*以下略*/

SecondViewControllerから関数の一部を引っ張り出して来ました。pitchはiPhoneが横向きの時に画面の奥から手前方向を軸とした回転角を表します。iPhoneが縦向きの時となぜか軸の向きが変わってしまうようです。回転角を度数法に直したのち、for文の部分で0~80までの偶数でもっとも近い値に収まるようにしています。Sliderの部分でもそうですが、なるべくラズパイからArduinoにシリアルで送るデータが少なくなるように、送信するためのデータに変化があった時だけ送信命令を出すようにしています。

参考にさせていただいたサイト

http://qiita.com/kentarohorie/items/b9549af9c71886860866
http://qiita.com/uzuki_aoba/items/346e28b6e9170ce85a6c

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13