terça-feira, 6 de janeiro de 2015

Interface de comunicação Arduino-Python através do módulo Tkinter


A ideia deste post é explicar como enviar parâmetros para o Arduino inserindo valores em uma janela, de forma que não seja necessário alterar toda vez a programação via IDE para mudar esses parâmetros. 


Para isso, criaremos uma interface gráfica em Python que envia as informações inseridas ao Arduino, que por sua vez as identifica e salva na EEPROM (a EEPROM é uma memória que mantém a informação salva quando a placa é desligada, como um pequeno HD. Utilizaremos a biblioteca EEPROM, que permite ler e gravar valores em posições especificadas da EEPROM).

Assim, quando o Arduino é conectado ao computador e roda-se o programa aqui descrito (em Python), uma janela se abre, através da qual podemos enviar valores que servirão como um comando para que o Arduino execute algo, como ligar e desligar o LED por um determinado tempo, por exemplo. Quando desconectamos o Arduino, ele continuará executando a função de acordo com os parâmetros salvos.

O módulo Tkinter permite o desenvolvimento de interfaces gráficas em Python. Utilizaremos o Tkinter nestes exemplos para criar as interfaces que enviam informações à EEPROM do Arduino por meio da biblioteca pySerial.


Lista de Materiais:

- Python instalado (estou usando a versão 3.4)
- Módulo Python-Serial (pySerial) - Versão 2.7 para Python 3.x
  https://pypi.python.org/pypi/pyserial - Download
  http://pyserial.sourceforge.net/pyserial.html - Documentação em inglês
- Arduino (estou usando o Arduino Leonardo)
- IDE Arduino instalada



Exemplo 1


O código abaixo é o programa mais básico possível utilizando o Tkinter, que cria uma janela que exibe o texto “Hello, world!”.




from tkinter import *
root = Tk
w = Label (root, text= "Hello, world!")
w.pack()
root.mainloop

Explicação do código:

Para usar o Tkinter, começamos importando o módulo Tkinter, que contém todas as classes, funções e outras coisas necessárias para trabalhar com o “Tk toolkit”:

from tkinter import *

Se você está usando a versão 3.x do Python, use a primeira letra minúscula (tkinter). Caso contrário, a primeira letra deve ser maiúscula (Tkinter).

Para inicializar o Tkinter, temos que criar uma janela, normalmente chamada de raíz (“root”):

root = Tk()

Na sequência, criamos um widget do tipo “Label” como “filha” da janela raíz. A palavra “widget” designa componentes de interface gráfica com o usuário de um modo geral, incluindo elementos como janelas, botões, menus, campos de entrada, barras de rolagem, etc. Um widget do tipo Label pode ser utilizado para exibir um texto ou imagem. Aqui exibimos o texto “Hello, world!”.

w = Label(root, text="Hello, world!")

Estamos utilizando o gerenciador de geometria pack. Existem também os gerenciadores grid e place.

w.pack()

Por fim, vem a parte que executa o método mainloop deste objeto raiz - que torna a janela visível esperando que eventos aconteçam. O programa ficará no loop até que fechemos a janela.

root.mainloop()


Exemplo 2

Este código cria uma janela através da qual enviaremos informações que serão gravadas na EEPROM do Arduino, utilizando o módulo pySerial - que realiza o acesso à porta serial. Para que o programa se comunique corretamente com o Arduino, é necessário que o firmware abaixo esteja gravado no Arduino e que a porta à qual ele está conectado esteja devidamente identificada na linha 6 do código em Python.

Na janela há 2 campos de entrada. O código em Python envia estes valores ao Arduino, que os lê e salva EEPROM. O código salvo no Arduino, além de conter a instrução de salvar na EEPROM as informações recebidas, identificará a informação salva na posição 0 da EEPROM (que vem do primeiro campo de entrada) como o tempo que o LED fica aceso, e  informação salva na posição 1 da EEPROM (que vem do segundo campo de entrada) como o tempo que ele fica apagado - fazendo-o piscar.




Como a comunicação funciona:


Gravar:

Código em Python:
Quando clicamos no botão "Grava", é enviada a letra "w" (escolhida arbitrariamente) e na sequência a letra "a" o valor do campo de entrada 1 (campo superior - tempo ligado) e a letra "b" + o valor do campo de entrada 2 (campo inferior - tempo desligado). Por exemplo, se escolhermos os tempos 10 (ligado) e 5 (desligado), ao clicar em "Grava" o programa enviará: w a10 b5.

