CBPeripheral: co to jest, metody i zarządzanie BLE-peryferią na iOS

Autor: IT Sectr Opublikowano: 2026-07-16 Czas czytania: 10 min

CBPeripheral — klasa frameworku Core Bluetooth reprezentująca zdalne urządzenie BLE na iOS. Każdy obiekt CBPeripheral hermetyzuje UUID, nazwę, RSSI i hierarchię serwisów GATT podłączonego urządzenia BLE. Deweloper współdziała z peryferią wyłącznie przez CBPeripheral: discovery serwisów (discoverServices:), odczyt charakterystyk (readValueForCharacteristic:), zapis danych (writeValue:forCharacteristic:type:) i subskrypcję powiadomień (setNotifyValue:forCharacteristic:). Według Apple Developer, 2026, CBPeripheral — centralny obiekt dla wszystkich operacji z BLE-peryferią, zwracany przez CBCentralManager przy wykryciu lub podłączeniu urządzenia.

Najważniejsze

  • CBPeripheral — klasa Core Bluetooth do pracy ze zdalnym urządzeniem BLE na iOS
  • Hierarchia GATT — Peripheral zawiera serwisy (CBService), serwisy zawierają charakterystyki (CBCharacteristic), charakterystyki zawierają deskryptory (CBDescriptor)
  • Discovery — discoverServices: i discoverCharacteristics:forService: do uzyskania struktury GATT urządzenia
  • Odczyt i zapis — readValueForCharacteristic: i writeValue:forCharacteristic:type: z potwierdzeniem (withResponse) lub bez (withoutResponse)
  • Powiadomienia — setNotifyValue:forCharacteristic: włącza subskrypcję zmian charakterystyk urządzenia BLE

Co to jest CBPeripheral: istota i przeznaczenie

CBPeripheral — to obiekt reprezentujący zdalne urządzenie BLE w aplikacji iOS. W przeciwieństwie do CBCentralManager, który zarządza lokalnym adapterem Bluetooth iPhone'a, CBPeripheral modeluje zewnętrzne urządzenie peryferyjne: czujnik, tracker fitness, beacon, urządzenie medyczne. Każda instancja CBPeripheral zawiera unikalny identyfikator (UUID), który jest zachowywany między sesjami połączenia — Apple wiąże UUID z konkretnym urządzeniem poprzez systemowy Bonding.

CBPeripheral nie jest tworzony bezpośrednio przez init. Framework Core Bluetooth zwraca obiekt CBPeripheral w dwóch scenariuszach: przy wykryciu urządzenia przez scanForPeripheralsWithServices: (delegat didDiscoverPeripheral) i przy podłączeniu do wcześniej znanego urządzenia przez retrievePeripheralsWithIdentifiers:. Po otrzymaniu obiektu deweloper wywołuje connectPeripheral: na CBCentralManager, po czym CBPeripheral staje się dostępny do operacji GATT.

Cykl życia CBPeripheral obejmuje sześć stanów: disconnected (początkowy), connecting (po wywołaniu connect), connected (po didConnectPeripheral), discovering (podczas wywołania discoverServices), discovered (po otrzymaniu serwisów) i disconnecting (po cancelPeripheralConnection). Każdy stan jest śledzony przez delegat CBPeripheralDelegate — obowiązkowy protokół dla każdej aplikacji BLE na iOS.

CBPeripheral i hierarchia GATT: serwisy, charakterystyki, deskryptory

CBPeripheral przechowuje hierarchiczną strukturę GATT składającą się z trzech poziomów. Poziom główny — tablica CBService (serwisy), każdy serwis zawiera tablicę CBCharacteristic (charakterystyki), każda charakterystyka zawiera tablicę CBDescriptor (deskryptory). Model ten w pełni odpowiada specyfikacji Bluetooth GATT: serwis — funkcja urządzenia (np. "Heart Rate Service"), charakterystyka — konkretna wartość (tętno 72 bpm), deskryptor — metadane charakterystyki (jednostki miary, konfiguracja powiadomień).

PoziomKlasa Core BluetoothOpis
SerwisCBServiceLogiczna grupa powiązanych charakterystyk, identyfikowana przez UUID (16-bit, 32-bit lub 128-bit)
CharakterystykaCBCharacteristicKonkretna wartość danych, obsługuje odczyt, zapis, powiadomienie
DeskryptorCBDescriptorMetadane charakterystyki: konfiguracja klienta CCCD, User Description, Presentation Format

