Conectando-se a dispositivos Bluetooth utilizando o Core Bluetooth

Conectando-se a dispositivos Bluetooth utilizando o Core Bluetooth

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.

Central and peripheral devices

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.

Advertising and discovery

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.

A peripheral's service and characteristics

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.

A remote peripheral's tree of services and characteristics

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.

Core Bluetooth objects on the peripheral side

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.

A local peripheral's tree of services and characteristics

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.

Main Screen

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.

Central Device 01

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.

Peripheral Device 01

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:

Peripheral Device Central Device

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

Publicidade

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s