Integrando aplicação web com dispositivo local

As aplicações web são cada vez mais comuns no nosso cotidiano, pois elas oferecem diversas vantagens se comparadas com as aplicações desktop, como portabilidade, facilidade de atualização, escalabilidade, etc. Porém, como uma aplicação web roda diretamente no navegador do usuário, ficamos limitados aos recursos que ele nos disponibiliza e também às suas restrições.

Vamos supor que nós desenvolvemos uma aplicação web de vendas para um estabelecimento comercial. Em um determinado momento essa aplicação precisará emitir cupom fiscal para os clientes e, no caso de São Paulo, será necessário um dispositivo SAT para a emissão dessas notas. A comunicação com o dispositivo SAT é feita através de uma DLL (no caso do Windows), porém como fazemos para que nossa aplicação web se comunique com esse dispositivo, visto que ele está instalado localmente no computador do usuário e o navegador não permite acessá-lo diretamente?

Algumas soluções que podemos pensar são:

  • Criar uma aplicação desktop que funcione como um servidor web local na máquina do usuário e então a aplicação web se comunica com essa aplicação desktop utilizando o protocolo HTTP que então se comunica com o dispositivo local;
  • Criar uma extensão para o navegador, assim a aplicação web irá se comunicar com a extensão através do protocolo Messaging, a extensão irá se comunicar com uma aplicação desktop através do protocolo Native Messaging que então se comunica com o dispositivo local;
  • Criar uma aplicação desktop que fique verificando em um intervalo predeterminado se há notas para serem emitidas através de uma API disponibilizada pela aplicação web.

Neste artigo vamos implementar a primeira solução. Para isso, iremos desenvolver uma aplicação web simples e uma aplicação desktop que irá funcionar como um servidor web local que fará a ponte entre a aplicação web e um dispositivo SAT.

Código fonte disponível em: https://github.com/rafaelcouto/integrando-aplicacao-web-com-dispositivo-local.

Fluxo de comunicação

O método que vamos utilizar então consiste na aplicação web se comunicar com o dispositivo SAT através de um servidor web local.

Imagem 1 – Fluxo de comunicação

Como podemos observar na Imagem 1, o fluxo fica da seguinte forma:

  1. A aplicação web se comunica com a aplicação desktop através do protocolo HTTP;
  2. A aplicação desktop se comunica com o dispositivo SAT através da DLL e retorna os dados para a aplicação web através do protocolo HTTP;
  3. A aplicação web pode então enviar os dados retornados para o servidor web remoto através do protocolo HTTP;

Emulador do SAT

Para que possamos testar as aplicações sem precisar ter um dispositivo SAT fisicamente instalado em nossa máquina, vamos utilizar o emulador disponibilizado pela receita federal. Caso você tenha um dispositivo SAT, então não precisa utilizar o emulador, basta apenas instalá-lo no computador e utilizar a DLL do fabricante. Caso contrário, basta seguir os passos abaixo.

  • Certifique-se que você tenha o Java 32 bits instalado;
  • Faça o download do emulador;
  • Execute o arquivo Setup-Emulador_OffLine_v2_9_4.exe (next, next, finish);
  • Copie a pasta SAT para o C:\.

Basta então executar o Emulador SAT-CFe e estamos prontos para continuar.

Imagem 2 – Emulador SAT-CFe em funcionamento

Aplicação desktop

A aplicação desktop será uma aplicação de console em C#. Estou utilizando C# pois é a linguagem desktop que eu tenho um certo conhecimento, porém você poderia utilizar qualquer linguagem desktop que permita a criação de um servidor web, como JAVA utilizando takes ou Node.js com express, por exemplo.

Vamos criar uma aplicação de console, pois o artigo é apenas para fins didáticos. Em uma aplicação real você precisaria de uma forma simples de distribuir e instalar para o usuário. A melhor opção seria a criação de um serviço ou uma aplicação que inicie com o computador e fique rodando em plano de fundo.

Para criação da aplicação em C#, vamos utilizar o Visual Studio 2019. Caso você não tenha, basta instalá-lo, a versão da comunidade é gratuita. Após instalado, vamos então criar um novo projeto.

  • Abra o Visual Studio 2019 e vá em Create New Project;
  • Selecione o template de Console Application em C#;
  • Defina um nome para o projeto (LocalWebServer);
  • Selecione o framework utilizado (.NET 5.0).
Animação 1 – Criando uma aplicação de console no Visual Studio 2019

