Thiết bị central có một số task cơ bản, ví dụ như tìm và kết nối tới các peripheral, sau đó sẽ đọc và tương tác với dữ liệu của peripheral đó. Còn thiết bị peripheral cũng có một số task cơ bản như là cung cấp, phát tán các service của nó, và trả lời các request từ central. Trong bài này, chúng ta sẽ đi tìm hiểu cách làm việc với Core Bluetooth framework để thực thi các nhiệm vụ cơ bản nhất của phía central. Cụ thể các nhiệm vụ mà ta đi nghiên cứu tìm hiểu là:

  • Khởi tạo central manager object
  • Tìm kiếm và kết nối tới một peripheral nào đó đang phát sóng.
  • Đọc dữ liệu trên peripheral sau khi kết nối thành công.
  • Gửi các request đọc và ghi một characteristic nào đó của các dịch vụ trong peripheral.
  • Đăng ký nhận thông báo khi một characteristic cập nhật.

Chú ý: Code trong chapter này chỉ là câu lệnh đơn giản và có phần trừu tượng, để tích hợp vào ứng dụng thật, bạn cần thay đổi sao cho hợp lý với yêu cầu của ứng dụng.

Khởi tạo Central Manager

CBCentralManager là một class trong Core Bluetooth, nó mô tả hay kiểu như là trừu tượng hóa thiết bị central. Để làm việc với Bluetooth, trước hết chúng ta cần có một instance của lớp này. Ta khởi tạo bằng câu lệnh:

let myCentralManager = CBCentralManager(delegate: self, queue: nil)

Trong câu lệnh trên, self được gán làm delegate và sẽ nhận các sự kiện bắn về từ CBCentralManager. Và khi ta gán queue bằng nil, các sự kiện bắn về được gửi vào main queue. Khi khởi tạo central manager, central manager sẽ gọi hàm centralManagerDidUpdateState(_:) từ delegate. Do đó bắt buộc bạn phải implement hàm này, để kiểm tra xem Bluetooth có được thiết bị hỗ trợ hay không. Chi tiết tham khảo thêm tại CBCentralManagerDelegate Protocol Reference

Tìm kiếm peripheral đang phát sóng

Sau khi khởi tạo, nhiệm vụ đầu tiên của centralManager là đi tìm peripheral. Như đã giới thiệu ở bài 1, peripheral phát ra sóng để được nhận biết. Để tìm kiếm các peripheral đang phát sóng gần thiết bị, ta gọi như sau:

centralManager.scanForPeripherals(withServices: nil, options: nil)

Chú ý: Khi gán nil cho services, hàm sẽ trả về tất cả các peripheral nó tìm thấy. Tất nhiên là chỉ các thiết bị được hỗ trợ. Trong thực tế, chúng ta thường gán một mảng CBUUID (Core Bluetooth UUID) để truyền vào, mỗi CBUUID thể hiện một UUID (Universally Unique Identifier) của service mà peripheral đang phát ra. Khi đó, hàm chỉ trả về các peripheral mà cung cấp các service tương ứng với UUID, như vậy bạn sẽ lọc được những thiết bị cần cho bài toán của bạn. Chúng ta sẽ nói nhiều hơn về CBUUID và UUID ở chapter tiếp theo. Mỗi khi central manager tìm thấy một peripheral, nó sẽ gọi hàm centralManager(_:didDiscover:advertisementData:rssi:) từ delegate. Peripheral được tìm thấy sẽ được trừu tượng dưới một đối tượng CBPeripheral. Nếu đây là peripheral mà bạn cần phải kết nối tới thì nhớ tạo một strong referrence cho nó không thì sẽ bị deallocate. Ví dụ thường thấy:

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,  advertisementData: [String : Any], rssi RSSI: NSNumber) {
    self.peripheral = peripheral
}

Khi không cần tìm nữa, bạn nên gọi hàm stopScan() để dừng việc tìm kiếm và giúp tiết kiệm điện.

centralManager.stopScan()