Firmware:
O código salvo no Arduino possui a condição de que, caso receba a letra "w", salva na posição 0 da EEPROM  o valor que vem precedido pela letra "a" e na posição 1 o valor precedido pela letra "b".

Ler:

Código em Python:
Quando clicamos no botão "Lê", é enviada a letra "r" e lidos os valores que serão recebidos na sequência - que por sua vez são inseridos pelo programa nos campos de entrada 1 e 2.

Firmware:
Caso receba a letra "r", envia os  valores das posições 0 e 1 da EEPROM precedidos pelas letras "a" e "b", respectivamente.


Firmware: 

#include < eeprom.h > 
char var = 0;
char var2 = 0;
int led = 13;

void setup(){  
  Serial.begin(115200);
}

void loop(){

  if(Serial.available()) {
    var = Serial.read();
    var2 = Serial.read();

    switch (var){
      
    case 'r': //envia os valores
        Serial.print('a');
        Serial.println(EEPROM.read(0)); // envia o valor do campo 0 da EEPROM precedida pela letra a    
        Serial.print('b');
        Serial.println(EEPROM.read(1)); // envia o valor do campo 1 da EEPROM precedida pela letra b    

        break;
        
    case 'w': //salva os valores
        if (var2 == 'a')
          EEPROM.write(0, Serial.parseInt());    //se receber a letra a precedida por w, salva o valor na posição 0 da EEPROM
        else if (var2 == 'b')
           EEPROM.write(1, Serial.parseInt());   //se receber a letra B precedida por w, salva o valor na posição 1 da EEPROM      
    break;         
    }
  }
  
  digitalWrite(led, HIGH);      // Liga o Led
  delay(EEPROM.read(0)*100);   // Espera o número de milisegundos definido no programa (salvo na posição 0 da EEPROM)  
  digitalWrite(led, LOW);       // Desliga o Led
  delay(EEPROM.read(1)*100);   // Espera o número de milisegundos definido no programa (salvo na posição 1 da EEPROM)
  
}

Código Python:

from tkinter import *
import tkinter.messagebox
import serial
import time

ser = serial.Serial('COM9', 115200, timeout=0)
# Definir a porta na qual está conectado o Arduino (no caso, COM9)

class GUIFramework(Frame):
       
    def __init__(self,master=None):

        self.root = Tk()
        Frame.__init__(self,master)
        self.master.title("Interface Python - Arduino")
        self.grid(padx=10,pady=10)        
        self.CreateWidgets()
   
    def CreateWidgets(self):

        self.Entrada1 = Entry(self, width=4) # Campo de entrada 1
        self.Entrada1.grid(row=1, column=1)
        self.Entrada2 = Entry(self, width=4)# Campo de entrada 2
        self.Entrada2.grid(row=2, column=1) 
        # Note que aqui utilizando o gerenciador .grid, no qual especificamos as linhas e colunas onde desejamos posicionar os widgets.
        
        Label(self, text = "CONTROLE DO LED", justify = RIGHT).grid (row=0, column=2)
        Label(self, text = "Tempo Ligado (s)    ", justify = LEFT).grid (row=1, column=2)
        Label(self, text = "Tempo Desigado (s)", justify = LEFT).grid (row=2, column=2)
        
        Botao_Grava = Button(self, text=" Grava ", command=self.Write) # Botão para gravar os valores na EEPROM do Arduino
        Botao_Grava.grid(row=3, column=1)
        Botao_Le = Button(self, text="   Lê   ", command=self.Read)   # Botão para ler os valores da EEPROM do Arduino
        Botao_Le.grid(row=3, column=2)

    def Write(self):  #Através da bibliteca serial, envia os valores dos campos de entrada para o Arduino
        
        if self.Entrada1.get() == ("") or self.Entrada2.get() == "":
            tkinter.messagebox.showwarning("Aviso", "Campo vazio")  #Se os campos estão vazios, exibe um aviso
        if  (int(self.Entrada1.get()) +  int(self.Entrada2.get()) > 20):
            tkinter.messagebox.showwarning("Aviso", "A soma dos campos não deve ultrapassar 20") #Caso contrário a leitura é muito demorada
       
        else:
            ser.write(("wa"+self.Entrada1.get()).encode()) # Envia os valores precedidos por letras
            ser.write(("wb"+self.Entrada2.get()).encode()) # para que o programa no Arduino identifique a informação
            tkinter.messagebox.showwarning("Upload", "O upload foi realizado com sucesso!") 

    def Read(self):
        
        self.Entrada1.delete(0, END) # Limpa os valores dos campos de entrada
        self.Entrada2.delete(0, END)
                          
        ser.write(("r").encode())
        time.sleep (3)
                     
        var = ser.readline().decode() 
        valor = var[1:].rstrip() # Extrai os valores a partir da 2a posição e tira o espaço do final
        self.Entrada1.insert(END, valor)

        var = ser.readline().decode()
        valor = var[1:].rstrip() 
        self.Entrada2.insert(END, valor)

        tkinter.messagebox.showwarning("Read", "O arquivo foi lido com sucesso!")
            
        