Como o emulador do SAT disponibiliza apenas a DLL em 32 bits, vamos colocar o tipo de plataforma da aplicação como x86.

  • Clique como botão direito no projeto e vá em Properties;
  • Selecione a aba Build e na opção Platform Target selecione x86.
Animação 2 – Definindo plataforma da aplicação

Vamos agora criar um arquivo de configuração.

  • Clique com o botão direito no projeto e vá em Add > New Item;
  • Escolha a opção Application Configuration File.
Animação 3 – Adicionando arquivo de configuração no projeto
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="port" value="9696"/>
    <add key="origin" value="https://rafaelcouto.com.br"/>
  </appSettings>
</configuration>
  • Linha 4: Como vamos criar um servidor web local, precisamos de uma porta para que esse servidor seja acessado e neste exemplo vamos utilizar a porta 9696. Lembrando que está porta deverá estar liberada no computador do usuário, caso haja um firewall, por exemplo;
  • Linha 5: Precisamos também, por questões de segurança, a origem do domínio que o servidor web irá aceitar as requisições, que neste caso será o endereço base da aplicação web.

Para criação do servidor web, vamos utilizar a biblioteca EmbedIO, que nos permite criar um servidor web de forma bem simples.

  • Vá em Tools > NuGet Package Manager > Package Manager Console;
  • Execute o comando Install-Package EmbedIO.
Animação 4 – Adicionando EmbedIO no projeto

Pronto, agora sim podemos criar o servidor web.

using System;
using System.Configuration;
using EmbedIO;
using EmbedIO.WebApi;
using LocalWebServer.Http.Controllers;
using LocalWebServer.Http;

namespace LocalWebServer
{
    class Program
    {
        static void Main(string[] args)
        {
            var url = string.Format("http://127.0.0.1:{0}", ConfigurationManager.AppSettings["port"]);

            using (var server = CreateWebServer(url))
            {
                server.RunAsync();
                Console.ReadKey(true);
            }
        }

        private static WebServer CreateWebServer(string url)
        {
            // Opções do servidor
            var options = new WebServerOptions();
            options.WithUrlPrefix(url);

            // Criação do servidor
            var server = new WebServer(options);
            server.WithCors(ConfigurationManager.AppSettings["origin"]);
            server.HandleUnhandledException(CustomExceptionHandler.ResponseForUnhandledException);
            server.HandleHttpException(CustomExceptionHandler.ResponseForHttpException);

            // Criação do módulo de API
            var module = new WebApiModule("/");
            module.WithController<SatController>();
            server.WithModule(module);

            return server;
        }
    }
}
  • Linha 14: Definimos o endereço e porta que será utilizado para se conectar no servidor web, como é local vamos deixar sempre 127.0.0.1 (localhost) e a porta pegamos do arquivo de configuração definido anteriormente;
  • Linha 16: Obtemos a instância do servidor web através do método CreateWebServer() que veremos em detalhes adiante;
  • Linha 18 e 19: Iniciamos o servidor de forma assíncrona através do método RunAsync() e utilizamos o Console.ReadKey() para que a aplicação fique em execução até uma tecla ser pressionada;
  • Linha 26 e 27: Definimos nas opções do servidor, a URL base que será utilizada, que no caso será http://127.0.0.1:9696;
  • Linha 31: Atenção, essa opção é muito importante! O CORS é um mecanismo de segurança para definir as origens que serão permitidas acessar nossa aplicação. Se não informamos nada, qualquer origem poderá acessar a aplicação, ou seja, qualquer site que o usuário entrar, se tentar acessar o http://127.0.0.1:9696, ele simplesmente terá acesso a aplicação e consequentemente ao dispositivo local. Portanto, passamos o endereço da aplicação web definido no arquivo de configuração;
  • Linha 32 e 33: Por padrão, o EmbedIO retorna uma página HTML quando lançado uma exceção, porém como é preferível utilizar JSON para comunicação com a aplicação web, criamos o CustomExceptionHandler para modificar esse padrão;
  • Linha 36 a 38: O EmbedIO trabalha com o conceito de módulos. Portanto, vamos criar um módulo WebApi que será na raiz da nossa aplicação, ou seja, http://127.0.0.1:9696/, e nesse módulo vamos adicionar o SatController que veremos adiante.

Antes de criar o Controller, vamos criar uma Interface para representar um dispositivo.

namespace LocalWebServer.Core.Sat
{
    interface Device
    {
        string consultar();
    }
}

