2025. 10. 12. 22:53ㆍ도메인/Bluetooth
Overview
- BLE Write: Central이 심박수 모니터링 중 심박수가 140을 넘어가면, Peripheral에 경고등을 켜는 기능 구현
- BLE State Save / Restore - BLE Life cycle Background 진입 또는 종료 후 상태 복구
BLE Write - Central
심박수가 140 이상이면, 경고등을 켜고 다시 정상 수치까지 내려가면 경고등을 끕니다.
custom characteristic을 분기해서 프로퍼티에 저장해놨다가 쓰기 작업이 필요할 때 저장해뒀던 charateristic과 data 그리고 응답 여부를 함께 전송합니다.
// 구독한 값을 수신하면, 디코딩하여 publish
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: (any Error)?) {
...
Task { @MainActor in
if heartRateValue > 140 {
self.send(to: peripheral, .redAlert)
} else if heartRateValue < 125 {
self.send(to: peripheral, .turnOff)
}
}
}
// BLE Write
private func send(to peripheral: CBPeripheral, _ command: Command) {
guard let alertCharacteristic else { return }
let commandData = Data(command.rawValue.utf8)
peripheral.writeValue(commandData, for: alertCharacteristic, type: .withResponse)
Self.log.info("🚨 Heart Rate command fired: \(command.rawValue)")
}
BLE Write - Peripheral
Peripheral에도 custom chracteristic을 추가해주고, service를 정의할 때 같이 넣어줍니다.
// PeripheralManager.swift
let heartAlertLevelCharateristic = CBMutableCharacteristic(
type: GATT.Characteristic.heartAlertLevel,
properties: [.write],
value: nil,
permissions: [.writeable]
)
// custom characteristic 추가
private func setupHeartRateService() {
let heartRateService = CBMutableService(type: GATT.Service.heartRate, primary: true)
heartRateService.characteristics = [heartRateCharacteristic, heartAlertLevelCharateristic]
cbPeripheralManager.add(heartRateService)
}
Central에서 요청을 보내면 didReceiveWrite delegate 메서드를 통해 받을 수 있습니다. 해당 요청에서 characteristic과 command 값을 추출하고, 해당 값을 적용합니다.
🚨 작업이 완료된 후, peripheral.respond를 해줍니다.
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
for request in requests {
if request.characteristic.uuid == GATT.Characteristic.heartAlertLevel,
let value = request.value {
let command = String(data: value, encoding: .utf8)
Self.log.info("Received command: \(command ?? "unknown")")
// 메인 스레드에서 UI 업데이트 처리
...
// 요청에 응답
peripheral.respond(to: request, withResult: .success)
}
}
}
State 저장
WebSocket 구독 상태와 비슷하게 BLE도 상태를 저장하고, 재진입했을 시 저장한 상태를 불러와 상태를 복원할 수 있습니다.
평소에 프로퍼티가 변경될 때 마다 저장해줍니다. 이후 재진입할 때, 매니저 생성자에서 인스턴스 생성 후, 복구를 시작합니다. 중요한 것은 powerOn 일 때 다시 광고를 시작한다는 점입니다.
private(set) var status: Status = .idle {
didSet {
saveState()
}
}
...
private(set) var isAdvertising = false {
didSet {
saveState()
}
}
init() {
...
await restoreState()
}
private func restoreState() async {
guard let savedState = await stateManager.loadPeripheralState() else { return }
Self.log.info("Restoring peripheral state: advertising=\(savedState.isAdvertising), heartRate=\(savedState.heartRateValue), alert=\(savedState.redAlertFlag)")
// 상태 복원
heartRateValue = savedState.heartRateValue
redAlertFlag = savedState.redAlertFlag
// 상태에 따른 복원
if let status = Status(rawValue: savedState.status) {
self.status = status
}
// ⚡️ 광고 상태 복원 (전원이 켜진 후에만)
if savedState.isAdvertising && cbPeripheralManager.state == .poweredOn {
startAdvertising()
}
}
저장은 UserDefault를 이용했고, validate 과정을 거쳐 1시간 이내에 저장된 것은 복구하고 이후는 저장된 값이 있더라도 무시하도록 하였습니다.
extension BLEStateManager: BLEStateManaging {
// MARK: - Central State Management
func saveCentralState(_ state: CentralState) async {
savedCentralState = state
if let encoded = try? JSONEncoder().encode(state) {
userDefaults.set(encoded, forKey: centralStateKey)
Self.log.info("Central state saved: scanning=\(state.isScanning), connected=\(state.connectedPeripheralIdentifier ?? "none")")
}
}
func loadCentralState() async -> CentralState? {
guard let data = userDefaults.data(forKey: centralStateKey),
let state = try? JSONDecoder().decode(CentralState.self, from: data) else {
return nil
}
if validate(for: state.lastUpdateTime) {
Self.log.info("Central state loaded: scanning=\(state.isScanning), connected=\(state.connectedPeripheralIdentifier ?? "none")")
savedCentralState = state
return state
} else {
savedCentralState = nil
}
return nil
}
Next Step
Terminated와 Background mode에서 App을 깨워서 상태 복구하기
전에 전자 도어락 회사를 잠깐 다녔을 때 BLE Life cycle 관련 이슈가 있었는데, 안드로이드는 문제가 없는데, Not Running 상태에서 아파트 동 입구의 도어락이 열리지 않았다.
한 번 키고 나면 잘 살아있지만, Terminated 되고 난 뒤 자동 복구가 되지 않았다. 그 때 당시에는 50m 넘는 거리 문제와 iOS 13 이후, 최적화 관련 업데이트로인하여 이슈가 생긴것으로 파악했었다.
'도메인 > Bluetooth' 카테고리의 다른 글
| [BLE] ⚡️ Peripheral 시뮬레이션 (2) (0) | 2025.10.12 |
|---|---|
| [BLE] ⚡️ BLE란? Core Bluetooth (1) (0) | 2025.10.12 |