【Swift UI】Bluejay(Bluetoothライブラリ)の使い方!

この記事からわかること

  • Swift UIBluejayを使用してBluetooth接続アプリ実装方法
  • セントラルの実装
  • インストールスキャン接続方法
  • Core Bluetooth移行するには?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Bluejayとは?

公式リファレンス:Bluejay

BluejayとはBluetooth(BLE)でアプリと機器を接続するための機能を提供しているiOSアプリライブラリです。iOSアプリでのBLE接続は公式のフレームワークである「Core Bluetooth」が用意されていますが、これをラップして使用しやすいようにしているのがBluejayになります。

BluejayはBLEの複数台接続には対応していないため、同時に周辺機器と接続したい際にはこのライブラリは使用できないので注意してください。

インストール

BluejayライブラリをCarthageやCocoa Podsでのインストールをサポートしています。

Carthage

github "steamclock/bluejay" ~> 0.8
github "DaveWoodCom/XCGLogger" ~> 6.1.0

Cocoa Pods


pod 'Bluejay', '~> 0.8'

Cocoa Podsを使用してXcode15でインストール後、ビルドすると以下のようなエラーが出ました。

セントラル機能の実装と使い方

BLE機能を試したい場合はiPhoneを2台用意してペリフェラル側を実装する必要があるのでCore Bluetoothを使用してペリフェラル側を実装した以下の記事を参考にしてください。

上記の記事で実装したペリフェラル機器があるとしてセントラル側の実装をBluejayを使用して実装していきます。また今回の実装はGitHubに上げてますので参考にしてください。

Privacy - Bluetooth Always Usage Descriptionキーの追加

iOSアプリからBluetooth機能を使用するためには「info.plist」に以下のキーを追加する必要があります。

Privacy - Bluetooth Always Usage Description

忘れずに追加してvalueには「アプリ内でBluetooth機能を使用するため」など、Bluetooth機能を使用する理由を記述しておきます。

使用するサービス/キャラクタリスティックの準備

サービスとキャラクタリスティックの定義にはServiceIdentifierCharacteristicIdentifierを使用します。引数にはUUIDを渡し、サービスとキャラクタリスティックの紐付けは引数にサービスを渡すだけになっています。

let service = ServiceIdentifier(uuid: "00000000-0000-1111-1111-111111111111")
let readCharacteristic = CharacteristicIdentifier(uuid: "00000000-1111-1111-1111-111111111111", service: service)
let writeCharacteristic = CharacteristicIdentifier(uuid: "00000000-2222-1111-1111-111111111111", service: service)

以下記事でUUIDについても解説しています。

初期設定

Bluejayを使用するためにはデリゲートのセットとstartメソッドを実行します。これはイニシャライザの中で実行しておきます。


class BluejayManager: ObservableObject {

    /// シングルトン
    static var shared = BluejayManager()

    /// ログ出力用
    @Published  var log = ""
    
    /// bluejayインスタンス
    public var bluejay = Bluejay()
    
    /// スキャン時に発見したペリフェラル一覧
    private var discoveries: [ScanDiscovery] = []
    
    init() {
        config()
    }

    /// 初期設定
    private func config() {
        self.bluejay.register(logObserver: self)
        bluejay.registerDisconnectHandler(handler: self)
        bluejay.register(connectionObserver: self)
        bluejay.register(serviceObserver: self)
        self.bluejay.start()
    }
}

必要なデリゲートプロトコルへ準拠させておきます。


extension BluejayManager: LogObserver, ConnectionObserver, ServiceObserver, DisconnectHandler {
    
    func didDisconnect(from peripheral: PeripheralIdentifier, with error: Error?, willReconnect autoReconnect: Bool) -> AutoReconnectMode {
        AutoReconnectMode.noChange
    }
    
    func didModifyServices(from peripheral: PeripheralIdentifier, invalidatedServices: [ServiceIdentifier]) { }
    
    func bluetoothAvailable(_ available: Bool) { }
    
    func connected(to peripheral: PeripheralIdentifier) { }

    func disconnected(from peripheral: PeripheralIdentifier) { }
    
    func debug(_ text: String) { }
}

スキャン処理

ペリフェラルをスキャンするにはscanメソッドを渡します。引数には検索したいサービスを配列形式で渡します。ペリフェラルが見つかるとdiscoveryクロージャーの中から配列形式でScanDiscovery型で取得できます。ScanDiscovery型はプロパティにペリフェラルのIDや名称を保持しています。discoveryクロージャーの返り値はScanAction型でスキャン後の挙動をコントロールすることができます。

bluejay.scan(
    serviceIdentifiers: [service], // 検索したいサービス一覧
    discovery: { (discovery, discoveries) -> ScanAction in
        // ScanDiscovery型でペリフェラル情報を取得
        return .continue
    },
    stopped: { (discoveries, error) in
        // エラー発生時
    }
)

ScanActionスキャンの続行、停止、ブラックリスト、コネクトの4つの挙動を決めることが可能です。

public enum ScanAction {
    case `continue`
    case blacklist
    case stop
    case connect(ScanDiscovery, (ConnectionResult) -> Void)
}

