iyOmSd/Title: Swift

[Swift] Core Bluetooth Basic

냄수 2025. 3. 18. 22:45
반응형

주위에 블루투스 기기가 많아지고 있는데요

블루투스 기기를 이용한 앱을 구현할때 사용하는 프레임워크가

Core Bluetooth 입니다!

어떻게 이용할 수 있을까 궁금해서 찍먹겸...

처음이니까 원리를 간단하게 알아보려합니다! 

 

 

이론

중앙장치 - Central

주변장치(주변기기) - Peripheral

저에너지 블루투스 - BLE(Bluetooth low energy)

 

지원기기 

iOS에서 BLE기기 + 클래식 블루투스 검색 가능 

 

BLE - 저에너지 블루투스, bluetooth 4.0

Classic Bluetooth - BR/EDR기기

  - Basic Rate -> bluetooth 1.0

  - Enhanced Data Rate -> bluetooth 2.0

 

 

페어링 상태 

iOS 설정앱에서 보이는 기기 목록에는 이미 페어링된 기기가 포함됨

Core Bluetooth에서는 페어링된 기기인지 여부를 알 수 없고, 직접 검색해야함

BLE기기는 원래 '페어링' 개념이 없음 -> 광고 형식이용

블루투스 2.0 -> 반드시 페어링을 통해 연결함

블루투스 4.0 (BLE) -> 기본적으로 페어링없이 연결 가능

성능에 따라 동시에 여러명이 접속가능(아이폰에 에어팟 2개연결 가능한 이유)

 

페어링 vs 연결

페어링 - 블루투스 기기간 안전한 통신을 위한 절차(장치연결을 위한 정보등록)

연결 - 블루투스를 통해 두 장치간의 직접적인 통신 수행(페어링된 장치를 서로 연결) -> 페어링 되있으면 바로연결 가능

 

 

페어링 기록을 위한 사용한 기기 저장 정책

- 앱내 블루투스 기기 UUID저장 및 조회로 사용 retrievePeripherals(withIdentifiers:)

    - 기기 UUID 변경가능성 주의

- BLE기기 한정으로 여전히 iOS앱과 연결된 상태라면 retrieveConnectedPeripherals(withServices:)

    - 이미 연결된 기기검색(끊기면 안됨)

    - 특정 서비스 UUID 필요

- 기기 이름 저장

    - 일부기기는 ad시 이름 표기안함

    - 이름변경 가능성 주의

 

CBUUID 예시


  
UUID = Manufacturer Name String - 2A29
UUID = Model Number String - 2A24
UUID = Serial Number String - 2A25
UUID = Firmware Revision String - 2A26
UUID = Software Revision String - 2A28
UUID = PnP ID - 2A50
UUID = Battery Level - 2A19

 

 

주변기기 발견 원리

 

주변기기는 보유한 데이터 중 일부를 광고 패킷의 형태로 브로드캐스트함

광고 패킷은 비교적 작은 데이터 묶음으로, 주변기기의 이름과 주요 기능 등 주변기기가 제공하는 유용한 정보를 포함

예) 디지털 온도 조절기는 방의 현재 온도를 제공한다고 광고

저에너지 Bluetooth(BLE)에서 광고는 주변 장치가 자신의 존재를 알리는 주요 방법

central은 그림 1-2와 같이 관심 있는 광고 정보를 제공하는 모든 주변 디바이스를 스캔하고 수신할 수 있음

central은 광고를 발견한 모든 주변 장치에 연결을 요청할 수 있습니다

 

 

구조

 

CBCentralManager

주변장치를 검색하고, 발견하고, 연결하고, 관리하는 객체

 

| CentralManager생성


  
// 일반 생성
self.centralManager = CBCentralManager(delegate: self, queue: nil)
// 복원 옵션시
self.centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: "id123"])

 

| 주변기기 scan


  
if centralManager.isScanning {
return
}
centralManager.scanForPeripherals(
withServices: nil,
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
)

 

| 연결시도


  
centralManager.connect(peripheral) // 페어링 알림창 노출

함수 호출 순서

주변 장치 검색 didDiscover → 장치연결 connect() → didConnect -> 서비스 검색 discoverServices() → didDiscoverServices → 서비스의 특성검색 discoverCharacteristics() → didDiscoverCharacteristicsFor → read or notifyValue → didUpdateValueFor

 

 

CBCentralManagerDelegate