Kết nối tới peripheral sau khi tìm thấy

Sau khi tìm thấy peripheral thì việc bây giờ là kết nối tới nó, để làm thế, chúng ta gọi hàm connect(_:options:) của central manager, truyền vào peripheral tìm được:

centralManager.connect(peripheral, options: nil)

Nếu kết nối thành công, central manager sẽ gọi hàm centralManager(_:didConnect:) từ delegate

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("Connected to \(peripheral.name ?? "Unknown Name")")
    peripheral.delegate = self
}

Và trước khi làm việc với peripheral, ta cần gán delegate cho nó:

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    peripheral.delegate = self
}

Tìm kiếm các service mà peripheral cung cấp

Sau khi kết nối thành công là ta có thể đọc được dữ liệu của nó. Bước đầu tiên là phải tìm ra peripheral đang cung cấp các service nào. Tuy gói tin dữ liệu của peripheral khi phát ra đã bao gồm các service, tuy nhiêu vì gói tin luôn chỉ có giới hạn dung lượng, chúng ta có thể mò xem nó liệu có nhiều service hơn không. Ta gọi hàm discoverServices() từ peripheral:

peripheral.discoverServices(nil)

Chú ý: Trong các ứng dụng thực tế, chúng ta thường sẽ không truyền nil vào biến như câu lệnh trên. Truyền nil là một cách để lấy ra tất cả các service có trên peripheral, như vậy sẽ gây ra tốn pin và thời gian chờ đợi truyền tải dữ liệu giữa hai thiết bị sẽ rất lâu. Thế nên, ta thường chỉ truyền UUID của các service à ta quan tâm đến.

Khi tìm kiếm thành công, peripheral sẽ gọi hàm peripheral(_:didDiscoverServices:) từ delegate. Core Bluetooth tạo ra một mảng CBService, để thể hiện các service được tìm thấy trong peripheral.

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    for service in peripheral.services! {
        print("Discovered service \(service)")
    }
}

Tìm kiếm các characteristic của một service

Khi tìm thấy service cần quan tâm, bước tiếp theo là tìm kiếm các characteristic của service. Đơn giản ta có thể gọi hàm discoverCharacteristics(_:for:)từ peripheral:

peripheral.discoverCharacteristics(nil, for: interestingService)

Cũng tương tự như khi ta tìm các service, khi tìm kiếm thành công, peripheral sẽ gọi hàm peripheral(_:didDiscoverCharacteristicsFor:error:) từ delegate. Core Bluetooth trả về một mảng CBCharacteristic, để thể hiện các characteristic tìm được.

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    for characteristic in service.characteristics! {
        print("Discovered characteristic \(characteristic)")
    }
}

Lấy giá trị của một characteristic

Một characteristic sẽ có một giá trị nào đó thể hiện thông tin của service. Ví dụ characteristic đo nhiệt độ của một service nhiệt kế sức khỏe có thể sẽ chỉ định một giá trị độ C nào đó. Ta có thể lấy giá trị này về bằng việc đọc trực tiếp characteristic hoặc đăng ký nhận thông báo.

Đọc giá trị của một characteristic

Sau khi tìm thấy một characteristic, bạn có thể đọc trực tiếp giá trị characteristic bằng cách gọi hàm readValue(for:) của peripheral

peripheral.readValue(for: interestingCharacteristic)

Cơ chế tương tự như các phần trước, peripheral sẽ gọi hàm peripheral(_:didUpdateValueFor:error:) từ delegate. Nếu đọc thành công, ta có thể truy cập giá trị đó được như sau:

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    let data = characteristic.value
    // parse the data as needed
} 

Chú ý: Không phải tất cả các characteristic đều đọc được. Ta có thể xác định characteristic có thể đọc được không bằng các kiểm tra nếu thuộc tính properties của nó có chứa hằng CBCharacteristicProperties.read không. Nếu ta cố gắng đọc giá trị của một characteristic không thể đọc, hàm delegate sẽ trả về lỗi.