if __name__ == "__main__":
    GUIFramework().mainloop()


Exemplo 3

Este código cria uma janela similar à anterior, com algumas pequenas modificações: 
- Identifica as portas nas quais há dispositivos conectados, e possui um menu no qual pode-se selecionar diretamente a porta desejada, sem necessidade de especificação da porta no código em si;
- Salva e lê as informações em um arquivo .csv.

Não há necessidade de alteração do firmware.



Código Python:

import os
import csv
from tkinter import *
import tkinter.messagebox
import serial
import time

class GUIFramework(Frame):
       
    def __init__(self,master=None):

        self.root = Tk()       
        Frame.__init__(self,master)
        self.master.title("Interface Python - Arduino")
        self.grid(padx=10,pady=10)
        self.Porta()
        self.Menu()
        self.CreateWidgets()


    def Menu(self): # Cria o Menu
        
        menubar = Menu(self.root)
        
        filemenu = Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Arquivo", menu=filemenu)
        filemenu.add_command(label="Sair", command=self.root.destroy)
 
        toolsmenu = Menu (menubar, tearoff=0)
        menubar.add_cascade(label="Ferramentas", menu=toolsmenu)
        portaMenu = Menu (menubar, tearoff = 0)
        toolsmenu.add_cascade(label="Portas", menu=portaMenu)     
        for self.port in self.result:
            portaMenu.add_radiobutton (label = self.port , variable = self.Portas, command= lambda arg0=self.port: self.Port(arg0) )

        self.root.config(menu=menubar)


    def Porta(self):  # Lista as portas disponiveis
        
        self.Portas = IntVar() #variavel para listar as portas
        
        if sys.platform.startswith('win'):
            self.ports = ['COM' + str(i + 1) for i in range(256)] # testei apenas no windows - nao sei se nos demais funciona corretamente
        elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
            self.ports = glob.glob('/dev/tty[A-Za-z]*')
        elif sys.platform.startswith('darwin'):
            self.ports = glob.glob('/dev/tty.*')

        else:
            raise EnvironmentError('Unsupported platform')

        self.result = []
        for self.port in self.ports:
            try:
                s = serial.Serial(self.port)
                s.close()
                self.result.append(self.port) 
            except (OSError, serial.SerialException):
                pass
                

    def Port(self, arg0): # Define a Porta
        print ("Voce selecionou a Porta", arg0) 
        self.ser = serial.Serial(str(arg0), 115200, timeout=1)

   
    def CreateWidgets(self):

        self.Entrada1 = Entry(self, width=4) # Campo de entrada 1
        self.Entrada1.grid(row=1, column=1)

        Label(self, text = "Tempo Ligado (s)    ", justify = LEFT).grid (row=1, column=2)           

        self.Entrada2 = Entry(self, width=4) # Campo de entrada 2
        self.Entrada2.grid(row=2, column=1)

        Label(self, text = "Tempo Desigado (s)", justify = LEFT).grid (row=2, column=2)
        
        Botao_Grava = Button(self, text=" Grava ", command=self.Write) # Botao para gravar os valores na EEPROM do Arduino
        Botao_Grava.grid(row=3, column=1)

        Botao_Le = Button(self, text="   Lê   ", command=self.Read)   # Botao para ler os valores da EEPROM do Arduino
        Botao_Le.grid(row=3, column=2)

        Botao_Grava = Button(self, text=" Grava CSV ", command=self.WriteCSV) # Botao para gravar os valores em um arquivo .csv
        Botao_Grava.grid(row=4, column=1)

        Botao_Le = Button(self, text="   Lê  CSV ", command=self.ReadCSV)   # Botao para ler os valores em um arquivo .csv
        Botao_Le.grid(row=4, column=2)



    def Write(self):  # Envia os valores dos campos de entrada para o Arduino

      if hasattr (self, 'ser'): # checa se o objeto self tem o atributo "ser" (ou seja, se a porta foi definida)
           
        if self.Entrada1.get() == ("" or 1) or self.Entrada2.get() == "":
            tkinter.messagebox.showwarning("Aviso", "Campo vazio")  #Se os campos estao vazios, exibe um aviso
        if  (int(self.Entrada1.get()) +  int(self.Entrada2.get()) > 20):
            tkinter.messagebox.showwarning("Aviso", "A soma dos campos nao deve ultrapassar 20") #Caso contrario a leitura eh muito demorada
        
        else:
            self.ser.write(("wa"+self.Entrada1.get()).encode()) # Envia os valores precedidos por letras 
            self.ser.write(("wb"+self.Entrada2.get()).encode()) # para que o firmware identifique a informacao
            tkinter.messagebox.showwarning("Upload", "O upload foi realizado com sucesso!")

      else:
           tkinter.messagebox.showwarning("Aviso", "Selecione uma porta no menu Ferramentas > Portas")
     

    def Read(self):  # Recebe os valores do Arduino

      if hasattr (self, 'ser'): # checa se o objeto self tem o atributo "ser" (ou seja, se a porta foi definida)
                   
            self.Entrada1.delete(0, END) # Limpa os valores dos campos de entrada
            self.Entrada2.delete(0, END)
            self.ser.write(("r").encode()) #envia a letra "r", que sera interpretada pelo firmware como instrucao para enviar os valores da EEPROM
            time.sleep (3) #aguarda, pois a resposta pode demorar alguns segundos, dependendo do tempo enviado para piscar o led
                     
            var = self.ser.readline().decode() #le os valores enviados pelo Arduino
            valor = var[1:].rstrip() # Extrai os valores a partir da 2a posicao e tira o espaco do final
            self.Entrada1.insert(END, valor) #Insere os valores nos campos de entrada
            var = self.ser.readline().decode() #Novamente, para a segunda posicao
            valor = var[1:].rstrip() 
            self.Entrada2.insert(END, valor)         

            tkinter.messagebox.showwarning("Read", "O arquivo foi lido com sucesso!")

      else:
           tkinter.messagebox.showwarning("Aviso", "Selecione uma porta no menu Ferramentas > Portas")

            
    def WriteCSV(self): # Salva os valores em formato .csv

            filename = tkinter.filedialog.asksaveasfilename()
            output_file = open(filename+".csv", 'w', newline='') 
            data = csv.writer(output_file)

            valor = self.Entrada1.get()
            send_value =  (chr(ord('a') ) + (valor))
            data.writerow([send_value])
            valor = self.Entrada2.get()
            send_value =  (chr(ord('a')+1 ) + (valor))
            data.writerow([send_value])

            tkinter.messagebox.showwarning("Upload", "O Arquivo foi salvo com sucesso!") 
             

    def ReadCSV(self): # Lê os valores do arquivo .csv

        
            self.Entrada1.delete(0, END) # Limpa os valores dos campos de entrada
            self.Entrada2.delete(0, END)

        
            filename = tkinter.filedialog.askopenfilename()
            input_file = open(filename, 'r', newline='')
            data = csv.reader(input_file, delimiter='\t', quoting=csv.QUOTE_NONE)
            
            line = next(data)
            var = line[0] 
            self.Entrada1.insert (END, var[1:])          
            line = next(data)
            var = line[0] 
            self.Entrada2.insert (END, var[1:])


                                     
if __name__ == "__main__":
    GUIFramework().mainloop()

Há muito mais possibilidades de widgets que podem ser utilizados. Para uma documentação mais detalhada, veja as seguintes referências (em português):
- Python - Módulo C - Tkinter: http://www.fem.unicamp.br/~labaki/Python/ModuloC.pdf
- Pensando em Tkinter: http://www.dcc.ufrj.br/~fabiom/mab225/PensandoTkinter.pdf


Grande parte da documentação encontrada para o Tkinter utiliza o Python versão 2.x. Segue uma lista das alterações necessárias para utilizar os módulos na versão 3.x:

Versão 2 > Versão 3:
Tkinter > tkinter
tkMessageBox > tkinter.messagebox
tkColorChooser > tkinter.colorchooser
tkFileDialog > tkinter.filedialog
tkCommonDialog > tkinter.commondialog
tkSimpleDialog > tkinter.simpledialog
tkFont > tkinter.font
Tkdnd > tkinter.dnd
ScrolledText > tkinter.scrolledtext
Tix > tkinter.tix
ttk > tkinter.ttk