Do.

Swift- CoreBluetooth 찍먹 본문

iOS

Swift- CoreBluetooth 찍먹

Hey_Hen 2022. 3. 1. 21:40

Bluetooth

iOS에서는 BLE(Bluetooth Low Energy) 무선 기술을 CoreBluetooth 프레임워크를 통해 연결 가능하다.

Central: Bluetooth 장치에서 데이터를 수신하는 개체

Peripheral: 다른 장치에서 사용할 데이터를 게시할 Bluetooth 장치

Advertising Packets: 블루투스는 Advertising Packets 형태로 가지고 있는 데이터 중 일부를 브로드캐스트 한다.

패킷에는 주변 장치의 이름, 기능 등과 같은 정보가 포함될 수 있다.

Central의 역할은 이러한 AdvertisingPackets 을 스캔하고 관련이 있는 주변 장치를 식별하고 개별 장치에 연결하고 추가 정보를 확인한다.

Service

주변 장치의 특정 기능 또는 특징을 설명하는 데이터 및 관련 동작의 모음, 예를들어 심박 센서에는 심박수 서비스가 있다.

Characteristics

자세한 정보를 제공한다. 예를 들어, 심박수 서비스에는 분방 비트 수 데이터가 포함된 심박수 측정 특성이 있다. 심박수 서비스가 가질 수 있는 또 다른 특징은 바디 센서 로케이션으로, 의도된 신체 위치를 설명하는 문자열이 포함된다.

각 서비스 및 특성은, 16bit or 128bit이 될 수 있는 UUID로 표시된다.

CBCentralManager

CBCentralManager는 코어 블루투스를 관리하는 모델로, 델리게이트를 통해서 이용할 수 있다.

뷰 컨트롤러 등에서 다음과 같이 프로퍼티와, 초기화를 실시한다.

var centralManager: CBCentralManager!
centralManager = CBCentralManager(delegate: self, queue: nil)
extension ViewController: CBCentralManagerDelegate {
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {

      case .unknown:
        print("central.state is .unknown")
      case .resetting:
        print("central.state is .resetting")
      case .unsupported:
        print("central.state is .unsupprted")
      case .unauthorized:
        print("central.state is .unauthorized")
      case .poweredOff:
        print("central.state is .poweredOff")
      case .poweredOn:
        print("central.state is .poerwedOn")
      @unknown default:
        print("central.state is .unknown default")
    }
  }
}

CBCentralManagerDelegate를 채용하게 되면, centralManagerDidUpdateState(_:) 를 필수적으로 구현하게 되는데, 말그대로 central의 상태로, 이상태에서 빌드를 하게 되면, 디바이스의 블루투스가 켜져있다면 poweredOn상태 블록을 실행하게 된다.

블루투스를 이용하기 위해서, 다음과 같은 동의를 얻어야 함 Privacy - Bluetooth Always Usage Description

Scanning for Peripherals

그 다음은 주변 기기를 검색할 차례다

주변 기기 검색은 CBCentralManager의 **scanForPeripherals(withServices:options:) 메소드를 통해 가능하다.

withServices 인수는 CBUUID를 통해, 앱에서 관심있는 블루투스 기기의 advertise만 확인하는데, 추천하는 값은 기본적으로 nil이다. 예를들면, 싸이클 관련 어플리케이션인 Strava를 보면, 싸이클 컴퓨터를 블루투스로 연결하는데, 관련기기가 아니면 서치 자체가 되지 안흔다. 아마 이러한 부분을 이용한 것으로 생각된다

options인수는 말그대로 검색을 커스터마이징 하는 방법으로, 이건 우선은 패스

그러면 해당 메서드를 호출해보자.

case .poweredOn:
  print("central.state is .poerwedOn")
  centralManager.scanForPeripherals(withServices: nil)

어떻게 보면 당연한 이야기 인데, 해당 메서드는 poweredOn상태에서만 호출할 수 있다. 그 외에 호출하게 되면 API MISUSE 메시지가 날아온다.

scanForperipherals는 검색을 시작하라는 명령어인데 그럼 검색된, 블루투스 기기는 어떻게 확인 할 수 있을까

CBCentralManagerDelegate 에서 didDiscover로 제공되는 메서드가 있다.

print(peripheral)

우선 프린트

빌드해보면 맥북 블루투스도 잡히고, 에어팟도 잡히고, 우리집에는 없는 기기도 잡힌다.

캡처 화면에는 일부만 보이지만, 실제로 해보면 꽤 많은 검색 결과가 나오기 때문에, 여기서 필요한 서비스만 걸러 내보고자 한다. 앞서 얘기했던 CBUUID를 이용해 볼 것이다. 에어팟과 같이 오디오 재생 장치만 걸러볼려고 한다!

