iOS Bluetooth Guide 3: Thực thi các task cơ bản của Peripheral
Ở phần trước, chúng ta đã tìm hiểu được cách thực thi các task cơ bản của Central. Lần này, chúng ta sẽ đóng vai trò Peripheral, cụ thể chúng ta sẽ đi thực hiện các việc sau: Khởi tạo một peripheral manager object. Thiết lập các service và characteristic trên thiết bị. Publish các service và ...
Ở phần trước, chúng ta đã tìm hiểu được cách thực thi các task cơ bản của Central. Lần này, chúng ta sẽ đóng vai trò Peripheral, cụ thể chúng ta sẽ đi thực hiện các việc sau:
- Khởi tạo một peripheral manager object.
- Thiết lập các service và characteristic trên thiết bị.
- Publish các service và characteristic tới database của thiết bị.
- Phát tán các service.
- Trả lời các yêu cầu đọc và ghi từ các central được kết nối.
- Gửi các giá trị của characteristic cho các central đã đăng ký (subscribe).
Phần code trong chapter này rất đơn giản và mang tính trừu tượng. Bạn cần phải thay đổi lại cho tương ứng với code của bạn khi làm ứng dụng thật. Các vấn đề nâng cao về việc thực thi vai trò peripheral sẽ được viết ở các chapter sau.
Bước đầu tiên để thực thi vai trò của một peripheral là khởi tạo peripheral manager object. Một pheripheral manager được thể hiện trong Core Bluetooth dưới class CBPeripheralManager. Ta khởi tạo như sau:
let pheripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: nil)
Trong ví dụ trên, self được chỉ đinh làm delegate để nhận các event bắn ra từ peripheral. Tiếp theo, tham số queue truyền nil có nghĩa là pheripheral manager sẽ dispatch các event vào main queue.
Khi bạn khởi tạo, peripheral manager sẽ gọi peripheralManagerDidUpdateState(:) từ delegate. Chúng ta phải thực thi hàm này để đảm bảo rằng thiết bị đang sử dụng có hỗ trợ Bluetooth. Và đây là hàm bắt buộc khi conform photocol CBPeripheralManagerDelegate.
Như đã thể hiện trên hình, cơ sở dữ liệu của các services và characteristic được thể hiện theo cấu trúc hình cây và đây chính là mô hình mà chúng ta sử dụng để đi thiết lập mấy cái thứ này.
Service và Characteristic được định danh bằng UUID
UUID là một chuỗi nhị phân dài 128bit. Nó được thể hiện trong Core Bluetooth dưới dạng CBUUID object.
Bluetooth SIG (Bluetooth Special Interest Group) đã định nghĩa sẵn một bộ UUID để định danh cho một số service mặc định. Ngoài ra để thuận tiện, bộ mã này được cung cấp thêm bản rút gọn 16bit. Ví dụ, service đo nhịp tim có mã 128bit đầy đủ là 0000180D-0000-1000-8000-00805F9B34FB. Với UUID 16bit rút gọn, service đo nhịp tim có mã 180D.
Với các UUID mặc định được Bluetooth SIG định nghĩa, ta có thể dùng dạng rút gọn của nó để khởi tạo CBUUID object. Ví dụ:
let heartRateServiceUUID = CBUUID(string: "180D")
Khi khởi tạo CBUUID object với dạng 16bit UUID, Core Bluetooth sẽ tự điền phần còn lại của chuỗi đầy đủ 128bit dựa trên tài liệu chuẩn "Bluetooth base UUID".
Tự tạo UUID cho các custom service và charateristic
Chúng ta có thể tự tạo ra các service và characteristic của mình, khi đó, ta cần tạo ra chuỗi 128bit UUID riêng để định danh. Và để làm thế chúng ta có thể sử dụng lệnh uuidgen trên terminal để gen ra một giá trị 128bit duy nhất ở dạng chuỗi ASCII.
$ uuidgen 47DFC6AB-D093-468B-9FAB-9396B57D31F0
Sau đó ta có thể sử dụng chuỗi UUID này để tạo CBUUID object như sau:
let myCustomServiceUUID = CBUUID(string: "47DFC6AB-D093-468B-9FAB-9396B57D31F0")
Xây dựng một cây service và characteristic
Sau khi ta có UUID, chúng ta có thể tạo các CBMutableService và CBMutableCharacteristic và tổ chức chúng dưới dạng cấu trúc cây như hình trên. Ví dụ, nếu ta có UUID của một characteristic, ta có thể tạo ra một CBMutableCharacteristic như sau:
let myCharacteristic = CBMutableCharacteristic(type: myCharacteristicUUID, properties: .read, value: myValue, permissions: .readable)
Ta cần phải thiết lập properties, value và permissions cho hàm khởi tạo.
Properties và permissions ta thiết lập để xác định xem là characteristic có thể đọc hoặc ghi hay không, rồi các central có thể subscribe characteristic hay không. Ở ví dụ trên, characteristic được thiết lập để có thể đọc bởi central.
Chú ý: Nếu ta chỉ định value cho characteristic, giá trị đó sẽ được cache, đồng thời properties và permissions sẽ được tự động thiết lập ở dạng readable. Do đó, nếu ta muốn characteristic có thể ghi được, hoặc có thể thay đổi được, thì phải chỉ định value là nil. Cách tiếp cận này đảm bảo việc value sẽ được xử lý động và tất nhiên là phải thông qua peripheral manager khi nhận được một yêu cầu đọc hay ghi nào đó.
Chúng ta đã có characteristic, tiếp theo là chúng ta sẽ tạo ra một CBMutableService để có thể gắn characteristic vào. Cụ thể như sau:
let myService = CBMutableService(type: myServiceUUID, primary: true)
Trong ví dụ trên, tham số thứ hai được gán là true, để chỉ định rằng service này là primary (chính) hay secondary (phụ). Một primary service sẽ chứa các tính năng chính của thiết bị và có thể được tham chiếu tới từ các service khác. Một secondary service sẽ chưa các service mà chỉ liên quan đến ngữ cảnh nào đó của một service khác mà tham chiếu tới nó. Ví dụ: một primary service của một máy đo nhịp tim có thể xuất ra dữ liệu nhịp tim từ sensor của thiết bị, trong khi một secondary service có thể là xuất ra dữ liệu về pin.
Sau khi tạo service, ta có thể gắn characteristic với service như sau:
myService.characteristics = [myCharacteristic]
Sau khi xây dựng và kết nối xong các service và characteristic, công việc tiếp theo là publish cái đống đó vào database của thiết bị. Sử dụng Core Bluetooth để làm cái việc này khác là dễ như sau:
myPheripheralManager.add(myService)
Khi ta gọi phương thức này để publish services, peripheral manager sẽ gọi hàm peripheralManager(:didAdd:error:) từ delegate. Nếu service không thể publish thì sẽ có lỗi xảy ra, ta cần implement hàm này để có thể xử lý lỗi, như sau:
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { if let error = error { print("Error publishing service: (error.localizedDescription)") } //... }
Chú ý: Sau khi publish service tới database của thiết bị, service sẽ được cache và ta không thể thay đổi nó nữa.
Sau khi publish thành công là chúng ta có thể tiến hành phát tán tới các central đang lắng nghe. Để thực hiện điều đó ta làm như sau:
myPheripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [myFirstService.uuid, mySecondService.uuid]])
Trong ví dụ trên, cái key CBAdvertisementDataServiceUUIDsKey theo sau nó là một mảng các CBUUID thể hiện UUID của các service mà ta muốn phát tán. Có một vài key khác mà chúng ta có thể đưa vào dictionary được mô tả chi tiết ở mục Advertisement Data Retrieval Keys trong CBCentralManagerDelegate Protocol Reference. Tuy nhiên, chỉ có 2 key được support cho peripheral manager object đó là CBAdvertisementDataLocalNameKey và CBAdvertisementDataServiceUUIDsKey.
Khi ta bắt đầu phát tán dữ liệu, peripheral manager sẽ gọi hàm peripheralManagerDidStartAdvertising(:error:) từ delegate. Nếu có lỗi xảy ra, ta sẽ nhận được trong hàm này:
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) { if let error = error { print("Error advertising: %@", error.localizedDescription) } //... }
Chú ý: Dữ liệu phát tán theo kiểu "best effort", vì không gian giới hạn và có thể có nhiều ứng dụng cùng phát tán. Các hành động phát tán cũng bị ảnh hưởng khi ứng dụng hoạt động ở chế độ nền. Vấn đề này sẽ được bàn luận ở chapter tiếp theo.
Sau khi phát tán, central có thể tìm thấy và khởi tạo kết nối với thiết bị của ta.
Sau khi kết nối với central, ta có thể nhận được các yêu cầu đọc và ghi dữ liệu. Khi đó, ta cần trả lời các yêu cầu với các hành động tương ứng. Ví dụ sau sẽ mô tả cách xử lý các yêu cầu như vậy.
Khi một central gửi yêu cầu đọc giá trị của một characteristic, peripheral manager sẽ gọi hàm peripheralManager(:didReceiveRead:) từ delegate. Hàm này sẽ chuyển cho ta các request dưới dạng CBATTRequest object. Nó sẽ chứa một số các thông tin giúp ta hoàn thành các request.
Ví dụ, khi ta nhận một request đơn giản như đọc giá trị của môt characteristic, bước đầu tiên ta phải làm là kiểm tra thông tin trong CBATTRequest object mà ta nhận được có tương ứng với characteristic trong thiết bị không. Ta bắt đầu thực thi như sau:
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) { if request.characteristic.uuid == myCharacteristic.uuid { //... } }
Nếu UUID của characteristic trùng, bước tiếp theo là kiểm tra xem request đọc có chỉ định index một cách hợp lệ không, tránh lỗi out of range khi đọc giá trị từ characteristic. Để làm vậy, ta sử dụng thuộc tính offset trong CBATTRequest để kiểm tra:
if request.offset > myCharacteristic.value!.count { myPheripheralManager.respond(to: request, withResult: CBATTError.Code.invalidOffset) }
Nếu offset không có vấn đề gì, tiếp theo chúng ta sẽ gán giá trị characteristic của ta cho giá trị của request
let range = Range(NSRange(location: request.offset, length: myCharacteristic.value!.count - request.offset)) request.value = myCharacteristic.value!.subdata(in: range!)
Sau khi thiết lập giá trị, ta trả lời lại central, và thế là request đã hoàn thành. Để làm vậy ta thực thi như sau:
myPeripheralManager.respond(to: request, withResult: .success)
Ta chỉ nên gọi hàm respond(to:withResult:) đúng một lần mỗi khi peripheralManager(:didReceiveRead:) được gọi.
Chú ý: Nếu UUID không trùng khớp, hoặc việc đọc giá trị không thể thực hiện vì một lý do nào đó khác, ta không nên cố hoàn thành request. Thay vào đó, ta cần phải gọi hàm respond(to:withResult:) và trả về lỗi, danh sách lỗi tham khảo tại CBATTError Constants
Đối với việc xử lý request ghi, khi nhận được từ central, peripheral manager sẽ gọi hàm peripheralManager(:didReceiveWrite:) từ delegate. Lần này thì request đưa tới được đặt vào một mảng CBATTRequest, mỗi một object thể hiện một request. Sau khi đảm bảo rằng request có thể hoàn thành, ta sẽ gán giá trị của request cho giá trị characteristic của mình:
myCharacteristic.value = request.value
Và tương tự như khi trả lời request đọc, ta cũng gọi hàm respond(to:withResult:) để trả lời request ghi. Nên chọn request để trả về là request đầu tiền trong mảng các requests:
myPeripheralManager.respond(to: requests.first!, withResult: .success)
Chú ý: Nên thực hiện xử lý nhiều request như đối với một request, có nghĩa là nếu có bất cứ một request nào trong mảng không thể hoàn thành, toàn bộ các requests là không thể hoàn thành. Trong trường hợp lỗi đó, ta cũng vẫn gọi hàm respond(to:withResult:) nhưng trả lại lỗi.
Thông thường, central sẽ subscribe một hoặc nhiều characteristic, như mô tả ở chapter trước. Khi làm vậy, ta có trách nhiệm gửi notification khi có một giá trị nào đó (mà central subscribe) thay đổi. Sau đây là cách thực hiện: Khi central subscribe giá trị của một characteristic nào đó, peripheral manager sẽ gọi hàm peripheralManager(:central:didSubscribeTo:) từ delegate:
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { print("Central subscribed to characteristic (characteristic)") //... }
Hàm delegate trên được coi là tín hiệu bắt đầu để gửi giá trị cập nhật cho central. Sau đó, bất cứ khi nào giá trị của characteristic được cập nhật, ta sẽ ra tín hiệu gửi cho central bằng việc gọi hàm updateValue(:for:onSubscribedCentrals:) của peripheral manager object.
let updatedValue = myCharacteristic.value let didSendValue = myPeripheralManager.updateValue(updatedValue!, for: myCharacteristic, onSubscribedCentrals: nil)
Ta có thể chỉ định cụ thể central nào sẽ nhận được tín hiệu này bằng việc gán CBCentral object ở tham số cuối cùng khi gọi hàm, còn khi gán nil là toàn bộ các central đang subscribe sẽ được thông báo.
Hàm updateValue(:for:onSubscribedCentrals:) trả về một giá trị Boolean xác định xem việc gửi thông tin update tới các central có thành công hay không, nếu hàng đợi dùng để gửi giá trị update bị đầy, hàm sẽ trả về false. Khi có thêm khoảng trống từ hàng đợi, Peripheral manager sẽ gọi hàm peripheralManagerIsReady(toUpdateSubscribers:) từ delegate . Ta có thể thực thi hàm này để gửi lại giá trị, đơn giản bằng việc gọi lại hàm updateValue(:for:onSubscribedCentrals:) một lần nữa.
Chú ý: Ta nên sử dụng hàm updateValue(:for:onSubscribedCentrals:) chỉ một lần khi cập nhật, và gửi toàn bộ thông tin qua notification đó (không tách làm nhiều lần gọi)
Dựa trên kích cỡ của giá trị characteristic, không phải tất cả dữ liệu có thể gửi bằng notification. Nếu xảy ra, trường hợp này tốt nhất là xử lý bên central thông qua việc gọi hàm readValue(for:) của CBPeripheral, để có thể nhận được toàn bộ dữ liệu.
Chapter 3 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 peripheral, cùng với đó là một số lưu ý. 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