Reputation: 244
I am making a data acquisition program that communicates with a measurement device. The status of the device needs to be checked periodically (e.g., every 0.1 sec) to see if acquisition is done. Furthermore, the program must have the 'abort' method because acquisition sometime takes longer than few minutes. Thus I need to use multi-threading.
I attached the flow-chart and the example code. But I have no idea how I call the main-thread to execute a method from the sub-thread.
python 3.7.2 wxpython 4.0.6
import wx
import time
from threading import Thread
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="Test Frame")
panel = wx.Panel(self)
self.Btn1 = wx.Button(panel, label="Start Measurement")
self.Btn1.Bind(wx.EVT_BUTTON, self.OnStart)
self.Btn2 = wx.Button(panel, label="Abort Measurement")
self.Btn2.Bind(wx.EVT_BUTTON, self.OnAbort)
self.Btn2.Enable(False)
self.DoneFlag = False
self.SubThread = Thread(target=self.Check, daemon=True)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.Btn1, 0, wx.EXPAND)
sizer.Add(self.Btn2, 0, wx.EXPAND)
panel.SetSizer(sizer)
def OnStart(self, event):
# self.N is the number of data points
self.N = 0
# self.N_max is the number of data points that is going to be acquired
self.N_max = int(input("How many data points do yo want? (greater than 1) : "))
self.DoneFlag = False
self.Btn1.Enable(False)
self.Btn2.Enable(True)
self.Start()
def OnAbort(self, event):
self.DoneFlag = True
def Start(self):
self.SubThread.start()
def Done(self):
if self.DoneFlag is True:
self.Finish()
elif self.DoneFlag is False:
self.Start()
def Finish(self):
print("Measurement done (N = {})\n".format(self.N))
self.Btn1.Enable(True)
self.Btn2.Enable(False)
def Check(self):
# In the actual program, this method communicates with a data acquisition device to check its status
# For example,
# "RunningStatus" is True when the device is still running (acquisition has not been done yet),
# is False when the device is in idle state (acquisition has done)
#
# [Structure of the actual program]
# while True:
# RunningStatus = GetStatusFromDevice()
# if RunningStatus is False or self.DoneFlag is True:
# break
# else:
# time.sleep(0.1)
# In below code, it just waits 3 seconds then assumes the acqusition is done
t = time.time()
time.sleep(1)
for i in range(3):
if self.DoneFlag is True:
break
print("{} sec left".format(int(5-time.time()+t)))
time.sleep(1)
# Proceed to the next steps after the acquisition is done.
if self.DoneFlag is False:
self.N += 1
print("Data acquired (N = {})\n".format(self.N))
if self.N == self.N_max:
self.DoneFlag = True
self.Done() # This method should be excuted in the main thread
if __name__ == "__main__":
app = wx.App()
frame = TestFrame()
frame.Show()
app.MainLoop()
Upvotes: 0
Views: 512
Reputation: 22443
When using a GUI it is not recommended to call GUI functions from another thread, see: https://docs.wxwidgets.org/trunk/overview_thread.html
One of your options, is to use events
to keep track of what is going on.
One function creates and dispatches an event
when something happens or to denote progress for example, whilst another function listens for and reacts to a specific event
.
So, just like pubsub
but native.
Here, I use one event to post information about progress and another for results but with different targets.
It certainly will not be an exact fit for your scenario but should give enough information to craft a solution of your own.
import time
import wx
from threading import Thread
import wx.lib.newevent
progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
results_event, EVT_RESULTS_EVENT = wx.lib.newevent.NewEvent()
class ThreadFrame(wx.Frame):
def __init__(self, title, parent=None):
wx.Frame.__init__(self, parent=parent, title=title)
panel = wx.Panel(self)
self.parent = parent
self.btn = wx.Button(panel,label='Stop Measurements', size=(200,30), pos=(10,10))
self.btn.Bind(wx.EVT_BUTTON, self.OnExit)
self.progress = wx.Gauge(panel,size=(240,10), pos=(10,50), range=30)
#Bind to the progress event issued by the thread
self.Bind(EVT_PROGRESS_EVENT, self.OnProgress)
#Bind to Exit on frame close
self.Bind(wx.EVT_CLOSE, self.OnExit)
self.Show()
self.mythread = TestThread(self)
def OnProgress(self, event):
self.progress.SetValue(event.count)
#or for indeterminate progress
#self.progress.Pulse()
if event.result != 0:
evt = results_event(result=event.result)
#Send back result to main frame
try:
wx.PostEvent(self.parent, evt)
except:
pass
def OnExit(self, event):
if self.mythread.isAlive():
self.mythread.terminate() # Shutdown the thread
self.mythread.join() # Wait for it to finish
self.Destroy()
class TestThread(Thread):
def __init__(self,parent_target):
Thread.__init__(self)
self.parent = parent_target
self.stopthread = False
self.start() # start the thread
def run(self):
curr_loop = 0
while self.stopthread == False:
curr_loop += 1
# Send a result every 3 seconds for test purposes
if curr_loop < 30:
time.sleep(0.1)
evt = progress_event(count=curr_loop,result=0)
#Send back current count for the progress bar
try:
wx.PostEvent(self.parent, evt)
except: # The parent frame has probably been destroyed
self.terminate()
else:
curr_loop = 0
evt = progress_event(count=curr_loop,result=time.time())
#Send back current count for the progress bar
try:
wx.PostEvent(self.parent, evt)
except: # The parent frame has probably been destroyed
self.terminate()
def terminate(self):
evt = progress_event(count=0,result="Measurements Ended")
try:
wx.PostEvent(self.parent, evt)
except:
pass
self.stopthread = True
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.text_count = 0
self.thread_count = 0
self.parent=parent
btn = wx.Button(self, wx.ID_ANY, label='Start Measurements', size=(200,30), pos=(10,10))
btn.Bind(wx.EVT_BUTTON, self.Thread_Frame)
btn2 = wx.Button(self, wx.ID_ANY, label='Is the GUI still active?', size=(200,30), pos=(10,50))
btn2.Bind(wx.EVT_BUTTON, self.AddText)
self.txt = wx.TextCtrl(self, wx.ID_ANY, style= wx.TE_MULTILINE, pos=(10,90),size=(400,100))
#Bind to the result event issued by the thread
self.Bind(EVT_RESULTS_EVENT, self.OnResult)
def Thread_Frame(self, event):
self.thread_count += 1
frame = ThreadFrame(title='Measurement Task '+str(self.thread_count), parent=self)
def AddText(self,event):
self.text_count += 1
txt = "Gui is still active " + str(self.text_count)+"\n"
self.txt.write(txt)
def OnResult(self,event):
txt = "Result received " + str(event.result)+"\n"
self.txt.write(txt)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title='Main Frame', size=(600,400))
panel = MyPanel(self)
self.Show()
if __name__ == '__main__':
app = wx.App(False)
frame = MainFrame()
app.MainLoop()
Upvotes: 1