블루투스 시방 확인 https://www.bluetooth.com/specifications/assigned-numbers/

시방서에 들어가면 16-bit UUID pdf를 받을 수 있는데, 여기서 AllocationType을 찾아보면 GATT Service가 있다.

잘 찾아보니 이거는 자전거에 들어가는 스피드, 케이던스 센서

인데, 내가 찾고싶은언 에어팟과 같이 오디오 관련도니 서비스 인데, 못찼겠다. 다음에 찾아보겠ㅇㅁ

서비스를 필터링 하는건 못했으니, 그냥 이름으로 기기를 잡아 보려고 한다.

var airpods: CBPeripheral!
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    if peripheral.name ?? "" == "둥둥팟" {
      print(peripheral)
      airpods = peripheral
      centralManager.stopScan()
    }
  }

에어팟의 이름이 “둥둥팟"이기 때문에, peripheral.name이 “둥둥팟”과 같을때 CBPeripheral 인스턴스로 전달해준뒤 centramManager를 stopScan하면 더이상 스캔을 계속하지 않는다.

Connecting to a Peripheral

방금 찾아서 인스턴스로 만들었기 때문에 이를 연결하는 방법은 centralManager에 **connect(_:options:) 메서드를 호출하고, 첫번째 인자에 Perpiheral 인스턴스를 전달하면 된다.

이를 스캔을 정지하기 전에 호출해주자!

블루투스 기기가 연결 되었다는 것을 어디서 알 수 있냐하면

같은 델리게이트 메서드에서 확인 가능하다., 만약 성공적으로 연결 되었다면, 이 부분이 호출 된다.

주변 기기를 연결 한 다음에는, 주변기기가 제공하는 서비스를 다시 검색해야 한다.

해당 내용은 연결된 기기의 Delegate에서 확인 가능하다. 따라서 우선 기기를 연결해준 다음

airpods.delegate = self

델리게이트 self

확인된 서비스 목록을 보아야 하는데, 이는 CBPeripheral 메서드로 호출 가능하다.

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    airpods.discoverServices(nil)
    print("Connect to: \\(peripheral)")
  }

해당 코드를 didConnect에서 호출 하면

extension ViewController: CBPeripheralDelegate {
  func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    guard let services = peripheral.services else {
      print("didDiscoverServices Error \\(error?.localizedDescription)")
      return
    }

    for service in services {
      print(service)
    }
  }
}

peripheral에서 service를 배열 형태로 받으 ㄹ수 있다.

그렇게 해서 받은 서비스는 위 사진처럼 나타내는데, 아직은 이게 뭘 의미하는지 모르겠다. 콩나물이 2개라 2개가 출력되나?

아니면 오디오 입/출력 2개로 나뉘어서 2개 서비스인지 모르겠다

예제를 보면, UUID 부분이 정의가 되어있으면 우리가 아는 영단어로 떨어지는데, 에어팟이라 그런가..저게 128Bit UUID인가 싶다.

예를들어, 아까 GAPP Service에서 확인한 것 처럼, 심박이면 “UUID = Heart Rate”, “UUID = Cycle Cencer” 이런식으로 나온다는 것

Discovering a Service’s Characteristics

맨 처음 설명했든 주변기기에는, Characteristics가 있는데, 주변기기가 가지는 특성을 전달한다. 케이던스 센서면, 분당 회전수를 특성으로 전달해줄 것이다. 에어팟은, 사운드 기기니까, 오디오 정보, 볼륨이나, 주파수를 전달하지 않을까?

for service in services {
      print(service)
      print("Characteristic: \\(service.characteristics)")
}
<CBService: 0x283400a00, isPrimary = YES, UUID = 9BD708D7-64C7-4E9F-9DED-F6B6C4551967>
Characteristic: nil
<CBService: 0x283400cc0, isPrimary = YES, UUID = 7798082B-B7B7-45A6-9933-563492EFE04E>
Characteristic: nil

놀랍게도 아무것도 엄슴.이 아니라 방법이 잘못 되었는데, Characteristic을 찾는 방법은

didDiscoverCharacteristicsFor 를 통해서 찾아야 한다.

service에서는

print(peripheral.discoverCharacteristics(nil, for: service))

위 코드를 호출 해 주고

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    guard let characteristics = service.characteristics else {
      print("didfail didscover characteristics with error: \\(error)")
      return
    }

    for characteristic in characteristics {
      print(characteristic)
    }
  }

델리게이트 메서드를 호출해서, 특성을 확인한다

