Ir para o conteúdo

Transferência de dados usando Bluetooth

Neste pequeno artigo vou explicar como se pode fazer uso do Bluetooth, uma tecnologia sem fios cujo alcance, dividido em classes, pode ir, actualmente, até aos 100m. Este tipo de tecnologia está maioritariamente presente em dispositivos móveis facilitando a comunicação, uma vez que normalmente esse tipo de dispositivos não possui muitas interfaces de comunicação. Este artigo torna-se mais interessante quando aplicado em dispositivos móveis, por isso irei falar na .Net Compact Framework (.Net CF) sobre Windows Compact Edition (Windows CE), ou semelhante.

Urge dizer que o desenvolvimento pode ser feito usando o Visual Studio com o módulo de dispositivos móveis que contempla um emulador de PDA com Windows CE. Para mais informação sobre como usar o Visual Studio para executar a aplicação no emulador e fazer o deploy no PDA há que consultar a documentação. Urge também dizer que o desenvolvimento sobre o emulador não permite tirar partido de todas as funcionalidades que se tem disponíveis no PDA. Em particular, não é possível usar o bluetooth dentro do emulador, mesmo que o PC onde se está a desenvolver a aplicação possua essa tecnologia. Para tal, aconselho que se use um PDA ligado ao Visual Studio para se fazer debug, de novo, é favor consultar a documentação sobre este tópico em caso de dúvida.

O artigo possui informação suficiente para, no final, se poder fazer uma pequena aplicação usando esta tecnologia. Assim, deixo como exercício a construção de um chat simples para dispositivos que correm Windows CE usando os conhecimentos aqui expostos.

Introdução

O Bluetooth é uma tecnologia que permite a comunicação entre dispositivos sem recurso a qualquer tipo de cabos e que disponibiliza um conjunto de serviços do próprio dispositivo que podem ser usados por outro dispositivio.

Descoberta

Um dispositivo com Bluetooth possui um processo de descoberta permite aos dispositivos interrogarem outros dispositivos para descobrir que serviços estes oferecem. Se um dispositivo oferece mais do que um serviço, o utilizador pode seleccionar qual o serviço que pretende utilizar desse dispositivo em particular.

Dado que o Windows CE não dispõe de uma API dedicada para comunicações por Bluetooth, torna-se necessário utilizar outros métodos, como programação de sockets ou a utilização de portas série. Para consultar quais os portos COM que estão atribuídos para comunicação Bluetooth é necessário aceder à configuração do Bluetooth ("Bluetooth Settings" —> "Serial Port"), onde se pode ver qual o porto utilizado para receber informação, e qual o porto utilizado para enviar informação.

A .Net CF não possui classes específicas para comunicação série, pelo que se torna necessário utilizar chamadas ao sistema operativo para que este forneça essa capacidade. Em particular, teremos de utilizar a biblioteca coredll.dll do Windows CE.

Chamadas ao sistema operativo

Da biblioteca coredll.dll utilizam-se as seguintes funções:

  • CreateFile Abre uma ligação a uma porta COM.
  • ReadFile Lê informação de uma porta COM.
  • WriteFile Escreve informação para uma porta COM.
  • CloseHandle Fecha uma ligação a uma porta COM.

Para se chamar estas funções dentro do código da aplicação é necessário recorrer à biblioteca System.Runtime.InteropServices.DllImport. É assuim necessário especificar as funções com as respectivas assinaturas dessa biblioteca:

[System.Runtime.InteropServices.DllImport("coredll.dll")]
private extern static int CreateFile(String lpFileName, // nome do ficheiro, ou neste caso da porta COM
                                     uint dwDesiredAccess, // modo de acesso: pode ser leitura (0x80000000),
                                     escrita (0x40000000), ou ambos (0xC0000000)
                                     int dwShareMode,
                                     int lpSecurityAttributes,
                                     int dwCreationDisposition,
                                     int dwFlagsAndAttributes,
                                     int hTemplateFile);

[System.Runtime.InteropServices.DllImport("coredll.dll")]
private extern static int ReadFile(int hFile, // handler do ficheiro que vai ser lido
                                   byte[] Buffer, // array de bytes para onde vai ser lida a informação
                                   int nNumberOfBytesToRead, 
                                   ref int lpNumberOfBytesRead, // inteiro passado por referência, retorna os bytes lidos
                                   ref int lpOverlapped);

[System.Runtime.InteropServices.DllImport("coredll.dll")]
private extern static int WriteFile(int hFile, // handler do ficheiro que vai ser escrito
                                    byte[] Buffer, // array de bytes com a informação que vai ser escrita
                                    int nNumberOfBytesToWrite,
                                    ref int lpNumberOfBytesWritten, // inteiro passado por referência, retorna os bytes escritos
                                    int lpOverlapped);                                   