Por enquanto vamos criar somente o método consultar(), porém em uma aplicação real teríamos vários métodos como enviarDadosVenda(), cancelarUltimaVenda(), etc.

Vamos então implementar o emulador.

using System;
using System.Runtime.InteropServices;

namespace LocalWebServer.Core.Sat.Devices
{
    class Emulator : Device
    {
        [DllImport("SAT.dll", CallingConvention = CallingConvention.Cdecl)] 
        public static extern IntPtr ConsultarSAT(int numeroSessao);

        public string consultar()
        {
            int numeroSessao = SatUtil.generateNumeroSessao();
            return Marshal.PtrToStringAnsi(ConsultarSAT(numeroSessao));
        }

    }
}
  • Linha 8 e 9: Declaramos o método ConsultarSAT() que fará a chamada do método no SAT.dll disponibilizada pelo emulador do SAT. Lembrando que essa DLL deverá estar na mesma pasta do executável;
  • Linha 13 e 14: O método ConsultarSAT() solicita como parâmetro um número de sessão, que é basicamente um número aleatório para controle da requisição, portanto geramos ele através da classe utilitária SatUtil. A resposta do método será um IntPtr (ponteiro), então precisamos utilizar o método Marshal.PtrToStringAnsi() para convertê-lo para string.

Para a aplicação ficar mais extensível, vamos utilizar a técnica de Simple Factory.

using LocalWebServer.Core.Sat.Devices;
using System;

namespace LocalWebServer.Core.Sat
{
    class DeviceFactory
    {
        public Device create(string deviceName)
        {
            switch(deviceName)
            {
                case "emulator":
                    return new Emulator();
            }

            throw new NotImplementedException();
        }
    }
}

Dessa forma, caso você precise implementar dispositivos de outras marcas (Elgin, Bematech, etc.), basta apenas criar uma classe implementando Device e então adicionar na DeviceFactory.

Por fim, vamos criar o Controller, que será responsável por gerenciar as requisições relacionadas ao dispositivo SAT.

using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using LocalWebServer.Core.Sat;
using System;

namespace LocalWebServer.Http.Controllers
{
    public sealed class SatController : WebApiController
    {
        [Route(HttpVerbs.Get, "/sat/consultar/{deviceName}")]
        public string consultar(string deviceName)
        {
            DeviceFactory deviceFactory = new DeviceFactory();
            Device device;

            try
            {
                device = deviceFactory.create(deviceName);
            } catch (NotImplementedException)
            {
                throw HttpException.BadRequest("Dispositivo não implementado");
            }

            return device.consultar();
        }
    }
}
  • Linha 11 e 12: Através dos atributos, definimos que este método consultar() será chamado quando utilizado o verbo HTTP Get e será acessado através da rota http://127.0.0.1:9696/sat/consultar/. Precisamos também passar o parâmetro deviceName, pois nossa aplicação pode ter vários usuários diferentes, e cada usuário ter um SAT de uma marca diferente, então o deviceName poderia ser emulator, elgin, bematech, etc;
  • Linha 17 a 23: Utilizamos a DeviceFactory para criar o Device de acordo com o deviceName passado no parâmetro, e caso o dispositivo não esteja implementado, irá lançar uma exceção HttpException.BadRequest, que irá gerar uma resposta HTTP 400 para a aplicação web;
  • Linha 25: Por fim, chamamos e retornamos o resultado do método consultar() do Device;

Aplicação web

A aplicação web, independente qual tecnologia você utiliza, precisa fazer uma requisição (utilizando Javascript) para a aplicação desktop, pegar o retorno e então enviá-lo para o servidor web remoto para serem gravados em um banco de dados, por exemplo.

Neste artigo, a aplicação web será uma página simples, utilizando HTML, CSS e Javascript, que irá fazer fazer a requisição à aplicação desktop para consultar a situação do dispositivo SAT e exibirá o resultado da requisição no navegador.

Para facilitar as coisas, vamos utilizar o Vue.JS para reatividade, Bootstrap 5 para estilização e o axios para fazer as requisições à aplicação desktop.

Vamos então criar a estrutura HTML da página.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Integrando aplicação web com dispositivo local</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet" 
    integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
    <style type="text/css">
        [v-cloak] {display: none}
    </style>