Standardowe BLE-serwisy są zarejestrowane w Bluetooth SIG: Heart Rate Service (UUID 180D), Battery Service (180F), Device Information (180A), Blood Pressure (1810). Dla niestandardowych serwisów używane są 128-bitowe UUID (np. E20A39F4-73F5-4BC4-A12F-17D1AD07A961). iOS automatycznie rozpoznaje standardowe UUID i wyświetla czytelne nazwy; niestandardowe UUID są wyświetlane w formacie hex.

Po podłączeniu CBPeripheral jego hierarchia jest pusta — serwisy i charakterystyki nie są załadowane. Deweloper musi wywołać discoverServices: aby uzyskać serwisy, a następnie dla każdego serwisu wywołać discoverCharacteristics:forService:. Jeśli serwis zawiera serwisy wbudowane (includedServices), dodatkowo wywoływane jest discoverIncludedServices:forService:. Dopiero po zakończeniu discovery hierarchii CBPeripheral zostaje wypełniony i staje się dostępny do odczytu i zapisu.

Discovery serwisów i charakterystyk: metody i delegaty

Discovery (wykrywanie) struktury GATT CBPeripheral — obowiązkowy krok przed jakimikolwiek operacjami odczytu lub zapisu. Metoda discoverServices: uruchamia asynchroniczne wyszukiwanie wszystkich serwisów urządzenia. Jeśli przekazano nil, wykrywane są wszystkie serwisy; jeśli przekazano tablicę CBUUID — tylko serwisy z określonymi UUID (optymalizacja czasu). Wynik przychodzi w delegacie peripheral:didDiscoverServices: — obiekt CBPeripheral wypełnia właściwość services tablicą CBService.

Po otrzymaniu serwisów dla każdego CBService należy wywołać discoverCharacteristics:forService:. Analogicznie, nil — wszystkie charakterystyki, tablica CBUUID — tylko określone. Wynik: peripheral:didDiscoverCharacteristicsForService:error:. Na tym etapie CBCharacteristic otrzymują właściwości (properties: .read, .write, .notify, .indicate), które określają dozwolone operacje.

swift
import CoreBluetooth

extension BLEViewController: CBPeripheralDelegate {

    // 1. Wykrywanie serwisów
    func peripheral(_ peripheral: CBPeripheral,
                     didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }

        for service in services {
            // Żądanie charakterystyk dla każdego serwisu
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }

    // 2. Wykrywanie charakterystyk
    func peripheral(_ peripheral: CBPeripheral,
                     didDiscoverCharacteristicsFor service: CBService,
                     error: Error?) {
        guard let characteristics = service.characteristics else { return }

        for characteristic in characteristics {
            if characteristic.properties.contains(.read) {
                peripheral.readValue(for: characteristic)
            }
            if characteristic.properties.contains(.notify) {
                peripheral.setNotifyValue(true, for: characteristic)
            }
        }
    }

    // 3. Odczyt wartości
    func peripheral(_ peripheral: CBPeripheral,
                     didUpdateValueFor characteristic: CBCharacteristic,
                     error: Error?) {
        guard let data = characteristic.value,
              let value = String(data: data, encoding: .utf8)
        else { return }
        print("Characteristic value: \(value)")
    }
}

W przykładzie CBPeripheralDelegate zaimplementowano trzy obowiązkowe metody wykrywania. didDiscoverServices przegląda wszystkie znalezione serwisy i żąda charakterystyk. didDiscoverCharacteristicsForService sprawdza właściwości każdej charakterystyki: dla .read wywołuje readValue, dla .notify — setNotifyValue(true). Metoda didUpdateValueForCharacteristic otrzymuje aktualną wartość w formacie Data.

Odczyt i zapis charakterystyk: withResponse i withoutResponse

Odczyt wartości CBCharacteristic wykonuje się metodą readValueForCharacteristic:. Wynik asynchronicznie przychodzi w peripheral:didUpdateValueForCharacteristic:error:. Ważne: urządzenie może mieć buforowaną wartość (characteristic.value jest dostępny od razu po discovery), ale aby uzyskać aktualną, wywołanie readValue jest obowiązkowe. iOS może buforować wartości dla efektywności energetycznej — readValue odświeża bufor.

