【SwiftUI】CoreBluetooth、BLE
今回はiPhone間でのBLE通信について解説していきます。
あまりiPhone間でBLE通信をする場面はありませんが、セントラル、ペリフェラルどちらも解説しているのでiPhone間だけでなく、他デバイスとのBLE通信の参考になればと思います。
軽く用語解説
BLEとは、
Bluetooth Low Energy
の略で、消費電力の低さ、障害物に強いのが特徴です。
セントラル(マスタ)
通信の制御を行う役割を持ちます。
BLEではセントラルがペリフェラルに要求を出すことでデータ通信が行われます。
ペリフェラル(スレーブ)
通信制御機能は持っていません。
セントラルからの要求に応えることで通信を行います。
ペリフェラルにはサービス、キャラリスティックが定義されおり、UUIDで管理されています。
サービス … 機能の単位を表します。
キャラリスティック … サービスの中に存在し、データが含まれています。
アドバタイズ
ペリフェラルがセントラルにブロードキャスト通信を使用して、「ここにいるよ」と伝えています。 接続待ちの仕組みをアドバタイズと呼びます。
ブロードキャスト通信とは、不特定多数にデータを送信する、一方通行通信のことです。
実装
ここからは実際に実装をしていきます。
今回は、Appleが用意してくれているサンプルをSwiftUIに書き換えていく形で解説していきます。
接続手順通りにプログラムを書いていく人もいれば、自分のようにセントラルとペリフェラルを分けて書いていく人もいるので、やりやすい方法をとってください。
その前に、BLEの接続手順について軽く解説します。
実装手順ではないです。
接続手順
- ペリフェラルからアドバタイズを送信。
- セントラルでアドバタイズを受け取る。
- セントラルが接続を要求する。
- 接続完了!
ここから実装手順です。
フレームワークの導入と使用許可
フレームワークを導入
import CoreBluetooth
使用許可
info.plistにKeyとValueを追加
// Key
Privacy - Bluetooth Always Usage Description
// Value
他デバイスとのメッセージでBluetoothを使用します。
Central
1. CBCentralManagerのインスタンスを生成
var centralManager: CBCentralManager!
centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
options の CBCentralManagerOptionShowPowerAlertKey
は、peripheralManager をインスタンス化する際に、 Bluetooth がオフ状態あるかどうかをシステムに警告させるかどうかを指定します。
デフォルトは false です。
nil でも動きますが、指定しておいて損はないと思います。
2. Delegateを追加
extension CentralViewModel: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
print(".powerOn")
return
case .poweredOff :
print(".powerOff")
return
case .resetting:
print(".restting")
return
case .unauthorized:
print(".unauthorized")
return
case .unknown:
print(".unknown")
return
case .unsupported:
print(".unsupported")
return
@unknown default:
print("A previously unknown central manager state occurred")
return
}
}
}
extension CentralViewModel: CBPeriPheralDelegate {
}
使用するクラスには CBCentralManagerDelegate
CBPeriPheralDelegate
が必要です。
また、CBCentralManagerDelegateには centralManagerDidUpdateState(_:)
を記述する必要があります。
centralManagerDidUpdateState(_:) はBluetoothの状態を示すメソッドです。
例として、Bluetoothが使える状態の時は、.poweredOn
を示します。
3. ペリフェラルを検索
let serviceUUID = CBUUID(string: "12345678-abcd-qwer-asdf-1234567890ab")
private func retrievePeripheral() {
let connectedPeripherals: [CBPeripheral] = (centralManager.retrieveConnectedPeripherals(withServices: [serviceUUID]))
os_log("Found connected Peripherals with transfer service: %@", connectedPeripherals)
if let connectedPeripheral = connectedPeripherals.last {
os_log("Connecting to peripheral %@", connectedPeripheral)
self.discoveredPeripheral = connectedPeripheral
centralManager.connect(connectedPeripheral, options: nil)
} else {
// ペリフェラルを検索
centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
}
}
func stopAction() {
centralManager.stopScan()
}
サンプルでは、centralManagerDidUpdateState(_:) が .poweredOn になったタイミング実行されています。
scanForPeripherals(withServices:options:)
を使用するとペリフェラルを検索でき、stopScan()
を使用するとペリフェラル検索を停止させることができます。
サンプルでは接続されているかどうか確認し、されていなければペリフェラルを検索しています。
scanForPeripherals の withServices にUUIDを指定してあげれば特定のペリフェラルに絞ることができ、nil を指定してしてあげればすべてのペリフェラルを検索することができます。
また、Appleのサンプルで options に書かれている、CBCentralManagerScanOptionAllowDuplicatesKey
は、スキャンを重複フィルタリングなしで実行させるかどうかを指定するものです。
true を指定するとフィルタリングを無効にし、ペリフェラルから受信するたびに、イベントを発生させます。 指定しない、か false を指定すると同じペリフェラルの複数の検出を1つのイベントに結合させます。
4. 検索したペリフェラルに接続
var discoveredPeripheral: CBPeripheral?
extension CentralViewModel: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// 略
}
+ func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
+ if discoveredPeripheral != peripheral {
+ discoveredPeripheral = peripheral
+ centralManager.connect(peripheral, options: nil)
+ }
+ }
}
一致する serviceUUID を持ったペリフェラルを検索すると、 centralManager(_:didDiscover:advertisementData:rssi:)
を呼び出します。
サンプルでは、discoveredPeripheral プロパティにペリフェラルを保存し、 connect(_:options:)
を呼び出してペリフェラルに接続します。
RSSI
パラメータを使用し、信号がデータ転送に十分な強度であるかを判断することもできます。
var discoveredPeripheral: CBPeripheral?
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
+ guard RSSI.intValue >= -50 else {
+ return
+ }
if discoveredPeripheral != peripheral {
discoveredPeripheral = peripheral
centralManager.connect(peripheral, options: nil)
}
}
5. サービスを検索
extension CentralViewModel: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// 略
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
// 略
}
+ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
+ // スキャン停止
+ centralManager.stopScan()
+
+ connectionIterationsComplete += 1
+ writeIterationsComplete = 0
+
+ // すでに情報を持っているかもしれないので削除
+ data.removeAll(keepingCapacity: false)
+
+ peripheral.delegate = self
+ peripheral.discoverServices([serviceUUID])
+ }
}
ペリフェラルに接続ができたら、 centralManager(_:didConnect:)
が呼ばれます。
ごちゃごちゃ書いてありますが、重要なのは最後の2行で、delegate を指定し、サービスを検索、取得しています。serviceUUID を nil にすることですべてのサービスを検索することもできます。
その前の数行は、スキャンを停止させ、すでに情報を持っていたら削除しています。
6. Characteristicを検索
let characteristicUUID = CBUUID(string: "09876543-asdf-qwer-zxcv-1234567890cd")
extension CentralViewModel: CBPeriPheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error {
print("Error discovering services: \(error.localizedDescription)")
cleanup()
return
}
guard let peripheralServices = peripheral.services else {
return
}
for service in peripheralServices {
peripheral.discoverCharacteristics([characteristicUUID], for: service)
}
}
}
サービスを取得すると、peripheral(_:didDiscoverServices:)
が呼ばれます。
サービスに紐づくCharacteristic を検索、取得します。
characteristicUUID を nil にすることですべてを検索対象にできます。
7. データを受け取る
@Published var message: String = ""
var data: Data = Data()
extension CentralViewModel: CBPeriPheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
// 略
}
+ func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
+ if let error = error {
+ print("Error discovering characteristics: \(error.localizedDescription)")
+ cleanup()
+ return
+ }
+
+ guard let characteristicData = characteristic.value,
+ let stringFromData = String(data: characteristicData, encoding: .utf8) else {
+ return
+ }
+
+ print("Received \(characteristicData.count) bytes: \(stringFromData)")
+
+ if stringFromData == "EOM" {
+ message = String(data: data, encoding: .utf8) ?? ""
+
+ } else {
+ data.append(characteristicData)
+ }
+ }
}
ペリフェラルからデータが届いたことを知らせるメソッドです。
ペリフェラルから “EOM” が送られてきたら、message プロパティに data プロパティをエンコードをして代入しています。 “EOM” が送られてくるまでは、data プロパティに送られてきたデータを入れておきます。
Centralの全体コード
Central
import Foundation
import CoreBluetooth
import os
class CentralViewModel: NSObject, ObservableObject {
@Published var message: String = ""
var centralManager: CBCentralManager!
var discoveredPeripheral: CBPeripheral?
var transferCharacteristic: CBCharacteristic?
var writeIterationsComplete = 0
var connectionIterationsComplete = 0
let defaultIterations = 5
var data: Data = Data()
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
}
func stopAction() {
centralManager.stopScan()
}
private func cleanup() {
guard let discoveredPeripheral = discoveredPeripheral,
case .connected = discoveredPeripheral.state else { return }
for service in (discoveredPeripheral.services ?? [] as [CBService]) {
for characteristic in (service.characteristics ?? [] as [CBCharacteristic]) {
if characteristic.uuid == TransferService.characteristicUUID && characteristic.isNotifying {
self.discoveredPeripheral?.setNotifyValue(false, for: characteristic)
}
}
}
centralManager.cancelPeripheralConnection(discoveredPeripheral)
}
private func writeData() {
guard let discoveredPeripheral = discoveredPeripheral, let transferCharacteristic = transferCharacteristic
else {
return
}
while writeIterationsComplete < defaultIterations && discoveredPeripheral.canSendWriteWithoutResponse {
writeIterationsComplete += 1
}
if writeIterationsComplete == defaultIterations {
discoveredPeripheral.setNotifyValue(false, for: transferCharacteristic)
}
}
private func retrievePeripheral() {
let connectedPeripherals: [CBPeripheral] = (centralManager.retrieveConnectedPeripherals(withServices: [TransferService.serviceUUID]))
os_log("Found connected Peripherals with transfer service: %@", connectedPeripherals)
if let connectedPeripheral = connectedPeripherals.last {
os_log("Connecting to peripheral %@", connectedPeripheral)
self.discoveredPeripheral = connectedPeripheral
centralManager.connect(connectedPeripheral, options: nil)
} else {
centralManager.scanForPeripherals(withServices: [TransferService.serviceUUID], options: nil)
}
}
}
extension CentralViewModel: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
print(".powerOn")
retrievePeripheral()
return
case .poweredOff :
print(".powerOff")
return
case .resetting:
print(".restting")
return
case .unauthorized:
print(".unauthorized")
return
case .unknown:
print(".unknown")
return
case .unsupported:
print(".unsupported")
return
@unknown default:
print("A previously unknown central manager state occurred")
return
}
}
// 検索したペリフェラルに接続
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
guard RSSI.intValue >= -50 else {
return
}
if discoveredPeripheral != peripheral {
discoveredPeripheral = peripheral
centralManager.connect(peripheral, options: nil)
}
}
// サービスを検索
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
centralManager.stopScan()
connectionIterationsComplete += 1
writeIterationsComplete = 0
data.removeAll(keepingCapacity: false)
peripheral.delegate = self
peripheral.discoverServices([TransferService.serviceUUID])
}
// ペリフェラルとの接続に失敗したとき
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
cleanup()
}
// ペリフェラルから切断されたとき
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
discoveredPeripheral = nil
if connectionIterationsComplete < defaultIterations {
retrievePeripheral()
} else {
print("Connection iterations completed")
}
}
}
extension CentralViewModel: CBPeripheralDelegate {
// Characteristicを検索
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if error != nil {
cleanup()
return
}
guard let peripheralServices = peripheral.services else {
return
}
for service in peripheralServices {
peripheral.discoverCharacteristics([TransferService.characteristicUUID], for: service)
}
}
// ペリフェラルがcharacteristicsを見つけたことを知らせる
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error {
print("Error discovering characteristics: \(error.localizedDescription)")
cleanup()
return
}
guard let serviceCharacteristics = service.characteristics else {
return
}
for characteristic in serviceCharacteristics where characteristic.uuid == TransferService.characteristicUUID {
transferCharacteristic = characteristic
peripheral.setNotifyValue(true, for: characteristic)
}
}
// ペリフェラルからデータが届いたことを知らせる
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print("Error discovering characteristics: \(error.localizedDescription)")
cleanup()
return
}
guard let characteristicData = characteristic.value,
let stringFromData = String(data: characteristicData, encoding: .utf8) else {
return
}
print("Received \(characteristicData.count) bytes: \(stringFromData)")
if stringFromData == "EOM" {
message = String(data: data, encoding: .utf8) ?? ""
writeData()
} else {
data.append(characteristicData)
}
}
// 指定されたcharacteristicの通知の要求をペリフェラルが受信したことを通知
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print("Error changing notification state: \(error.localizedDescription)")
return
}
guard characteristic.uuid == TransferService.characteristicUUID else {
return
}
if characteristic.isNotifying {
// 通知開始
print("Notification began on \(characteristic)")
} else {
// 通知が停止してるからペリフェラルとの接続を解除
print("Notification stopped on \(characteristic). Disconnecting")
cleanup()
}
}
// ペリフェラルがcharacteristicのアップデートを送信する準備が整ったことを通知
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
print("Peripheral is ready, send data")
writeData()
}
}
Peripheral
1. CBPeripheralManger のインスタンスを生成
var pripheralManager: CBPripheralManager!
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true])
optionsのCBPeripheralManagerOptionShowPowerAlertKey
はCBCentralManagerOptionShowPowerAlertKey と同様のシステムです。
2. Delegateの追加
extension PeripheralViewModel: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ central: CBPeripheralManager) {
switch peripheral.state {
case .poweredOn:
print(".powerOn")
return
case .poweredOff :
print(".powerOff")
return
case .resetting:
print(".restting")
return
case .unauthorized:
print(".unauthorized")
return
case .unknown:
print(".unknown")
return
case .unsupported:
print(".unsupported")
return
@unknown default:
print("A previously unknown peripheral manager state occurred")
return
}
}
}
クラスには、 CBPeripheralManagerDelegate
が必要です。
また、セントラルと同様に peripheralManagerDidUpdateState(_:)
を記述する必要があります。
3. サービスを追加
let characteristicUUID = CBUUID(string: "09876543-asdf-qwer-zxcv-1234567890cd")
private func setupPeripheral() {
let transferCharacteristic = CBMutableCharacteristic(type: characteristicUUID,
properties: [.notify, .writeWithoutResponse],
value: nil,
permissions: [.readable, .writeable])
// サービスを作成
let transferService = CBMutableService(type: TransferService.serviceUUID, primary: true)
// サービスにcharacteristicsを追加
transferService.characteristics = [transferCharacteristic]
// peripharalManagerに追加
peripheralManager.add(transferService)
self.transferCharacteristic = transferCharacteristic
}
peripheralManagerDidUpdateState(_:) が .poweredOn を示すとサンプルでは、setupPeripheral() を呼び出して transferCharacteristic という CBMutableCharacteristic を生成しています。
その characteristic から CBMutableServiceを作成し、そのサービスをperipheralManagerに追加しています。
4. アドバタイズを開始
let serviceUUID = CBUUID(string: "12345678-abcd-qwer-asdf-1234567890ab")
func switchChanged() {
if toggleFrag {
peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: serviceUUID]])
} else {
stopAction()
}
}
func stopAction() {
peripheralManager.stopAdvertising()
}
サービスの追加が完了したら、アドバタイズを開始します。
アドバタイズの開始には、startAdvertising(_ advertisementData: [String : Any]?)
を使用します。
引数には、CBAdvertisementDataServiceUUIDsKey
を指定します。 このキーはサービスUUIDを配列で指定します。
stopAdvertising()
を使用すれば、アドバタイズを停止させることもできます。
Peripheralの全体コード
Peripheral
import Foundation
import CoreBluetooth
import os
class PeripheralViewModel: NSObject, ObservableObject {
@Published var message: String = ""
@Published var toggleFrag: Bool = false
var peripheralManager: CBPeripheralManager!
var transferCharacteristic: CBMutableCharacteristic?
var connectedCentral: CBCentral?
var dataToSend = Data()
var sendDataIndex: Int = 0
override init() {
super.init()
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true])
}
func switchChanged() {
if toggleFrag {
peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [TransferService.serviceUUID]])
} else {
stopAction()
}
}
func stopAction() {
peripheralManager.stopAdvertising()
}
private func setUpPeripheral() {
let transferCharacteristic = CBMutableCharacteristic(type: TransferService.characteristicUUID,
properties: [.notify, .writeWithoutResponse],
value: nil,
permissions: [.readable, .writeable])
// サービスの作成
let transferService = CBMutableService(type: TransferService.serviceUUID, primary: true)
// サービスにcharacteristicsを追加
transferService.characteristics = [transferCharacteristic]
// periphralManagerに追加
peripheralManager.add(transferService)
self.transferCharacteristic = transferCharacteristic
}
static var sendingEOM = false
private func sendData() {
guard let transferCharacteristic = transferCharacteristic else {
return
}
// EOMを送信する必要があるかどうかを確認
if PeripheralViewModel.sendingEOM {
let didSend = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: transferCharacteristic, onSubscribedCentrals: nil)
if didSend {
PeripheralViewModel.sendingEOM = false
print("Sent: EOM")
}
return
}
if sendDataIndex >= dataToSend.count {
return
}
var didSend = true
while didSend {
var amountToSend = dataToSend.count - sendDataIndex
if let mtu = connectedCentral?.maximumUpdateValueLength {
amountToSend = min(amountToSend, mtu)
}
let chunk = dataToSend.subdata(in: sendDataIndex..<(sendDataIndex + amountToSend))
didSend = peripheralManager.updateValue(chunk, for: transferCharacteristic, onSubscribedCentrals: nil)
if !didSend {
return
}
let stringFromData = String(data: chunk, encoding: .utf8)
print("Sent \(chunk.count) bytes: \(String(describing: stringFromData))")
sendDataIndex += amountToSend
if sendDataIndex >= dataToSend.count {
// 送信に失敗した場合、次回送信できるように設定
PeripheralViewModel.sendingEOM = true
// 送信
let eomSent = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: transferCharacteristic, onSubscribedCentrals: nil)
if eomSent {
PeripheralViewModel.sendingEOM = false
print("Sent: EOM")
}
return
}
}
}
}
extension PeripheralViewModel: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .poweredOn:
print(".powerOn")
setUpPeripheral()
return
case .poweredOff :
print(".powerOff")
return
case .resetting:
print(".restting")
return
case .unauthorized:
print(".unauthorized")
return
case .unknown:
print(".unknown")
return
case .unsupported:
print(".unsupported")
return
default:
print("A previously unknown central manager state occurred")
return
}
}
// characteristicが読み込まれたときにキャッチし、データ送信を開始
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
print("Central subscribed to characteristic")
if let message = message.data(using: .utf8) {
dataToSend = message
}
sendDataIndex = 0
connectedCentral = central
// 送信開始
sendData()
}
// セントラルが停止したときに呼び出される
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
print("Central unsubscribed from characteristic")
connectedCentral = nil
}
// peripheralManagerが次のデータを送信する準備ができたときに呼び出される
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
sendData()
}
// peripheralManagerがcharacteristicsへの書き込みを受信したときに呼び出される
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
for aRequest in requests {
guard let requestValue = aRequest.value,
let stringFromData = String(data: requestValue, encoding: .utf8) else {
continue
}
print("Received write request of \(requestValue.count) bytes: \(stringFromData)")
message = stringFromData
}
}
}
View
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
NavigationLink(destination: CentralView()) {
Text("Central")
}
.buttonStyle(.borderedProminent)
.padding()
NavigationLink(destination: PeripheralView()) {
Text("Peripharal")
}
.buttonStyle(.borderedProminent)
.padding()
}
}
}
}
struct CentralView: View {
@StateObject var central: CentralViewModel = CentralViewModel()
var body: some View {
Text(central.message)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(20)
.onDisappear {
central.stopAction()
}
}
}
struct PeripheralView: View {
@StateObject var peripheral: PeripheralViewModel = PeripheralViewModel()
var body: some View {
VStack {
TextEditor(text: $peripheral.message)
.padding(20)
Toggle("Advertising", isOn: $peripheral.toggleFrag)
.padding(20)
.onChange(of: peripheral.toggleFrag) { _ in
peripheral.switchChanged()
}
}
.onDisappear {
peripheral.stopAction()
}
}
}
Storyboard で作られていたものを SwiftUI に直しただけです。
少し私好みのViewになっていますが、大体同じViewになっていると思います。
.onDisappear
はViewが消えるタイミングで実行されるコールバックメソッドです。
CentralView では stopScan() を実行し、PeripheralView では stopAdvertising() を実行しています。
最後に
今回は、Appleが用意してくれているサンプルをSwiftUIに書き換える形で解説させていただきました。
GitHubでSwiftUIに書き換えたサンプルを公開しています。
Appleのサンプルとともに参考にしてください。AppleのサンプルはUIKitで作られていますが、BLEの基本的な使い方は変わらないので、参考になると思います。
Discussion