Recentemente um amigo me apresentou uma empresa que fornece um serviço de monitoramento dos dados do carro através de um dispositivo Bluetooth conectado a um smartphone. Ao ver o produto fiquei curioso em entender como o iOS se comunica com um dispositivo Bluetooth. No iOS o framework Core Bluetooth é o responsável pela comunicação com estes dispositivos.
Atualmente existem centenas de dispositivos conectados, principalmente na área de fitness e automação residencial. A Apple vem investindo nesta área a alguns anos e o iOS já conta com os aplicativos Saúde e Casa. Podemos encontrar vários aplicativos na Apple Store de dispositivos que se conectam ao iOS.
Neste artigo iremos desenvolver um aplicativo que irá se comunicar com outro dispositivo executando as funções de “cliente” e “servidor”. Aproveitaremos para explorar as classes do Core Bluetooth.
O artigo foi baseado na documentação Core Bluetooth Programming Guide da Apple.
Visão Geral do Core Bluetooth
O framework Core Bluetooth permite que seus aplicativos iOS e Mac se comuniquem com dispositivos Bluetooth de baixo consumo. Por exemplo, seu aplicativo pode descobrir, explorar e interagir com dispositivos periféricos de baixo consumo, como monitores de freqüência cardíaca, termostatos digitais e até mesmo outros dispositivos iOS.
O framework é uma abstração da especificação Bluetooth 4.0 para uso com dispositivos de baixo consumo. Dito isto, ele esconde muitos dos detalhes de baixo nível da especificação de você, o desenvolvedor, tornando muito mais fácil para você desenvolver aplicativos que interagem com dispositivos Bluetooth de baixo consumo. Como o framework é baseado na especificação, alguns conceitos e terminologias da especificação foram adotados. Este capítulo apresenta os termos e conceitos-chave que você precisa saber para começar a desenvolver excelentes aplicativos usando o framework Core Bluetooth.
Dispositivos Central e Periférico e Seus Papéis na Comunicação Bluetooth
Há dois indivíduos principais envolvidos na comunicação de dispositivos Bluetooth de baixo consumo: a central (central) e o periférico (peripheral). Baseado na arquitetura cliente-servidor tradicional, o periférico tipicamente possui dados que são necessários a outros dispositivos. A central tipicamente utiliza a informação fornecida por um periférico para realizar alguma tarefa. A imagem abaixo mostra o exemplo de um monitor de frequência cardíaca que pode ter informações úteis que o seu aplicativo Mac ou iOS precisa pra mostrar os batimentos cardíacos do usuário de uma forma amigável.
Centrais Descobrem e Conectam-se a Periféricos que Estão Anunciando
Periféricos transmitem alguns dos dados que possuem na forma de pacotes de anúncios. Um pacote de anúncio é um conjunto de dados relativamente pequeno que pode conter informações úteis sobre o que o periférico tem a oferecer, tais como o nome do periférico e sua função principal. Por exemplo, um termostato digital pode anunciar que fornece a temperatura atual de uma sala. No Bluetooth de baixo consumo, anunciar é a forma principal de um periférico informar a sua presença.
Uma central, por outro lado, pode escanear e escutar qualquer dispositivo periférico que esteja anunciando informações que lhe interessem, como mostrado na imagem abaixo. Uma central pode pedir para se conectar a qualquer periférico que tenha descoberto.
Como a Informação de um Periférico é Estruturada
O objetivo de se conectar a um periférico é começar a explorar e interagir com os dados que ele tem a oferecer. Antes que você possa fazer isso, no entanto, ajuda entender como os dados de um periférico são estruturados.
Os periféricos podem conter um ou mais serviços ou fornecer informações úteis sobre a intensidade do sinal de conexão. Um serviço é uma coleção de dados e comportamentos associados para realizar uma função ou recurso de um dispositivo (ou partes desse dispositivo). Por exemplo, um serviço de um monitor de frequência cardíaca pode expor os dados de frequência cardíaca do sensor do monitor de frequência cardíaca.
Os próprios serviços são constituídos por características ou serviços incluídos (ou seja, referências a outros serviços). Uma característica fornece detalhes adicionais sobre o serviço de um periférico. Por exemplo, o serviço de frequência cardíaca que acabou de ser descrito pode conter uma característica que descreve a localização pretendida do dispositivo do sensor de frequência cardíaca no corpo e outra característica que transmite dados de medição de frequência cardíaca. A imagem a seguir ilustra uma possível estrutura do serviço e características do monitor de freqüência cardíaca.
Centrais Exploram e Interagem com os Dados em um Periférico
Depois que uma central estabelece uma conexão com um periférico com êxito, ela pode descobrir toda a gama de serviços e características que o periférico tem para oferecer (os dados anunciados podem conter apenas uma fração dos serviços disponíveis).
Uma central também pode interagir com o serviço de um periférico lendo ou escrevendo o valor da característica desse serviço. Por exemplo, seu aplicativo pode solicitar a temperatura ambiente atual de um termostato digital, ou pode fornecer ao termostato um valor para definir a temperatura do quarto.
Como Centrais, Periféricos e os Dados dos Periféricos São Representados
Os principais indivíduos e dados envolvidos na comunicação Bluetooth de baixo consumo são mapeados para o framework do Core Bluetooth de forma simples e direta.
Objetos no Lado da Central
Quando você estiver usando uma central local para interagir com um periférico remoto, você está executando ações no lado central da comunicação Bluetooth de baixo consumo. A menos que você esteja configurando um dispositivo periférico local – e usá-lo para responder às solicitações de uma central – a maioria das suas transações Bluetooth ocorrerá no lado central.
Centrais Locais e Periféricos Remotos
No lado central, um dispositivo central local é representado por um objeto CBCentralManager
. Esses objetos são usados para gerenciar dispositivos periféricos remotos descobertos ou conectados (representados por objetos CBPeripheral
), incluindo varredura, descoberta e conexão a periféricos de publicidade. A imagem a seguir mostra como as centrais locais e os periféricos remotos são representados no framework Core Bluetooth.
<img src="undefined" alt="Core Bluetooth objects on the central side” width=”441″ height=”195″ />
Os Dados do Periférico Remoto São Representados pelos Objetos CBService e CBCharacteristics
Quando você está interagindo com os dados em um periférico remoto (representado por um objeto CBPeripheral
), você está lidando com seus serviços e características. No framework Core Bluetooth, os serviços de um periférico remoto são representados por objetos CBService
. Da mesma forma, as características de um serviço de um periférico remoto são representadas por objetos CBCharacteristic
. A imagem a seguir ilustra a estrutura básica dos serviços e características de um periférico remoto.
Objetos no lado Periférico
A partir do OS X v10.9 e iOS 6, os dispositivos Mac e iOS podem funcionar como periféricos Bluetooth de baixo consumo, servindo dados para outros dispositivos, incluindo outros dispositivos Mac, iPhone e iPad. Ao configurar seu dispositivo para implementar a função de periférico, você está executando ações no lado periférico da comunicação Bluetooth de baixo consumo.
Periféricos Locais e Centrais Remotas
No lado periférico, um dispositivo periférico local é representado por um objeto CBPeripheralManager
. Esses objetos são usados para gerenciar serviços publicados no banco de dados de serviços e características do dispositivo periférico local e para anunciar esses serviços para dispositivos centrais remotos (representados por objetos CBCentral
). Objetos de gerenciador de periféricos também são usados para responder a solicitações de leitura e gravação dessas centrais remotas. A abaixo mostra como os periféricos locais e centrais remotas são representados no framework Core Bluetooth.
Os Dados dos Periféricos Locais São Representados pelos Objetos CBMutableService e CBMutable Characteristics
Quando você está configurando e interagindo com os dados em um periférico local (representado por um objeto CBPeripheralManager
), você está lidando com versões mutable de seus serviços e características. No framework Core Bluetooth, os serviços de um periférico local são representados por objetos CBMutableService
. Da mesma forma, as características do serviço de um periférico local são representadas por objetos CBMutableCharacteristic
. A imagem a seguir ilustra a estrutura básica dos serviços e características de um periférico local.
Construindo o aplicativo
O nosso aplicativo terá três telas e desempenhará duas funções distintas. Ele pode atuar como uma central ou como um periférico.
No papel de central ele irá pesquisar um periférico específico pelo UUID e se conectará a este periférico. Consumirá a característica Device Name do serviço Device Information Service que retornará o nome do dispositivo iOS do periférico. A outra característica deste serviço que será consumida é a característica Message que recebe uma mensagem string e grava no dispositivo periférico.
O segundo serviço que será consumido é o Device Brightness Service. Ele possui uma características. A característica Brightness Value retorna o valor do brilho que está configurado no periférico e permiti que a central se inscreva para receber uma notificação todas as vezes que o valor do brilho for alterado no periférico.
No papel de periférico criaremos dois serviços Device Information Service e Device Brightness Service.
O primeiro serviço possui duas caracteristicas. A característica Device Name é do tipo leitura e retorn o nome do device iOS do periférico. Já a característica Message é do tipo escrita e recebe uma string enviada pela central conectada.
O segundo serviço possui a característica Brightness Value que é do tipo leitura e notificação. Ela retorna o valor do brilho da tela configurado no periférico e também serve para que a central se inscreva para receber todas as alterações no valor da característica.
Criando a estrutura de serviço
Vamos criar o arquivo BluetoothServices.swift
onde iremos definir um struct que nos auxiliará na geração da nossa estrutura de serviços. Como vimos anteriormente o aplicativo conterá dois serviços e três características.
Vamos começar criando os UUIDs que serão utilizados no aplicativo. Você pode criá-los com o comando uuidgen
no terminal do Mac.
import Foundation
import CoreBluetooth
let DEVICE_INFORMATION_SERVICE_UUID = "908D66AF-2751-4483-BD8E-8E99A92CC2F3"
let DEVICE_INFORMATION_NAME_CHARACTERISTIC_UUID = "06BBE1E1-100E-430D-A62B-7B2421810813"
let DEVICE_INFORMATION_MESSAGE_CHARACTERISTIC_UUID = "B53D35B6-ADA0-42F7-8A75-C4433D81E729"
let DEVICE_BRIGHTNESS_SERVICE_UUID = "70DE0751-1127-456C-AB03-D00E202E3754"
let DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID = "66D311CB-4AD7-4CD7-A8B7-D3225ACB779E"
let DEVICE_UUID = "F59F037A-4992-42FD-8F0D-8B8D0594FF59"
struct Services {
//
}
Depois criamos as estruturas pra armazenar os dados dos serviços e das características.
struct Service {
var uuid: String
var primary: Bool
var name: String
var characteristics: [Characteristic]
init(uuid: String, primary: Bool, name: String, characteristics: [Characteristic]) {
self.uuid = uuid
self.primary = primary
self.name = name
self.characteristics = characteristics
}
}
struct Characteristic {
var uuid: String
var property: CBCharacteristicProperties
var value: Any?
var permission: CBAttributePermissions
var userDescription: String
var characteristicFormat: String
}
Com a estrutura pronta iremos criar o método para gerar os serviços e as características.
extension Services {
fileprivate static func createService(uuid: String, primary: Bool, name: String, characteristics: [Characteristic]) -> CBMutableService {
let service = CBMutableService(type: CBUUID.init(string: uuid), primary: true)
service.characteristics = [CBMutableCharacteristic]()
for characteristic in characteristics {
var value: Data? = nil
if characteristic.value == nil {
value = nil
} else if characteristic.value is String {
value = (characteristic.value as! String).data(using: .utf8)
} else { //Float, Double, Int
value = Data(from: characteristic.value)
}
let serviceCharacteristic = CBMutableCharacteristic(type: CBUUID.init(string: characteristic.uuid), properties: characteristic.property, value: value, permissions: characteristic.permission)
serviceCharacteristic.descriptors?.append(CBMutableDescriptor(type: CBUUID.init(string: CBUUIDCharacteristicUserDescriptionString), value: characteristic.userDescription))
serviceCharacteristic.descriptors?.append(CBMutableDescriptor(type: CBUUID.init(string: CBUUIDCharacteristicFormatString), value: characteristic.characteristicFormat.data(using: .utf8)))
service.characteristics?.append(serviceCharacteristic)
}
return service
}
}
O serviço é representado pela classe CBMutableService
cujo initializer é init(type:primary:)
o type é um identificador único representado pelo objeto CBUUID
e podemos criá-lo com o método init(string:)
passando o UUID gerado anteriormente.
As características são representadas pelo objeto CBMutableCharacteristic
cujo initializer é init(type:properties:value:permissions:)
. O parâmetro properties
é representado pelo struct CBCharacteristicProperties
e indica se a característica será de leitura, escrita ou notificação. O parâmetro value
será definido apenas se o valor da característica for uma constante. Caso seja um valor dinâmico ele deverá ser nulo. O parâmetro permission
é representado pelo struct CBAttributePermissions
e define o tipo de permissão para leitura e escrita.
O CBMutableCharacteristic
tem um propriedade descriptors
onde podemos definir objetos da classe CBMutableDescriptor
. O iOS possue algumas constantes para representar o descriptor e iremos utilizar duas delas. A CBUUIDCharacteristicUserDescriptionString
para definirmos uma string com a descrição da característica e a CBUUIDCharacteristicFormatString
onde iremos definir o tipo de dado da característica.
Para a geração dos dados do serviço vamos criar alguns métodos estáticos na struct Services
.
O método getDeviceInformationService()
retorna o serviço Device Information Service.
static func getDeviceInformationService() -> CBMutableService {
var characteristics = [Characteristic]()
// Create the Device Name characteristics
characteristics.append(getdeviceNameCharacteristic())
// Create the Message characteristics
characteristics.append(getMessageCharacteristic())
// Create the Device Information Service
let service = createService(uuid: DEVICE_INFORMATION_SERVICE_UUID, primary: true, name: "Device Information Service", characteristics: characteristics)
return service
}
private static func getdeviceNameCharacteristic() -> Characteristic {
return Characteristic(uuid: DEVICE_INFORMATION_NAME_CHARACTERISTIC_UUID, property: .read, value: Device.current.name, permission: .readable, userDescription: "Device Name", characteristicFormat: "String")
}
private static func getMessageCharacteristic() -> Characteristic {
return Characteristic(uuid: DEVICE_INFORMATION_MESSAGE_CHARACTERISTIC_UUID, property: .writeWithoutResponse, value: nil, permission: .writeable, userDescription: "Message", characteristicFormat: "String")
}
O método getDeviceBrightnessService()
retorna o serviço Device Brightness Service.
static func getDeviceBrightnessService() -> CBMutableService {
var characteristics = [Characteristic]()
// Create the Brightness Value characteristics
characteristics.append(getBrightnessCharacteristic())
// Create the Device Brightness Service
let service = createService(uuid: DEVICE_BRIGHTNESS_SERVICE_UUID, primary: true, name: "Device Brightness Service", characteristics: characteristics)
return service
}
private static func getBrightnessCharacteristic() -> Characteristic {
return Characteristic(uuid: DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID, property: [.read, .notify], value: nil, permission: .readable, userDescription: "Brightness Value", characteristicFormat: "Float")
}
Criando o periférico
Começaremos criando o arquivo PeripheralManager.swift
onde definiremos a classe PeripheralManager
. Esta classe será responsável por gerenciar a comunicação com a central.
import Foundation
import CoreBluetooth
class PeripheralManager: NSObject {
// MARK: - Properties
var manager: CBPeripheralManager!
var brightnessCharacteristic: CBMutableCharacteristic!
var subscribedCentralToBrightness: CBCentral?
var delegate: PeripheralViewControllerDelegate?
var isSubscribedToBrightness:Bool {
return subscribedCentralToBrightness != nil
}
var deviceBroadcast: String {
return "Peripheral Device"
}
var deviceName: Data? {
return Device.current.name.data(using: .utf8)
}
var isAdvertising: Bool {
return manager.isAdvertising
}
// MARK: - Initializer
override init() {
super.init()
self.manager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true])
}
// MARK: - Methods
func stopAdvertising() {
manager.stopAdvertising()
}
func updateBrightnessCharacteristic(with value: Float) {
if isSubscribedToBrightness {
manager.updateValue(Data(from: value), for: brightnessCharacteristic!, onSubscribedCentrals: [subscribedCentralToBrightness!])
}
}
}
A principal propriedade da classe é a manager
. Ela é um objeto da classe CBPeripheralManager
que é a responsável por gerenciar toda a comunicação entre o periférico e a central.
As propriedades brightnessCharacteristic
e subscribedCentralToBrightness
servem para armazenarmos os dados da característica e da central que se inscrever para receber notificações da característica.
A propriedade delegate
serve para acessar e enviar os dados para o controller.
No initializer inicializamos o manager com o método init(delegate:queue:options:)
onde passamos self
para definirmos a propria classe PeripheralManager
como delegate. A propriedade queue
serve para informamos a fila onde será executado o manager. Passando nil
definimos para roda na fila Main
. Na propriedade options passamos a chave CBPeripheralManagerOptionShowPowerAlertKey
com o valor true
para que seja exibida uma mensagem para o usuário caso o Bluetooth esteja desligado.
O método updateBrightnessCharacteristic(with:)
serve para que o controller informe o model de que o usuário alterou o valor do brilho na interface. Verificamos se alguma central se inscreveu para receber notificações da característica do valor do brilho. Caso haja uma central inscrita, enviamos o valor com o método updateValue(_:for:onSubscribedCentrals:)
. Onde o valor do tipo Float
é enviado com o initializer da struct Data
que veremos a seguir.
extension Data {
init(from value: T) {
var value = value
self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
}
func to(type: T.Type) -> T {
return self.withUnsafeBytes { $0.pointee }
}
}
No fonte acima temos o extension criado para inicializarmos um objeto Data
de objeto numérico.
Ao instanciarmos um objeto CBPeripheralManager
ele invocará o método peripheralManagerDidUpdateState(_:)
do protocolo CBPeripheralManagerDelegate
.
// MARK: -
extension PeripheralManager: CBPeripheralManagerDelegate {
// MARK: - PeripheralManagerDelegate
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .poweredOff:
print("PeripheralManagerState: poweredOff")
case .poweredOn:
print("PeripheralManagerState: poweredOn")
manager.add(Services.getDeviceInformationService())
let service = Services.getDeviceBrightnessService()
brightnessCharacteristic = service.characteristics!.first as! CBMutableCharacteristic
manager.add(service)
manager.startAdvertising([CBAdvertisementDataLocalNameKey: deviceBroadcast, CBAdvertisementDataServiceUUIDsKey: [CBUUID.init(string: DEVICE_UUID)]])
delegate?.updateDeviceName(deviceBroadcast)
delegate?.updateDeviceUUID(DEVICE_UUID)
case .resetting:
print("PeripheralManagerState: resetting")
case .unauthorized:
print("PeripheralManagerState: unauthorized")
case .unknown:
print("PeripheralManagerState: unknown")
case .unsupported:
print("PeripheralManagerState: unsupported")
let title = NSLocalizedString("PeripheralManagerState.unsupported.Title", value: "Unsupported", comment: "")
let message = NSLocalizedString("PeripheralManagerState.unsupported.Message", value: "Your device does not support Bluetooth.", comment: "")
delegate?.showMessage(title: title, message: message)
}
}
}
Caso a propriedade state
do periférico seja poweredOn
adicionamos os serviços definidos na struct Services
com o método add(_:)
. Ao adicionarmos o serviço será acionado o método peripheralManager(_:didAdd:error:)
do protocolo CBPeripheralManagerDelegate
.
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
if let error = error {
print("Error publishing service: \(error.localizedDescription)")
}
print("Added service \(service.uuid)")
}
Após adicionarmos os serviços iremos chamar o método startAdvertising(_:)
para que o periférico comece a anunciar os serviços para as centrais. Como parâmtetro passamos duas chaves. A chave CBAdvertisementDataLocalNameKey
serve para que passemos o nome do que aparecerá na central que ouvir o anúncio. A key CBAdvertisementDataServiceUUIDsKey
serve para passarmos um array de identificadores únicos do nosso periférico.
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
if let error = error {
print("Error advertising: \(error.localizedDescription)")
}
delegate?.updateStatus(.advertising)
}
Quando iniciarmos o anúncio chamamos o método updateStatus(_:)
do controller para atualizar o status na view.
Após o periférico se conectar com uma central a mesma pode enviar um requisição de leitura de uma das características dos serviços. Quando isto ocorrer o método peripheralManager(_:didReceiveRead:)
será chamado pelo manager.
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
print("Received read request \(request.characteristic.getUserDescription())")
// The device name characteristic was sent automatically because it was defined on CBMutableCharacteristic init
if request.characteristic.uuid.uuidString == DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID {
request.value = delegate?.getBrightness()
manager.respond(to: request, withResult: .success)
}
}
Quando instanciamos o objeto da classe CBMutableCharacteristic
para a característica Device Name, informamos o nome do dispositivo iOS e o Core Bluetooth guardou esta informação em cache. Desta forma, ele responderá de forma automática as requisições de leitura para esta característica.
Caso a solicitação de leitura seja para a característica Brightness Value respondemos a solicitação com o método respond(to:withResult:)
. O valor do brilho buscamos no controller com o método getBrightness()
e atribuimos a proprieade value
do CBATTRequest
.
Se a central conectada enviar uma requisição de escrita, o manager chamará o método peripheralManager(_:didReceiveWrite:)
.
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
for request in requests {
print("Received write request \(request.characteristic.getUserDescription())")
if request.characteristic.uuid.uuidString == DEVICE_INFORMATION_MESSAGE_CHARACTERISTIC_UUID {
if let data = request.value {
delegate?.updateMessage(String(data: data, encoding: .utf8)!)
manager.respond(to: request, withResult: .success)
} else {
manager.respond(to: request, withResult: .unlikelyError)
}
}
}
}
Caso a característica recebida for a Message e o valor recebido não for nulo, atualizamos o controller com o método updateMessage(_:)
.
Algumas características também permitem receber uma inscrição para enviar atualizações sempre que o valor da mesma seja alterado. Quando a central enviar uma requisição de inscrição o manager chamará o método peripheralManager(_:central:didSubscribeTo:)
.
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
print("Central subscribed to characteristic \(characteristic.getUserDescription())")
if characteristic.uuid.uuidString == DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID {
subscribedCentralToBrightness = central
manager.updateValue(delegate!.getBrightness(), for: brightnessCharacteristic, onSubscribedCentrals: [subscribedCentralToBrightness!])
}
}
O método informa a central que fez a inscrição e a característica que foi inscrita. Armazenamos a central na propriedade subscribedCentralToBrightness
e aproveitamos para enviar o valor atualizado do brilho. Todas as vezes que o valor do brilho for alterado na interface pelo usuário o controller irá chamar o método updateBrightnessCharacteristic(with:)
que irá enviar o valor atualizado para a central.
Quando a central se desinscreve de uma característica o método peripheralManager(_:central:didUnsubscribeFrom:)
é chamado pelo manager.
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
print("Central unsubscribed to characteristic \(characteristic.getUserDescription())")
if characteristic.uuid.uuidString == DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID {
subscribedCentralToBrightness = nil
}
}
Neste método atribuimos nil
a propriedade subscribedCentralToBrightness
para parar o envio automático do valor do brilho.
Com a parte do periférico terminada podemos começar com a parte da central.
Criando a central
Para implementar a parte da central vamos criar a classe CentralManager
import Foundation
import CoreBluetooth
class CentralManager: NSObject {
// MARK: - Properties
var manager: CBCentralManager!
var discoveredPeripheral: CBPeripheral?
var messageCharacteristic: CBCharacteristic?
var delegate: CentralViewControllerDelegate?
var deviceBroadcast: String {
return "Central Device"
}
var isScanning: Bool {
return manager.isScanning
}
// MARK: - Initializer
override init() {
super.init()
manager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
}
// MARK: - Methods
func stopScan() {
manager.stopScan()
}
func sendMessage(_ message: String) {
if let messageCharacteristic = messageCharacteristic, let discoveredPeripheral = discoveredPeripheral, let value = message.data(using: .utf8), manager.state == .poweredOn {
discoveredPeripheral.writeValue(value, for: messageCharacteristic, type: .withoutResponse)
print("Send update message \(value)")
}
}
}
A propriedade principal da classe é a manager
da classe CBCentralManager
. Ela é responsável por gerenciar a descoberta e as conexões da central com os periféricos. Depois temos a propriedade discoveredPeripheral
do tipo CBPeripheral
onde iremos armazenar o periférico conectado. A propriedade messageCharacteristic
da classe CBCharacteristic
servirá para armazenarmos os dados da característica de mensagem. Usaremos esta propriedade na hora de enviar a mensagem para o periférico.
No initializer init(delegate:queue:options:)
atribuimos self
ao delegate e passamos a chave CBCentralManagerOptionShowPowerAlertKey
com o valor true
nas opções.
O método sendMessage(_:)
serve para que o controller possa indicar quando a mensagem for alterada pelo usuário. Nela, verificamos se o manager está ligado, se está conectado com o periférico, se temos uma referência a característica Message e se o valor da mensagem é válido. Caso valide enviamos a mensagem com o método writeValue(_:for:type:)
.
Após instaciarmos o manager ele irá chamar o método centralManagerDidUpdateState(_:)
do protocolo CBCentralManagerDelegate
.
// MARK: -
extension CentralManager: CBCentralManagerDelegate {
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOff:
print("poweredOff")
case .poweredOn:
print("poweredOn")
manager.scanForPeripherals(withServices: [CBUUID.init(string: DEVICE_UUID)], options: nil)
delegate?.updateStatus(.searching)
case .resetting:
print("resetting")
case .unauthorized:
print("unauthorized")
case .unknown:
print("unknown")
case .unsupported:
print("unsupported")
let title = NSLocalizedString("PeripheralManagerState.unsupported.Title", value: "Unsupported", comment: "")
let message = NSLocalizedString("PeripheralManagerState.unsupported.Message", value: "Your device does not support Bluetooth.", comment: "")
delegate?.showMessage(title: title, message: message)
}
}
}
Caso a propriedade state
da central seja poweredOn
chamamos o método scanForPeripherals(withServices:options:)
do manager passando o indentificador único do nosso periférico. Este método irá escutar as notificações do periférico e irá chamar o método centralManager(_:didDiscover:advertisementData:rssi:)
quando ouvir um novo periférico.
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("Discovered: \(peripheral.name) (\(peripheral.identifier))")
discoveredPeripheral = peripheral
discoveredPeripheral?.delegate = self
manager.connect(discoveredPeripheral!, options: nil)
}
Como filtramos para ouvir apenas o periférico com o identificador do nosso aplicativo podemos atribuir o periférico encontrado a nossa propriedade discoveredPeripheral
e atribuir self
como seu delegate.
Depois mandamos o manager conectar-se ao periférico com o comando connect(_:options:)
. Caso ele consiga se conectar é chamado o método centralManager(_:didConnect:)
senão o método centralManager(_:didFailToConnect:error:)
é chamado.
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
if let error = error {
print("Error on didFailToConnect: \(error.localizedDescription)")
return
}
print("Erro on connect to: \(peripheral.name) (\(peripheral.identifier))")
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
manager.stopScan()
if let name = peripheral.name {
delegate?.updateConnected(name)
}
discoveredPeripheral?.discoverServices(nil)
delegate?.updateStatus(.connected)
}
Ao conectar-mos com o periférico atualizamos o controller com os métodos updateConnected(_:)
e updateStatus(_:)
. Também chamamos o método discoverServices(_:)
do periférico conectado para listar os serviços disponíveis.
Caso a central se desconecte do periférico o método centralManager(_:didDisconnectPeripheral:error:)
será chamado.
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
if let error = error {
print("Error on didDisconnectPeripheral: \(error.localizedDescription)")
return
}
if peripheral.identifier == discoveredPeripheral?.identifier {
discoveredPeripheral = nil
if manager.isScanning {
delegate?.updateStatus(.searching)
} else {
if manager.state == .poweredOn {
manager.scanForPeripherals(withServices: [CBUUID.init(string: DEVICE_UUID)], options: nil)
delegate?.updateStatus(.searching)
} else {
delegate?.updateStatus(.disconnected)
}
}
}
}
}
No método definimos a propriedade discoveredPeripheral
como nil
e caso o Bluetooth esteja ativo iniciamos a escuta pelo periférico novamente.
Ao descobrir os serviços o método peripheral(_:didDiscoverServices:)
do delegate CBPeripheralDelegate
será chamado.
// MARK: -
extension CentralManager: CBPeripheralDelegate {
// MARK: - CBPeripheralDelegate
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error {
print("Error on didDiscoverServices: \(error.localizedDescription)")
return
}
if let services = peripheral.services {
for service in services {
print("Discovered service \(service)")
peripheral.discoverCharacteristics(nil, for: service)
}
}
}
}
Para cada serviço retornado pelo método fazemos a chamada ao método discoverCharacteristics(_:for:)
para descobrir as características de cada serviço. Ao serem descobertas o CBPeripheral
chamará o método peripheral(_:didDiscoverCharacteristicsFor:error:)
.
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error {
print("Error on didDiscoverCharacteristicsFor: \(error.localizedDescription)")
return
}
if let characteristics = service.characteristics {
for characteristic in characteristics {
print("Discovered characteristic \(characteristic.getUserDescription()) for service \(service.uuid)")
if characteristic.uuid.uuidString == DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID {
peripheral.setNotifyValue(true, for: characteristic)
}
if characteristic.uuid.uuidString == DEVICE_INFORMATION_NAME_CHARACTERISTIC_UUID {
peripheral.readValue(for: characteristic)
}
if characteristic.uuid.uuidString == DEVICE_INFORMATION_MESSAGE_CHARACTERISTIC_UUID {
if let message = delegate?.getMessage(), let value = message.data(using: .utf8) {
peripheral.writeValue(value, for: characteristic, type: .withoutResponse)
messageCharacteristic = characteristic
}
}
}
}
}
Neste método verificamos todas as características retornadas e caso seja Brightness Value chamamos o método setNotifyValue(_:for:)
para nos inscrevermos para receber todas as atualizações no valor da característica. Caso seja Device Name chamamos o método readValue(for:)
para recebermos o nome do dispositivo. E caso seja Message obtemos o valor atual da mensagem do controller e passamos o valor para o método writeValue(_:for:type:)
.
A chamada do método setNotifyValue(_:for:)
irá disparar o método peripheral(_:didUpdateNotificationStateFor:error:)
.
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print("Error on didUpdateNotificationStateFor: \(error.localizedDescription)")
return
}
print("Updated notification state for characteristic \(characteristic.uuid)")
if characteristic.uuid.uuidString == DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID {
if let value = characteristic.value {
delegate?.updateBrightness(value.to(type: Float.self))
}
}
}
Neste método verificamos se o valor da característica brilho é válido e passamos este valor ao controller.
A chamada do método readValue(for:)
irá disparar o método peripheral(_:didUpdateValueFor:error:)
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print("Error on didUpdateValueFor: \(error.localizedDescription) Characteristic: \(characteristic.uuid)")
return
}
print("Updated value for characteristic \(characteristic.uuid)")
if characteristic.uuid.uuidString == DEVICE_INFORMATION_NAME_CHARACTERISTIC_UUID {
if let value = characteristic.value {
delegate?.updateDeviceName(String(data: value, encoding: .utf8)!)
}
}
if characteristic.uuid.uuidString == DEVICE_BRIGHTNESS_VALUE_CHARACTERISTIC_UUID {
if let value = characteristic.value {
delegate?.updateBrightness(value.to(type: Float.self))
}
}
}
Neste método podemos receber o valor do brilho ou o nome do dispositivos. Verificamos se o valor recebido é válido e o passamos ao controller.
Criando o controller do periférico
No PeripheralViewController
implementamos o protocolo PeripheralViewControllerDelegate
protocol PeripheralViewControllerDelegate {
func getBrightness() -> Data
func updateDeviceName(_ value: String)
func updateDeviceUUID(_ value: String)
func updateStatus(_ value: PeripheralViewControllerStatus)
func updateMessage(_ value: String)
func showMessage(title: String, message: String)
}
Para armazenar o valor do brilho foi craida a propriedade brightness
.
fileprivate var brightness: Float! {
willSet {
if (0...1).contains(newValue) {
UIScreen.setMainBrightness(CGFloat(newValue))
brightnessSlider.value = Float(newValue)
brightnessLabel.text = numberFormatter.string(from: NSNumber(value: newValue))
if manager.isSubscribedToBrightness {
manager.updateBrightnessCharacteristic(with: newValue)
}
}
}
}
O método setMainBrightness(_:)
foi definido na extension a seguir.
public extension UIScreen {
public static func setMainBrightness(_brightness: CGFloat) {
guard (0...1).contains(brightness) else {
print("Attempt to set the screen brightness to an invalid value: \(brightness) should be between 0 and 1 inclusive.")
return
}
self.main.brightness = brightness
}
}
No método viewDidiLoad()
criamos o PeripheralManager
.
override func viewDidLoad() {
super.viewDidLoad()
manager = PeripheralManager()
manager.delegate = self
title = manager.deviceBroadcast
deviceNameLabel.text = ""
deviceUUIDLabel.text = ""
messageLabel.text = ""
brightness = 0.5
status = .none
}
E no método viewWillDisappear(_:)
paramos o anúncio.
override func viewWillDisappear(_ animated: Bool) {
if manager.isAdvertising {
manager.stopAdvertising()
}
super.viewWillDisappear(animated)
}
Criando o controller da central
No controller CentralViewController
implementamos o protocolo CentralViewControllerDelegate
para comunicação com o model.
protocol CentralViewControllerDelegate {
func getMessage() -> String
func updateConnected(_ value: String)
func updateStatus(_ value: CentralViewControllerStatus)
func updateBrightness(_ value: Float)
func updateDeviceName(_ value: String)
func showMessage(title: String, message: String)
}
No método viewDidLoad()
instanciamos o objeto CentralManager
.
override func viewDidLoad() {
super.viewDidLoad()
manager = CentralManager()
manager.delegate = self
title = manager.deviceBroadcast
connectedLabel.text = ""
deviceNameLabel.text = ""
messageTextField.text = ""
brightness = 0.5
status = .none
}
No método viewWillDisappear(_:)
paramos a busca por periféricos.
override func viewWillDisappear(_ animated: Bool) {
if manager.isScanning {
manager.stopScan()
}
super.viewWillDisappear(animated)
}
Conclusão
O nosso aplicativo finalizado ficou assim:
A biblioteca Core Bluetooth abstrai o conceito da conexão Bluetooth facilitando pro desenvolvedor a implementação da conexão do iOS com os demais dispositivos Bluetooth. A biblioteca também permite implementar o iOS como um periférico para conectar-se com outros dispositivos que fazem o papel de central.
Existem outros modos que não foram abordados no artigo como execução em segundo plano mas que podem ser encontrados na documentação da Apple.
Espero que tenham gostado do tutorial. Qualquer dúvida, crítica ou sugestão de novos assuntos deixe um comentário abaixo. Caso queira entrar em contato pode me chamar no twitter @mateusfsilva. Se gostou compartilhe.
Photo credit: IntelFreePress via Visualhunt / CC BY-SA