Zapis wartości wykonuje się metodą writeValue:forCharacteristic:type:. Parametr type określa typ zapisu: .withResponse (CBCharacteristicWriteWithResponse) — urządzenie potwierdza zapis przez didWriteValueForCharacteristic; .withoutResponse (CBCharacteristicWriteWithoutResponse) — zapis bez potwierdzenia, maksymalna prędkość, ale bez gwarancji dostarczenia. Specyfikacja BLE ogranicza MTU (Maximum Transmission Unit): do 23 bajtów dla BLE 4.0, do 251 bajtów dla BLE 5.0+. Dla danych większych niż MTU wymagana jest fragmentacja na poziomie aplikacji.

swift
// Odczyt i zapis charakterystyk CBPeripheral
class BLEService {

    private let peripheral: CBPeripheral
    private let serviceUUID = CBUUID(string: "180D")
    private let charUUID = CBUUID(string: "2A37")

    init(peripheral: CBPeripheral) {
        self.peripheral = peripheral
    }

    // Odczyt z potwierdzeniem
    func readHeartRate() {
        guard let service = peripheral.services?.first(where: { $0.uuid == serviceUUID }),
              let characteristic = service.characteristics?.first(where: { $0.uuid == charUUID })
        else { return }
        peripheral.readValue(for: characteristic)
    }

    // Zapis z potwierdzeniem (withResponse)
    func writeWithResponse(data: Data) {
        guard let characteristic = findCharacteristic() else { return }
        peripheral.writeValue(data, for: characteristic,
                             type: .withResponse)
    }

    // Zapis bez potwierdzenia (withoutResponse)
    // Maksymalna przepustowość, brak gwarancji dostarczenia
    func writeWithoutResponse(data: Data) {
        guard let characteristic = findCharacteristic() else { return }
        peripheral.writeValue(data, for: characteristic,
                             type: .withoutResponse)
    }

    private func findCharacteristic() -> CBCharacteristic? {
        return peripheral.services?
            .flatMap { $0.characteristics ?? [] }
            .first(where: { $0.uuid == charUUID })
    }
}

Wybór typu zapisu withResponse lub withoutResponse zależy od wymagań dotyczących niezawodności. Dla komend (włącz światło, otwórz zamek) używaj withResponse — gwarancja dostarczenia jest krytyczna. Dla danych strumieniowych (tętno, temperatura) używaj withoutResponse — utrata jednego pakietu jest nieistotna. Urządzenie BLE może obsługiwać tylko jeden typ zapisu — sprawdzaj właściwość characteristic.properties.contains(.write) i .writeWithoutResponse.

Subskrypcja powiadomień BLE przez setNotifyValue

Powiadomienia (notifications) — mechanizm BLE, w którym urządzenie peryferyjne wysyła wartość charakterystyki do urządzenia centralnego asynchronicznie, bez ciągłego pollingu ze strony centralnego. CBPeripheral włącza subskrypcję przez metodę setNotifyValue:forCharacteristic:. Po aktywacji subskrypcji iOS automatycznie zapisuje CCCD (Client Characteristic Configuration Descriptor) na peryferii, a urządzenie zaczyna wysyłać aktualizacje przy każdej zmianie wartości.

W przeciwieństwie do indykacji (indicate), powiadomienia nie wymagają potwierdzenia od centralnego — pakiet jest wysłany i zapomniany. Daje to maksymalną przepustowość, ale możliwa jest utrata pakietów. Indykacje wymagają potwierdzenia na poziomie protokołu (L2CAP) — są bardziej niezawodne, ale wolniejsze. CBCharacteristic poprzez właściwość properties dokładnie wskazuje, który tryb obsługuje: .notify, .indicate lub oba.

Po rozłączeniu CBPeripheral (disconnect, wyjście z zasięgu) wszystkie aktywne subskrypcje są automatycznie resetowane. Przy ponownym połączeniu należy ponownie wywołać setNotifyValue:true dla każdej charakterystyki. iOS traci również subskrypcje przy wyjściu aplikacji z foreground (jeśli nie włączono trybu background) — do pracy w tle wymagane jest włączenie capability "Uses Bluetooth LE accessories" w Info.plist.

