かなり前の記事から引き続いてiPhoneアプリを作っていきます
かなり前の記事↓
cha-shu00.hatenablog.com
iPhoneアプリ 〜概要〜
・追加で使用する主なフレームワークはCoreBluetoothとCoreMotion。それぞれBLEデータを送信する用、iPhoneの姿勢データを読み取る用に使う。
・はじめのビューのボタンをタップするとBLEによるPeripheralの探索が始まり、次のビューに遷移する。
・二つ目のビューではラジコンのDCモータの出力を制御するためのスライダーと、サーボモータの角度を制御するために姿勢センサの実装をする。
・画面遷移はSegueを使って行う。方法は
【Xcode8.3.1,swift3.1】異なるビュー間での値の受け渡し方法 その2(Segue使うよ) - 叉焼.logを参照。
iPhoneアプリ 〜実装〜
詳しくはgithubを見たまえ
GitHub - teru01/BLE-radio-controller: An iPhone app to send data to raspberry pi via Bluetooth Low Energy to handle a radio-controlled car.
これで終わりと言いたいところですがそんなことすると読者離れが深刻になりそうなので説明します。
ViewController.swift
import UIKit import CoreBluetooth class ViewController: UIViewController,CBCentralManagerDelegate,CBPeripheralDelegate{ var myCentralManager: CBCentralManager! var myTargetPeripheral: CBPeripheral! var myTargetService: CBService! var myTargetCharacteristic: CBCharacteristic! let serviceUuids = [CBUUID(string: "abcd")] let characteristicUuids = [CBUUID(string: "12ab")] override func viewDidLoad() { super.viewDidLoad() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } //startボタンを押下した時の処理 @IBAction func tapStartBtn(_ sender: Any) { self.myCentralManager = CBCentralManager(delegate: self, queue: nil, options: nil) } //CBCentralManagerDelegateプロトコルで指定されているメソッド func centralManagerDidUpdateState(_ central: CBCentralManager) { print("state:\(central.state)") switch central.state{ case .poweredOff: print("Bluetooth-Off") //BluetoothがOffの時にアラートを出して知らせる let bleOffAlert=UIAlertController(title: "警告", message: "bluettothをONにしてください", preferredStyle: .alert) bleOffAlert.addAction( UIAlertAction( title: "OK", style: .default, handler: nil ) ) self.present(bleOffAlert, animated: true, completion:nil ) case .poweredOn: print("Bluetooth-On") //指定UUIDでPeripheralを検索する self.myCentralManager.scanForPeripherals(withServices: self.serviceUuids, options: nil) default: print("bluetoothが準備中又は無効") } } //peripheralが見つかると呼び出される。 func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { self.myTargetPeripheral = peripheral //アラートを出してユーザーの接続許可を得る let bleOnAlert = UIAlertController(title: "Peripheralを発見",message: "接続します",preferredStyle:.alert) bleOnAlert.addAction( UIAlertAction( title: "OK", style: .default, //Peripheralへの接続命令 handler: {(action)->Void in self.myCentralManager.connect(self.myTargetPeripheral, options: nil)} ) ) bleOnAlert.addAction( UIAlertAction( title: "cencel", style: UIAlertActionStyle.cancel, handler: {(action)->Void in print("canceled") self.myCentralManager.stopScan()} ) ) self.present(bleOnAlert, animated: true, completion: nil) } //Peripheralへの接続が成功した時呼ばれる func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("connected") peripheral.delegate = self //指定されたUUIDでサービスを検索 peripheral.discoverServices(serviceUuids) } //サービスを検索した時に呼び出される func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { peripheral.delegate = self self.myTargetService = peripheral.services![0] //指定のUUIDでcharacteristicを検索する peripheral.discoverCharacteristics(characteristicUuids, for:self.myTargetService) } //characteristicを検索した時に呼び出される func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let e = error{ print("error:\(e.localizedDescription)") }else{ myTargetCharacteristic = service.characteristics![0] segueToSecondViewController() } } //segueを用いて次のビューへ遷移 func segueToSecondViewController() { //次のビューへ渡すプロパティ let targetTuple = (myTargetPeripheral,myTargetCharacteristic) self.performSegue(withIdentifier: "mySegue", sender: targetTuple) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "mySegue" { let secondViewController = segue.destination as! SecondViewController secondViewController.targetTuple = sender as! (CBPeripheral,CBCharacteristic) } } }
はじめのビューはこんな感じです。シンプルすぎてもはやどこに画像の境界があるのかわかりませんが心眼を使って見てください。基本的にはフレームワークであるCoreBluetoothにしたがって記述していけばいいので特に問題はないです。
startボタンをタップするとこんな感じでアラートが出ます。
SecondViewController.swift
import UIKit import CoreBluetooth import CoreMotion class SecondViewController: UIViewController,CBPeripheralDelegate{ var targetTuple: (CBPeripheral,CBCharacteristic)! var myTargetPeripheral: CBPeripheral! var myTargetService: CBService! var myTargetCharacteristic: CBCharacteristic! var sliderValue: UInt8 = 4 var angleValue: UInt8 = 70 var centralData: Data! var tempPitch: Int! var prevPitch: Int = 40 var tempSlider: Int = 4 var prevSlider: Int = 4 @IBOutlet weak var pitchLabel: UILabel! @IBOutlet weak var accelLabel: UILabel! @IBOutlet weak var mySlider: UISlider!{ //スライダーを縦表示する didSet{ mySlider.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / -2)) } } let cmManager = CMMotionManager() override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } override func viewDidLoad() { super.viewDidLoad() myTargetPeripheral = targetTuple.0 myTargetCharacteristic = targetTuple.1 //モーションセンサが発行するキューの実行間隔(秒) cmManager.deviceMotionUpdateInterval = 0.3 //キューで実行するクロージャを定義 let handler: CMDeviceMotionHandler = { (motionData: CMDeviceMotion?,error: Error?) -> Void in self.motionAnimation(motionData,error) } //更新で実行するキューを登録してモーションセンサをスタート cmManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: handler) } //クロージャの中で実行される 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 } } //スライダーの変化を検知 @IBAction func changeSlider(_ sender: UISlider) { tempSlider = Int(sender.value + 0.5) if tempSlider != prevSlider{ accelLabel.text = String(tempSlider-4) print("slider:\(tempSlider)") sliderValue = UInt8(tempSlider) sendData(sliderValue,angleValue) } prevSlider = tempSlider } //データの送信用関数 func sendData(_ senderSlider:UInt8,_ senderAngle:UInt8){ if self.myTargetCharacteristic != nil{ let uintAry = [senderSlider,senderAngle] centralData = Data(uintAry) myTargetPeripheral.writeValue(centralData, for: myTargetCharacteristic,type:CBCharacteristicWriteType.withResponse) print("complete") } } }
こちらは少しややこしいですね。
pitchはiPhoneが横向きの時に画面の奥から手前方向を軸とした回転角を表します。iPhoneが縦向きの時となぜか軸の向きが変わってしまうようです。回転角を度数法に直したのち、for文の部分で0~80までの偶数でもっとも近い値に収まるようにしています。Sliderの部分でもそうですが、なるべくラズパイからArduinoにシリアルで送るデータが少なくなるように、送信するためのデータに変化があった時だけ送信命令を出すようにしています。
iPhone姿勢センサーの謎
プロジェクト設定から General>Deployment Info>Device Orientationのところで、デバイスの向きを設定できます。上の画像はこの時に"Portrait"だけ、"Landscape Left"だけにチェックを入れた時に、roll,pitch,yowの右ねじの回転軸をあらわしています。これは私が実験して観測したもので、ネットで調べても日本語英語に関わらず似たような情報が発見できませんでした。公式AppleのTwitterにクソな英語で問い合わせてみましたが「Developer Program(¥11800/年)に入ってから聞いてねっ❤️」と言われる始末(Appleさんごめんなさい)なのでなぜこのようなことが起きるのか、はたまた私の実験が悪かったのかは結局わからずでした。
一応、実験の方法と使ったソースコード載せておきます。方法としては上記のように、デバイス向き設定を変えてその度にiPhoneに書き込み、デバイスを3軸周りに回転させて対応するラベルの値の変化を見るといった具合です。使用環境等は前の記事(http://cha-shu00.hatenablog.com/entry/2017/04/25/195725)に書いてあります
import UIKit import CoreMotion class nextPageViewController: UIViewController { @IBOutlet weak var numLabel: UILabel! @IBOutlet weak var yawLabel: UILabel! @IBOutlet weak var pitchLabel: UILabel! @IBOutlet weak var rollLabel: UILabel! //coreMotionマネージャを作る let cmManager=CMMotionManager() override func viewDidLoad() { super.viewDidLoad() //キューの実行間隔 cmManager.deviceMotionUpdateInterval = 0.1 //キューで実行するクロージャを定義 let handler:CMDeviceMotionHandler = {(motionData:CMDeviceMotion?,error:Error?)->Void in self.motionAnimation(motionData,error) } //更新で実行するキューを登録してモーションセンサをスタート cmManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: handler) } func motionAnimation(_ motionData:CMDeviceMotion?,_ error:Error?){ if let motion = motionData{ var yaw = motion.attitude.yaw yaw = round(yaw*100)/100 yawLabel.text = String(yaw) var pitch = motion.attitude.pitch pitch = round(pitch*100)/100 pitchLabel.text = String(pitch) var roll = motion.attitude.roll roll = round(roll*100)/100 rollLabel.text = String(roll) } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. }
真相が分かる方コメントで教えてください・・・。