Threading - Continuously Running Thread

I have gone through some articles and examples for Threading in wxPython, but posting this question because :-

  1. Most examples were that of a thread running for a finite time, i.e. which returns something after it’s execution is completed. I don’t want my thread to terminate.

  2. Most of the articles are pretty old.

So my situation is, I am getting data from a sensor, at the frame rate of 200 ms. I need to process the data, and plot the relevant things.

So what I want is, this data extraction and processing should be happening in a thread, which should keep updating the value of my structure.

I am using FuncAnimation to plot. But the GUI is getting blocked.

I am able to plot this sensor data using FuncAnimation by not using wxPython (using just simple plt.show()).

Attaching the code snippet for the non-wxPython potting which is using thread

def update(i):
        global detObj
        print("Update")
        if(configParameters["pointcloud"] ==1):
            x = detObj["range"]
            y = detObj["v"]

            a1.set_data(x,y)

        if(configParameters["rangeprof"] ==1):
            y1 = detObj["RangeProfile"]
            # print(max(y1))
            x1 = np.linspace(0,63,64)
            x1 = x1/10
            
            a2.set_data(x1,y1)

oThread1_ReadSensors = threading.Thread(target = readAndParseData)
oThread1_ReadSensors.daemon = True
oThread1_ReadSensors.start()

# ani = FuncAnimation(fig, update, init_func=func_init, interval=100, blit=True, save_count=0, cache_frame_data=False)

ani = animation.FuncAnimation(fig, update, interval=400)

plt.show()



oThread1_ReadSensors.join()

The readAndParseData basically modifies ‘detObj’ structure, which is global in nature

How to make the wxPython GUI keep updating the plots like the independent plt.show() window?

PS : I have integrated matplotlib with wxPython, and it is working for normal plotting (when not using threads) so no issues there.

This might help python - Embedding matplotlib FuncAnimation in wxPython: Unwanted figure pop-up - Stack Overflow

The Thread example in the wxPython demo shows one way to use a persistent background thread in a wxPython application.

after some time, the plot gets pretty compressed! very good help but @Tanmay_Agrawal , I’m afraid, the conception seems to be quite foggy: data collection, data analysis & data presentation is all in a single loop (leaves only the posting) :roll_eyes:

You are right @da-dada.
Right now I am doing this same thing. Calling my readAndParseData inside update() of FuncAnimation.
But unfortunately, it doesn’t work.
The time taken by the update() keeps on increasing as iterations increase. Due to this, I get the wrong data to Parse after sometime ( maybe due to messing up of accumulated Data in the input buffer), and the plot update also slows down

If as you say, “The time taken by the update() keeps on increasing as iterations increase.” that will happen no matter how you access the data, so limit the amount you display.
The Stackoverflow answer quoted is mine and it’s still relevant, if simplistic.
You can thread it and post the result back as a wx.lib.newevent but for demonstration purposes and to keep it simple, using a wx.timer does the job.
Here’s a variation on the theme, limiting the number of data events displayed to 100, which forms a physical cap on the resources needed to process the plot.

import wx
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg as NavigationToolbar
from collections import deque
#import serial
import matplotlib.pyplot as plt
import random