swift
// Zarządzanie subskrypcją powiadomień CBPeripheral
class NotificationManager: NSObject {

    private var peripheral: CBPeripheral?
    private var subscribedCharacteristics: Set<CBUUID> = []

    // Subskrybuj powiadomienia dla wszystkich charakterystyk .notify
    func subscribeToAllNotifications(peripheral: CBPeripheral) {
        self.peripheral = peripheral

        guard let services = peripheral.services else { return }

        for service in services {
            guard let characteristics = service.characteristics else { continue }

            for characteristic in characteristics {
                if characteristic.properties.contains(.notify)
                    || characteristic.properties.contains(.indicate) {
                    peripheral.setNotifyValue(true, for: characteristic)
                    subscribedCharacteristics.insert(characteristic.uuid)
                }
            }
        }
    }

    // Anuluj subskrypcję wszystkich powiadomień
    func unsubscribeFromAll() {
        guard let peripheral = peripheral else { return }
        guard let services = peripheral.services else { return }

        for service in services {
            guard let characteristics = service.characteristics else { continue }

            for characteristic in characteristics {
                if subscribedCharacteristics.contains(characteristic.uuid) {
                    peripheral.setNotifyValue(false, for: characteristic)
                }
            }
        }
        subscribedCharacteristics.removeAll()
    }

    // Handler powiadomień
    func peripheral(_ peripheral: CBPeripheral,
                     didUpdateNotificationStateFor characteristic: CBCharacteristic,
                     error: Error?) {
        if characteristic.isNotifying {
            print("Subscription active: \(characteristic.uuid)")
        } else {
            print("Subscription inactive: \(characteristic.uuid)")
        }
    }
}

Menedżer subskrypcji NotificationManager demonstruje poprawną pracę z powiadomieniami CBPeripheral. subscribeToAllNotifications przegląda wszystkie serwisy i charakterystyki, aktywując .notify i .indicate. subscribedCharacteristics śledzi aktywne subskrypcje dla poprawnego anulowania. didUpdateNotificationStateForCharacteristic potwierdza pomyślną zmianę stanu subskrypcji przez właściwość characteristic.isNotifying.

Pełny przykład pracy z CBPeripheral w Swift

Pełny cykl pracy z CBPeripheral obejmuje: otrzymanie obiektu od CBCentralManager, podłączenie, discovery, odczyt/zapis, subskrypcję powiadomień i rozłączenie. W przykładzie poniżej zaimplementowano klasę BLEConnection, która zarządza pełnym cyklem życia BLE-peryferii w Swift z użyciem nowoczesnego async/await API (iOS 15+).

swift
import CoreBluetooth

// Pełny przykład zarządzania CBPeripheral z async/await
class BLEConnection: NSObject {

    private let centralManager: CBCentralManager
    private var peripheral: CBPeripheral?
    private var continuation: CheckedContinuation<Void, Error>?

    override init() {
        centralManager = CBCentralManager(delegate: nil, queue: .main)
        super.init()
        centralManager.delegate = self
    }

    // 1. Połącz z urządzeniem peryferyjnym
    func connect(to peripheral: CBPeripheral) async throws {
        self.peripheral = peripheral
        peripheral.delegate = self
        centralManager.connect(peripheral, options: nil)

        try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation
        }
    }

    // 2. Wykrywanie  
    func discoverServices() async throws {
        guard let peripheral = peripheral else {
            throw BLEError.notConnected
        }
        peripheral.discoverServices(nil)
        try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation
        }
    }
}

// 3. CBCentralManager
extension BLEConnection: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        // Obsługa stanu urządzenia Bluetooth
    }

    func centralManager(_ central: CBCentralManager,
                        didConnect peripheral: CBPeripheral) {
        continuation?.resume()
        continuation = nil
    }

    func centralManager(_ central: CBCentralManager,
                        didFailToConnect peripheral: CBPeripheral,
                        error: Error?) {
        continuation?.resume(throwing: error ?? BLEError.connectionFailed)
        continuation = nil
    }
}

enum BLEError: Error {
    case notConnected
    case connectionFailed
    case serviceNotFound
    case characteristicNotFound
}