例えばスキャンして最初に見つかったペリフェラルにそのまま接続したい場合は以下のようにconnectを指定します。


public func startScan() {
    log.append("スキャン開始\n")
    
    bluejay.scan(
        serviceIdentifiers: [service],
        discovery: { [weak self] (discovery, discoveries) -> ScanAction in
            guard let self = self else { return .stop }
            
            if discoveries.count != 0 {
                self.log.append("発見: \(discoveries.count)個\n")
                self.discoveries = discoveries
                return .connect(discoveries.first!, .none, .default) { result in
                    switch result {
                    case .success:
                        self.log.append("コネクト成功: \n")
                    case .failure(let error):
                        self.log.append("コネクト失敗エラー: \(error.localizedDescription)\n")
                    }
                }
            } else {
                self.log.append("見つかりませんでした\n")
                return .continue
            }
            
        },
        stopped: { [weak self]  (discoveries, error) in
            guard let self = self else { return }
            if let error = error {
                self.log.append("スキャン停止エラー: \(error.localizedDescription)\n")
            } else {
                self.log.append("スキャン停止\n")
            }
        })
}

接続処理

スキャンの流れでコネクトしない場合はスキャンで検出したペリフェラルを保持しておきconnectメソッドを使用して接続処理を後から実行することも可能です。引数には接続したいペリフェラルのIDを渡します。


/// コネクト
public func connect() {
    log.append("コネクト\n")
    guard let peripheral = discoveries.first else {
        log.append("サービスなし\n")
        return
    }
    bluejay.connect(peripheral.peripheralIdentifier, timeout: .seconds(15)) { result in
        switch result {
        case .success:
            self.log.append("コネクト成功: \(peripheral.peripheralIdentifier)\n")
        case .failure(let error):
            self.log.append("コネクト失敗エラー: \(error.localizedDescription)\n")
        }
    }
}

Read処理

Readキャラクタリスティックを使用するにはreadメソッドを使用します。


/// Read処理
public func read() {
    bluejay.read(from: readCharacteristic) { [weak self] (result: ReadResult<UInt8>) in
        guard let self = self else { return }
        switch result {
        case .success(let location):
            self.log.append("Read成功: \(location)\n")
        case .failure(let error):
            self.log.append("Read失敗: \(error.localizedDescription)\n")
        }
    }
}

Write処理

Writeキャラクタリスティックを使用するにはwriteメソッドを使用します。


/// Write処理
public func registerData() {
    bluejay.write(to: writeCharacteristic, value: "Hello") { [weak self] (result: WriteResult) in
        guard let self = self else { return }
        switch result {
        case .success:
            self.log.append("Write成功: \n")
        case .failure(let error):
            self.log.append("Write失敗: \(error.localizedDescription)\n")
        }
    }
}

切断

接続中のペリフェラルと切断するためにはdisconnectメソッドを使用します。bluejay.isConnectedプロパティから接続中であるかどうかを識別することが可能です。immediatetrueを渡すと溜まっているキューを無視して即時切断することが可能です。


 /// 切断
public func disconnect() {
    if bluejay.isConnected {
        log.append("切断\n")
        /// 切断(キューの終了を待って)
        bluejay.disconnect()
        /// 即時切断(キューの終了を待たずに)
        // bluejay.disconnect(immediate: true)
    }
}

またcancelEverythingメソッドでは接続は維持しつつも行われている操作をリセットすることが可能です。


/// リセット
public func cancelEverything() {
    log.append("リセット\n")
    /// キューをリセット
    bluejay.cancelEverything()
}

Core Bluetoothへ移行する

BluejayではCore BluetoothをラップしているのでCBPeripheralCBCentralManagerは外部に公開していません。利用したい場合はstopAndExtractBluetoothStateメソッドを使用することでタプルでそれぞれを取得することが可能です。stopAndExtractBluetoothStateメソッドはすべての操作を停止し、Bluejayのすべての状態をクリアにしてCore Bluetoothを返します

これによりCore Bluetoothまたは別のBLEライブラリへ移行することが可能です。


/// 次節の「Core Bluetoothから移行する」のために保持
private var cbPeripheral: CBPeripheral?
private var cbCentralManager: CBCentralManager!

/// CoreBluetoothへの移行
public func stopAndExtractBluetoothState() {
    log.append("移行\n")
    // 定義:public func stopAndExtractBluetoothState() -> (manager: CBCentralManager, peripheral: CBPeripheral?) {}
    let status = bluejay.stopAndExtractBluetoothState()
    cbCentralManager = status.manager
    if let peripheral = status.peripheral {
        cbPeripheral = peripheral
    }
}

Core Bluetoothから移行する

逆にCore BluetoothからBluejayへ移行したい場合はstartメソッドのmodeの引数にuse(manager:, peripheral:)を使用します。



/// 移行後に再スタート
public func reStart() {
    guard let cbPeripheral = self.cbPeripheral else {
        return
    }
    log.append("再スタート\n")
    bluejay.start(mode: .use(manager: cbCentralManager, peripheral: cbPeripheral))
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index