Um Chat via Bluetooth para Android, MacOS e Windows

File:ClassicBluetoothVsLowEnergyBluetooth.png

Introdução

A inspiração para este artigo surgiu durante uma POC que estamos fazendo com um cliente implementando uma solução RFID com mobile.

Mas qual a relação do RFID com Bluetooth? Bem, a principio nenhuma relação direta, exceto pelo fato de que a ampla maioria dos leitores RFID que suportam integração para mobile (Android) o fazem via comunicação Bluetooth SPP (serial port profile).

Bluetooth SPP é um dos muitos profiles suportados pela tecnologia Bluetooth. Na prática, estamos falando de comunicação serial (socket) sobre Bluetooth. Neste link você encontra uma lista de todos os profiles suportados oficialmente pelo Bluetooth standard, lembrando que diferentes devices suportam diferentes conjuntos de profiles.

Com isso chegamos ao chat via Bluetooth…

Bluetooth no Delphi e C++ Builder

O RAD Studio oferece suporte nativo para Bluetooth e BluetoothLE (low energy). Apenas para contextualizar, BluetoothLE é diferente de Bluetooth, e se presta a outras necessidadesd – principalmente a implementações de dispositivos IoT, como beacons e etc…

Voltando ao Bluetooth standard, este que você utiliza quando faz o pareamento de dois devices, ou ainda de seu smartphone com o rádio de seu carro, observe que no título do artigo não está mencionado suporte para iOS, e por uma razão bastante simples: o iOS não implementa Bluetooth SPP.

Por qual razão? Bem, perdemos a oportunidade de perguntar ao Steve (Jobs), então temos que nos contentar apenas com esta lista de profiles suportados: https://support.apple.com/en-us/HT204387.

Obviamente há uma explicação científica para o tema. Caso alguém saiba mais a respeito, acrescente aos comentários deste artigo!Com isso, o suporte a Bluetooth e BLE no RAD Studio também apresenta esta “limitação”, digamos assim.

Mais detalhes sobre versões e protocolos Bluetooth no Delphi e C++ estão neste link da documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Using_Bluetooth.

Código pelo Amor de Deus?

Sim, já estamos chegando lá. Mas esta teoria é importante para compreender o que estamos fazendo.Para estabelecer uma conexão utilizando SPP, leia-se porta serial, utilizamos basicamente uma conexão socket, muito parecido com qualquer solução socket que você já tenha implementado.

Quando estabelecemos uma conexão socket via Bluetooth (formalmente um rfcomm server e respectivo client), devemos especificar um UUID. Esse cara funciona como um identificador único para que o client possa se conectar ao serviço correto, já que podemos criar múltiplos canais de comunicação de maneira simultânea. Uma boa discussão sobre este tema você encontra aqui: http://stackoverflow.com/questions/13964342/android-how-do-bluetooth-uuids-work.

