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.
Como podemos observar na Imagem 1, o fluxo fica da seguinte forma:
- A aplicação web se comunica com a aplicação desktop através do protocolo HTTP;
- 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;
- 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 oC:\
.
Basta então executar o Emulador SAT-CFe e estamos prontos para continuar.
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).
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.
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.
<?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
.
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 oConsole.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 oSatController
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 noSAT.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áriaSatUtil
. A resposta do método será um IntPtr (ponteiro), então precisamos utilizar o métodoMarshal.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 verboHTTP Get
e será acessado através da rotahttp://127.0.0.1:9696/sat/consultar/
. Precisamos também passar o parâmetrodeviceName
, pois nossa aplicação pode ter vários usuários diferentes, e cada usuário ter um SAT de uma marca diferente, então odeviceName
poderia ser emulator, elgin, bematech, etc; - Linha 17 a 23: Utilizamos a
DeviceFactory
para criar oDevice
de acordo com odeviceName
passado no parâmetro, e caso o dispositivo não esteja implementado, irá lançar uma exceçãoHttpException.BadRequest
, que irá gerar uma respostaHTTP 400
para a aplicação web; - Linha 25: Por fim, chamamos e retornamos o resultado do método
consultar()
doDevice
;
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 propriedadeerror
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.
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
- Browser Architecture: Web-to-App Communication Overview. <https://textslashplain.com/2019/08/28/browser-architecture-web-to-app-communication-overview>. Acesso em 7 de maio de 2021.
- Especificação de Requisitos do SAT. <https://portal.fazenda.sp.gov.br/servicos/sat/Downloads/Especificacao_SAT_v_ER_2_28_05.pdf>. Acesso em 8 de maio de 2021.
- EmbedIO. <https://github.com/unosquare/embedio>. Acesso em 9 de maio de 2021.
- Vue.JS Guide. <https://vuejs.org/v2/guide/>. Acesso em 11 de maio de 2021.