iPhone(iOS)で Manufacturer Data を受信する

前回の続きで、FolkBears 型と Manufacturer Data 形式の受信機を iOS 版も作っていきます。FolkBears 型というのは GATT サービスで接続する方式のことです。実際のところは、コネクションして TempUserId を取得するのですが、ひとまず相手のデバイスを見つけるところまで実装しておきます。このあたり、コネクション型で接触確認アプリを作ろうとすると、TempUserId の交換よりも、最初に相手のデバイスを見つけるところで接触遅延するのではないか、という懸念があるためです。これを後に実測していきます。

GATT 通信のために相手デバイスの発見

centralManager.scanForPeripherals を呼び出すときに、重複をゆるすようにして CBCentralManagerScanOptionAllowDuplicatesKey = true を指定します。通常は、ペリフェラルの発見は一度だけでよいのですが、今回のモニタリングの場合はデバイスとの接触頻度をみるためにわざと重複させるようにします。

    func startScanning() {
        guard let centralManager = centralManager,
              centralManager.state == .poweredOn,
              !isScanning else {
            print("Bluetooth が利用できないか、既にスキャン中です")
            return
        }
        
        peripherals.removeAll()
        // discoveredPeripherals.removeAll()
        
        // 特定のサービスUUIDでスキャン(nilで全デバイス)
        centralManager.scanForPeripherals(withServices: [targetServiceUUID], options: [
            CBCentralManagerScanOptionAllowDuplicatesKey: true // 重複を許可してスキャン
        ])
        
        isScanning = true
        scanningStatus = "スキャン中..."
        print("GATT クライアント スキャン開始")
    }

FolkBears 本体からコードを抜き出してきたので、無駄なコードが多いですが、CBCentralManagerDelegate の centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) で相手のデバイスが発見しているイベントです。

最小コードとしては、先の CBCentralManagerScanOptionAllowDuplicatesKey: true の設定と、CBCentralManagerDelegate の centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) の実装だけで十分です。

extension GattClient: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        DispatchQueue.main.async {
            switch central.state {
            case .poweredOn:
                self.bluetoothState = "Powered On"
                print("Bluetooth が有効になりました")
            case .poweredOff:
                self.bluetoothState = "Powered Off"
                self.stopScanning()
                self.disconnectFromPeripheral()
                print("Bluetooth が無効です")
            case .resetting:
                self.bluetoothState = "Resetting"
                print("Bluetooth リセット中")
            case .unauthorized:
                self.bluetoothState = "Unauthorized"
                print("Bluetooth 使用権限がありません")
            case .unsupported:
                self.bluetoothState = "Unsupported"
                print("Bluetooth がサポートされていません")
            case .unknown:
                self.bluetoothState = "Unknown"
                print("Bluetooth 状態不明")
            @unknown default:
                self.bluetoothState = "Unknown"
                break
            }
        }
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        
        let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
        
        let discoveredPeripheral = DiscoveredPeripheral(
            peripheral: peripheral,
            name: name,
            rssi: RSSI,
            advertisementData: advertisementData
        )
        
        DispatchQueue.main.async {
            self.peripherals.append(discoveredPeripheral)
            self.scanningStatus = "発見: \(self.peripherals.count)個"
            self.onDiscover?(discoveredPeripheral, Date())
        }
        print("ペリフェラル発見: \(discoveredPeripheral.displayName), RSSI: \(RSSI)")
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        DispatchQueue.main.async {
            self.connectedPeripheral = peripheral
            self.connectionStatus = "接続済み"
        }
        
        print("ペリフェラル接続成功: \(peripheral.name ?? "Unknown")")
        
        // MTU要求を実行
        requestMTU(requestedMTU)
        
        // サービス探索開始
        peripheral.discoverServices([targetServiceUUID])
    }
    
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        DispatchQueue.main.async {
            self.connectionStatus = "接続失敗"
        }
        
        print("ペリフェラル接続失敗: \(error?.localizedDescription ?? "Unknown error")")
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        DispatchQueue.main.async {
            self.connectedPeripheral = nil
            self.connectionStatus = "切断済み"
            self.targetService = nil
            self.targetCharacteristic = nil
        }
        
        print("ペリフェラル切断: \(peripheral.name ?? "Unknown")")
    }
}

Manufacturer Data 形式の受信

書き方としては Android のときと同じで CBCentralManagerDelegate だけを使います。 iBeacon のように CLLocationManager と CLBeaconRegion を使いません。

COCOA/EN API のように特定のデータを配信する形は、この Manufacturer Data 形式でやるのが一番いいのですが、後で記事にしますが iOS では Manufacturer Data での発信ができません。Manufacturer Data 形式で発信できるのは iBeacon 形式だけで、実際に発信しようとするとデータ部分がランダム値(?)になってしまうことになります。これは、実際に iOS 用の発信ツールを作ったときに確認します。

final class ManufacturerDataScan: NSObject, ObservableObject {
    /// 受信時のコールバック。keyはCompany ID(16bit)を0xXXXXで表記。
    var onManufacturerData: ((String, Data, NSNumber, CBPeripheral, Data) -> Void)?

    @Published var isScanning = false
    @Published var scanningStatus = "停止中"

    private var centralManager: CBCentralManager!

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }

    func startScan() {
        guard centralManager.state == .poweredOn else {
            print("ManufacturerDataScan: Bluetooth未準備 state=\(centralManager.state.rawValue)")
            return
        }
        guard !isScanning else { return }

        centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
        isScanning = true
        scanningStatus = "スキャン中..."
    }

    func stopScan() {
        guard isScanning else { return }
        centralManager.stopScan()
        isScanning = false
        scanningStatus = "停止中"
    }
}

extension ManufacturerDataScan: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            print("ManufacturerDataScan: Bluetooth On")
        case .unauthorized:
            print("ManufacturerDataScan: unauthorized")
        case .unsupported:
            print("ManufacturerDataScan: unsupported")
        case .poweredOff:
            print("ManufacturerDataScan: Bluetooth Off")
        default:
            break
        }
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        guard let data = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data, !data.isEmpty else { return }
        // Company IDは先頭2バイトLittle Endianで格納される
        let companyId = data.prefix(2).reduce(0) { acc, byte in (acc << 8) | Int(byte) }
        let key = String(format: "0x%04X", companyId)
        let beacon_type = data[2]
        let beacon_length = data[3]


        if ( companyId == 0xFFFF ) {
            if ( beacon_type == 0x02 && beacon_length == 0x10 ) {
                let tempid = data.dropFirst(4)
                onManufacturerData?(key, data, RSSI, peripheral, tempid)
            }
        }
    }
}   

実行した様子

参考先

https://github.com/FolkBearsGroup/ble-tools/tree/master/folkbears-monitor-ios

カテゴリー: 開発, FolkBears パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

*