Basicamente, existem algumas UUID previamente definidos (https://www.bluetooth.com/specifications/assigned-numbers/service-discovery), os quais atendem serviços específicos, e você pode definir seu próprio identificar para uma aplicação em particular.

Felizmente nosso TBluetooth implementa tudo o que precisamos, e de maneira simples – só pra variar 😉

Agora sim, código!

Antes de mais nada, lembre-se que os devices devem estar devidamente pareados para que a conexão Bluetooth ocorra. Existem forma de automatizar o pareamento, mas não para todas as plataformas.

Em linhas gerais, do ponto de vista do “client”, tudo que temos que fazer é estabelecer a conexão socket, e então enviar o que desejamos.Já do lado “server”, utilizaremos um TTask para manter o Socket Server “ouvindo” em segundo plano.

Este é o conceito geral.

Aqui temos o código de ambas as classes, um “writer” e um “reader“:

unit uBlueChat;

interface

uses SysUtils, System.Classes, System.StrUtils, System.Threading,
  System.Bluetooth, System.Types, System.Generics.Collections,
{$IFDEF MACOS}
  Macapi.CoreFoundation,
{$ENDIF}
  FMX.Memo;

type
  TTextEvent = procedure(const Sender: TObject; const AText: string;
    const aDeviceName: string) of object;

type
  TBlueChatWriter = class(TComponent)
  private
    fSendUUID: TGUID;

    fDeviceName: string;
    fBlueDevice: TBluetoothDevice;

    fSendSocket: TBluetoothSocket;
    procedure SetBlueDevice(Value: string);
  public
    constructor Create(AOwner: TComponent); override;

    function SendMessage(sMessage: string): boolean;
    property DeviceName: string read fDeviceName write SetBlueDevice;
  end;

type
  TBlueChatReader = class(TComponent)
  private
    fReadUUID: TGUID;

    fDeviceName: string;
    fTaskReader: ITask;

    fReadSocket: TBluetoothSocket;
    fServerSocket: TBluetoothServerSocket;

    FOnTextReceived: TTextEvent;
    procedure SetOnTextReceived(const Value: TTextEvent);
  public
    constructor Create(AOwner: TComponent); override;
    procedure StartReader;
    procedure StopReader;

    property OnTextReceived: TTextEvent read FOnTextReceived
      write SetOnTextReceived;
    property DeviceName: string read fDeviceName write fDeviceName;
  end;

implementation

{ TBlueChatSend }

const
  cTimeOut: integer = 5000;

function FindBlueDevice(DeviceName: string): TBluetoothDevice;
var
  i: integer;
  aBTDeviceList: TBluetoothDeviceList;
begin
  aBTDeviceList := TBluetoothManager.Current.CurrentAdapter.PairedDevices;
  for i := 0 to aBTDeviceList.Count - 1 do
    if aBTDeviceList.Items[i].DeviceName = DeviceName then
      exit(aBTDeviceList.Items[i]);
  Result := nil;
end;

constructor TBlueChatWriter.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  fBlueDevice := nil;
  fSendSocket := nil;

  fSendUUID := StringToGuid('{14800546-CF05-481F-BE41-4EC0246D862D}');
end;

function TBlueChatWriter.SendMessage(sMessage: string): boolean;
begin
  try
    try
      if fBlueDevice = nil then
        raise Exception.Create('Select a bluetooth device first...');

      fSendSocket := fBlueDevice.CreateClientSocket(fSendUUID, False);
      if fSendSocket = nil then
        raise Exception.Create('Cannot create client socket to ' + fDeviceName);

      fSendSocket.Connect;
      if fSendSocket.Connected then
        fSendSocket.SendData(TEncoding.ASCII.GetBytes(sMessage))
      else
        raise Exception.Create('Cannot connect to ' + fDeviceName);

      Result := True;
    except
      on E: Exception do
        raise Exception.Create('Exception raised sending message: ' +
          E.Message);
    end;
  finally
    if fSendSocket.Connected then
      fSendSocket.Close;
    FreeAndNil(fSendSocket);
  end;
end;

procedure TBlueChatWriter.SetBlueDevice(Value: string);
begin
  if Value <> fDeviceName then
  begin
    fDeviceName := Value;
    fBlueDevice := FindBlueDevice(fDeviceName);
    if fBlueDevice = nil then
      raise Exception.Create('Cannot find device ' + fDeviceName);
  end;
end;

{ TBlueChatRead }

constructor TBlueChatReader.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  fTaskReader := nil;
  fReadSocket := nil;
  fServerSocket := nil;

  fReadUUID := StringToGuid('{14800546-CF05-481F-BE41-4EC0246D862D}');
end;

procedure TBlueChatReader.SetOnTextReceived(const Value: TTextEvent);
begin
  FOnTextReceived := Value;
end;

procedure TBlueChatReader.StartReader;
var
  Data: TBytes;
begin
  if fServerSocket <> nil then
    FreeAndNil(fServerSocket);

  fServerSocket := TBluetoothManager.Current.CurrentAdapter.CreateServerSocket
    ('FMXBlueChat', fReadUUID, False);

  fTaskReader := TTask.Create(
    procedure()
    begin
      fReadSocket := nil;
      while (fTaskReader.Status <> TTaskStatus.Canceled) do
      begin
        try
          fReadSocket := fServerSocket.Accept(cTimeOut);
          if (fReadSocket <> nil) and (fReadSocket.Connected) then
          begin
            Data := fReadSocket.ReceiveData;
            if Length(Data) > 0 then
            begin
              if Assigned(FOnTextReceived) then
                FOnTextReceived(Self, TEncoding.ASCII.GetString(Data),
                  fDeviceName);
            end;
          end;
        except
          on E: Exception do
          begin
            FreeAndNil(fReadSocket);
            raise Exception.Create('Exception raised receiving message: ' +
              E.Message);
          end;
        end;
        FreeAndNil(fReadSocket);
      end;
    end);
  fTaskReader.Start;
end;

procedure TBlueChatReader.StopReader;
begin
  if fTaskReader <> nil then
    if fTaskReader.Status <> TTaskStatus.Canceled then
      fTaskReader.Cancel;
end;

end.

A interface visual, por sua vez, é algo bastante simples. Estou certo que você vai compreender facilmente como tudo está funcionando investigando o exemplo que estou disponibilizando nos links abaixo, mas o essencial mesmo está nestas classes.

Advertisements

Delphi Tour Brasil

Banner-Facebook---Tour.png

Alô Brasil, chegou a hora!

Vamos cair na estrada por esse Brasil adentro para mostrar como a nova versão pode transformá-lo em um verdadeiro ninja com seu desenvolvimento! Entre as novidades, como compilar nativamente para todas as principais plataformas – Windows, Linux, macOS, iOS e Android – com segurança e velocidade.

Confira a programação e inscreva-se gratuitamente!
http://www.embarcaderobr.com.br/tour/

Um leitor de código de barras portátil com FMX e App Tethering

Screen Shot 2017-04-08 at 19.40.29

A inspiração para este exemplo veio da discussão com um cliente sobre opções para leitura de código de barras em aplicações mobile. Não somente utilizando a câmera do próprio celular, mas também possíveis integrações com leitores externos, suporte a RFID, etc.

Neste artigo vou abordar a captura e decodificação do código de barras pela câmera do celular, para Android e iOS. Nos próximos vamos falar da integração de leitores externos via Bluetooth, inclusive para leitores de RFID.

Uma vez capturado o código de barras, o segundo passo é transferir o mesmo para uma aplicação “host” que estará em execução no desktop, podendo utilizá-lo para pagamento de uma conta no seu internet banking, uma aplicação de entrada/saída de materiais, etc.

Barcode em Android e iOS

Muito já se publicou sobre isso, e existem diversas boas soluções e exemplos a um “Google” de distância, para Delphi e também C++ Builder.

Para este exemplo vou utilizar algumas abordagens de leitura que me pareceram sólidas, mas unificando os mesmos afim de simplificar sua utilização em um projeto multiplataforma.

Para cumprir esta tarefa, existem duas abordagens possíveis: bibliotecas externas dinâmicas e/ou estáticas, ou código nativo compilado diretamente em sua aplicação.

ZXing.Delphi

Iniciando pelo código nativo, o ZXing (zebra crossing) é provavelmente o projeto multiplataforma de código aberto mais ativo. A boa noticia é que, entre tantas outras linguagens, ele possui um “port” para Delphi, aqui: https://github.com/Spelt/ZXing.Delphi.

Sua integração ao FireMonkey é bastante simples, seja para iOS ou Android. O código utilizado em nosso app está baseado em um exemplo disponível no próprio repositório do ZXing.Delphi (folder aTestApp) mas basicamente estamos falando de algo assim:

FScanManager := TScanManager.Create(TBarcodeFormat.CODE_128, nil);
FReadResult := FScanManager.Scan(scanBitmap);

Fora isso, é adicionar todos os paths da biblioteca no Search Path do seu projeto e compilar. Para facilitar, aqui estão todos os paths a serem incluídos (obviamente você deverá ajustar de acordo com a localização em seu computador):

C:\COMPs\ZXing;C:\COMPs\ZXing\Filtering;C:\COMPs\ZXing\Common;C:\COMPs\ZXing\Common\Detector;C:\COMPs\ZXing\Common\ReedSolomon;C:\COMPs\ZXing\1D Barcodes;C:\COMPs\ZXing\2D Barcodes;C:\COMPs\ZXing\2D Barcodes\Decoder;C:\COMPs\ZXing\2D Barcodes\Detector;C:\COMPs\ZXing\2D Barcodes\Encoder

ZXing Android App

Uma outra alternativa, neste caso específico para Android, e também baseada no projeto ZXing, é uma app disponível no Google Play, a qual pode ser invocada de sua app Delphi via “Intent”. Sua integração requer a execução de um “Intent”, e o retorno é capturado via o retorno do “activity”. Para a criação desta classe minha referência foram os posts do MVP Brian Long, o qual escreve muito (e bem!) sobre integração com o ecossistema do Android. A classe resultante você encontra no exemplo que estou compartilhando logo abaixo.

O próximo passo é encontrar funcionalidade similar, com uma biblioteca externa para iOS, criando assim uma classe “similar” a utilizada pelo versão Android.

ZBar barcode reader para iOS

Esta biblioteca, também de código aberto, também está disponível para várias plataformas, entre elas iOS. Para integrar um módulo Xcode em uma aplicação Delphi faz-se necessário a criação de um “wrapper” deste assembly. Você encontra alguma explicação sobre isso aqui, mas não trata-se de um processo óbvio.

Para esta classe, tomei como exemplo a implementação feita pela TMS Software, um componente também de código aberto, o qual permite que sua app iOS faça a leitura de um barcode com apenas duas linhas de código. A partir da classe contida neste componente, apliquei algumas pequenas modificações, inclusive de nomenclatura, para criar uma classe similar a versão Android.

Importante observar que, para compilar esta classe, você precisa do módulo “libzbar.a” disponível juntamente com seu projeto, já que trata-se de um link estático. Você encontrará a versão 32 e 64 bits da biblioteca no repositório do ZBar, e também no download que estou disponibilizando.

A classe resultante também está disponível no exemplo abaixo, e a integração dela em seu app fica também extremamente simples:

  fFMXBarcode := TFMXBarcode.Create(Application);
  fFMXBarcode.OnGetResult := OnFMXBarcodeResult;
  ...
  fFMXBarcode.Show(False)

Este evento acima será o responsável por devolver o código de barras resultante, em ambas as implementações, iOS e Android.

App Tethering

A partir do momento em que temos o barcode capturado e disponível, não importando qual das metodologias acima você tenha escolhido, código nativo ou biblioteca externa, nosso próximo objetivo é enviá-lo para uma aplicação host, onde poderá ser utilizado para qualquer fim, como por exemplo, capturar o código de barras de uma conta a pagar no celular e “colar” o mesmo no browser do seu desktop, em um internet banking por exemplo.

Para tal, utilizaremos a tecnologia do App Tethering, disponível para Delphi e C++ Builder. Este framework permite o “pareamento” de aplicações em qualquer plataforma, via Wifi ou Bluetooth, e o envio de informações entre elas. Se você ainda não teve contato com o mesmo, no Delphi Academy temos um episódio completo sobre ele disponível.

Em nossa aplicação mobile, adicionamos um “TTetheringManager” e um “TTetheringAppProfile” e configuramos o protocolo para Network. Para iniciar o “pareamento”, basta uma linha de código:

  TetheringManager1.AutoConnect

E para enviar o código de barras capturado, temos o seguinte:

  TetheringAppProfile1.SendString(TetheringManager1.RemoteProfiles.First,
    'Barcode', edtResult.Text);

Criamos então uma app “host”, também em FireMonkey, a qual irá receber o código enviado, e copiá-lo para a memória de seu desktop, ficando o mesmo disponível para ser “colado” em qualquer lugar em que o necessite.

Nesta app também devemos adicionar um “TTetheringManager” e um “TTetheringAppProfile”. No evento “ResourceReceived” temos:

procedure TMainForm.TetheringAppProfile1ResourceReceived(const Sender: TObject;
  const AResource: TRemoteResource);
begin
  if AResource.Hint = 'Barcode' then
  begin
    edtResult.Text := AResource.Value.AsString;
    SetClipboard(edtResult.Text);
  end;
end;

E finalmente, este é o método que coloca a informação no clipboard de seu desktop, seja ele Windows ou macOS:

procedure TMainForm.SetClipboard(s: string);
var
  Svc: IFMXClipboardService;
begin
  if TPlatformServices.Current.SupportsPlatformService(IFMXClipboardService, Svc) then
    Svc.SetClipboard(s);
end;

Conclusão

Este exemplo mostra como o Delphi e o  C++ Builder são plataformas poderosas e completas. Com o mínimo de código você tem uma app rodando em Android e iOS, capturando códigos de barra, e enviando-o para uma aplicação “host” que poderá estar em Windows ou macOS. Tudo com uma linguagem, um código fonte, é para poucos!

Para encerrar, aqui você pode encontrar o código fonte da aplicação para estudos e, possivelmente, implementação de algo similar em sua própria solução:

Embarcadero Code Central:

https://cc.embarcadero.com/item/30760

Repositório GitHub:

https://github.com/flrizzato/Barcode