| 블루투스 상태 업데이트 이벤트


  
// 블루투스 상태 업데이트시 호출
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .unknown:
print("unknown")
case .resetting:
print("restting")
case .unsupported:
print("unsupported")
case .unauthorized:
print("unauthorized")
case .poweredOff:
print("power Off")
case .poweredOn:
print("power on")
self.searchUsedDevice() // 전원켜지고 탐색해야함
@unknown default:
print("default")
}
}

 

 

 

| 연결 기록 불러오기 

retrievePeripherals


  
/// 과거에 연결한 기록있는 기기 연결
func searchUsedDevice() {
guard let uuidString = UserDefaults.standard.string(forKey: "uuid"),
let uuid = UUID(uuidString: uuidString) else {
return
}
let devices = centralManager.retrievePeripherals(withIdentifiers: [uuid])
// 기록있는경우 바로연결 (페어링 알림창안뜸)
if let device = devices.first {
centralManager.connect(device, options: nil)
}
}

 

| scan시 주변기기 발견되면 호출 되는 함수


  
// 주변 블루투스 장치 검색시 호출
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print(peripheral)
// <CBPeripheral: 0x300304340, identifier = AD4A05D4-EEE2-B33D-E78B-8AA9863B1F10, name = (null), mtu = 0, state = disconnected>
print(advertisementData)
// ["kCBAdvDataRxSecondaryPHY": 0, "kCBAdvDataTxPowerLevel": 12, "kCBAdvDataIsConnectable": 1, "kCBAdvDataTimestamp": 760242236.546977, "kCBAdvDataRxPrimaryPHY": 129]
}

 

| 발견된 장치중 connect() 완료되면 호출되는 함수


  
// 장치연결시 서비스검색
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print(#function)
// 연결했던 기기 기록저장
UserDefaults.standard.set(peripheral.identifier.uuidString, forKey: "uuid")
print("uuid \(peripheral.identifier.uuidString), \(peripheral.name)")
// uuid DA857435-7B4F-0F42-8F2D-53B3FD3017F9, Optional("LIFT")
peripheral.delegate = self
peripheral.discoverServices(nil) // 서비스 검색
}

 

| 저장된 상태 복원시

CBCentralManager생성시 options으로 CBCentralManagerOptionRestoreIdentifierKey 제공시 가능

앱이 다시 시작될 때, CoreBluetooth는 centralManager(_:willRestoreState:) 메서드를 호출하여 복원 가능한 상태 정보를 제공합니다.


  
self.centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: "id123"])
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
for peripheral in peripherals {
// 이미 발견된 주변 장치에 대한 복원 처리
print("restore: \(peripheral)")
// restore: <CBPeripheral: 0x303ce8000, identifier = DA857435-7B4F-0F42-8F2D-53B3FD3017F9, name = LIFT, mtu = 23, state = connected>
}
}
if let services = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] {
// 이전에 스캔하던 서비스 정보 복원
print("Restored scan services: \(services)")
central.scanForPeripherals(withServices: services, options: nil)
}
}

⚠️  호출시점은 아래와같이 블루투스 활성화전이라서 해당 delegate에는 connect()를 호출할 수 없음

저장해두고 있다가 활성화되는 시점에 장치재연결 시도해야함


  
centralManager(_:willRestoreState:)
centralManagerDidUpdateState(_:)
power on

 

재연결 하는 방식은 2가지

  1.  willRestoreState사용
    • 자동방식
    • 이전에 연결된 주변장치 제공(CBPeripheral)
    • 스캔중이던 서비스제공(UBUUID)
    • 커넥션 상태제공(CentralManager)
    • 복구된장치는 기기범위내에 있어야 연결가능
    • 실시간 동작중요한 경우 (피트니스, 헬스앱, 등)
  2. retrievePeripherals를 사용
    • 수동방식
    • 이전연결 여부와는 상관없음(저장된 정보사용)
    • 기기범위와 무관

 

CBPeripheralDelegate

