本記事について
【M5Stack】段階的に動かして学ぶBluetooth通信
上記の記事のセントラルを iOS の CoreBluetooth で置き換えた。
その際に学べたことと、コードをまとめる。
環境
macOS Venture 13.3 (M2)
Xcode 14.3
iOS 16.5
M5Atom S3 (ペリフェラル)
前提
上記の記事で作った以下のリポジトリを使う。
ロジックを SwiftPackage に書く。
準備 Sample 用クラスを作って view から呼び出す
import Foundation
public class BLESample {
public init() {
}
public func helloWorld() {
print("Hello World!")
}
}
この BLESample クラスにどんどん機能を足していく。
プロパティー追加
iOS 13 以降では Core Bluetooth を使用するためにはNSBluetoothAlwaysUsageDescription
が必要
セントラルデバイスからペリフェラルデバイスを探す
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 でサクッと作られる。
セントラルからペリフェラルに接続する
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>
接続中かどうかを取得
Button(action: {
if bleSample.isConnected() {
print("connected")
} else {
bleSample.scan()
}
public func isConnected() -> Bool {
if peripheral == nil {
return false
}
return peripheral.state == .connected
}
一つのボタンでタイミングで別の処理を行うために、接続してるかどうかを取れるようにする
Read
コードの一部
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
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
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 と同様のメソっドで実行される