Ao desenvolver um projeto com Arduino, Raspberry Pi ou qualquer outro microcontrolador, será certamente necessário criar uma interface gráfica, como um monitor série, para gerir o sistema (depurar, observar medições, lançar acções, etc.). Existem muitas ferramentas disponíveis para criar interfaces gráficas. Neste projeto, vamos criar uma interface com Python usando a estrutura Qt(PySide2).
Objetivo
Para este projeto, queremos criar uma interface gráfica baseada no Windows em Python que se comporte de forma semelhante ao monitor de série do IDE do Arduino. Para fazer isso, precisaremos executar as seguintes funções
- Zona de seleção da porta USB
- Botão de ligação
- Área de escrita para encomenda
- Botão Enviar
- Consola que apresenta os dados enviados pelo Arduino
Código Arduino
O código Arduino para a comunicação em série é bastante simples: definimos a comunicação em série e escutamos na porta série em cada ciclo. Se aparecer alguma informação, o algoritmo envia de volta o que recebeu.
/*---------------------------------------------------------------------- * Project : Test * Author : AranaCorp * Version : V01 * Date : 7/4/2020 * * Summary : * Hardware : - Arduino UNO x1 - usbserial x1 - usbserialIn x1 https://www.aranacorp.com/fr/communiquez-avec-votre-arduino/ * * www.aranacorp.com ----------------------------------------------------------------------*/ //Variables String msg ; void setup(){ //Init Serial USB Serial.begin(115200); delay(500); Serial.println(F(">> Initialize System")); } void loop(){ readSerialPort(); } void readSerialPort(){/* function readSerialPort */ ////Write something into the serial monitor to test serial communication while (Serial.available()) { delay(10); if (Serial.available() >0) { char c = Serial.read(); //gets one byte from serial buffer msg += c; //makes the string readString } } if (msg.length() >0){ Serial.println(">> "+msg); msg=""; } }
Para este projeto, utilizámos o Arduino UNO, mas este código pode ser aplicado a qualquer microcontrolador que possa ser programado utilizando o Arduino IDE.
Recuperar dispositivos USB ligados
Para obter as portas USB utilizadas, existe a biblioteca serial.tools.list_ports
def find_USB_device(): myports = [tuple(p) for p in list(serial.tools.list_ports.comports())] print(myports) usb_port_list = [p[0] for p in myports] return usb_port_list
No Linux, pode utilizar o subprocesso com o comando lsusb.
Programa Python
Primeiro, vamos criar a janela da aplicação, a que chamaremos SerialInterface e que se comportará como o monitor de série.
#!/usr/bin/python3 # -*-coding:Utf-8 -* import sys,os, time import platform from random import randint import serial,serial.tools.list_ports #interface import from PySide2.QtWidgets import QApplication, QMainWindow,QDesktopWidget, QTextEdit, QLineEdit, QPushButton, QMessageBox, QWidget, QGridLayout, QTextEdit, QGroupBox, QVBoxLayout,QHBoxLayout, QComboBox, QLabel from PySide2.QtGui import QIcon, QScreen class SerialInterface(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.width=650 self.height=350 self.resize(self.width, self.height) self.setWindowIcon(QIcon('./resources/logo-100.png')) self.setWindowTitle( 'Serial Monitor') #center window on screen qr = self.frameGeometry() cp = QScreen().availableGeometry().center() qr.moveCenter(cp) #init layout centralwidget = QWidget(self) centralLayout=QHBoxLayout(centralwidget) self.setCentralWidget(centralwidget) #add connect group #self.connectgrp=GroupClass(self) #centralLayout.addWidget(self.connectgrp) if __name__ == "__main__": app = QApplication(sys.argv) frame = SerialInterface() frame.show() sys.exit(app.exec_())
De seguida, criaremos uma classe que contém todos os Widgets necessários para criar o monitor série Python (QLineEdit,QTextEdit,QButton).
class GroupClass(QGroupBox): def __init__(self,widget,title="Connection Configuration"): super().__init__(widget) self.widget=widget self.title=title self.sep="-" self.id=-1 self.name='' self.items=find_USB_device() self.serial=None self.init() def init(self): self.setTitle(self.title) self.selectlbl = QLabel("Select port:") #label self.typeBox=QComboBox() self.typeBox.addItems(self.items)#database getMotionType() self.typeBox.setCurrentIndex(self.typeBox.count()-1) #btn button = QPushButton("Connect") button.clicked.connect(self.connect) sendBtn = QPushButton("send") sendBtn.clicked.connect(self.sendData) titlelbl= QLabel("Enter") self.title = QLineEdit("") desclbl=QLabel("Console") self.desc = QTextEdit("") self.fields=QGridLayout() self.fields.addWidget(self.selectlbl,0,0,1,1) self.fields.addWidget(self.typeBox,0,1,1,1) self.fields.addWidget(button,0,2,1,1) self.fields.addWidget(titlelbl,1,0,1,1) self.fields.addWidget(self.title,1,1,1,1) self.fields.addWidget(sendBtn,1,2,1,1) self.fields.addWidget(desclbl,2,0,1,1) self.fields.addWidget(self.desc,3,1,1,1) self.setLayout(self.fields)
Descomente estas duas linhas para a adicionar à janela principal:
#self.connectgrp=GroupClass(self) #centralLayout.addWidget(self.connectgrp)
Quando os nossos widgets estiverem no sítio, podemos começar a trabalhar com eles. Antes de mais, vamos tratar da ligação. Para o fazer, vamos ligar o botão “Ligar” à função da seguinte forma:
button.clicked.connect(self.connect)
De seguida, vamos definir a função correspondente que irá verificar se a porta série ainda não está aberta para evitar erros. De seguida, criamos e abrimos a porta série, para ler os dados enviados pelo Arduino.
def connect(self): self.desc.setText("") self.desc.setText(">> trying to connect to port %s ..." % self.typeBox.currentText()) if self.serial is None: self.serial=serial.Serial(self.typeBox.currentText(), 115200, timeout=1) time.sleep(0.05) #self.serial.write(b'hello') answer=self.readData() if answer!="": self.desc.setText(self.desc.toPlainText()+"\n>> Connected!\n"+answer) else: self.desc.setText(">> {} already Opened!\n".format(self.typeBox.currentText()))
Para recuperar e formatar os dados enviados pelo Arduino, vamos criar outra função, self.readData. Como podemos ver, ela armazenará os dados enviados pelo Arduino enquanto houver dados disponíveis no buffer.
def readData(self): self.serial.flush() # it is buffering. required to get the data out *now* answer="" while self.serial.inWaiting()>0: #self.serial.readable() and answer += "\n"+str(self.serial.readline()).replace("\\r","").replace("\\n","").replace("'","").replace("b","") return answer
Quando a ligação e a leitura estiverem operacionais, criaremos o comando sendData para podermos enviar comandos para o Arduino através do monitor de série Python. A função recupera o texto do QLineEdit correspondente e envia-o para a porta série. Em seguida, usamos a função readData para obter a resposta do Arduino.
def sendData(self): if self.serial.isOpen(): if self.title.text() != "": self.serial.write(self.title.text().encode()) answer=self.readData() self.desc.setText(self.desc.toPlainText()+"\n"+answer)
Código completo
#!/usr/bin/python3 # -*-coding:Utf-8 -* import sys,os, time import platform from random import randint import serial,serial.tools.list_ports #interface import import PySide2 from PySide2.QtWidgets import QApplication, QMainWindow,QDesktopWidget, QTextEdit, QLineEdit, QPushButton, QMessageBox, QWidget, QGridLayout, QTextEdit, QGroupBox, QVBoxLayout,QHBoxLayout, QComboBox, QLabel from PySide2.QtGui import QIcon, QScreen __prgm__ = 'Serial Monitor' __version__ = '0.0.2' def find_USB_device(USB_DEV_NAME=None): myports = [tuple(p) for p in list(serial.tools.list_ports.comports())] print(myports) usb_port_list = [p[0] for p in myports] usb_device_list = [p[1] for p in myports] print(usb_device_list) if USB_DEV_NAME is None: return myports else: USB_DEV_NAME=str(USB_DEV_NAME).replace("'","").replace("b","") for device in usb_device_list: print("{} -> {}".format(USB_DEV_NAME,device)) print(USB_DEV_NAME in device) if USB_DEV_NAME in device: print(device) usb_id = device[device.index("COM"):device.index("COM")+4] print("{} port is {}".format(USB_DEV_NAME,usb_id)) return usb_id class GroupClass(QGroupBox): def __init__(self,widget,title="Connection Configuration"): super().__init__(widget) self.widget=widget self.title=title self.sep="-" self.id=-1 self.name='' self.portlist=find_USB_device() self.items=[p[0] for p in self.portlist]#["COM1","COM2"] self.serial=None #self.motionDict={"POSITION BASED":" Describe motion based on position","VELOCITY BASED":" Describe motion based on velocity", "LOOP":" Describe loop motion", "PINGPONG":" Describe pingpong motion", "INTERACTIF":" Describe interactive motion"} self.init() def init(self): self.setTitle(self.title) self.selectlbl = QLabel("Select port:") #label self.typeBox=QComboBox() self.typeBox.addItems(self.items)#database getMotionType() self.typeBox.setCurrentIndex(self.typeBox.count()-1) #btn button = QPushButton("Connect") button.clicked.connect(self.connect) #hbox.addWidget(button) sendBtn = QPushButton("send") sendBtn.clicked.connect(self.sendData) #hbox.addWidget(button) titlelbl= QLabel("Enter") self.title = QLineEdit("") desclbl=QLabel("Console") self.desc = QTextEdit("") #self.add=QPushButton("Ajouter/Modifier") #self.add.clicked.connect(self.addItem) #self.rem=QPushButton("Supprimer") #self.rem.clicked.connect(self.remItem) self.fields=QGridLayout() self.fields.addWidget(self.selectlbl,0,0,1,1) self.fields.addWidget(self.typeBox,0,1,1,1) self.fields.addWidget(button,0,2,1,1) self.fields.addWidget(titlelbl,1,0,1,1) self.fields.addWidget(self.title,1,1,1,1) self.fields.addWidget(sendBtn,1,2,1,1) self.fields.addWidget(desclbl,2,0,1,1) self.fields.addWidget(self.desc,3,1,1,1) #self.fields.addWidget(self.add,2,2,1,1) #self.fields.addWidget(self.rem,3,2,1,1) self.setLayout(self.fields) def connect(self): self.desc.setText("") self.desc.setText(">> trying to connect to port %s ..." % self.typeBox.currentText()) #with serial.Serial(self.typeBox.currentText(), 115200, timeout=1) as self.serial: if self.serial is None: self.serial=serial.Serial(self.typeBox.currentText(), 115200, timeout=1) time.sleep(0.05) #self.serial.write(b'hello') answer=self.readData() if answer!="": self.desc.setText(self.desc.toPlainText()+"\n>> Connected!\n"+answer) else: self.desc.setText(">> {} already Opened!\n".format(self.typeBox.currentText())) def sendData(self): if self.serial.isOpen(): if self.title.text() != "": self.serial.write(self.title.text().encode()) answer=self.readData() if(self.title.text().encode()=="scan"): print("scanning results -> "+answer.find("0x")) else: print(answer.find("0x")) self.desc.setText(self.desc.toPlainText()+"\n"+answer) def readData(self): #self.serial.flush() # it is buffering. required to get the data out *now* answer="" while self.serial.inWaiting()>0: #self.serial.readable() and print(self.serial.inWaiting()) answer += "\n"+str(self.serial.readline()).replace("\\r","").replace("\\n","").replace("'","").replace("b","") #print(self.serial.inWaiting()) #self.desc.setText(self.desc.toPlainText()+"\n"+answer) return answer class SerialInterface(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.width=650 self.height=350 self.resize(self.width, self.height) self.setWindowIcon(QIcon('./resources/logo-100.png')) self.setWindowTitle(__prgm__) #center window on screen qr = self.frameGeometry() cp = QScreen().availableGeometry().center() qr.moveCenter(cp) #init layout centralwidget = QWidget(self) centralLayout=QHBoxLayout(centralwidget) self.setCentralWidget(centralwidget) #add connect group self.connectgrp=GroupClass(self) centralLayout.addWidget(self.connectgrp) if __name__ == "__main__": app = QApplication(sys.argv) frame = SerialInterface() frame.show() sys.exit(app.exec_())