quarta-feira, 15 de abril de 2015

Tutorial comunicação serial, node.js e gráficos em tempo real via socket.

Com o surgimento do Arduino, foi possível ampliar as aplicações de eletrônica e robótica em geral com a rápida adesão e criação de bibliotecas e módulos específicos e fáceis de manusear. Tratando-se de software, o Node.js tornou-se muito popular com o surgimento de milhares de pacotes e a possibilidade de estabelecer conexões com várias linguagens, tipos de bancos de dados e inúmeras tecnologias. Dentre elas, a comunicação serial, padrão usado na maioria dos projetos de sistemas microcontrolados.
Estabelecendo a conexão via serial de seu sistema embarcado com o Node.js torna possível expandir a sua aplicação uma vez que o node implementa um servidor web. Você pode facilmente utilizar as milhares de APIs web disponíveis além de hospedar seu sistema em um servidor e fornecer acesso por smartphones, tablets e demais dispositivos que se conectam à rede mundial de computadores.
Esse tutorial tem como objetivo explicar como estabelecer a comunicação serial entre seu sistema microcontrolado (não necessariamente Arduino, mas qualquer um que implemente comunicação serial) e o Node.js, além de utilizar uma biblioteca em JavaScript para plotar gráficos em tempo real utilizando uma página web.

Primeiro passo, é preciso instalar o Node.js em sua máquina, basta acessar o site https://nodejs.org/download/ e seguir para a seção de downloads. 
A instalação segue um fluxo tranquilo em Windows e Mac. No Windows, você precisa adicionar o caminho do executável do node para a variável de ambiente do sistema PATH para poder executá-lo do terminal do Windows. Isso pode ser feito em painel de controle, configurações avançadas.

Se está usando ambiente GNU/Linux, basta baixar o arquivo compactado com o código-fonte, extrair os dados para uma pasta qualquer, em seguida, acesse a pasta e digite os comandos:

./configure 
make
#make install 

Se você encontrou algum erro nessa parte, certifique-se de ter instalado o pacote build-essential que contém os compiladores necessários para gerar o executável do node. O comando make pode demorar mais de 5 minutos porque ele está compilando o sistema e isso demora mesmo. Lembre-se que o terceiro comando deve ser feito com privilégios de administrador (sudo), uma vez que ele copia o arquivo para a pasta bin do sistema.

