1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

iOSのBluetooth通信サンプル(セントラル)

Posted at

本記事について

【M5Stack】段階的に動かして学ぶBluetooth通信

上記の記事のセントラルを iOS の CoreBluetooth で置き換えた。
その際に学べたことと、コードをまとめる。

環境

macOS Venture 13.3 (M2)
Xcode 14.3
iOS 16.5
M5Atom S3 (ペリフェラル)

前提

上記の記事で作った以下のリポジトリを使う。

ロジックを SwiftPackage に書く。

準備 Sample 用クラスを作って view から呼び出す

commit

BLESample.swift
import Foundation

public class BLESample {
    public init() {
    }

    public func helloWorld() {
        print("Hello World!")
    }
}

この BLESample クラスにどんどん機能を足していく。

プロパティー追加

iOS 13 以降では Core Bluetooth を使用するためにはNSBluetoothAlwaysUsageDescriptionが必要

ドキュメント

スクリーンショット 2023-07-22 16.07.57.png

セントラルデバイスからペリフェラルデバイスを探す

ペリフェラルのアドバタイズの実装

BLESample.swift
import Foundation
import CoreBluetooth

public class BLESample: NSObject, CBCentralManagerDelegate {
    var centralManager: CBCentralManager!
    var serviceUUID: CBUUID!
    
    public override init() {
        super.init()
        // queueはnilの場合はメインスレッドで実行される
        // https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1519001-init
        centralManager = CBCentralManager(delegate: self, queue: nil)
        serviceUUID = CBUUID(string: "068c47b7-fc04-4d47-975a-7952be1a576f")
    }
    
    public func scan() {
        if centralManager.state != .poweredOn {
            print("Bluetooth is not powered on.")
            return
        }
        
        if centralManager.isScanning {
            print("Already scanning.")
            return
        }
        
        let options: [String: Any] = [
            CBCentralManagerScanOptionAllowDuplicatesKey: false
        ]
        
        // serviceUUIDを指定すると、指定したサービスを持つペリフェラルのみをスキャンする。(推奨設定)
        // https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518986-scanforperipherals
        centralManager.scanForPeripherals(withServices: [serviceUUID], options: options)
    }
    
    func cbManagerStateName(_ state: CBManagerState) -> String {
        switch state {
        case .unknown:
            return "unknown"
        case .resetting:
            return "resetting"
        case .unsupported:
            return "unsupported"
        case .unauthorized:
            return "unauthorized"
        case .poweredOff:
            return "poweredOff"
        case .poweredOn:
            return "poweredOn"
        @unknown default:
            return "unknown default"
        }
    }
    
    // MARK: - CBCentralManagerDelegate
    
    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print("centralManagerDidUpdateState: \(cbManagerStateName(central.state))")
    }
    
    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        print("Device found: \(peripheral)")
        central.stopScan()
    }
}

シミュレーターでは Bluetooth の機能が使用できない(unsupported)ため、動作確認は iOS 実機で行う。
実機の Bluetooth は ON にしておくこと。

まず、アプリを起動した際に以下のログが出る

centralManagerDidUpdateState: poweredOn

ペリフェラルを起動。
ボタンを押して以下のようにスキャンされたデータがログに出力される

Device found: <CBPeripheral: 0x283510000, identifier = 36958114-4913-C3C3-C16F-1CF3831B8211, name = M5AtomS3 BLE Server, mtu = 0, state = disconnected>

ポイント

NSObject を継承している理由

ないとエラーでるから。

options について

同じデバイスを検知したくはないため明示的に設定、多分デフォルト挙動と同じだが、何がデフォルトなのかを調べる手間をなくすために明示。

アクティブスキャンをしないようにする設定は?

ドキュメントを読んでも見つからなかった。

delegate (イベントハンドラ) を centralManagerのインスタンスを持っているクラスと同じにしている理由

1つのクラスで書いた方がシンプルだから。特にこだわりはない。

cbManagerStateName メソッドの意味

これがないと state が数字で表示されてなんだかわからないから。
Copilot でサクッと作られる。

セントラルからペリフェラルに接続する

diff

BLESample.swift
public class BLESample: NSObject, CBCentralManagerDelegate {
    var centralManager: CBCentralManager!
    var serviceUUID: CBUUID!
    var peripheral: CBPeripheral!

...

    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        print("Device found: \(peripheral)")
        self.peripheral = peripheral
        central.stopScan()
        // optionsは特に指定しないといけなそうなものがなかったため、nilを指定している
        // https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518766-connect
        central.connect(peripheral)
    }
    
    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("centralManager:didConnect: \(peripheral)")
    }
    
    public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print("centralManager:didFailToConnect: \(peripheral)")
    }
    
    public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("centralManager:didDisconnectPeripheral: \(peripheral)")
    }
