본문 바로가기
  • 원하는 게 있으면 주문을 말해봐~ 디딩 보딩 디보디보딩🎶
iOS

[Core Bluetooth] iOS BLE 통신 프레임워크(with swiftUI)

by 딩보 2024. 10. 25.

이 글은 내가 함수 하나하나 뜯어 보면서 공부를 하지 않았기 때문에 꼭 필요한 함수와 클래스 위주로 설명이 될 예정이다. 그러니 자세한 공부를 하고 싶다면 공식문서나 아래 참고 링크를 확인하는 것이 좋을 듯...

 

🟨 Core Bluetooth

ios에서 블루투스를 다루는 프레임워크

 

🌼 BLE(Bluetooth Low Energy)

블루투스 통신의 단점이었던 전력소비를 보완한 저전력 블루투스

  • Core Bluetooth는 BLE를 쉽게 사용할 수 있도록 애플에서 제공하는 프레임워크임
  • 블루투스와 애플리케이션이 통신하는데 필요한 다양한 클래스를 제공

 

 

현재 iOS는 기존 블루투스 방식은 제공하지 않고 오직 BLE 방식만을 제공하고 있다

External Accessory Framework를 통해 Classic Bluetooth를 사용하긴 하나 iOS app에서 지원하는 MFi 악세서리에서만 사용이 가능하다(자세한 내용은 참고에 적힌 링크를 확인하기)


[사담]

그 덕에 내가 입사하기 전 이미 완성이 되어있던 안드로이드 앱도 다 고쳐야 했다!ㅎㅎㅎㅎㅎ

난 원래 안드로이드 개발이 익숙하고 iOS 개발은 한 번도 해본적이 없기 때문에 블루투스 통신 체계를 공부하는 과정에서 이 편이 훨씬 편하긴 했다

만들어져 있던 안드 앱은 클린한 코드라곤 찾아볼 수 없을 정도로 고칠 부분이 많았기 때문에 더 재밌기도 했다


 

🟨 주 클래스 및 프로토콜

Central, Peripheral, Service, 데이터전송과 관련된 클래스와 프로토콜은 Core Bluetooth가 동작하는데 핵심적인 역할을 한다

 

🟠 Central, Peripheral

이 둘은 Core Bluetooth의 핵심으로, Central은 중앙, Peripheral은 주변이라는 사전적 의미를 가지고 있다

즉, Central은 앱이고 peripheral은 주변 블루투스 기기이다

  • Peripheral는 ios에 연결하는 주변 기기
  • Central은 이를 제어하는 ios 애플리케이션

🟠 Service, Characteristic

이 둘은 블루투스 기기가 가지고 있는 기능 같은 것이다

그냥 특성(characteristic)을 그룹으로 모아놓은 것이 Service이다

  • Service는 특정 기능이나 데이터를 제공하는 논리적 그룹
  • Characteristic은 특정 기능이나 데이터

 

 

 

 

 

 

 

🟨  실습

 

블루투스 연결 과정

  1. 블루투스 상태 확인
  2. 주변 장치 검색
  3. 연결하고자 하는 장치 선택 및 연결
  4. 블루투스 장치의 서비스 검색, 선언한 serviceUUID와 같은 서비스를 찾기
  5. 찾아낸 서비스의 특성 검색, 선언한 characteristicUUID와 같은 서비스를 찾기
  6. 찾아낸 특성으로 데이터 전송

 

🔸 BluetoothSerial 생성

import CoreBluetooth

class BluetoothSerial: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, ObservableObject {
    @Published var isConnected = false
    weak var delegate: BluetoothSerialDelegate?
    var centralManager: CBCentralManager!
    var pendingPeripheral: CBPeripheral?
    var connectedPeripheral: CBPeripheral?
    weak var writeCharacteristic: CBCharacteristic?
    // 데이터를 전송하기 위한 type
    // 만약 데이터 전송이 필요하지 않다면 .withoutResponse로 대체
    private var writeType: CBCharacteristicWriteType = .withResponse
    // 연결을 원하는 블루투스 장치의 service uuid와 characteristic uuid 찾아서 입력하기
    // 혹시 uuid가 어떤 것인지 모른다면 아래 코드를 조금만 변형하면 찾아낼 수 있다. 그 부분은 챗 gpt에게 물어보면 금방 알려줄 듯
    var serviceUUID = CBUUID(string: "[Service UUID]") 
    var characteristicUUID = CBUUID(string: "[Characteristic UUID]")
    
    @Published var peripheralList: [Peripheral] = [] // Peripheral 목록을 BluetoothSerial에서 관리

    override init() {
        super.init()
        self.centralManager = CBCentralManager(delegate: self, queue: nil)
    }
    
