Plotagem de dados “em tempo real” com Python usando matPlotLib

Introdução

Uma das tarefas mais comuns quando se trabalha com aquisição de dados, é a representação dos mesmos em gráficos, de forma a serem mais facilmente interpretáveis pelo utilizador. Na verdade seja para um uso de hobbie, seja para uma utilização mais científica ou profissional, na maior parte dos casos, acabamos quase sempre por preferir a leitura gráfica dos dados, o que nos permite rapidamente uma noção mais ampla do contexto visível.

Como é do conhecimento do leitor, hoje em dia existe uma grande quantidade de dispositivos que transmitem dados via USB, usando conversão USB – Série, para a transmissão dos dados adquiridos, referentes a um ou mais parâmetros num contexto ou ambiente. No entanto o que a maioria dos dispositivos tem em comum, é a forma como transmite os dados para o computador, normalmente usando um cabo USB ou série e transmitindo os dados em formato de texto simples. Ao longo deste artigo irá ser apresentado o código para receber dados de um conjunto de quatro sensores independentes, ligados a um mesmo dispositivo que os transmite via RS232 e posterior apresentação em modo gráfico, usando a linguagem Python e a biblioteca matplotlib, para nos desenhar o gráfico em tempo “quase real”. (Para os mais distraídos a transmissão via RS-232 é um padrão de protocolo para troca serial de dados binários entre um DTE – Data Terminal Equipment e um DCE – Data Communication Equipment. Normalmente usado nas portas série dos computadores.)

Neste artigo não iremos focar o software que corre no dispositivo, apenas a aquisição de dados e a plotagem dos mesmos, sendo que os diagramas do circuito, bem como o software que corre no micro-controlador, saem claramente do âmbito do que é pretendido neste artigo.

A biblioteca matplotlib, fornece a capacidade de representar dados em gráficos artesianos, entre outros sendo quase uma alternativa livre, a outras peças de software, mais complexas e proprietárias. Além da matplotlib iremos usar a biblioteca numpy, para a parte de cálculos que iremos realizar antes da plotagem dos dados.

O código

Ao longo dos próximos parágrafos, iremos apresentar o código, que será utilizado no final para realizar a plotagem, isto é, a impressão de desenhos em larga escala,dos dados. Em todo o artigo o código escrito é Python versão 2.7, pelo que se recomenda ao leitor que siga esta mesma versão.

Posto isto, vamos ao nosso programa! Para isso começamos por importar a biblioteca que nos permitirá lidar com as comunicações, as duas bibliotecas que nos irão permitir fazer o plot dos dados, primeiro a que nos irá permitir desenhar os gráficos em “tempo real” e por fim a que nos permite armazenar os dados em memória, pelo tempo necessário à sua utilização no âmbito desta aplicação:

import  serial # carrega a biblioteca Serial para
               # comunicações série
import numpy # carrega a biblioteca numpy
import matplotlib.pyplot as plt #carrega a
                                #biblioteca pyplot
import matplotlib.gridspec as gridspec #carrega a
                                       # biblioteca gridspec
from drawnow import * #carrega a biblioteca
                      # drawnow
from array import array #carrega a biblioteca
                        # array

A biblioteca gridspec, será utilizada para disponibilizar múltiplos gráficos numa mesma “janela”, cada gráfico com unidades diferentes e escalas diferentes. Neste caso teremos quatro grupos de gráficos, disponibilizados em “tempo real”, pelo que a biblioteca gridspec nos vai permitir dispô-los na janela de forma conveniente e controlar os gráficos de forma independente, como iremos ver mais adiante neste artigo.

Feito o carregamento das bibliotecas, precisamos de criar os objectos onde vamos armazenar os valores, antes mesmo de iniciarmos a comunicação com o dispositivo. Neste exemplo usaremos seis vectores de variáveis do tipo float. Para comunicar via série, iremos precisar de um objecto do tipo serial, que recebe dois parâmetros, neste caso o nome da porta série, e a velocidade de comunicação, também conhecida por baud rate. Por fim vamos usar um contador, pois não vamos querer todos os dados armazenados em memória, apenas as últimas posições e iniciamos a matplotlib em modo interactivo utilizando o método ion().