<CBCharacteristic: 0x2826204e0, UUID = F195B8FB-A9E2-4401-858B-2F87A06928A8, properties = 0x2, value = {length = 8, bytes = 0x0101000407020101}, notifying = NO>
<CBCharacteristic: 0x282620540, UUID = E1F9B835-7E47-413D-AF94-C68E574B8F7E, properties = 0x4, value = (null), notifying = NO>
<CBCharacteristic: 0x282620480, UUID = A08CE5EF-698A-42A2-B980-7F3AC00B3845, properties = 0x14, value = (null), notifying = NO>
<CBCharacteristic: 0x2826203c0, UUID = 6288EA2D-7B89-47AD-890B-9FA6BF3CFC58, properties = 0x14, value = (null), notifying = NO>
<CBCharacteristic: 0x282620420, UUID = 3F1C161D-6473-4746-91F5-6D27610780C6, properties = 0x10, value = {length = 91, bytes = 0x01003856 0001d041 0151b83d fabea0b8 ... 962aac7f 17a0757f }, notifying = NO>
<CBCharacteristic: 0x2826205a0, UUID = C7C6947D-3165-4BCB-8EAF-B328896CB531, properties = 0x14, value = {length = 4, bytes = 0x01000300}, notifying = NO>
<CBCharacteristic: 0x282620720, UUID = 82F6BC0A-1BCA-4873-AFC9-EC5890E3469A, properties = 0x2, value = (null), notifying = NO>
<CBCharacteristic: 0x2826206c0, UUID = D5F96AFA-2F2C-41BB-A7E6-F54ABE6235B4, properties = 0x2, value = (null), notifying = NO>
<CBCharacteristic: 0x282620660, UUID = D5621CC1-F7AB-45DB-9403-9EAF744D5390, properties = 0x6, value = (null), notifying = NO>

그래서 뭔가 잔뜩 나왔는데, 뭐가 뭔지는 모르겠고, 어떤 거는 value를 가지고 있는 것도 있다.

UUID에 따라서 어떤 내용인지 파악하면 되는데, 에어팟을 내가 생각한게 아니고, 블루투스 스펙에도 나와있지 않다. 애플이 임의로 정의 해 둔 것이라는 것

만약 실제로 개발하는 입장이라면 아마

“D5621CC1-F7AB-45DB-9403-9EAF744D5390” 이 ID는 현재 배터리 상태를 보내는 거고

“D5F96AFA-2F2C-41BB-A7E6-F54ABE6235B4”는 현재 착용중인지 나타내는 값이고

이런식으로 아마 차트를 정리 해 두었을 것이다.

그래서 각 특성에 따라서 Properties를 가지는데, 가상으로 생각해보면, 현재 배터리 상태를 나타내는 것은 에어팟에서 값을 가져올 것이기 때문에 read를 가지고 있을 것이다.

https://developer.apple.com/documentation/corebluetooth/cbcharacteristicproperties

여기부터는 가상의 코딩.ㅎ

peripheral.readValue(for: characteristic)

특성을 readValue하게 되면 이 결과가

요 델리게이트 메서드로 넘어가게 된다.

그러면 아래 처럼 상태 처리를 해서

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    if characteristic.uuid == CBUUID(string: "배터리상태") {
      print(batteryState(from: characteristic))
    }
  }

battryState를 계산해주는 메서드를 호출하면 되고

private func batteryState(from characteristic: CBCharacteristic) -> Int {
    //byte 처리
    return -1
}

batteryState 메서드에서는 characteristic으로 전달된 value값(Data)를 처리해야 한다.

이 부분도 실제로 해보고 싶은데, 에어팟 블루투스 시방서가 없어서.. 가상으로 해보자면

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 224, 1, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 50, 46, 49, 49, 56, 55, 48, 54, 48, 48, 48, 52, 48, 48, 48, 48, 48, 48, 46, 57, 124, 99, 115, 118, 49, 58, 50, 46, 48, 46, 53, 124, 99, 115, 118, 50, 58, 49, 46, 57, 46, 52, 124, 100, 97, 116, 101, 58, 50, 48, 50, 49, 32, 49, 49, 32, 49, 49, 124, 117, 112, 100, 118, 58, 49, 54, 53]

이런식으로 바이트 쪼가리가 데이터로 들어오는데, 이를 시방서를 보고

예를들어 뭐 12번째 자리가, uint8 형식으로, 배터리를 0~100까지 나타낸다고 그러면, 알아서 잘 변환해서 처리하면 된다.

집에 범용적인 블루투스 기계가 없어서 실제로 해보지 못하는게 너무 아쉽다.

 

전체코드보기

https://github.com/urijan44/CoreBluetoothPractice/blob/main/CoreBlueToothPractice/ViewController.swift

'iOS' 카테고리의 다른 글

Run Loop vs DispatchQueue  (0) 2022.03.13
DiffableDataSource와 Realm  (0) 2022.03.03
NMapsMap M1 Build Error  (0) 2022.02.26
Diffable Data Source  (3) 2022.02.18
Design Pattern - Coordinator Part1  (0) 2022.02.09
Comments