| 서비스 탐색되면 호출되는 함수


  
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: (any Error)?) {
print(#function)
if let services = peripheral.services {
for service in services {
print("service", service)
peripheral.discoverCharacteristics(nil, for: service) // 특성 탐색
}
}
}
/*
service <CBService: 0x303c48d00, isPrimary = YES, UUID = Device Information>
service <CBService: 0x303c48e00, isPrimary = YES, UUID = Battery>
service <CBService: 0x303c48e40, isPrimary = YES, UUID = FD72>
service <CBService: 0x303c48f00, isPrimary = YES, UUID = 00010000-0000-1000-8000-011F2000046D>
*/

 

| 특성 탐색되면 호출되는 함수

readValue: 1회만 동작

setNotifyValue: 값 변경시 계속 동작


  
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: (any Error)?) {
if let characters = service.characteristics {
for character in characters {
print("character", character)
// characteristic.properties: 특성 값의 사용 방법 또는 설명자에 액세스하는 방법을 결정합니다
if character.properties.contains(.read) {
// readValue: 1회 읽어옴
peripheral.readValue(for: character)
} else if character.properties.contains(.notify) {
// setNotifyValue: 값변경시 알림받음
peripheral.setNotifyValue(true, for: character)
}
}
}
}

 

| 값 업데이트시 호출되는 함수

readValue, setNotifyValue값 모두 여기서 처리 


  
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: (any Error)?) {
print("update", characteristic, characteristic.uuid.uuidString)
let batteryLevelUUID = CBUUID(string: "2A19")
if characteristic.uuid == batteryLevelUUID {
let value = [UInt8](characteristic.value!)
print("Battery ", value[0], "%")
}
let softwareRevisionUUID = CBUUID(string: "2A29")
if characteristic.uuid == softwareRevisionUUID {
let value = String(data: characteristic.value!, encoding: .utf8)!
print("software revision ", value)
}
}

 

CBUUID(string: "2A19")는 배터리 상태를 나타내는 CBUUID이고 해당 서비스의

characteristic.value 타입은 Data타입이고 이를 각 특성 타입에 맞게 변환해서 사용합니다

여기서는 배터리 상태가 [UInt8]형태로 나타나기때문에 [UInt8]로 받아서사용합니다

배열의 0인덱스에 존재하는게 실제 배터리 상태이며 이를 Int(value[0])형태로 사용합니다

 

 

 

CBCentral(아이폰)

데이터를 받고 처리하는 기기, 중앙장치

 

CBPeripheral(온도 측정 장치)

앱을 통해 검색되는 원격 주변장치

 

CBService(온도 측정서비스, 기기 정보 서비스, 기기 배터리 서비스)

디바이스의 기능이나 Characteristic(특성)을 수행하는 데이터 및 관련 동작의 모음

원격 주변장치의 서비스를 나타냄

여러 특성을 포함하거나, 서비스를 포함할 수 있음 

 

CBCharacteristic(온도 값, 온도계 위치, 배터리 잔량, 모델명, 시리얼넘버) 

원격 주변기기의 서비스 특성


  
CBCharacteristicProperties
// 특성 값의 사용 방법 또는 설명자에 액세스하는 방법을 결정합니다
broadcast - 1 (iOS불가능)
read - 2
wirteWithoutResponse - 4
write - 8
notify - 16
indicate - 32
authenticatedSignedWrite - 64
extened - 128

 

 

Central입장 - CentralManager 사용

 

 

Peripheral입장 - CBPeripheralManager 사용

 

 

백그라운드

대부분 iOS앱과 마찬가지로 백그라운드 작업 권한요청을 하지않은 앱은 백그라운드 상태로 들어간 직후 일시 중단(suspended) 상태로 전환됨

suspended 상태에서는 블루투스 관련 작업을 수행할 수 없음

앱이 선언할 수 있는 2가지 백그라운드 실행 모드가 존재하고 central역할 앱용과 peripheral역할을 구현하는 앱용이 있음

info.plist에 UIBackgroundModes 키값으로 bluetooth-central, bluetooth-peripheral 중 필요한 값을 배열로 선언

 

백그라운드 실행모드를 지원하는 앱은 몇가지 지침을 따라야함

- 사용자가 블루투스 관련 이벤트의 전송을 언제시작하고 중단할지 결정할 수 있는 인터페이스 제공해야함

- 앱이 깨어난후 약 10초 동안 작업을 완료해야함

- 앱이 깨어난 것을 시스템에서 앱이 깨어난 이유와 관련이 없는 불필요한 작업을 수행하는 기회로 사용하면 안됨

 

 

상태 보존 및 복원

앱이 종료되더라도 bluetooth 작업을 지속할 수 있음

시스템이 BLE 연결 요청을 모니터링하고 필요한경우 앱을 백그라운드에서 다시 실행하여 연결 요청이 완료되면 

centralManager:didConnectPeripheral: delegate콜백을 처리함

 