class Serial_Plot(wx.Panel):
    def __init__(self, parent, strPort, id=-1, dpi=None, **kwargs):
        super().__init__(parent, id=id, **kwargs)
        self.figure  = plt.figure(figsize=(40, 60))
        self.ax = plt.axes(xlim=(0, 10), ylim=(0, 500))
        self.ax.set_xlim(0.0,100.00)
        self.plot_data, = self.ax.plot([], [])
        self.canvas = FigureCanvas(self, -1, self.figure)
        self.toolbar = NavigationToolbar(self.canvas)
        self.toolbar.Realize()
        self.start = wx.Button(self, -1, "Start")
        self.stop = wx.Button(self, -1, "Stop")
        self.active = wx.Button(self, -1, "Active?")

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.canvas, 1, wx.EXPAND)
        sizer.Add(self.toolbar, 0, wx.RIGHT | wx.EXPAND)
        sizer1.Add(self.start, 0, wx.ALL, 0)
        sizer1.Add(self.stop, 0, wx.ALL, 0)
        sizer1.Add(self.active, 0, wx.ALL, 0)
        sizer.Add(sizer1)
        self.SetSizer(sizer)
        self.start.Bind(wx.EVT_BUTTON, self.OnStart)
        self.stop.Bind(wx.EVT_BUTTON, self.OnStop)
        self.active.Bind(wx.EVT_BUTTON, self.OnActive)
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.OnUpdate, self.timer)
        # Serial communication
        # self.ser = serial.Serial(strPort, 115200)
        # replaced by random values for testing

    def OnStart(self, event):
        self.vals = deque()
        plt.ion() #Turn on interactive plot
        self.timer.Start(100)

    def OnStop(self, event):
        self.timer.Stop()

    def OnActive(self, event):
        # Is the Wx app active?
        wx.MessageBox("Yep! Still Active", "Active", parent=self)

    def OnUpdate(self,event):
        # read serial line
        # data = float(self.ser.readline().decode('utf-8'))
        data = float(random.randint(1, 500))
        length = len(self.vals)
        # limit display the last 100 values
        if length < 100:
            self.vals.append(data)
            length += 1
        else:
            self.vals.popleft()
            self.vals.append(data)
            length = 100     
  
        self.plot_data.set_data(range(length), self.vals)
        plt.plot()

    def OnClose(self):
        pass
        # close serial
        #self.ser.flush()
        #self.ser.close()

def main():
    app = wx.App(False)
    frame = wx.Frame(None, -1, "Wx Python")
    demo_plot = Serial_Plot(frame,'COM3')
    frame.Show()
    app.MainLoop()

if __name__ == "__main__":
    main()

The idea of @rolfofsaxony and @jmoraleda to use wx.Timer worked.
What I did is, I am calling my data parsing Function readAndParseData() using wx.Timer(), and using FuncAnimation separately.

This is giving great results. Updating plots using wx.Timer is not giving good results.

However there’s one problem now -
Whenever I pane my plots (move left/right) the data parsing is getting bad. This is only happening when I’m panning. Zooming etc. doesn’t cause anything like this.

Also, my GUI has many other elements too. Accessing them doesn’t cause this problem.
This problem also occured when I was tried updating plots using wx.Timer also.

Below is the code snippet:-

def getData(event):

        readAndParseData()

self.timer = wx.Timer(self)

self.Bind(wx.EVT_TIMER, getData, self.timer)

self.timer.Start(1)

self.figure = plt.figure(constrained_layout=True)

fig = self.figure

color = [0.149,0.149,0.149]

fig.patch.set_facecolor(color)

ax1 = self.ax1 = fig.add_subplot(2,3,1)

ax2 = self.ax2 = fig.add_subplot(2,3,2)

ax9 = self.ax3 = fig.add_subplot(2,3,3)

ax4 = self.ax4 = fig.add_subplot(2,3,4)

ax10 = self.ax10 = fig.add_subplot(2,3,5)

ax11 = self.ax11 = fig.add_subplot(2,3,6)

self.canvas = FigureCanvas(self.nb2, -1, fig)

self.toolbar = NavigationToolbar(self.canvas)

self.toolbar.Realize()


sizer_33.Add(self.canvas, 1, wx.EXPAND)

sizer_33.Add(self.toolbar, 0, wx.GROW)

def update(i):
        global detObj
        print("Update")
        if(configParameters["pointcloud"] ==1):
            x = detObj["range"]
            y = detObj["v"]

            a1.set_data(x,y)

        if(configParameters["rangeprof"] ==1):
            y1 = detObj["RangeProfile"]
            # print(max(y1))
            x1 = np.linspace(0,63,64)
            x1 = x1/10
            
            a2.set_data(x1,y1)

self.ani = FuncAnimation(fig, update, init_func=func_init, interval=10, blit=True, save_count=0)

I’m attaching a video of the problem.
The inverted peaks that you can see in the video, that is the bad data.
https://drive.google.com/file/d/1hurhp-7J6GStBJ3nJLfvY4Fr1gmH-3T6/view?usp=sharing