Pronto, node.js já está instalado em sua máquina. Junto com ele, existe o npm (node package manager) que é o gerenciador de pacotes do node. A partir do qual é possível instalar pacotes para as mais variadas aplicações (acesse esse link para conferir do que estou falando: https://www.npmjs.com/).

Vamos inicialmente criar um diretório para organizar nosso app (geralmente os projetos em node são chamados de app - aplicação). Abre um terminal e navegue até o diretório. Feito isso, digite o seguinte comando:

npm install express

Esse é o primeiro pacote que vamos instalar. Verifique que após a instalação, um subdiretório foi criado: node_modules. Todos os módulos que você instalar ficarão localizados dentro dessa pasta. A menos que use a opção -g em que o módulo é instalado globalmente no diretório do node no seu sistema (opção usada quando vamos instalar o socket.io - algumas vezes ele dá problema e exige que seja instalado globalmente).

http://expressjs.com/ esse pacote é interessante porque ele é um framework que encapsula muitas funções do node para você e permite um desenvolvimento mais fluido para seu app. Para maiores informações, acesse a página dele.

Agora digite o seguinte comando:

npm install serialport

Esse módulo é o responsável em estabelecer a comunicação serial com o node. Maiores detalhes aqui: https://www.npmjs.com/package/serialport .

Por fim:

npm install socket.io

Já que temos nossos pacotes básicos, vamos criar um arquivo chamado server.js dentro do nosso diretório de trabalho. Esse é o arquivo principal de nossa aplicação. Vamos iniciar a codificação. Primeiro requisite os seguintes arquivos (semelhante ao include do C e require do PHP):

var app      = require('express')();
var express = require('express');

Essas variáveis irão armazenar objetos que utilizaremos mais adiante. Em seguida vamos utilizar um diretório para servir arquivos públicos como arquivos .css e .js.

app.use(express.static(__dirname + '/public'));

Definimos executando o método use do app que instanciamos mais adiante. Eu separei o arquivo express para justamente poder utilizar a chamada express.static acima. Agora crie também um diretório chamado public na pasta do projeto. Iremos adicionar alguns arquivos mais na frente no tutorial.

O próximo passo é criar o nosso servidor http e instanciar o socket.

var http = require('http').Server(app);
var io = require('socket.io')(http);

Feito isso, temos o nosso servidor e o socket prontos para serem usados. Agora definimos rotas para o servidor web responder às requisições dos usuários com a seguinte linha:

app.get('/', function(req, res) {
  res.sendfile('index.html');
});

A função de callback aceita dois parâmetros, uma requisição e uma resposta. Isso quer dizer que, quando o usuário requisitar o endereço raiz da nossa aplicação "/", o node vai retornar uma página chamada index.html que depois criaremos.

Agora vamos criar a variável do socket:

var mySocket;

Registramos o socket quando a conexão é estabelecida da seguinte maneira:

io.on('connection', function(socket) {
  mySocket = socket;
  console.log('a user connected');
  socket.on('disconnect', function() {
      mySerialPort.close(function(err) {
          if (err) console.log("Error at disconnect event: " + err);
          console.log('port closed');
      });
  });
});

Registramos eventos usando o método on. Quando o usuário emitir um sinal para fechar a conexão, o nosso socket irá encerrar a comunicação (on 'disconnect').

Vamos definir agora a porta serial:

var serialport = require("serialport");
var SerialPort = serialport.SerialPort;

Em seguida instanciamos o objeto com as configurações:

var mySerialPort = new SerialPort("/dev/ttyACM0", {
    baudrate: 115200,
    parser: serialport.parsers.readline("\n")
  });

Observe que é preciso dizer a porta serial onde seu microcontrolador está conectado. No Debian ela foi reconhecida como /dev/ttyACM0, mas no Windows será COMx e no Mac como /dev/tty.usbmodemx. Certifique-se de colocar o caminho correto do seu dispositivo. Definimos também a taxa de transmissão dos dados e escolhemos um parser para a serial. Ele vai ler linhas e reconhecer quando houver mudança de linha "\n". Isso pode ser encontrado na documentação do serialport.

Agora vamos criar eventos para a nossa porta serial.

Quando ela for aberta corretamente, vamos indicar uma mensagem para o usuário:

mySerialPort.on("open", function() {
    console.log("Port Open");
});

Veja que em cada evento, existe uma callback relacionada como ele a qual é executada quando o evento é disparado.

Um evento especial chamado 'data' é quando os dados chegam na porta serial. Vamos implementar a leitura dos dados que chegam:

mySerialPort.on("data", function(data) {
  console.log('data received: ' + data); // apenas debug
  io.emit('serialData', {
    dado: parseFloat(data)
  });

});


Observe que o socket já foi incluído dentro do evento, e, quando os dados chegam, já emitimos um evento via socket para que o usuário que está acessando a página veja o resultado. O evento 'serialData' foi criado por mim e esse nome será utilizado posteriormente na página index.html para gerenciar a chegada de novos dados pelo socket. Eu configurei o Arduino para de tempos em tempos enviar um número ponto flutuante (float) pela serial.

Por fim, vamos iniciar o nosso servidor:

http.listen(3000, function() {
  console.log('listening on *:3000');
});

Esse comando faz com que o servidor esteja ligado e funcione na porta 3000.

Vamos agora implementar o nosso index.html. Primeiramente, vamos acessar a página da biblioteca Chart.js que vamos utilizar para plotar os gráficos. Crie um diretório js dentro da pasta public do projeto. Acesse a página http://www.chartjs.org/ e clique em download. Quando abrir o repositório do github, clique no arquivo Chart.js, depois na opção Raw e salve o arquivo dentro do diretório public/js de sua aplicação.

Estarei utilizando gráfico de linhas e, para maiores informações, acesse a documentação dele aqui: http://www.chartjs.org/docs/ .

Vamos criar nosso index.html. Basicamente configuramos o socket, instanciamos o Chartjs, configuramos alguns parâmetros e tratamos os eventos do socket para que os dados sejam plotados. Definimos um canvas, que é o lugar onde o gráfico será plotado. No meu caso, usei algumas configurações globais e coloquei dentro do diretório js também, o arquivo está em logo abaixo.

// Conteúdo do arquivo ChartConfig.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
Chart.defaults.global = {
    // Boolean - Whether to animate the chart
    animation: false,

    // Number - Number of animation steps
    animationSteps: 0,

    // String - Animation easing effect
    animationEasing: "easeOutQuart",

    // Boolean - If we should show the scale at all
    showScale: true,

    // Boolean - If we want to override with a hard coded scale
    scaleOverride: false,

    // ** Required if scaleOverride is true **
    // Number - The number of steps in a hard coded scale
    scaleSteps: null,
    // Number - The value jump in the hard coded scale
    scaleStepWidth: null,
    // Number - The scale starting value
    scaleStartValue: null,

    // String - Colour of the scale line
    scaleLineColor: "rgba(0,0,0,.1)",

    // Number - Pixel width of the scale line
    scaleLineWidth: 1,

    // Boolean - Whether to show labels on the scale
    scaleShowLabels: true,

    // Interpolated JS string - can access value
    scaleLabel: "<%=value%>",

    // Boolean - Whether the scale should stick to integers, not floats even if drawing space is there
    scaleIntegersOnly: true,

    // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
    scaleBeginAtZero: false,

    // String - Scale label font declaration for the scale label
    scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",

    // Number - Scale label font size in pixels
    scaleFontSize: 12,

    // String - Scale label font weight style
    scaleFontStyle: "normal",

    // String - Scale label font colour
    scaleFontColor: "#666",

    // Boolean - whether or not the chart should be responsive and resize when the browser does.
    responsive: true,

    // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
    maintainAspectRatio: true,

    // Boolean - Determines whether to draw tooltips on the canvas or not
    showTooltips: false,

    // Function - Determines whether to execute the customTooltips function instead of drawing the built in tooltips (See [Advanced - External Tooltips](#advanced-usage-custom-tooltips))
    customTooltips: false,

    // Array - Array of string names to attach tooltip events
    tooltipEvents: ["mousemove", "touchstart", "touchmove"],

    // String - Tooltip background colour
    tooltipFillColor: "rgba(0,0,0,0.8)",

    // String - Tooltip label font declaration for the scale label
    tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",

    // Number - Tooltip label font size in pixels
    tooltipFontSize: 14,

    // String - Tooltip font weight style
    tooltipFontStyle: "normal",

    // String - Tooltip label font colour
    tooltipFontColor: "#fff",

    // String - Tooltip title font declaration for the scale label
    tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",

    // Number - Tooltip title font size in pixels
    tooltipTitleFontSize: 14,

    // String - Tooltip title font weight style
    tooltipTitleFontStyle: "bold",

    // String - Tooltip title font colour
    tooltipTitleFontColor: "#fff",

    // Number - pixel width of padding around tooltip text
    tooltipYPadding: 10,

    // Number - pixel width of padding around tooltip text
    tooltipXPadding: 10,

    // Number - Size of the caret on the tooltip
    tooltipCaretSize: 8,

    // Number - Pixel radius of the tooltip border
    tooltipCornerRadius: 6,

    // Number - Pixel offset from point x to tooltip edge
    tooltipXOffset: 10,

    // String - Template string for single tooltips
    tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",

    // String - Template string for multiple tooltips
    multiTooltipTemplate: "<%= value %>",

    // Function - Will fire on animation progression.
    onAnimationProgress: function() {},

    // Function - Will fire on animation completion.
    onAnimationComplete: function() {}
  }

// fim do arquivo ChartConfig.js

Aqui começa o index.html:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <title>Realtime plot</title>
  <script src="/socket.io/socket.io.js"></script>
</head>

<body>
  <canvas id="meusDados" width="900" height="400"></canvas>
  <script src="js/Chart.js"></script>
  <script src="js/ChartConfig.js"></script>
  <script language="javascript">
  // conjunto de dados vazio
  var formatoDados = {
    labels: [],
    datasets: [{
      fillColor: "rgba(172,194,132,0.4)",
      strokeColor: "#ACC26D",
      pointColor: "#fff",
      pointStrokeColor: "#9DB86D",
      data: []
    }]
  };
  var meusDados = document.getElementById("meusDados").getContext("2d");
  var myChart = new Chart(meusDados).Line(formatoDados);
  </script>
  <script>
  var socket = io();
  var cont = 1;
  var valores = [];
  socket.on('serialData', function(res) {
    
    if (cont > 81) {
      myChart.removeData();
      //socket.disconnect();
      //console.log("serialport closed");
    }

    myChart.addData([res.dado], cont++);

    //console.log(res.dado);
  });
  </script>
</body>

</html>

Observe que, após instanciar o objeto do socket, eu verifico o evento que foi criado no servidor 'serialData' e efetuo a leitura desse valor através de uma callback. Observe que ele chega em forma de objeto e precisamos acessar o campo 'dado' para obter o valor desejado. 

Utilizo a variável cont para servir como eixo das abscissas, os dados provenientes da serial comporão o eixo das ordenadas. Inicialmente o conjunto de dados é vazio e ele é preenchido com os dados da serial. Por motivos de visualização, eu decidi remover o conjunto de dados quando o contador for maior do que 81, caso contrário, se o fluxo de dados for muito grande, você poderá perceber travamentos no seu browser.

Pronto, o seu aplicativo está pronto para receber dados da serial e plotar em tempo real.

Execute o seguinte comando para iniciar o servidor node em terminal:

node server.js

Você verá uma mensagem indicando que o servidor está "escutando" na porta 3000.
Para verificar o funcionamento, abra um navegador e digite o endereço http://localhost:3000

Algumas melhorias ainda precisam ser feitas na utilização da biblioteca Chart.js. Se o fluxo de chegada de dados for grande, pode ser que o sistema trave. É preciso ter um maior controle disso e evitar que ocorre estouro de buffer pelo uso exagerado de memória.

Todo esse processo foi feito em videoaulas e você acompanhá-las pelos links abaixo:


Parte 1: https://www.youtube.com/watch?v=LNtmLapUSzw



Parte 2: https://www.youtube.com/watch?v=ewMkFQ6surQ

Parte 3: https://www.youtube.com/watch?v=d_SoD0iE54Y



Qualquer dúvida, pode comentar nos vídeos que eu responderei.

Um abraço,

Bruno Pinho