    // 블루투스 상태를 알려줌
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .unknown:
            print("unknown")
        case .resetting:
            print("resetting")
        case .unsupported:
            print("unsupported")
        case .unauthorized:
            print("unauthorized")
        case .poweredOff:
            print("power Off")
        case .poweredOn:
            print("power on")
        @unknown default:
            fatalError()
        }
    }
    
    func startScan() {
        guard centralManager.state == .poweredOn else { return }
        print("Scan Start")
        peripheralList = [] // 스캔 시작 시 목록 초기화
        centralManager.scanForPeripherals(withServices: nil, options: nil)
    }
    
    func stopScan() {
        print("Scan Stop")
        centralManager.stopScan()
    }
    
    func connectToPeripheral(_ peripheral: CBPeripheral) {
        pendingPeripheral = peripheral
        centralManager.connect(peripheral, options: nil)
    }
    
    // peripheral이 발견되었을 때 실행
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        // Peripheral 이름이 "Unknown"이 아닌 경우에만 추가
        guard let name = peripheral.name, name != "Unknown" else { return }
        
        // 중복 체크
        guard !peripheralList.contains(where: { $0.peripheral.identifier == peripheral.identifier }) else { return }
        print("Discovered \(name)")
        
        let fRSSI = RSSI.floatValue
        let newPeripheral = Peripheral(peripheral: peripheral, RSSI: fRSSI)
        peripheralList.append(newPeripheral)
        peripheralList.sort { $0.RSSI < $1.RSSI }
        
        delegate?.serialDidDiscoverPeripheral(peripheral: peripheral, RSSI: RSSI)
    }
    
    // peripheral이 연결되었을 때 실행
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        isConnected = true
        peripheral.delegate = self
        pendingPeripheral = nil
        connectedPeripheral = peripheral
        peripheral.discoverServices([serviceUUID])
        
        // 연결 성공 시 delegate 호출
        delegate?.serialDidConnectPeripheral(peripheral: peripheral)
        print("Connected to \(peripheral.name ?? "Unknown Device")")
    }
    
    // service 검색
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let error = error {
            print("Error discovering services: \(error.localizedDescription)")
            return
        }
        
        guard let services = peripheral.services else { return }
        
        for service in services {
            print("Discovered service: \(service.uuid)")
            
            if service.uuid == serviceUUID {
                // 특성 검색
                peripheral.discoverCharacteristics(nil, for: service) // 모든 characteristic 검색
                print("Searching for characteristics for service: \(service.uuid)")
            } else {
                print("Service \(service.uuid) is not the desired service.")
            }
        }
    }
    
    // charecteristic 탐색
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
            if let error = error {
                print("Error discovering characteristics: \(error.localizedDescription)")
                return
            }
            
            guard let characteristics = service.characteristics else { return }
            
            for characteristic in characteristics {
                print("Discovered characteristic: \(characteristic.uuid)")
                if characteristic.uuid == characteristicUUID {
                    writeCharacteristic = characteristic // 전송할 characteristic 저장
                    print("Write characteristic found: \(characteristic.uuid)")
                }
            }
        
        if writeCharacteristic == nil {
            print("No characteristic found for writing data for service \(service.uuid)")
        }
    }

		// 블루투스로 데이터를 전송하는 부분(만약 데이터 전송을 원치 않는다면 제거 가능)
    func sendData(_ data: String) {
        guard let characteristic = writeCharacteristic else {
            print("No characteristic found for writing data")
            return
        }
        
        let dataToSend = data.data(using: .utf8)!
        connectedPeripheral?.writeValue(dataToSend, for: characteristic, type: writeType)
        print("Sent data: \(data)")
    }
}

protocol BluetoothSerialDelegate: AnyObject {
    func serialDidDiscoverPeripheral(peripheral: CBPeripheral, RSSI: NSNumber?)
    func serialDidConnectPeripheral(peripheral: CBPeripheral)
}

// 프로토콜에 포함된 일부 함수를 옵셔널로 설정합니다.
extension BluetoothSerialDelegate {
    func serialDidDiscoverPeripheral(peripheral: CBPeripheral, RSSI: NSNumber?) {}
    func serialDidConnectPeripheral(peripheral: CBPeripheral) {}
}

 

 

 

 

🔸 Bluetooth ViewModel

블루투스 시리얼을 뷰와 연결할 뷰모델을 만들어야 한다

import SwiftUI
import CoreBluetooth

struct Peripheral: Identifiable {
    var id: UUID { peripheral.identifier }
    let peripheral: CBPeripheral
    let RSSI: Float
}

class BluetoothViewModel: NSObject, ObservableObject, BluetoothSerialDelegate {
    @Published var peripheralList: [Peripheral] = []
    @Published var showAlert = false
    var alertMessage = ""
    
