swift使ってEddystone-URLを受信するiOSアプリを作った

アプリを作っています。Eddystone-URLを受信するアプリです。 iOSだとどうにも情報が無くて。そしてswiftはバージョンにころころ変わるらしく。ちくしょうという感じです。

swiftは何にも分からないので、あまりエレガントなコードではないと思いますが、よしなに。

検証した端末はiPhone 6S、iOS 10.2.1(14D27)。Base SDKはiOS 10.2です。 swiftのバージョンは3.0.2です。swiftlang-800.0.63 clang-800.0.42.1らしいです。 バージョン問題に悩まされたのでとことん書きます。ちくしょうという感じです。

で、とりあえずメイン部分。ViewController.swiftに書くやつ。

import UIKit
import CoreBluetooth


class ViewController: UIViewController, CBCentralManagerDelegate {
    var centralManager: CBCentralManager!

    override func viewDidLoad() {
        super.viewDidLoad()

        centralManager = CBCentralManager(delegate: self, queue: nil)  // centralManagerDidUpdateState
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        centralManager.stopScan()  // 
    }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == CBManagerState.poweredOn {  // 
            print("start scan")

            // 
            central.scanForPeripherals(withServices: [CBUUID(string: "FEAA")],
                                       options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
        } else {
            print("not ready")
        }
    }

    @objc(centralManager:didDiscoverPeripheral:advertisementData:RSSI:) func centralManager(_ central: CBCentralManager,
                                                                                            didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {

        if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [NSObject : AnyObject] {
            let data = serviceData[CBUUID(string: "FEAA")]

            let es: EddystoneURL  // 
            do {
                try es = EddystoneURL(RawData: data as! NSData)
            } catch {
                return
            }

            print("data: \(es)")
        }
    }
}

超重要ってコメントで書いた部分が超重要です。iOS 8あたりから必要になったらしいです。 ググって出てくるサンプルを見ているとこのif文無しでやっているのですが、そうすると以下のようなエラーが出ます。

[CoreBluetooth] API MISUSE: <CBCentralManager: 0x17426af00> can only accept this command while in the powered on state

CBCentralManagerのstateってやつを確認して、poweredOnであることを確認してからスキャンを開始するようにすれば問題ありません。

で、BLEのペイロード部分のパースは次のクラスで。基本的には公式の仕様に従ってひたすら実装しただけのやつです。

import Foundation

class Eddystone : NSObject {
    static let URLEncodings: [UInt8: String] = [0x00: "http://www.",
                                                0x01: "https://www.",
                                                0x02: "http://",
                                                0x03: "https://"]

    static let DomainExpansions: [UInt8: String] = [0x00: ".com/",
                                                    0x01: ".org/",
                                                    0x02: ".edu/",
                                                    0x03: ".net/",
                                                    0x04: ".info/",
                                                    0x05: ".biz/",
                                                    0x06: ".gov/",
                                                    0x07: ".com",
                                                    0x08: ".org",
                                                    0x09: ".edu",
                                                    0x0a: ".net",
                                                    0x0b: ".info",
                                                    0x0c: ".biz",
                                                    0x0d: ".gov"]

    var TxPower: Int8
    var url: String


    init(RawData data: NSData) throws {
        var bytes = [UInt8](repeating: 0, count: data.length)
        data.getBytes(&bytes, length: data.length)

        // 1Frame SpecificationEddystone-URL0x10
        if bytes[0] != 0x10 {
            throw NSError(domain: "it isn't EddystoneURL", code: -1, userInfo: nil)
        }

        // 2Tx Power Level
        TxPower = Int8(bitPattern: bytes[1])

        // 3URL Scheme Prefix
        if let t = Eddystone.URLEncodings[bytes[2]] {
            url = String(t)
        } else {
            throw NSError(domain: "invalid payload", code: -1, userInfo: nil)
        }

        // 4URL
        for i in 3..<data.length {
            // 1
            if let t = Eddystone.DomainExpansions[bytes[i]] {
                url += String(t)
            } else {
                url += String(format: "%C", bytes[i])
            }
        }
    }

    override var description: String {
        return String(format: "EddystoneURL(power: %d): " + url, TxPower)
    }
}

愚直に実装しただけって感じです。

この二つのクラスを書いて実行してやると、デバッグコンソールに受信したEddystone URLのTx PowerとURLが表示されるはずです。 ご武運を。うへぇ。


参考: