I'm new to asyncio, threading, subrocess and I'm trying to build an app that reads data from the serial continuesly, put them inot a queue used by another process/thread/asyncio function to consume them and show into a tkinter GUI.
I was able to make the GUI non blocking while continue reading the data with the code below.
import tkinter as tk
import time
import queue
import logging
import serial
import sys
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.serial_text_label = tk.Label(self, text="String")
self.serial_text_label.pack()
self.serial_text = tk.Text(self, height=1, width=21)
self.serial_text.pack()
self.port = 'COM3'
self.baud = 38400
self.ser = serial.Serial(self.port, self.baud, timeout=0)
if self.ser.isOpen():
self.ser.close()
self.ser.open()
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
logging.info("created serial port")
# start the serial_text_label "ticking"
self.update_screen()
def update_screen(self):
self.serial_text.delete('1.0', tk.END)
data = ""
data_raw = self.ser.read(1)
if data_raw == b'\x02':
data_raw = self.ser.read(6)
data = "02-" str(data_raw.hex('-'))
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
self.serial_text.insert(tk.END, data)
# self.serial_text_label.configure(text=data)
# call this function again when want to refresh
self.after(500, self.update_screen)
if __name__== "__main__":
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)', )
app = SampleApp()
app.mainloop()
The only issue with my code is that all the reading and the elaboration of thedata coming from the serial port are inside the refresh cycle that update the screen. I would like to detach the function to some sort of thread/subprocess that works concurrently with the refresh of the GUI.
What I've tried is to create an async def do_serial()
function inside class SampleApp(tk.Tk)
as below:
async def do_serial():
logging.debug("do serial")
data = ""
data_raw = ser.read(1)
if data_raw == b'\x02':
data_raw = ser.read(6)
data = "02-" str(data_raw.hex('-'))
ser.reset_input_buffer()
ser.reset_output_buffer()
# add data to queue
if data != "":
logging.debug('put:' str(data))
incoming_serial_queue.put(data)
await asyncio.sleep(1)
and inside the update_screen
function I call asyncio.run(do_serial())
if not incoming_serial_queue.empty():
data = incoming_serial_queue.get()
Unfortunately it doesn't work and the code doesn't even show the GUI
Is there a way to process the data from the serial in an asyncronus/parallel way without having to write all the function inside the refresh GUI function?
CodePudding user response:
Try making a blocking calls in a separate thread. Inside update_screen, you should make calls fast enough to not no freeze GUI. That means, you should not read the input there.
import tkinter as tk
import time
import queue
import logging
import serial
import sys
import threading
from concurrent.futures import ThreadPoolExecutor
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.serial_text_label = tk.Label(self, text="String")
self.serial_text_label.pack()
self.serial_text = tk.Text(self, height=1, width=21)
self.serial_text.pack()
self.port = 'COM3'
self.baud = 38400
self.ser = serial.Serial(self.port, self.baud, timeout=0)
if self.ser.isOpen():
self.ser.close()
self.ser.open()
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
logging.info("created serial port")
# start the serial_text_label "ticking"
self._update_scheduled = threading.Condition()
self._terminating = threading.Event()
self.update_screen()
def mainloop(self):
with ThreadPoolExecutor() as executor:
future = executor.submit(self._do_update_screen_loop)
try:
return super().mainloop()
finally:
# letting the thread to know we're done
self._terminating.set()
with self._update_scheduled:
self._update_scheduled.notify_all()
def update_screen(self):
with self._update_scheduled:
self._update_scheduled.notify_all()
self.after(500, self.update_screen)
def _do_update_screen_loop(self):
while True:
with self._update_scheduled:
self._update_scheduled.wait()
if self._terminating.is_set():
return
self._do_update_screen()
def _do_update_screen(self):
self.serial_text.delete('1.0', tk.END)
data = ""
data_raw = self.ser.read(1)
if data_raw == b'\x02':
data_raw = self.ser.read(6)
data = "02-" str(data_raw.hex('-'))
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
self.serial_text.insert(tk.END, data)
# self.serial_text_label.configure(text=data)
if __name__== "__main__":
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)', )
app = SampleApp()
app.mainloop()
CodePudding user response:
Usually, you would use root.mainloop()
as the funtion, but for async, you'll have to use something else, however you'll have to create a loop yourself if you want the window to update every frame. The async function is simply root.update()
.
Hope this helped!