Tenho tido contato com Bluetooth desde 2005, mas apenas agora tive a oportunidade de trabalhar "a sério" com ele. É uma tecnologia interessante, que está em todo lugar hoje em dia, mas os "detalhes sórdidos" são pouco conhecidos. Assim, acho que vai ser interessante escrever um pouco a respeito.
Bluetooth (BT, daqui para frente) tem por objetivo fazer desaparecer aquele monte de fios e cabos ligados a seu computador. Comparado com o "concorrente" 802.11 WiFi, BT é lento (2Mbps) porém gasta muito menos energia (um Magic Mouse funciona várias semanas com duas pilhas AA). Muitas decisões de arquitetura do BT, aparentemente absurdas, visam atender a certos casos de uso (e algumas são absurdas mesmo, sem apelação).
A velocidade baixa delimita seu uso a dispositivos como mouse, teclado, celular, modem do celular... Enfim, aquelas coisas para as quais usávamos algum tipo de cabo serial. Há fones de ouvido BT e o controle de qualidade de serviço garante que o áudio terá banda reservada, mas é áudio comprimido, não particularmente adequado para escutar seus WAV de música clássica.
Cada dispositivo BT tem um endereço, semelhante ao endereço MAC de uma placa de rede. Poderiam haver vários, mas normalmente há apenas um por máquina:
epx@neosaldina:~$ hcitool dev Devices: hci0 00:3F:5B:7B:ED:B4
Dizem que dispositivos "xing-ling" às vezes vêm com endereços repetidos ou obviamente não-únicos, e isso causa todo tipo de problema. Não muito diferente do que acontece com placas de rede.
O exemplo acima utilizou a ferramenta hcitool do BlueZ, a pilha BT para Linux. Vou utilizar muito o BlueZ e o PyBlueZ em exemplos. Existe API BT no Mac OS X, no Windows etc. mas a do Linux é a mais fácil de usar, e provavelmente a de melhor qualidade.
Nas APIs de alto nível, este endereço é utilizado quase da mesma forma que TCP/IP. Se meu aplicativo quer conectar ao dispositivo A, ele vai chamar connect() passando o endereço de A, que de alguma forma tinha sido descoberto. Se meu computador tem dois ou mais dongles BT, posso especificar através de qual a conexão vai sair, usando bind(), tal qual no TCP/IP.
Comando para achar os dispositivos BT nas imediações:
epx@neosaldina:~$ hcitool inq Inquiring ... 0C:9B:5B:D3:7A:FD clock offset: 0x7079 class: 0x5a0100
Os dispositivos BT formam uma rede de forma automática, denominada "piconet". Esta é uma grande vantagem do BT: não há o que configurar, nem há necessidade de designar um mestre ou access point. Não há rotas nem servidor DNS, nem os problemas que vêm junto com eles.
Uma característica central do BT é o pareamento, que a maioria dos leitores já deve ter feito diversas vezes entre seu computador e seu celular.
O pareamento tem dois objetivos: autenticar os dispositivos ("provar" que os donos de ambos realmente querem comunicar-se) e trocar chaves para que toda comunicação seja criptografada.
Um aplicativo que ofereça um serviço via BT pode configurar o BT de modo a não exigir pareamento. Por exemplo, o serviço OBEX normalmente não exige. (OBEX é o protocolo que permite, por exemplo, mandar mensagens entre celulares.) Os demais serviços exigem pareamento, pois do contrário seria extremamente inseguro andar com seu computador ou celular dentro de um shopping ou aeroporto.
O pareamento é intimamente relacionado com o endereço BT das duas partes, e as chaves são administradas pelos respectivos sistemas operacionais. Assim, é muito fácil que a chave se perca numa atualização do sistema, ou torne-se inútil se você troca um dongle BT por outro (que terá endereço diferente). É por isso que de vez em quando o troço não funciona e somos obrigados a parear novamente.
A comunicação entre dois dispositivos pareados acontece facilmente. O Linux estende a API Sockets para Bluetooth, dando acesso a três protocolos: RFCOMM, L2CAP e HCI. Falarei mais sobre eles, mas por enquanto assuma que RFCOMM é equivalente a TCP, L2CAP a UDP e HCI a "soquete RAW".
Os exemplos condensados a seguir vêm do PyBlueZ, um projeto de bindings Python para a API do BlueZ.
# Cliente sock=BluetoothSocket( RFCOMM ) host = "00:11:22:33:44:55" port = 10 sock.connect((host, port)) print "connected. type stuff" while True: data = raw_input() if len(data) == 0: break sock.send(data) sock.close() # Servidor server_sock=BluetoothSocket( RFCOMM ) server_sock.bind(("", 10)) server_sock.listen(1) client_sock, client_info = server_sock.accept() print "Accepted connection from ", client_info try: while True: data = client_sock.recv(1024) if len(data) == 0: break print "received [%s]" % data except IOError: pass client_sock.close() server_sock.close()
Quem já mexeu com programação sockets está "em casa". Embora o PyBlueZ manipule ligeiramente a semântica dos soquetes Bluetooth, eles são essencialmente iguais a soquetes TCP/IP.
Conforme dissemos, o RFCOMM é análogo a TCP, transmite mensagens byte a byte, sem formatação, como se fosse um cabo serial. Já o L2CAP transmite "pacotes", que são recebidos atomicamente no outro lado, o que lembra UDP.
Para o desenvolvedor que deseja apenas colocar dois dispositivos para conversar, esta metáfora é suficiente.
Embora à primeira vista o L2CAP se pareça com UDP, o L2CAP é muito mais poderoso e suporta diversos modos de confiabilidade, retransmissão e segmentação/remontagem de pacotes. A analogia correta seria com o SCTP, não com UDP.
Na verdade, o RFCOMM é implementado sobre o L2CAP. Isto é bem diferente do TCP/IP, onde TCP e UDP são protocolos independentes.
Há 31 portas RFCOMM à disposição das aplicações, mas na verdade tais portas são uma abstração. Mesmo que haja diversos canais RFCOMM abertos entre dois dispositivos, todos eles trafegam sobre uma mesma conexão L2CAP.
O L2CAP tem algo parecido com o conceito de "porta": o PSM. Por exemplo, o L2CAP PSM=3 é justamente aquele utilizado para trafegar os dados RFCOMM (e, por estar reservado a este fim, não pode ser utilizado por aplicativos user-level). Se um aplicativo quiser oferecer um serviço via L2CAP no PSM 0x1001, ele simplesmente chama bind(("", 0x1001)). O PSM é um número de 16 bits.
Já no lado cliente do L2CAP, não existe PSM. O PSM existe apenas para atrelar a um serviço/aplicativo no lado servidor. É outra diferença em relação a UDP, onde ambos os lados sempre têm um número de porta. Embora o cliente TCP/IP costume escolher uma porta alta e aleatória, ela existe, enquanto o PSM simplesmente não existe para o cliente L2CAP.
Num nível mais baixo, cada dispositivo BT atribui um código CID (Channel IDentifier) único para cada conexão L2CAP, esteja ele do lado cliente ou do lado servidor. Embora o CID seja mais análogo ao número de porta TCP ou UDP, ele está num nível mais baixo que o PSM, e os aplicativos normalmente não tomam conhecimento dele (e nem precisam tomar).
Num nível ainda mais baixo, todos os pacotes de todas as conexões L2CAP passam por um único canal, o ACL. Existe apenas um canal ACL para cada par de dispositivos; é neste nível que a criptografia é realizada.
Além do ACL, existe o canal SCO, que destina-se a comunicação síncrona/isócrona, com banda garantida. Por exemplo, fones de ouvido e caixinhas de som Bluetooth trafegam o áudio via SCO. É algo completamente separado e diferente do L2CAP.
Do ponto de vista do software, a camada mais baixa do BT é o HCI: Host Controller Interface. É o protocolo que realmente "fala" com o dongle Bluetooth. Tudo passa pelo HCI, tanto pacotes de dados quanto de controle.
Surpreendentemente, é possível abrir soquetes HCI em qualquer aplicativo, inclusive usando PyBlueZ. Há diversos exemplos no PyBlueZ que usam soquetes HCI para fazer coisas "ninjas".
Um grande trunfo do BT enquanto padrão, é que tanto o protocolo HCI quanto suas adaptações sobre USB, cabo serial etc. são rigidamente definidas pelo Bluetooth SIG. Para ser certificado, um chip ou dongle BT tem de implementar a coisa do jeito "certo". O driver de um dongle BT USB é forçosamente simples.
É esta padronização que garante que um dongle qualquer funcione facilmente no Linux, no Mac OS X, no Windows etc. em geral sem a necessidade de instalação de um driver.
E assim como há o tcpdump, também há o hcidump:
epx@neosaldina:~$ sudo hcidump -XVV HCI sniffer - Bluetooth packet analyzer ver 1.42 device: hci0 snap_len: 1028 filter: 0xffffffff < HCI Command: Inquiry (0x01|0x0001) plen 5 lap 0x9e8b33 len 8 num 0 > HCI Event: Command Status (0x0f) plen 4 Inquiry (0x01|0x0001) status 0x00 ncmd 1 > HCI Event: Inquiry Result with RSSI (0x22) plen 15 bdaddr 0C:9B:5B:D3:7A:FD mode 1 clkoffset 0x6faa class 0x5a0100 rssi -71 > HCI Event: Inquiry Result with RSSI (0x22) plen 15 bdaddr 0C:9B:5B:D3:7A:FD mode 1 clkoffset 0x6faa class 0x5a0100 rssi -83