Klasa BLEConnection używa Swift Concurrency (async/await) przez CheckedContinuation — nowoczesny wzorzec do pracy z delegatowymi API Core Bluetooth. connect(to:) oczekuje potwierdzenia połączenia przez didConnectPeripheral, discoverServices() — przez didDiscoverServices. Takie podejście eliminuje zagnieżdżone delegaty i czyni kod BLE liniowym i czytelnym. Obsługa błędów przez BLEError pokrywa wszystkie typowe scenariusze awarii połączenia BLE.

Często zadawane pytania

Jak uzyskać CBPeripheral bez skanowania?

CBPeripheral dla wcześniej podłączonego urządzenia można uzyskać przez retrievePeripheralsWithIdentifiers: na CBCentralManager. Przekaż tablicę UUID (NSUUID) wcześniej zapisanych urządzeń — framework zwróci tablicę CBPeripheral dla urządzeń w systemowej bazie BLE-bondingu. Działa to tylko dla urządzeń, z którymi iPhone był wcześniej sparowany. Dla nowego urządzenia skanowanie jest obowiązkowe.

Dlaczego CBPeripheral nie wykrywa serwisów?

Główne przyczyny: urządzenie jest poza zasięgiem (RSSI poniżej progu), radio BLE jest wyłączone (CBCentralManager.state != .poweredOn), delegat CBPeripheralDelegate nie jest ustawiony (peripheral.delegate = self) lub wywołanie discoverServices nastąpiło przed podłączeniem. Sprawdź status centralManager.state, upewnij się, że delegat jest ustawiony przed wywołaniem connect i użyj retry z timeoutem 5–10 sekund.

Co zrobić, gdy writeValue nie odpowiada?

Przyczyna — użycie .withResponse na charakterystyce obsługującej tylko .writeWithoutResponse lub odwrotnie. Sprawdź characteristic.properties przed wywołaniem. Możliwy jest również problem MTU: jeśli dane > 20 bajtów (BLE 4.0 MTU), konieczne jest uzgodnienie MTU przez negotiateMTU lub fragmentacja. Użyj peripheral.maximumWriteValueLength(for: .withResponse) do określenia maksymalnego rozmiaru pakietu.

Jak odróżnić CBPeripheral w zasięgu od niedostępnego?

CBPeripheral poza zasięgiem nie rozłącza się natychmiast — iOS przełącza go w stan .disconnected po timeoutie (zwykle 20–30 sekund). Do monitorowania używaj readRSSI na CBPeripheral — przy niedostępności zwróci błąd z kodem CBError.connectionTimeout. Śledź również centralManager:didDisconnectPeripheral:error: dla szybkiego wykrycia zerwania połączenia.

Czy można używać jednego CBPeripheral z wielu wątków?

Core Bluetooth nie jest bezpieczny wątkowo — wszystkie wywołania CBPeripheral muszą być wykonywane z jednej kolejki (zwykle main queue lub sekwencyjna serial queue, określona przy inicjalizacji CBCentralManager). Równoczesne wywołania z różnych wątków prowadzą do race condition i crashu aplikacji. Używaj DispatchQueue(label: "com.app.ble") dla wszystkich operacji BLE i DispatchQueue.main.async do aktualizacji UI.

Podsumowanie

  • CBPeripheral — klasa Core Bluetooth do pracy ze zdalnym urządzeniem BLE na iOS, zwracana przez CBCentralManager
  • Hierarchia GATT składa się z serwisów (CBService), charakterystyk (CBCharacteristic) i deskryptorów (CBDescriptor) z 16-bit lub 128-bit UUID
  • Discovery wykonuje się sekwencyjnie: discoverServices: → discoverCharacteristics:forService: z obsługą przez delegat
  • Odczyt — readValueForCharacteristic:, zapis — writeValue:forCharacteristic:type: (.withResponse lub .withoutResponse)
  • Powiadomienia — setNotifyValue:forCharacteristic: aktywuje asynchroniczne wysyłanie danych z peryferii do centrali
  • MTU BLE 4.0 ogranicza pakiet do 23 bajtów, BLE 5.0+ — do 251 bajtów, dane większe niż MTU wymagają fragmentacji
  • Async/await Swift przez CheckedContinuation upraszcza kod BLE, zastępując zagnieżdżone delegaty liniowymi wywołaniami

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również