central role을 구현한 시스템이 앱을 종료하려고 할때 CentralManager 객체의 상태를 저장하여 메모리를 확보함

시스템은 다음을 추적함

- central manager이 스캔하던 서비스

- central manager이 연결하려고 햇거나 이미 연결한 주변장치

- central manager이 구독한 특성

 

앱에서 기능을 지원하려면 아래의 프로세스를 따릅니다

1. Opt In to State Preservation and Restoration

central manager 혹은 peripheral manager 객체를 할당하고 초기화할때 고유한 복원 식별자를 제공하기만 하면됨

option: CBCentralManagerOptionRestoreIdentifierKey 제공

문자열의 값은 코드에만 중요하지만 이 문자열이 있으면 태그가 지정된 객체의 상태를 보존해야 한다는것을

Core Bluetooth에 알림

Core Bluetooth는 복원 식별자가 있는 객체의상태만 보존함


  
CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: "id"])

 

 

2. Reinstantiate Your Central and Peripheral Managers

앱이 시스템에 의해 백그라운드에서 다시 시작되면 가장 먼저해야 할일은 앱을 처음 만들 때와 동일한 식별자를 사용하여 다시 적절한 인스턴스를 만드는 것임

 

| 1개의 관리자인경우

앱에서 central 또는 peripheral manager를 하나만 사용하고 해당 관리자가 앱의 수명기간동안 존재하는 경우에는

이 단계에서 더이상 수행할 작업이 없음

 

| 2개이상 관리자인경우

앱의 수명기간 동안 존재하지 않을 관리자를 사용하는 경우 앱은 시스템에서 앱을 다시시작 할때 복원할 관리자 알아야함

 

앱 델리게이트의 application:didFinishLaunchingWithOptions: 메서드를 구현할 때 적절한 실행 옵션 키
UIApplicationLaunchOptionsBluetoothCentralsKey 또는 UIApplicationLaunchOptionsBluetoothPeripheralsKey 를 사용하여 앱이 종료될 당시 시스템이 보존했던 관리자 객체에 대한 모든 복원 식별자 목록에 액세스할 수 있습니다.

 

 

3. Implement the Appropriate Restoration Delegate Method

적절한 관리자를 다시 설치한 후 관리자의 상태를 bluetooth 시스템의 상태와 동기화하여 복원함

앱이 실행되지 않는동안 시스템이 앱을 대신하여 수행한 작업을 최신 상태로 가져오려면 복원 delegate를 구현해야함

central manager인경우


  
centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any])

 

를 구현해줌

 

복원기능을 구현한경우 

centralManager:willRestoreState

가 제일 먼저 호출

복원기능 없는 앱인경우

centralManagerDidUpdateState

가 제일 먼저 호출됨

 

CBCentralManagerRestoredStatePeripheralsKey 키값을 이용해서 central manager가 연결되었거나 연결시도한 모든 주변장치 목록을 가져올 수 있음


  
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
dict[CBCentralManagerRestoredStatePeripheralsKey]
}

 

 

재연결 흐름도

 

[알고있는 기기가 있는경우]

  • 검색 성공시 -> 연결 시도
    1. 사용가능한 디바이스상태 → 재연결 완료
    2. 사용불가능한 디바이스상태
      • 주변기기 검색 → 연결 → 재연결
  • 검색 실패시 -> service CBUUID로 검색
    1. 검색 성공시
      • 연결 → 재연결 완료
    2. 검색 실패시
      • 주변기기 검색 → 연결 → 재연결

 

이때 과거 기기 기록을 검색할때 블루투스 연결 상태 poweredOn 상태여야함

(retrievePeripherals, retrieveConnectedPeripherals 사용시)

 

 

 

 

 

 

참고

https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothOverview/CoreBluetoothOverview.html#//apple_ref/doc/uid/TP40013257-CH2-SW1

 

Core Bluetooth Overview

Core Bluetooth Overview The Core Bluetooth framework lets your iOS and Mac apps communicate with Bluetooth low energy devices. For example, your app can discover, explore, and interact with low energy peripheral devices, such as heart rate monitors, digita

developer.apple.com

https://github.com/SiliconLabs/thunderboard-ios/blob/master/ThunderBoard/CBUUIDExtensions.swift

 

thunderboard-ios/ThunderBoard/CBUUIDExtensions.swift at master · SiliconLabs/thunderboard-ios

This is the source code for Thunderboard application for iOS. - SiliconLabs/thunderboard-ios

github.com

 

반응형