iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Mobile Development

我將成為Swift之強者系列 第 27

Day27 - iOS 藍芽開發實作:掃描裝置、連線與接收資料

  • 分享至 

  • xImage
  •  

Day27 - iOS 藍芽開發實作:掃描裝置、連線與接收資料

昨天我們完成了藍芽的基本環境設定,並建立了一個簡單的 BluetoothService 架構。
今天要繼續進化,讓我們的 App 能夠:

  • 掃描藍芽裝置
  • 連接指定裝置
  • 發現服務與特徵值
  • 訂閱資料更新並回傳給 UI

新增 Delegate 協議

首先,我們定義一個 BluetoothServiceDelegate 協議,讓 Service 可以把 掃描到的裝置接收到的資料 傳回 UI。

protocol BluetoothServiceDelegate: AnyObject {
    func getBLEPeripherals(peripherals: [CBPeripheral])   // 回傳掃描到的裝置
    func getBLEPeripheralsValue(value: String)            // 回傳接收到的數據
}

透過這種設計,我們能讓 Service 與 UI 完全解耦
UI 只要實作這兩個方法,就能即時取得藍芽掃描結果與資料更新。


建立 Singleton 藍芽服務

接著,我們建立 BluetoothService 類別。
這裡採用 單例模式 (Singleton),確保全專案只存在一個藍芽實例,方便集中管理。

class BluetoothService: NSObject {
    
    static let shared = BluetoothService()
    weak var delegate: BluetoothServiceDelegate?
    
    var central: CBCentralManager?            // 中央管理器
    var peripheral: CBPeripheralManager?      // 外圍管理器
    var connectedPeripheral: CBPeripheral?    // 已連線裝置
    var rxtxCharacteristic: CBCharacteristic? // 資料收發特徵值
    
    private var bluePeripherals: [CBPeripheral] = []
    
    private override init() {
        super.init()
        let queue = DispatchQueue.global()
        central = CBCentralManager(delegate: self, queue: queue)
        peripheral = CBPeripheralManager(delegate: self, queue: queue)
    }
}

這邊同時初始化了 中央管理器 (Central)外圍管理器 (Peripheral)
目前我們主要使用 Central 角色,但未來若想讓手機變成 BLE 外設,也能直接擴充。


掃描與連線方法

我們接著實作幾個公開方法來控制藍芽操作:

func startScan() {
    central?.scanForPeripherals(withServices: nil, options: nil)
}

func stopScan() {
    central?.stopScan()
}

func connectPeripheral(peripheral: CBPeripheral) {
    self.connectedPeripheral = peripheral
    central?.connect(peripheral, options: nil)
}

func disconnectPeripheral(peripheral: CBPeripheral) {
    central?.cancelPeripheralConnection(peripheral)
}

這些方法負責控制藍芽掃描、連線與中斷:

  • startScan / stopScan → 控制掃描流程
  • connectPeripheral → 嘗試連線到指定裝置
  • disconnectPeripheral → 主動斷開連線

CBCentralManagerDelegate:掃描與連線流程

當藍芽狀態變化或掃描到裝置時,CBCentralManagerDelegate 會觸發對應事件。

藍芽狀態監控:

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .poweredOn:
        print("藍牙開啟,開始掃描")
        startScan()
    case .poweredOff:
        print("藍牙關閉")
    case .unauthorized:
        print("沒有藍牙權限")
    default:
        print("狀態: \(central.state.rawValue)")
    }
}

掃描到裝置:

func centralManager(_ central: CBCentralManager,
                    didDiscover peripheral: CBPeripheral,
                    advertisementData: [String : Any],
                    rssi RSSI: NSNumber) {
    
    guard !bluePeripherals.contains(where: { $0.name == peripheral.name }) else { return }
    
    if let name = peripheral.name {
        bluePeripherals.append(peripheral)
        print("找到裝置: \(name)")
        delegate?.getBLEPeripherals(peripherals: bluePeripherals)
    }
}

成功連線:

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("已連線到:\(peripheral.name ?? "未知裝置")")
    peripheral.delegate = self
    peripheral.discoverServices(nil)
}

CBPeripheralDelegate:發現服務與接收資料

當成功連線後,就會進入「服務 → 特徵值 → 資料」的流程。

發現服務:

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    guard let services = peripheral.services else { return }
    for service in services {
        print("服務: \(service)")
        peripheral.discoverCharacteristics(nil, for: service)
    }
}

發現特徵值:

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    guard let characteristics = service.characteristics else { return }
    
    for characteristic in characteristics {
        print("特徵值: \(characteristic)")
        
        // 偵測特定 UUID (例如 FFE1)
        if characteristic.uuid.isEqual(CBUUID(string: "FFE1")) {
            rxtxCharacteristic = characteristic
            peripheral.readValue(for: characteristic)
            peripheral.setNotifyValue(true, for: characteristic)
        }
    }
}

接收到資料:

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    guard characteristic == rxtxCharacteristic,
          let data = characteristic.value,
          let stringValue = String(data: data, encoding: .utf8) else { return }
    
    print("接收到資料: \(stringValue)")
    delegate?.getBLEPeripheralsValue(value: stringValue)
}

這樣就能讓 UI 端即時接收 BLE 資料更新 🎉


結語

今天我們完成了整個藍芽連線流程的關鍵部分:

  • 掃描藍芽裝置並回傳給 UI
  • 連線到指定裝置
  • 發現服務與特徵值
  • 訂閱資料並即時接收

到這裡,我們已經擁有一個可「掃描 → 連線 → 收資料」的藍芽核心服務。
明天(Day28)我們將進一步整合 UI 介面,讓使用者能選擇藍芽裝置並查看即時資料更新!



上一篇
Day26 - iOS 藍芽開發實作:建立藍芽服務與掃描連接
下一篇
Day28 - iOS 藍牙開發實作:從掃描到 UI 呈現,打造藍牙設備清單
系列文
我將成為Swift之強者30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言