...
Device found: <CBPeripheral: 0x2827f15f0, identifier = 36958114-4913-C3C3-C16F-1CF3831B8211, name = M5AtomS3 BLE Server, mtu = 0, state = disconnected>
centralManager:didConnect: <CBPeripheral: 0x2827f15f0, identifier = 36958114-4913-C3C3-C16F-1CF3831B8211, name = M5AtomS3 BLE Server, mtu = 23, state = connected>

接続中かどうかを取得

ContentView.swift
            Button(action: {
                if bleSample.isConnected() {
                    print("connected")
                } else {
                    bleSample.scan()
                }
BLESample.swift
    public func isConnected() -> Bool {
        if peripheral == nil {
            return false
        }
        return peripheral.state == .connected
    }

一つのボタンでタイミングで別の処理を行うために、接続してるかどうかを取れるようにする

diff

Read

diff

コードの一部

BLESample.swift
    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("centralManager:didConnect: \(peripheral)")
        
        peripheral.discoverServices([serviceUUID])
    }
    
...
    // MARK: - CBPeripheralDelegate
    
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        let services = peripheral.services ?? []
        print("peripheral:didDiscoverServices: \(services)")
        if let error = error {
            print("error: \(error)")
            return
        }
        
        for service in services {
            peripheral.discoverCharacteristics([characteristicUUID], for: service)
        }
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        let characteristics = service.characteristics ?? []
        print("peripheral:didDiscoverCharacteristicsFor: \(characteristics)")
        if let error = error {
            print("error: \(error)")
            return
        }
        
        for characteristic in characteristics {
            if characteristic.properties.contains(.read) {
                peripheral.readValue(for: characteristic)
            }
        }
    }
    
    // セントラルのread, ペリフェラルのnotifyによりvalueが更新されたときに呼ばれる
    public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        print("peripheral:didUpdateValueFor: \(characteristic)")
        if let error = error {
            print("error: \(error)")
            return
        }
        
        if let data = characteristic.value {
            print("data: \(data)")
            print("string: \(String(data: data, encoding: .utf8) ?? "")")
        }
    }

手続き的じゃなく、追いづらいがやってることは以下

ペリフェラルに接続
-> サービスを取得
-> キャラクタリスティックを取得
-> キャラクタリスティックの値を読む
-> 読めたらprint
peripheral:didDiscoverServices: [<CBService: 0x283a694c0, isPrimary = YES, UUID = 068C47B7-FC04-4D47-975A-7952BE1A576F>]
peripheral:didDiscoverCharacteristicsFor: [<CBCharacteristic: 0x280b300c0, UUID = E3737B3F-A08D-405B-B32D-35A8F6C64C5D, properties = 0xA, value = (null), notifying = NO>]
peripheral:didUpdateValueFor: <CBCharacteristic: 0x280b300c0, UUID = E3737B3F-A08D-405B-B32D-35A8F6C64C5D, properties = 0xA, value = {length = 11, bytes = 0x48656c6c6f20576f726c64}, notifying = NO>
data: 11 bytes
string: Hello World

Write

diff

BLESample.swift
    public func writeData() {
        if peripheral == nil {
            print("peripheral is nil")
            return
        }

        guard let characteristic = remoteCharacteristic else {
            print("characteristic is nil")
            return
        }

        // ランダムな三桁の数値の文字列を書き込む
        let value = "Write Data " + String(format: "%03d", Int.random(in: 100..<1000))
        // type はwithResponseである必要はないが、取れるようにしておく
        // https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518747-writevalue
        peripheral.writeValue(value.data(using: .utf8)!, for: characteristic, type: .withResponse)
    }

...

    // writeValue を .withResponse で実行した場合に呼ばれる
    public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        print("peripheral:didWriteValueFor: \(characteristic)")
        if let error = error {
            print("error: \(error)")
            return
        }
    }

これでセントラルからデータを送信できる

応答がない類のデータに関しては .withResponse ではなく、 .withoutResponse にする

Notify

diff

BLESample.swift
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
...
        self.remoteCharacteristic = characteristics.first { $0.uuid == characteristicUUID }
        self.notifyRemoteCharacteristic = characteristics.first { $0.uuid == notifyCharacteristicUUID }
        if let notifyRemoteCharacteristic = notifyRemoteCharacteristic {
            // https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518949-setnotifyvalue
            peripheral.setNotifyValue(true, for: notifyRemoteCharacteristic)
        }

...

    public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        print("peripheral:didUpdateNotificationStateFor: \(characteristic)")
        if let error = error {
            print("error: \(error)")
            return
        }
    }

peripheral:didUpdateValueFor: <CBCharacteristic: 0x283610360, UUID = C9DA2CE8-D119-40D5-90F7-EF24627E8193, properties = 0x10, value = {length = 15, bytes = 0x4e6f74696679204461746120373131}, notifying = YES>
data: 15 bytes
string: Notify Data 711

notify されたデータの print は Read と同様のメソっドで実行される

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?