    @ObservedObject var serial: BluetoothSerial

    init(serial: BluetoothSerial) {
        self.serial = serial
        super.init()
        self.serial.delegate = self
    }

    func startScanning() {
        serial.startScan() // BluetoothSerial의 스캔 시작
    }
    
    func stopScanning() {
        serial.stopScan() // BluetoothSerial의 스캔 중지
    }
    
    func connectToPeripheral(_ peripheral: CBPeripheral) {
        stopScanning()
        serial.connectToPeripheral(peripheral) // BluetoothSerial에 연결 요청
    }
    
    // 발견된 peripheral 리스트 가져오기
    func serialDidDiscoverPeripheral(peripheral: CBPeripheral, RSSI: NSNumber?) {
        // BluetoothSerial에서 업데이트된 peripheralList를 가져옴
        self.peripheralList = serial.peripheralList
    }
    
    func serialDidConnectPeripheral(peripheral: CBPeripheral) {
        // 연결 성공 알림
        alertMessage = "\(peripheral.name ?? "알수없는기기")와 성공적으로 연결되었습니다."
        showAlert = true
        
        print("Alert Message: \(alertMessage), Show Alert: \(showAlert)")
    }
    
    // 블루투스가 잘 연결되어 있는지
    func isConnected() -> Bool {
        return serial.isConnected
    }
    
    func sendData(message: String) {
        serial.sendData(message)
    }
}

 

 

 

 

🔸 View

 

view 부분은 각자가 원하는 ui에 맞게 새로 작성하는 것이 좋을 듯 하지만 나는 이렇게 썼다!

 

🌼 장치를 스캔 후 검색된 peripheral 리스트를 보여주는 Dialog

import SwiftUI

struct ScanView: View {
    @ObservedObject var viewModel: BluetoothViewModel
    @State private var showDeviceList = false // 다이얼로그 표시 여부
    @State private var navigateToControllerView = false // NavigationView로 이동할 상태
    var onDismiss: () -> Void // 현재 화면의 상태 관리

    var body: some View {
        VStack {
            // 스캔 시작 후 자동으로 다이얼로그를 보여줍니다.
            if navigateToControllerView {
		            // 실제로 블루투스를 사용할 view
		            // 연결 성공 시 ControllerView로 이동
                ControllerView(viewModel: NavigationViewModel(), bluetoothViewModel: viewModel) 
            }
        }
        .navigationTitle("Scan Devices")
        .onAppear {
            viewModel.startScanning()  // 앱이 시작될 때 스캔 시작
            showDeviceList = true       // 다이얼로그 표시
        }
        .sheet(isPresented: $showDeviceList) {
            deviceListView
        }
        .alert(isPresented: $viewModel.showAlert) {
            Alert(title: Text("블루투스 연결 성공"),
                  message: Text(viewModel.alertMessage),
                  dismissButton: .default(Text("확인")) {
                navigateToControllerView = true
            })
        }
    }

    private var deviceListView: some View {
        VStack {
            Text("발견된 기기 목록")
                .font(.headline)
                .padding()

            List(viewModel.peripheralList) { peripheral in
                Button(action: {
                    viewModel.connectToPeripheral(peripheral.peripheral) // 기기 선택 시 연결 요청
                    showDeviceList = false // 연결 요청 후 다이얼로그 닫기
                }) {
                    Text(peripheral.peripheral.name ?? "Unknown Peripheral")
                }
            }

            Button(action: {
                showDeviceList = false // 다이얼로그 닫기
                onDismiss()
            }) {
                Text("닫기")
                    .font(.system(size: 16))
                    .padding()
                    .frame(width: 100, height: 30)
                    .background(Color.gray)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            .padding()
        }
    }
}

 

 

 

 

🌼 버튼을 클릭하면 블루투스 장치로 데이터를 전송하는 View

import SwiftUI

struct RemoteControlView: View {
    @State private var isLedButton = false
    @ObservedObject var bluetoothViewModel : BluetoothViewModel
    
    var body: some View {
        VStack {
		        ledButton
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
    
    private var ledButton: some View {
        HStack {
            Image("led")
            Button(action: {
                isLedButton.toggle()
                let message = isLedButton ? "6" : "7"
                bluetoothViewModel.sendData(message: message)
            }) {
                Image(isLedButton ? "led_on" : "led_off")
            }
        }
    }
}

 

 

 

 

 

 

 

 

🟨  참고

https://staktree.github.io/ios/iOS-Bluetooth-01-about-CoreBluetooth/

https://devxoul.gitbooks.io/ios-with-swift-in-40-hours/content/Chapter-1/ios-project.html

https://jiwonxdoori.tistory.com/44