[System.Runtime.InteropServices.DllImport("coredll.dll")]
private static extern int CloseHandle(int hObject);

É de salientar a presença de variáveis passadas por referência, que irão corresponder a apontadores nas funções definidas na biblioteca coredll.dll. A informação é lida e escrita usando um array de bytes, que também é implementado com apontadores na biblioteca coredll.dll.

Abrir portos COM

O primeiro passo para estabelecer comunicação entre dois dispositivos é a abertura das portas COM. Para tal, uza-se a função CreateFile. Supondo que as variáveis inPort e outPort armazenam o número dos portos de entrada e saída, a abertura dos portos pode ser efectuada da seguinte forma:

infileHandler = CreateFile("COM" + inPort + ":", 0xC0000000, 0, 0, 3, 0, 0);
Application.DoEvents();

outfileHandler = CreateFile("COM" + outPort + ":", 0xC0000000, 0, 0, 3, 0, 0);
Application.DoEvents();

Esta função retorna um handler, inteiro, que identifica o ficheiro, neste caso a porta COM, e que será igual a zero em caso de insucesso na abertura da porta.

A utilização do método Application.DoEvents() prende-se com a necessidade de garantir que a aplicação continue a responder a eventos, com origem na interface ou noutra fonte, enquanto processa as chamadas a outros métodos, neste caso à função CreateFile.

Como a função ReadFile, que será usada para ler as mensagens que chegam, é bloqueante, é necessário invocar uma System.Threading.Thread para escutar continuamente a informação que possa estar a chegar, garantindo que a aplicação continua a responder aos comandos do utilizador.

t1 = new System.Threading.Thread(new System.Threading.ThreadStart(receiveLoop));
t1.Start();

receiveLoop é o nome do método onde a escuta irá ser feita.

Escrever num porto COM

Para escrever num porto COM utiliza-se a função WriteFile.

int retCode = WriteFile(outfileHandler, stringToByteArray(message), message.Length, ref n, 0);

No código anterior message é uma variável do tipo String que armazena a mensagem que se pretende enviar. n é um int passado por referência, que irá ficar com o número de bytes escritos. O método stringToByteArray é responsável por converter uma String num array de bytes. Esta função retorna um inteiro, que será igual a zero em caso de insucesso.

public byte[] stringToByteArray(String str)
{
  char[] s;
  s = str.ToCharArray();
  byte[] b = new byte[s.Length];
  for (int i = 0; i < (s.Length); i++)
  {
    b[i] = System.Convert.ToByte(s[i]);
  }
  return b;
}

Ler de um porto COM

A leitura é feita recorrendo à função ReadFile, normalmente colocada dentro de um ciclo noutra thread, para poder estar continuamente a escutar novas mensagens. Como aceder a controlos não é seguro quando estes estão noutra thread, recorre-se a um delegate para chamar uma função na thread dos controlos, responsável por processar a informação recebida.

public delegate void myDelegate(String str);

public void processMessage(String str)
{
  // processar a mensagem recebida
}

public void receiveLoop()
{
  int n = 0; // variaveis passadas por referencia na chamada a ReadFile
  int n1 = 0;
  byte[] inbuff = new byte[300]; // array de bytes que vai conter a informacao recebida

  int retCode = ReadFile(infileHandler, inbuff, inbuff.Length, ref n, ref n1);
  Application.DoEvents();
  while ( !((retCode == 0) || stopThread ) ) // nao parar enquanto nao houver um erro de leitura ou uma indicacao para parar a thread
  {
    myDelegate processDelegate = new myDelegate(processMessage); // criar um delegate para chamar a funcao processMessage
    processDelegate(byteArrayToString(inbuff)); // chamar a funcao processMessage passando a mensagem como parametro
    inbuff = new byte[300]; // reinicializar o array de bytes
    retCode = ReadFile(infileHandler, inbuff, inbuff.Length, ref n, ref n1);
    Application.DoEvents();
  }
}

É de realçar mais uma vez a necessidade da utilização do método Application.DoEvents(), de modo a evitar bloquear a aplicação durante o processo de escuta. Esta função retorna um inteiro, que será igual a zero em caso de insucesso. O método byteArrayToString é responsável por converter o array de bytes recebido numa String.

public String byteArrayToString(byte[] b)
{
  String str;
  System.Text.ASCIIEncoding enc;
  enc = new System.Text.ASCIIEncoding();
  str = enc.GetString(b, 0, b.Length);
  return str;
}

Fechar as ligações

Para fechar as ligações às portas COM utiliza-se a função CloseHandler que recebe como parâmetro o handler do ficheiro, ou a porta COM neste caso.

CloseHandle(infileHandler);
CloseHandle(outfileHandler);