#instanciação dos objectos
tempF = array('f')
tempF2 = array('f')
humF1 = array('f')
humF2 = array('f')
lum = array('f')
moist = array('f')
#comunicacao
arduinoData = serial.Serial('com3', 115200) #Cria
# um objecto do tipo serial chamado
# arduinoData
plt.ion()
cnt=0

Finda esta parte, vamos agora criar a função que irá ser chamada a cada nova leitura de dados, para desenhar e actualizar o gráfico com os novos dados obtidos.

Nesta função, chamemos-lhe makeFig, iremos desenhar uma grelha de 3 por 3 recorrendo à biblioteca gridspec, a fim de suportar a disposição dos 4 gráficos apresentados, deixando espaço entre eles. A grelha será algo semelhante à figura abaixo:

MatPlotLib: grelha com gráficos

Criada a grelha, cada gráfico passa a ser uma “imagem”, separada, com as suas propriedades independentes, como o caso da posição, legendas, escalas, etiquetas, símbolos e cores das linhas. De igual modo cada um dos restantes quatro gráficos, terá essa mesma definição, como pode ser observado no código da função. Para simplificar, apenas está comentado o código da primeira “figura”, acreditando que para o leitor, será simples entender o restante código, uma vez que as instruções são as mesmas, mudando apenas alguns parâmetros, específicos para cada figura.

