Comunicação serial PC Arduino em Python


Comunicação serial através do protocolo RS232-C

Finalizando o projeto do sistema de medida de temperatura com sensores NTC, e uma vez definida a interface com o usuário, o próximo passo é a implementação do protocolo de comunicação com o dispositivo remoto. Esta implementação ocorre em duas etapas. Em primeiro lugar implementamos a comunicação propriamente dita e sem segundo lugar implementamos os procedimentos que processam a troca de mensagens entre os dois dispositivos. Para a implementação da funcionalidade da comunicação serial vamos utilizar a biblioteca pyserial. Utilizaremos também o python 3.5 e o sistema operacional Linux Mint 18.1.

Instalando a biblioteca  pyserial

Digite pip install pyserial em um terminal. Se você estiver utilizando um ambiente virtual para o desenvolvimento de aplicações em python 3.x, não se esqueça de entrar no ambiente. Após a execução do programa tivemos a resposta “Successfully installed pyserial-3.4”, informando a versão da biblioteca pyserial instalada.

Utilizar esta biblioteca é muito simples, como pode-se ver nas linhas de código a seguir:

.acessando a serial
import   serial           #necessário para importar a biblioteca pyserial
 
# abre a primeira porta disponível
ser = serial.Serial(0)
 
# escreve a string teste nesta porta
ser.write("teste")
 
#le a linha enviada pelo dispositivo remoto como resposta a string Teste
linhaLida = ser.readline()
 
# fecha a porta
ser.close()

Identificando em que porta serial o Arduino está ligado

Para listar as portas seriais utilizadas no momento em sua máquina, digite:

 dmesg | grep tty

No nosso caso retornou:

 [   13.672516] cdc_acm 2-1:1.0: ttyACM0: USB ACM device

ou seja, a porta serial onde está ligado o nosso Arduino Uno está em ttyACM0

Experimento 1. Conectando-se com a porta serial e apresentando os comandos dela advindos

Como primeira etapa no desenvolvimento vamos nos conectar com a porta serial e mostrar os dados recebidos na área de texto da interface.

Para abrir a porta serial e ler os dados advindos da serial , o seguinte trecho de código deve ser utilizado:

.abrindo a porta e iniciando a leitura
 self.idPorta= "/dev/ttyACM0"      #especifica a porta 
       
 try:
    self.portaSerial = serial.Serial(port=self.idPorta,timeout=10) # tenta abrir a porta   
 
    except serial.SerialException as e:  #em caso de erro mostra aqui qual foi 
        sys.stderr.write("Impossivel abrir porta  %r: %s\n" % (port,e))
        sys.exit(1)      
        
 self.leSerial() # se nao teve erro inicia a leitura da serial 


 def leSerial(self):      # metodo para ler o bytes disponíveis na serial
        if self.portaSerial.isOpen():  # so executa se a porta estiver aberta
            while self.portaSerial.inWaiting() > 0:  # enquanto tiver caracteres na serial 
                caracLido = self.portaSerial.read(1)  # le um caracter da serial
                self.text1.insert(tk.END, caracLido); # e o insere no campo de texto na tela
        self.root.after(1,self.leSerial)    # coloca o metodo na fila para execução em 1 milisegundo



O código completo até o momento fica da seguinte forma:

.acessando a serial
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  tkTempInterface.py
#  Copyright 2017-08-02 tavares <tavares arroba cadernodelaboratorio.com.br>
#  0.1
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  
#  

import tkinter as tk
from tkinter import font

import sys
import serial    # importa a biblioteca pyserial

