When developing a project with Arduino, Raspberry Pi or any microcontroller you will certainly have to create a graphical interface like, a serial monitor, for the management of the system (debugging, observing measurements, launching actions, etc.). There are a lot of tools to create graphical interfaces. In this project, we will create an interface with Python using the QT(PySide2) framework.
Objectif
For this project, we want to create a graphical interface in Python, under Windows, behaving similarly to the serial monitor of the Arduino IDE. For this, we will need to perform the following functions
- USB port selection area
- Connection button
- Writing area for the order
- Send button
- Console displaying the data sent by Arduino
Code Arduino
The Arduino code for serial communication is quite simple, we define the serial communication and listen on the serial port at each loop. If a piece of information appears, the algorithm returns what it has received.
/*---------------------------------------------------------------------- * 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=""; } }
For this project we used the Arduino UNO, but this code can be applied to all microcontrollers that can be programmed using the Arduino IDE.
Recovering connected USB devices
To retrieve the used USB ports, there is the serial.tools.list_ports library.
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
Under linux, it is possible to use subprocess with the lsusb control.
Python program
First we will create the application window we call Serial Interface, which will behave like the serial monitor.
#!/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_())
We will then create a class that will contain all the Widgets we need to create the Python Serial Monitor (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)
Uncomment these two lines to add it to the main window:
#self.connectgrp=GroupClass(self)
#centralLayout.addWidget(self.connectgrp)
Once our widgets are in place, we will be able to exploit them. First of all, we’re going to take care of the connection. To do this, we will link the “Connect” button to the function in the following way:
button.clicked.connect(self.connect)
Then we will define the corresponding function that will check if the serial port is not already open to avoid errors. Then create and open the serial port, in order to read the data sent by 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()))
To recover and put the data sent by Arduino in the right format, we will create another self.readData function. As we can see, it will store the data sent by Arduino as long as there is data available in the 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
Once the connection and playback is operational, we will create the sendData command to be able to send commands to the Arduino via the Python serial monitor. The function retrieves the text from the corresponding QLineEdit and sends it to the serial port. We then use the readData function to recover the response from the 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)
Complete code
#!/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_())