def makeFig():
 gs = gridspec.GridSpec(3, 3) #gridspec 3x3
 #Plot 1
 plt.subplot(gs[0, :])#posicao do subplot
 plt.ylim([-30,50])#valor min e max de y
 plt.title('Temperatura em Graus C')#titulo
 plt.grid(True)
 plt.ylabel('Temp-1 c')#etiquetas do eixo y
 plt.plot(tempF, 'ro-', label='temperatura em
 graus') #plot de temperature
 plt.legend(loc='upper left')#plot da legenda
 plt2=plt.twinx()#cria um Segundo eixo y
 plt.ylim(-30,50)#define os limites do Segundo eixo y
 plt2.plot(tempF2, 'b^-', label='Temp-2 c')
 # desenha os val. De tempF2
 plt2.set_ylabel('Temp-2 c') #Etiqueta do
 # Segundo eixo
 plt2.ticklabel_format(useOffset=False)# impede
 # a escala do eixo X
 plt2.legend(loc='upper right')
 #Plot 2
 plt.subplot(gs[1, :])
 plt.ylim([0,100])
 plt.title('Humidade do Ar em Percentagem')
 plt.grid(True)
 plt.ylabel('Humidade -1 %')
 plt.plot(humF1, 'ro-', label='Humidade')
 plt.legend(loc='upper left')
 plt2=plt.twinx()
 plt.ylim(0,100)
 plt2.plot(humF2, 'b^-', label='Humidade-2 %')
 plt2.set_ylabel('Humidade-2 %')
 plt2.ticklabel_format(useOffset=False)
 plt2.legend(loc='upper right')
 #Plot 3
 plt.subplot(gs[-1,0])
 plt.ylim([0,100])
 plt.title('Humidade do solo')
 plt.grid(True)
 plt.ylabel('Humidade %')
 plt.plot(moist, 'ro-', label='Humidade')
 plt.legend(loc='upper left')
 #Plot 4
 plt.subplot(gs[-1,-1])
 plt.ylim([0,2000])
 plt.title('Luminosidade')
 plt.grid(True)
 plt.ylabel('Luminosidade (lux)')
 plt.plot(lum, 'ro-', label='Luminosidade')
 plt.legend(loc='upper left')

Chegados a esta parte do código, falta-nos escrever o programa principal que será executado. Um dos problemas mais comuns com aquisição de dados via série, são os atrasos e as falhas de transmissão de dados. Para evitar algumas dessas situações podemos indicar ao computador que a menos que existam dados para serem recebidos, não executa o restante código. Neste exemplo concreto usamos um ciclo while para o fazer, verificando se existem dados no buffer. Se existirem, o restante código é executado, caso contrário repete o ciclo, aguardado a disponibilidade dos mesmos.

while (arduinoData.inWaiting()==0): #aguarda por
                                    # dados
    pass #não executa nada

Existindo dados no buffer, procedemos à leitura dos mesmos recorrendo ao método readline(), como veremos no código exemplo. Neste caso convém alertar para o facto de os dados virem em formato de texto (string) e ser necessária a sua conversão a float, para ser feito o plot. Adicionalmente os dados vêm numa única linha, separados por vírgula (,), noutros casos podem utilizar qualquer outro caracter de separação, dependendo do sistema utilizado. A primeira tarefa a fazer, após a recepção da informação, será a separação da string, nas múltiplas variáveis e posterior armazenamento dos dados em memória RAM, antes de fazermos a sua conversão para float e armazenamento no vector de valores do tipo float, que será usado para gerar o gráfico.

Uma vez que as dimensões de um ecrã, são limitadas, vamos optar por manter apenas visíveis os últimos cinquenta pontos de cada gráfico, seguindo uma lógica FIFO (first in, first out), onde os valores que “entraram” primeiro (mais antigos), são eliminados e os novos são acrescentados uma posição à frente da última posição utilizada recorrendo ao método pop(), que recebe como argumento a posição que queremos eliminar o valor nela contido.

Como o desenho será em “tempo real”, teremos de colocar todas estas tarefas num ciclo infinito, por forma a manter a sua execução ininterrupta, neste caso, novamente um ciclo while. Poderíamos ter usado qualquer outro tipo de ciclo, mas por uma questão de comodidade, usou-se um while, deixando-se ao critério do leitor, usar qualquer outro ciclo da sua preferência.

#mainProgram
while True:
  while (arduinoData.inWaiting()==0):
    pass
  arduinoString = arduinoData.readline()
  #"A tarefa mais urgente da vida, é: O que
  # estás a fazer pelos outros?" (Martin
  # Luther King Jr.)
  splitedArray = [float(s) for s in
  arduinoString.split(',')]
  temp = splitedArray[0]
  hum1 = splitedArray[1]
  temp2 = splitedArray[2]
  hum2 = splitedArray[3]
  moisture = splitedArray[4]
  lumen = splitedArray[5]
  tempF.append(temp)
  tempF2.append(temp2)
  humF1.append(hum1)
  humF2.append(hum2)
  moist.append(moisture)
  lum.append(lumen)
  drawnow(makeFig)
  plt.pause(.000005)
  cnt=cnt+1
  if(cnt>50):
    tempF.pop(0)
    tempF2.pop(0)
    humF1.pop(0)
    humF2.pop(0)
    moist.pop(0)
    lum.pop(0)
#"The best way to learn is to
# teach" (Openhimmer)
MatPlotLib: Imagem do programa em execução
Imagem do programa em execução

Conclusão

Ao longo deste artigo apresentamos a matplotlib e as restantes bibliotecas de Python que utilizamos para gerar gráficos em tempo real. Exploramos a criação de múltiplos gráficos com escalas diferentes numa mesma figura, e por fim a recepção dos dados, provenientes de um equipamento que comunique com o computador por porta série, ou qualquer emulação dessa mesma porta e a sua representação gráfica, em cada um dos gráficos do layout, de forma independente. Como pudemos ver, é possível criar os mais variados layouts para a apresentação de gráficos usando a linguagem de programação Python e a biblioteca matplotlib. É de facto simples comunicar via porta série usando a biblioteca serial e desenhar objectos gráficos no ecrã recorrendo ao conjunto de bibliotecas drawnow.

Deixamos as referências que utilizamos neste artigo e esperamos que o leitor se sinta entusiasmado a experimentar a linguagem Python e as bibliotecas referidas no exemplo.

Referências

  1. Documentação oficial da biblioteca matplotlib: http://matplotlib.org/
  2. Documentação oficial da biblioteca Numpy: http:// www.numpy.org/
  3. Documentação oficial da biblioteca pySerial: https://pythonhosted.org/pyserial/
  4. Python – Algoritmia e Programação Web (José Braga de Vasconcelos), FCA Editora.
  5. Scientific Computing in Python – NumPy, SciPy, Matplotlib (Dr. Axel KohlMeyer). http://www.ictp-saifr.org/wp-content/uploads/2014/09/numpy-scipy-matplotlib.pdf
  6. Instrument Control (iC) – An Open-Source Software to Automate Test Equipment. Journal of Research of the National Institute of Standards and Technology, Volume 117 (2012). http://dx.doi.org/10.6028/jres.117.010
  7. Real World Instrumentation with Python (J. M. Hughes), O’Reilly.

Publicado na edição 51 (PDF) da Revista PROGRAMAR.