class myApp(object):
    
    def __init__(self, **kw):
        #insira toda a inicialização aqui 
                           
        self.root = tk.Tk()
        self.root.title("Interface Tk tempNTC")
        self.root.geometry('550x300')
        self.create_menu_bar()
        self.create_canvas_area()
        self.create_status_bar()
        
        self.idPorta= "/dev/ttyACM0"
        
        # abre a  porta disponível
        try:
            self.portaSerial = serial.Serial(port=self.idPorta,timeout=10)    
 
        except serial.SerialException as e:
            sys.stderr.write("Impossivel abrir porta  %r: %s\n" % (port,e))
            sys.exit(1)      
        
        self.leSerial()
        
    
    
    
    def leSerial(self):
        if self.portaSerial.isOpen():
            while self.portaSerial.inWaiting() > 0: 
                caracLido = self.portaSerial.read(1) 
                self.text1.insert(tk.END, caracLido);
        self.root.after(1,self.leSerial)    
        
    
        
        
    def create_status_bar(self):
        self.status = tk.Label(self.root, 
                               text="Bemvindo a Interface tk tempNTC", 
                               bd=1, relief=tk.SUNKEN)
        self.status.pack(side= tk.BOTTOM, fill = tk.X)



    def clear_status_bar(self):
        self.status.config(text="")
        self.status.update_idletasks()  
        
    def set_status_bar(self, texto):
        self.status.config(text=texto)
        self.status.update_idletasks()        

    def create_menu_bar(self):            
        menubar = tk.Menu(self.root)
        
        filemenu = tk.Menu(menubar, tearoff=0)
        filemenu.add_command(label="Exit", command=self.finaliza_software)
       
       
        menubar.add_cascade(label="File", menu=filemenu)
        
        helpmenu = tk.Menu(menubar, tearoff=0)
        helpmenu.add_command(label="About", command=self.mnu_about)
        menubar.add_cascade(label="Help", menu=helpmenu)
        
        self.root.config(menu=menubar)

    def create_canvas_area(self):
        self.lbl1 = tk.Label(self.root, text="Interface tempNTC",fg= "blue", font= ("Arial" ,"28", "bold"))

        
        self.text1 = tk.Text(self.root, height=10, width=60)
        self.text1.insert(tk.END, "\t\tPronto para entrar em operação!\n")
        self.text1.insert(tk.END, "\tNão esqueça configuração da porta serial")

        
        
        self.frame1 = tk.Frame(self.root)
        
        self.btnVersao= tk.Button(self.frame1, text = "Versão?")
        self.btnTemp= tk.Button(self.frame1, text = "Temperatura?")
        self.btnIntervalo= tk.Button(self.frame1, text ="Intervalo:")
        self.entry1= tk.Entry(self.frame1,width=5 )
        self.entry1.insert( 0,"10") 
        
        self.btnVersao.pack(side = tk.LEFT, padx= 10, pady= 15)
        self.btnTemp.pack(side = tk.LEFT,  padx= 10, pady= 15)
        self.btnIntervalo.pack(side = tk.LEFT, padx= 10, pady= 15)
        self.entry1.pack(side = tk.LEFT,pady= 15)
                
        self.lbl1.pack()
        self.text1.pack()
        self.frame1.pack()
        
        
        
        
        
    def finaliza_software(self):
        if self.portaSerial.isOpen():
            self.portaSerial.close()
        self.root.quit()        
    
        
    def mnu_about(self):
        pass
 
    
    def execute(self):
        self.root.mainloop()




def main(args):
    app_proc = myApp()
    app_proc.execute()
    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))

Para o teste deixe o módulo remoto no modo autônomo. Execute o script a seguir. O video deve ficar similar ao apresentado na figura a seguir:

Recebendo mensagens do sistema remoto

Experimento 2. Enviando comandos através da porta serial

Em primeiro lugar definimos os métodos que enviarão as mensagens:

.metodos que enviam as mensagens
   def sendMsg0(self):     #envio da msg0
        if self.portaSerial.isOpen():  #so envia se a porta estiver aberta
            msg= "0\n"
            self.portaSerial.write(msg.encode()); # envio da mensagem
            
    def sendMsg1(self):
        if self.portaSerial.isOpen():
            msg= "1\n"
            self.portaSerial.write(msg.encode());   

    def sendMsg2(self):    # a mensagem 2 necessita ser montada com os dados de intervalo
        if self.portaSerial.isOpen(): # so envia se a porta estiver aberta
            msg= "2,"          # monta a mensagem
            msg += self.entry1.get()  #le o conteúdo do widget "entry"
            msg += "\n"
            self.portaSerial.write(msg.encode()); #envia a mensagem

Em python3 você precisa codificar a string original (unicode) em string ascii ao utilizar o método write.
Para associar o pressionar de um botão ao envio de uma mensagem, temos que utilizar o parâmetro “command” na criação do buttom, conforme mostra o trecho de código a seguir:

