iOSエンジニアのしだです。
最近、Bluetooth Low Energy(BLE)をつかう場面が多くなってきたので、iOSでBLEを利用する方法を勉強中であります。
特に目新しい話ではないのですが、iOSでCentral側とPeripheral側の実装を試してみたので共有したいと思います。
準備
- Xcode 8.2(Swift 3)
- RxSwift 3.4.1、 RxCocoa 3.4.1
- Charts 3.0.2
実装したもの
Peripheral側の加速度(xyz)の値をCentral側で受け取るアプリを実装してます。
- Central
- 特定のアドバタイズ名(BLE001)をスキャンする
- 接続するデバイスを選ぶとPeripheral 側から加速度の値が送られてくる
- Peripheral
- Central側から通知の受取が有効にされたらデバイスの加速度の値を送信する
やり取りする加速度のデータ
実際にCentral側とPeripheral側でやり取りされる加速度の値を管理するクラスです。
// MARK: - Acceleration struct Acceleration { var x: Float var y: Float var z: Float /// ... /// Central側で受け取ったデータをパースする init(bytes: Data) { x = Data(bytes[0..<4]).to(type: Float.self) y = Data(bytes[4..<8]).to(type: Float.self) z = Data(bytes[8..<12]).to(type: Float.self) } /// Peripheral側から送るデータ形式 /// XYZ軸の各値Floatで4バイト×3軸で12バイトの値を送信します var data: Data { var data = Data() data.append(x.bytes) data.append(y.bytes) data.append(z.bytes) return data } enum UUID: String { /// Peripheral側のサービスのUUIDを "0011" case service = "0011" /// 加速度の値を送信するキャラクタリスティックのUUID case characteristic = "0012" var uuid: CBUUID { return CBUUID(string: self.rawValue) } } /// ... }
Peripheral側の実装
// PeripheralViewController.swift class PeripheralViewController: UIViewController { let disposeBag = DisposeBag() @IBOutlet weak var tableView: UITableView! @IBOutlet weak var logTextView: UITextView! /// CoreMotionの初期化 fileprivate let motionManager = CMMotionManager() fileprivate var peripheralManager: CBPeripheralManager? /// 加速度を通知するキャラクタリスティック fileprivate var characteristic: CBMutableCharacteristic? /// 加速度の値を更新 fileprivate var acceleration = Variable<Acceleration>(Acceleration()) // ... override func viewDidLoad() { super.viewDidLoad() /// peripheralManagerの初期化 peripheralManager = CBPeripheralManager(delegate: self, queue: nil) if let queue = OperationQueue.current { /// CoreMotionから加速度取得する間隔を設定 motionManager.accelerometerUpdateInterval = 0.2 /// CoreMotionから加速度の値の受取を開始する motionManager.startAccelerometerUpdates(to: queue) { [weak self] (data, _) in guard let data = data else { return } self?.acceleration.value = Acceleration(acceleration: data.acceleration) } } /// ... Observable.just(items) .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: PeripheralCell.self)) { [weak self] (_, item: Item, cell) in cell.titleLabel?.text = item.title cell.textField?.text = item.text.value cell.textField?.rx.text .throttle(0.3, scheduler: MainScheduler.instance) .bind(to: item.text) .addDisposableTo(cell.disposeBag) cell.switchButton?.rx.value .bind(onNext: { (active) in if active { /// UI上にスイッチを用意しておりオンにするとアドバタイジングを開始します let data: [String: Any] = [ CBAdvertisementDataLocalNameKey: "\(Acceleration.advertisementName)", CBAdvertisementDataServiceUUIDsKey: [Acceleration.UUID.service.uuid] ] self?.peripheralManager?.startAdvertising(data) } else { /// スイッチをオフにするとアドバタイジングを終了します self?.peripheralManager?.stopAdvertising() } }) .addDisposableTo(cell.disposeBag) } .addDisposableTo(disposeBag) /// 加速度の値が更新された場合Central側に通知を行う acceleration.asObservable() .subscribe( onNext: { [weak self] (acceleration) in guard let peripheral = self?.peripheralManager else { return } guard let characteristic = self?.characteristic, let centrals = characteristic.subscribedCentrals, !centrals.isEmpty else { return } /// Centralと接続している場合は、Central側に加速度の値を通知する peripheral.updateValue(acceleration.data, for: characteristic, onSubscribedCentrals: nil) } ) .addDisposableTo(disposeBag) } }
CBPeripheralManagerDelegate
の実装を行い、ServiceとCharacteristicを設定します
logTextView.appendLog
はコンソールログぽいUIを用意しているのでそこに状態を表示するようにしています。
// MARK: - CBPeripheralManagerDelegate extension PeripheralViewController: CBPeripheralManagerDelegate { /// Peripheralのステータスが変更されたら通知されます func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { logTextView.appendLog(text: "Update State: \(peripheral.state)") guard peripheral.state == .poweredOn else { return } /// 加速度の値を通知するキャラクタリスティックを設定 let characteristic = CBMutableCharacteristic( type: Acceleration.UUID.characteristic.uuid, properties: .notify, value: nil, permissions: .readable ) /// Peripheral側のサービスを設定 let service = CBMutableService(type: Acceleration.UUID.service.uuid, primary: true) service.characteristics = [characteristic] self.characteristic = characteristic self.peripheralManager?.add(service) } func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { logTextView.appendLog(text: "Add: \(service.description)") } func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) { logTextView.appendLog(text: "Start Advertising") } func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { logTextView.appendLog(text: "Subscription: \(central.description)") } func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) { logTextView?.appendLog(text: "Unsubscription") } func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { logTextView?.appendLog(text: "IsReady") } func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) { logTextView?.appendLog(text: "Receive read: \(request.description)") } func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any]) { logTextView?.appendLog(text: "Restore State: \(dict)") } func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { } }
Central側の実装
デバイスのスキャン
BLEデバイスの検出と検出されたデバイスの表示を行います
/// ScanViewController.swift class ScanViewController: UIViewController { let disposeBag = DisposeBag() @IBOutlet weak var tableView: UITableView! let scanTime: Double = 5 // seconds var centralManager: CBCentralManager? fileprivate var peripherals = Variable<[CBPeripheral]>([]) /// ... override func viewDidLoad() { super.viewDidLoad() /// Centralの初期化 centralManager = CBCentralManager(delegate: self, queue: nil) /// ... } /// BLEデバイスのスキャンを開始する。5秒経過後スキャンを停止します func scan() { guard let manager = centralManager else { return } if !manager.isScanning { manager.scanForPeripherals(withServices: nil, options: nil) DispatchQueue.main.asyncAfter(deadline: .now() + scanTime) { [weak self] in manager.stopScan() self?.refreshControl.endRefreshing() } } else { refreshControl.endRefreshing() } } /// ... } // MARK: - CBCentralManagerDelegate extension ScanViewController: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) {} func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { /// 今回は Peripheral側の名前を `BLE(001)` にしたのでそれだけ検出できるようにする if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String, name.lowercased().contains("ble") { if peripherals.value.filter({ $0 == peripheral }).isEmpty { peripherals.value.append(peripheral) } } } }
Peripheral側から加速度を受信
CentralViewController.swift
: BLEHelperから受け取った加速度の値をグラフを表示するBLEHelper.swift
: Peripheral側から加速度の値を受信する
/// BLEHelper.swift class BLEHelper: NSObject { fileprivate var centralManager: CBCentralManager! fileprivate var peripheral: CBPeripheral? var action: ((Acceleration) -> Void)? var uuid: UUID? override init() { super.init() centralManager = CBCentralManager(delegate: self, queue: nil) } /// PeripheralのUUIDを指定してPeripheralに接続開始します func connect(identifier: String, action: ((Acceleration) -> Void)?) { self.uuid = UUID(uuidString: identifier) self.action = action centralManager.scanForPeripherals(withServices: nil, options: nil) } /// Peripheralとの接続を切ります func cancel() { if let peripheral = peripheral { centralManager.cancelPeripheralConnection(peripheral) } if centralManager.isScanning { centralManager.stopScan() } } } extension BLEHelper: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) {} func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String, name.lowercased().contains("ble") { if peripheral.identifier == uuid { self.peripheral = peripheral /// Peripheralに接続を開始します centralManager.connect(peripheral, options: nil) } } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { /// Peripheralに接続できたらサービスを探します peripheral.delegate = self peripheral.discoverServices(nil) } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {} func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {} } // MARK: - CBPeripheralDelegate extension BLEHelper: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard let services = peripheral.services, !services.isEmpty else { return } for service in services { if service.uuid == Acceleration.UUID.service.uuid { peripheral.discoverCharacteristics(nil, for: service) } } } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard let characteristics = service.characteristics, !characteristics.isEmpty else { return } for characteristic in characteristics { if characteristic.uuid == Acceleration.UUID.characteristic.uuid { /// 加速度を取得するために通知を有効にします peripheral.setNotifyValue(true, for: characteristic) } } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard let data = characteristic.value else { return } if characteristic.uuid == Acceleration.UUID.characteristic.uuid { /// 加速度の通知を受け取ったらコールバックする let acc = Acceleration(bytes: data) self.action?(acc) } } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) { } }
デモ
はじめにiOSデバイスを2台用意して、Peripheral側を起動してアドバタイジングしている状態にします。
この状態でCentral側を起動するとPeripheral側のデバイスを検出・接続できるようになります。
検出したデバイスを選択するとそのデバイスに接続するようになります。 各XYZ軸の値をそのままグラフ表示します。Peripheral側のデバイスを動かすと動きに応じてグラフが変化します。
まとめ
まだまだBluetoothについて勉強中なところはあるのですが、CentralとPeripheralの実装を行うことでより理解が深められました。
実際のBLEデバイスがなくてもiOSでPeripheral側を実装して擬似的に用意すれば、Central側のテストにも使えるかなと思います。
ソースコードの全体は以下に置いてあります