はじめに
ラズパイ3にはBLE(Bluetooth Low Energy)が標準で搭載されました。という訳でiPhoneから操作できるラジコンを作ってみようと思います。この記事は以前自分のブログに乗せたものとほぼ同じですが、ここでは環境設定などの詳しい話は端折って書くことにします。それらが気になる方はぜひブログ(ラズパイ3内蔵BluetoothでiPhone操作のラジコンカーを作るまで(1))に遊びに来てください。
目標
春なのに落ち葉のような服で操縦してるのが私です。カメラワークが酷くてすみません。
仕様など
・ラジコンの制御には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
するよう注意してください。
ラズパイ側ソースコード
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');
}
});
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側ソースコード
#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
わかり辛い点があるのでいくつか解説させてください。
/*前略*/
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