.associando um botão a um método
self.btnVersao= tk.Button(self.frame1, text = "Versão?",command=self.sendMsg0)
self.btnTemp= tk.Button(self.frame1, text = "Temperatura?",command=self.sendMsg1)

self.btnIntervalo= tk.Button(self.frame1, text ="Intervalo:",command=self.sendMsg2) 

Para o teste, altere o modo de operação do dispositivo remoto para modo “escravo”, colocando o pino D12 em terra. Pressionado os botões “versão e temperatura”, você verá o resultado na tela. Alterando o intervalo, você pode voltar ao modo autônomo e verá que o intervalo de amostragem foi modificado de acordo.

Mas, da forma como estão apresentadas as mensagens, mostrando todo o protocolo, não fica muito amigável. O usuário merece algo mais mastigado. Temos que decodificar automaticamente estas mensagens e mostra-las devidamente identificadas.

Para isto precisamos de um código similar ao mostrado a seguir:

.decodificando as mensagens

def leSerial(self):                           # chega aqui a cada 1 ms
        if self.portaSerial.isOpen():         # so entra se a porta serial estiver aberta
            while self.portaSerial.inWaiting() > 0:  #enquanto tiver caracteres na serial fica dentro do while
                caracLido = self.portaSerial.read(1) # le um caracter da serial
                self.decodificaComando(caracLido)  # envia para a rotina que monta e decodifica o comando
        self.root.after(1,self.leSerial)    #insere o metodo leSerial para ser chamado novamente depois de 1 ms
        
    
    def decodificaComando(self,caracLido):  #metodo para decodificar a msg
            
        if caracLido.decode() != '\n':      # se o caracter nao for um NL (nova linha)
            self.msgRecebida += caracLido.decode() # armazena o caracter numa string
            
            
        else:
            msg1= self.msgRecebida.replace('\n'," ") #após ser detectado um NL (fim de comando)
                                                     #substitui o NL por um espaço
            
           
            cmd,param= msg1.split(",")  #separa o comando do parametro 
                    
            
            if cmd == "$0":    #este comando solicita a versão do firmware remoto
                saida= "Versão: " + param  #monta a msg de apresentação do firmware
                self.text1.insert(tk.END, saida + '\n')   #insere  no campo de texto da interface tkinter
            
            if cmd == "$1": #este comando solicita a temperatura medida no dispositivo remoto
                saida= "Temperatura: " + param   #monta a msg de apresentação da temperatura
                self.text1.insert(tk.END, saida + '\n')  #insere  no campo de texto da interface tkinter
                
            self.msgRecebida= "" #reinicia a área que recebe os comandos advindos da serial
        
        

Agora, quando você pressiona o botão ou mesmo configura o modo “autônomo” no dispositivo, as mensagens apresentadas na tela são bem autoexplicativas:

Decodificando as mensagens

O programa final é o seguinte:

.tkTempInterface1.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  tkTempInterface1.py
#  Copyright 2017-08-02 tavares <tavares arroba cadernodelaboratorio.com.br>
#  0.1
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
#  



import tkinter as tk
from tkinter import font

import sys
import serial    # importa a biblioteca pyserial