Đăng ký nhận thông báo một characteristic

Việc đọc trực tiếp giá trị của characteristic chỉ phù hợp với các giá trị tĩnh. Tuy nhiên đây không phải cách hiệu quả nếu muốn lấy các giá trị động. Lấy về giá trị charateristic thay đổi theo thời gian, ví dụ như nhịp tim, ta cần phải đăng ký nhận thông báo, hay còn gọi là subscribe. Khi subscribe một characteristic, ta sẽ nhận được thông báo từ peripheral khi giá trị thay đổi. Ta subcribe một charateristic bằng cách gọi hàm setNotifyValue(_:for:) của peripheral như sau:

peripheral.setNotifyValue(true, for: interestingCharacteristic)

Vẫn tương tự cơ chế trên, sau khi ta subcribe hoặc unsubscribe một characteristic, peripheral sẽ gọi hàm peripheral(_:didUpdateNotificationStateFor:error:) từ delegate, nếu có lỗi hàm delegate sẽ gửi về và ta có thể xử lý trong đó.

func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
    if let error = error {
        print("Error changing notification state: \(error.localizedDescription ?? "")")
    }
    let data = characteristic.value
    // parse the data as needed
}

Chú ý: Cũng như việc đọc trực tiếp, không phải tất cả các characteristic đều cung cấp khả năng subcribe. Chúng ta có thể xác định xem characteristic có cung cấp subscibe không bằng các kiểm tra thuộc tính properties của nó có chứa một trong hai hằng số CBCharacteristicProperties.notify hoặc CBCharacteristicProperties.indicate không. Sau khi subscribe, mỗi khi giá trị của characteristic có thay đổi, peripheral sẽ thông báo cho app biết bằng cách gọi hàm peripheral(_:didUpdateNotificationStateFor:error:) từ delegate.

Ghi giá trị vào một characteristic

Sẽ có một lúc nào đó ta cần phải ghi giá trị vào characteristic. Ví dụ, nếu ứng dụng của bạn tương tác với một máy điều hòa, bạn có thể sẽ phải cung cấp cho máy điều hòa một giá trị nhiệt độ mà bạn mong muốn để máy có thể điều chỉnh lại nhiệt độ phòng tương ứng. Nếu characteristic là dạng có thể ghi được, chúng ta có thể truyền vào một giá trị thể hiện dưới dạng Data bằng cách gọi hàm writeValue(_:for:type:) từ peripheral, như sau:

peripheral.writeValue(dataToWrite, for: interestingCharacteristic, type: .withResponse)

Ở đây khi gọi hàm ghi giá tri, chúng ta sẽ phải truyền vào kiểu ghi. Nếu kiểu ghi là .withResponse, khi ghi thành công, peripheral sẽ gọi hàm peripheral(_:didWriteValueFor:error:) từ delegate. Và chúng ta sẽ đi implement hàm này để kiểm tra và xử lý lỗi, như sau:

func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
    if let error = error {
        print("Error writing characteristic value \(error.localizedDescription)")
    }
}

Nếu ta chỉ định kiểu ghi là .withoutResponse, việc ghi sẽ diễn ra theo kiểu best-effort, và ta chẳng thể biết là nó có hoàn thành hay không. Chi tiết về các kiểu ghi được hỗ trợ, tham khảo CBPeripheral Class Reference Chú ý: Characteristic có thể chỉ hỗ trợ một vài kiểu ghi nhất định, hoặc không cho ghi. Để xác định điều này, ta kiểm tra xem thuộc tính properties của nó có bao gồm một trong các hằng số CBCharacteristicProperties.writeWithoutResponse hoặc CBCharacteristicProperties.write không.


Kết

Chapter 2 chủ yếu hướng dẫn bạn sử dụng API mà framework cung cấp, để thực thi với vai trò như một central. Trên đây là các common task, để chi tiết hơn, các bạn có thể vọc vạch thêm trong API để học hỏi.


Dịch và chỉnh sửa từ Core Bluetooth Programming Guide