</head>
<body>
   
    <div id="app" class="container mt-5" v-cloak>
        
        <div class="d-flex flex-column align-items-center justify-content-center" v-if="loading">
            <div class="spinner-border" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
             <p class="lead">Conectando ao dispositivo, aguarde. </p>
         </div>

        <div class="alert alert-success" v-bind:class="{ 'alert-success': !error, 'alert-danger': error }" 
        role="alert" v-if="!loading">
            <h4 class="alert-heading" v-text="status"></h4>
            <hr>
            <p v-text="result"></p>
        </div>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="app.js"></script>
</body>
</html>
  • Linha 18 a 23: Criamos apenas um indicador de carregamento que será exibido quando a propriedade loading for verdadeira;
  • Linha 25 a 30: Criamos uma caixa de alerta que será exibida se loading for falso, será estilizada de acordo com a propriedade error e irá exibir o status e o resultado da comunicação com o dispositivo.

Vamos então criar o arquivo Javascript para fazer as interações na página. Como é apenas para fins didáticos, vamos utilizar recursos do ES6 como async/await, porém em uma aplicação real você precisaria pensar na compatibilidade com navegadores mais antigos.

const app = new Vue({
    el: '#app',
    data: {
        status: null,
        loading: false,
        result: null,
        error: false
    },
    methods: {
        consultarSat: async function() {
            
            this.loading = true;

            try {
                let response = await axios.get('http://127.0.0.1:9696/sat/consultar/emulator');
                this.status = 'Conectado';
                this.result = response.data;
            } catch (e) {

                this.status = 'Erro';
                this.error = true;

                if (!e.response) {
                    this.result = 'Não foi possível se conectar ao dispositivo';
                    return;
                }

                if (e.response.status === 400) {
                    this.result = e.response.data.message;
                    return;
                }

                this.result = 'Erro desconhecido';

            } finally {
                this.loading = false;
            }

        }
    },
    mounted() {
        this.consultarSat();
    }
});
  • Linha 15 a 17: Fazemos a requisição com o servidor web local da aplicação desktop e caso não ocorra nenhuma exceção definimos o status e o resultado; Neste momento, poderíamos enviar esse resultado para o servidor web remoto da aplicação web ao invés de exibir no navegador;
  • Linha 23 a 25: Se o e.response for nulo, significa que o axios não conseguiu concluir a conexão, ou seja, o servidor web local não está em funcionamento ou existe algo bloqueando o acesso à ele;
  • Linha 28 a 31: Se o código da resposta for HTTP 400 (Bad Request), significa que é algum erro tratado no servidor web local, portanto exibimos o resultado (Dispositivo não implementado, por exemplo).

Testando a integração

Agora que temos o emulador instalado, as aplicações desenvolvidas, basta executar o emulador, executar a aplicação desktop e então acessar a aplicação web na sua máquina.

Caso você queria testar apenas a aplicação desktop, vou deixar disponibilizado a aplicação web neste link. Basta executar o emulador, executar a aplicação desktop e então acessar o endereço da aplicação web, assim como demonstrado na Animação 5.

Animação 5 – Testando as aplicações

Conclusão

Neste artigo vimos uma das formas de integrar uma aplicação web com um dispositivo instalado localmente no computador do usuário. Acredito que este método seja o mais simples, pois utilizamos somente o protocolo HTTP para comunicação entre as aplicações. Se você quiser ir mais longe, pode utilizar Websockets também.

Utilizamos como exemplo um dispositivo SAT, porém essa solução não se limita a somente a esse tipo de dispositivo. Podemos integrar a qualquer tipo de dispositivo de hardware que necessite de uma DLL para comunicação, como impressoras, leitores biométricos, balanças, etc.

Vimos a aplicação desktop apenas rodando no Windows, porém em uma aplicação real o ideal seria que ela fosse multiplataforma, ou seja, que funcionasse em Windows, Linux e macOS, pois assim você conseguiria atingir uma quantidade maior de usuários. Com C#, a partir do .NET 5, é possível publicar a aplicação para vários sistemas operacionais, porém poderíamos utilizar JAVA também. Independente da linguagem, teríamos que adaptar o código para utilizar a biblioteca de acordo com a arquitetura do sistema (x86 ou x64) e o sistema operacional (no Linux, por exemplo, o formato da biblioteca é .so e não .dll).

Enfim, apesar do navegador não oferecer recursos para acessar dispositivos locais (por questões de segurança), é sim possível integrar uma aplicação web com dispositivos locais através de uma aplicação desktop que faça a ponte entre o navegador e o dispositivo local.

Código fonte disponível em: https://github.com/rafaelcouto/integrando-aplicacao-web-com-dispositivo-local.

Referências