Search code examples
pythonmultithreadingtimerserial-portnonblocking

How can i get data without blocking?


I have a serial port external device for getting data. I set two timers. One of them has to be for plotting(0.5sn) and the other for writing to a text file(15sn). Timers shouldn't get data from each other by list or array. Because sometimes I need to close plotting button.

So I have to get data from the same resource (coming continuous data), right? But when I try this, it blocked.

And how to get data without blocking? As an example the below code runs without blocking:

# -*- coding: utf-8 -*- 
#!/usr/bin/python
import time
import numpy as np
import sys
import wx

def next_data():
    t0 = time.time()
    return t0

class MyFrame1 ( wx.Frame ):

    def __init__( self, parent ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 223,183 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )

        bSizer1 = wx.BoxSizer( wx.VERTICAL )

        self.toggleBtn1 = wx.ToggleButton( self, wx.ID_ANY, u"Btn 1(enabled)", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer1.Add( self.toggleBtn1, 1, wx.ALL|wx.EXPAND, 5 )

        self.toggleBtn2 = wx.ToggleButton( self, wx.ID_ANY, u"Btn 2(enabled)", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer1.Add( self.toggleBtn2, 1, wx.ALL|wx.EXPAND, 5 )


        self.SetSizer( bSizer1 )
        self.Layout()
        self.timer1 = wx.Timer()
        self.timer1.SetOwner( self, 1 )
        self.timer1.Start( 1000 )

        self.timer2 = wx.Timer()
        self.timer2.SetOwner( self, 2 )
        self.timer2.Start( 1000 )


        self.Centre( wx.BOTH )

        # Connect Events
        self.toggleBtn1.Bind( wx.EVT_TOGGLEBUTTON, self.btn1_f )
        self.toggleBtn2.Bind( wx.EVT_TOGGLEBUTTON, self.btn2_f )
        self.Bind( wx.EVT_TIMER, self.timer1_f, id=1 )
        self.Bind( wx.EVT_TIMER, self.timer2_f, id=2 )

    def btn1_f( self, event ):
        event.Skip()

    def btn2_f( self, event ):
        event.Skip()

    def timer1_f( self, event ):
        t1 = next_data()
        print("t1     :    ",t1)

    def timer2_f( self, event ):
        t2  = next_data()
        print("t2*****:    ",t2)

app = wx.App()
f = MyFrame1(None)
f.Show(True)
app.MainLoop()

And it prints(as expected):

t2*****:     1555568620.1363716
t1     :     1555568620.1363716
t2*****:     1555568621.1300163
t1     :     1555568621.1300163

However my serial port doesn't work correctly like above:

# -*- coding: utf-8 -*- 
#!/usr/bin/python
import time
import numpy as np
import sys
import wx
import serial

ser = serial.Serial('COM9',9600)

def next_data():
    #===========================================================================
    # for line in ser:       
    #     return line
    #===========================================================================
    data_str = ser.read(ser.inWaiting())  
    return data_str

## also the commented above 2 lines gave same output.


class MyFrame1 ( wx.Frame ):

    def __init__( self, parent ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 223,183 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )

        bSizer1 = wx.BoxSizer( wx.VERTICAL )

        self.toggleBtn1 = wx.ToggleButton( self, wx.ID_ANY, u"Btn 1(enabled)", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer1.Add( self.toggleBtn1, 1, wx.ALL|wx.EXPAND, 5 )

        self.toggleBtn2 = wx.ToggleButton( self, wx.ID_ANY, u"Btn 2(enabled)", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer1.Add( self.toggleBtn2, 1, wx.ALL|wx.EXPAND, 5 )


        self.SetSizer( bSizer1 )
        self.Layout()
        self.timer1 = wx.Timer()
        self.timer1.SetOwner( self, 1 )
        self.timer1.Start( 1000 )

        self.timer2 = wx.Timer()
        self.timer2.SetOwner( self, 2 )
        self.timer2.Start( 1000 )


        self.Centre( wx.BOTH )

        # Connect Events
        self.toggleBtn1.Bind( wx.EVT_TOGGLEBUTTON, self.btn1_f )
        self.toggleBtn2.Bind( wx.EVT_TOGGLEBUTTON, self.btn2_f )
        self.Bind( wx.EVT_TIMER, self.timer1_f, id=1 )
        self.Bind( wx.EVT_TIMER, self.timer2_f, id=2 )

    def btn1_f( self, event ):
        event.Skip()

    def btn2_f( self, event ):
        event.Skip()

    def timer1_f( self, event ):
        t1 = next_data()
        print("t1     :    ",t1)

    def timer2_f( self, event ):
        t2 = next_data()
        print("t2*****:    ",t2)

app = wx.App()
f = MyFrame1(None)
f.Show(True)
app.MainLoop()

and its output like those:

t2*****:     b'\xb5b\x010\x04\x018\>$GNRMC,063337.00.....$GNGGA...
t1     :     b''
t2*****:     b'\xb5b\x010\x04\x01\x18\>$GNRMC,063338.00.....$GNGGA...
t1     :     b''    

and (for commented two lines):

t2*****:    b'$GPGSV,3,1,11,05,43,248,30,07,31,068,12,08,15,048,23,09,16,128,30*7B\r\n'
t1     :    b'$GPGSV,3,2,11,13,42,311,27,15,10,310,18,17,04,157,09,28,70,161,27*72\r\n'

As seen, whether t1 doesn't get data or t1 just get the next one. I also set timers (100ms), but same output. Could anybody guide me on this, what am I missing?


Solution

  • You don't need to use 2 Timers to periodically save data. And you do not need to use Threads either. Just keep a counter variable in your main Frame class (or the class with the Timer and collecting the data -- your example is simple enough that it may not need splitting) and use that to determine when to write data. Also: keep the data read from the serial port in an array in the same class so you can plot it or save it or whatever else you might want:

    in MyFrame1.__init__() add

    self.last_saved_time = 0
    self.plotting = True
    self.data = []
    

    Then in MyFrame1.timer1() do

     # read and save data
     t1 = next_data()
     self.data.append(t1) # (or do more parsing, convert to numpy arrays, etc
    
     # send to plotting if it is enabled
     if self.plotting:  
          self.plot_data()
    
     # save if needed
     now = time.time()
     if (now - self.last_saved_time) > 15:
          self.save_data_to_file() 
    

    Again, you do not necessarily need two timers or threads. You could do the plotting or the i/o in a separate thread, but you're going relatively slowly.

    It would probably be wiser to eventually split up the code as "data collector" class with its own event loop and then transfer that to the GUI frame to plot as needed. But the example is small enough that such a refactoring is not needed yet.