class myApp(object):
    
    def __init__(self, **kw):
        #insira toda a inicialização aqui 
                           
        self.root = tk.Tk()
        self.root.title("Interface Tk tempNTC")
        self.root.geometry('550x300')
        self.create_menu_bar()
        self.create_canvas_area()
        self.create_status_bar()
        
        self.idPorta= "/dev/ttyACM0"
        self.msgRecebida = ""  
        
        # abre a  porta disponível
        try:
            self.portaSerial = serial.Serial(port=self.idPorta,timeout=10)    
 
        except serial.SerialException as e:
            sys.stderr.write("Impossivel abrir porta  %r: %s\n" % (port,e))
            sys.exit(1)      
        
        self.leSerial()
        
    
    
    
    def leSerial(self):
        if self.portaSerial.isOpen():
            while self.portaSerial.inWaiting() > 0: 
                caracLido = self.portaSerial.read(1) 
                self.decodificaComando(caracLido)
        self.root.after(1,self.leSerial)    
        
    
    def decodificaComando(self,caracLido):
            
        if caracLido.decode() != '\n':
            self.msgRecebida += caracLido.decode()
            
            
        else:
            msg1= self.msgRecebida.replace('\n'," ")
            
           
            cmd,param= msg1.split(",")
                    
            
            if cmd == "$0":
                saida= "Versão: " + param 
                self.text1.insert(tk.END, saida + '\n')   
            if cmd == "$1":
                saida= "Temperatura: " + param 
                self.text1.insert(tk.END, saida + '\n')  
                
            self.msgRecebida= ""
        
        
        
        
    def create_status_bar(self):
        self.status = tk.Label(self.root, 
                               text="Bemvindo a Interface tk tempNTC", 
                               bd=1, relief=tk.SUNKEN)
        self.status.pack(side= tk.BOTTOM, fill = tk.X)



    def clear_status_bar(self):
        self.status.config(text="")
        self.status.update_idletasks()  
        
    def set_status_bar(self, texto):
        self.status.config(text=texto)
        self.status.update_idletasks()        

    def create_menu_bar(self):            
        menubar = tk.Menu(self.root)
        
        filemenu = tk.Menu(menubar, tearoff=0)
        filemenu.add_command(label="Exit", command=self.finaliza_software)
       
       
        menubar.add_cascade(label="File", menu=filemenu)
        
        helpmenu = tk.Menu(menubar, tearoff=0)
        helpmenu.add_command(label="About", command=self.mnu_about)
        menubar.add_cascade(label="Help", menu=helpmenu)
        
        self.root.config(menu=menubar)

    def create_canvas_area(self):
        self.lbl1 = tk.Label(self.root, text="Interface tempNTC",fg= "blue", font= ("Arial" ,"28", "bold"))

        
        self.text1 = tk.Text(self.root, height=10, width=60)
        self.text1.insert(tk.END, "\t\tPronto para entrar em operação!\n")
        self.text1.insert(tk.END, "\tNão esqueça configuração da porta serial\n")

        
        
        self.frame1 = tk.Frame(self.root)
        
        self.btnVersao= tk.Button(self.frame1, text = "Versão?",command=self.sendMsg0)
        self.btnTemp= tk.Button(self.frame1, text = "Temperatura?",command=self.sendMsg1)
        self.btnIntervalo= tk.Button(self.frame1, text ="Intervalo:",command=self.sendMsg2)
        self.entry1= tk.Entry(self.frame1,width=5 )
        self.entry1.insert( 0,"10") 
        
        self.btnVersao.pack(side = tk.LEFT, padx= 10, pady= 15)
        self.btnTemp.pack(side = tk.LEFT,  padx= 10, pady= 15)
        self.btnIntervalo.pack(side = tk.LEFT, padx= 10, pady= 15)
        self.entry1.pack(side = tk.LEFT,pady= 15)
                
        self.lbl1.pack()
        self.text1.pack()
        self.frame1.pack()
        
        
        
        
        
    def finaliza_software(self):
        if self.portaSerial.isOpen():
            self.portaSerial.close()
        self.root.quit()        
    
        
    def mnu_about(self):
        pass
 
    
    def execute(self):
        self.root.mainloop()


    def sendMsg0(self):
        if self.portaSerial.isOpen():
            msg= "0\n"
            self.portaSerial.write(msg.encode());
            
    def sendMsg1(self):
        if self.portaSerial.isOpen():
            msg= "1\n"
            self.portaSerial.write(msg.encode());   

    def sendMsg2(self):
        if self.portaSerial.isOpen():
            msg= "2,"
            msg += self.entry1.get()
            msg += "\n"
            self.portaSerial.write(msg.encode());  



def main(args):
    app_proc = myApp()
    app_proc.execute()
    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))

Este procedimento se repete sempre que você tiver desenvolvendo um instrumento que opera de forma integrada com um PC. Para tornar as coisas mais compactas, o PC pode ser substituído por um RaspberryPi, que pode operar o mesmo script aqui desenvolvido e se interfacear com uma tela sensível ao toque, tornando a operação do instrumento muito intuitiva.

Se você preferir, pode obter os programas  diretamente na página de downloads do caderno, http://wp.me/P6gfIl-BF , no arquivo de nome pack170811.tar.gz.

Deixe um comentário