0

I have been running this script:

from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as Tk
from tkinter.ttk import Frame

class serialPlot:
    def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=4):
        self.port = serialPort
        self.baud = serialBaud
        self.plotMaxLength = plotLength
        self.dataNumBytes = dataNumBytes
        self.numPlots = numPlots
        self.rawData = bytearray(numPlots * dataNumBytes)
        self.dataType = None
        if dataNumBytes == 2:
            self.dataType = 'h'     # 2 byte integer
        elif dataNumBytes == 4:
            self.dataType = 'f'     # 4 byte float
        self.data = []
        self.privateData = None
        for i in range(numPlots):   # give an array for each type of data and store them in a list
            self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
        self.isRun = True
        self.isReceiving = False
        self.thread = None
        self.plotTimer = 0
        self.previousTimer = 0
        # self.csvData = []

        print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        try:
            self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
            print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        except:
            print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')

    def readSerialStart(self):
        if self.thread == None:
            self.thread = Thread(target=self.backgroundThread)
            self.thread.start()
            # Block till we start receiving values
            while self.isReceiving != True:
                time.sleep(0.1)

    def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
        if pltNumber == 0:  # in order to make all the clocks show the same reading
            currentTimer = time.perf_counter()
            self.plotTimer = int((currentTimer - self.previousTimer) * 1000)     # the first reading will be erroneous
            self.previousTimer = currentTimer
        self.privateData = copy.deepcopy(self.rawData)    # so that the 3 values in our plots will be synchronized to the same sample time
        timeText.set_text('' + str(self.plotTimer) + '')
        data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
        value,  = struct.unpack(self.dataType, data)
        self.data[pltNumber].append(value)    # we get the latest data point and append it to our array
        lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
        lineValueText.set_text('[' + lineLabel + '] = ' + str(value))

    def backgroundThread(self):    # retrieve data
        time.sleep(1.0)  # give some buffer time for retrieving data
        self.serialConnection.reset_input_buffer()
        while (self.isRun):
            self.serialConnection.readinto(self.rawData)
            self.isReceiving = True
            #print(self.rawData)

    def sendSerialData(self, data):
        self.serialConnection.write(data.encode('utf-8'))

    def close(self):
        self.isRun = False
        self.thread.join()
        self.serialConnection.close()
        print('Disconnected...')
        # df = pd.DataFrame(self.csvData)
        # df.to_csv('/home/rikisenia/Desktop/data.csv')


class Window(Frame):
    def __init__(self, figure, master, SerialReference):
        Frame.__init__(self, master)
        self.entry = []
        self.setPoint = None
        self.master = master        # a reference to the master window
        self.serialReference = SerialReference      # keep a reference to our serial connection so that we can use it for bi-directional communicate from this class
        self.initWindow(figure)     # initialize the window with our settings

    def initWindow(self, figure):
        self.master.title("Haptic Feedback Grasping Controller")
        canvas = FigureCanvasTkAgg(figure, master=self.master)
        toolbar = NavigationToolbar2Tk(canvas, self.master)
        canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)

        # create out widgets in the master frame
        lbl1 = Tk.Label(self.master, text="Distance")
        lbl1.pack(padx=5, pady=5)
        self.entry = Tk.Entry(self.master)
        self.entry.insert(0, '0')     # (index, string)
        self.entry.pack(padx=5)
        SendButton = Tk.Button(self.master, text='Send', command=self.sendFactorToMCU)
        SendButton.pack(padx=5)

    def sendFactorToMCU(self):
        self.serialReference.sendSerialData(self.entry.get() + '%')     # '%' is our ending marker

def main():
    # portName = 'COM5'
    portName = '/dev/ttyACM0'
    baudRate = 38400
    maxPlotLength = 100     # number of points in x-axis of real time plot
    dataNumBytes = 4        # number of bytes of 1 data point
    numPlots = 1            # number of plots in 1 graph
    s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots)   # initializes all required variables
    s.readSerialStart()                                               # starts background thread

    # plotting starts below
    pltInterval = 50    # Period at which the plot animation updates [ms]
    xmin = 0
    xmax = maxPlotLength
    ymin = -(1)
    ymax = 200
    fig = plt.figure()
    ax = plt.axes(xlim=(xmin, xmax), ylim=(float(ymin - (ymax - ymin) / 10), float(ymax + (ymax - ymin) / 10)))
    ax.set_title('Strain Gauge/ToF')
    ax.set_xlabel("Time")
    ax.set_ylabel("Force/Distance")

    # put our plot onto Tkinter's GUI
    root = Tk.Tk()
    app = Window(fig, root, s)

    lineLabel = ['W', 'X', 'Y', 'Z']
    style = ['y-', 'r-', 'c-', 'b-']  # linestyles for the different plots
    timeText = ax.text(0.70, 0.95, '', transform=ax.transAxes)
    lines = []
    lineValueText = []
    for i in range(numPlots):
        lines.append(ax.plot([], [], style[i], label=lineLabel[i])[0])
        lineValueText.append(ax.text(0.70, 0.90-i*0.05, '', transform=ax.transAxes))
    anim = animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabel, timeText), interval=pltInterval)    # fargs has to be a tuple

    plt.legend(loc="upper left")
    root.mainloop()   # use this instead of plt.show() since we are encapsulating everything in Tkinter

    s.close()


if __name__ == '__main__':
    main()

A window shows up with no data passing through it even though I have 4 sensors that have data coming from an Arduino. The window contains 1 graph with 4 plots in it currently. I want 4 graphs each with one plot all in one window. I have been using https://thepoorengineer.com/en/python-gui/ as a reference to make graphs within python. The code for the data transfer is within the link as well. I tried to combine his 2 different codes and debugging it to make 4 graphs each with one plot to work with one Tkinter GUI window but it doesn't work. I also get an error of TypeError: getSerialData() missing 1 required positional argument: 'pltNumber' . Not sure why I get this error if pltNumber is in the parentheses in the code. I'm a beginner at python. What should I change to make the code work?

Script that can generate 4 separate graphs each with one plot that are not within a Tkinter GUI(works with 4 sensors but I need them within a Tkinter window):

from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy


class serialPlot:
    def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=1):
        self.port = serialPort
        self.baud = serialBaud
        self.plotMaxLength = plotLength
        self.dataNumBytes = dataNumBytes
        self.numPlots = numPlots
        self.rawData = bytearray(numPlots * dataNumBytes)
        self.dataType = None
        if dataNumBytes == 2:
            self.dataType = 'h'     # 2 byte integer
        elif dataNumBytes == 4:
            self.dataType = 'f'     # 4 byte float
        self.data = []
        self.privateData = None     # for storing a copy of the data so all plots are synchronized
        for i in range(numPlots):   # give an array for each type of data and store them in a list
            self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
        self.isRun = True
        self.isReceiving = False
        self.thread = None
        self.plotTimer = 0
        self.previousTimer = 0

        print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        try:
            self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
            print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
        except:
            print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')

    def readSerialStart(self):
        if self.thread == None:
            self.thread = Thread(target=self.backgroundThread)
            self.thread.start()
            # Block till we start receiving values
            while self.isReceiving != True:
                time.sleep(0.1)

    def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
        if pltNumber == 0:  # in order to make all the clocks show the same reading
            currentTimer = time.perf_counter()
            self.plotTimer = int((currentTimer - self.previousTimer) * 1000)     # the first reading will be erroneous
            self.previousTimer = currentTimer
        self.privateData = copy.deepcopy(self.rawData)    # so that the 3 values in our plots will be synchronized to the same sample time
        timeText.set_text('' + str(self.plotTimer) + '')
        data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
        value,  = struct.unpack(self.dataType, data)
        self.data[pltNumber].append(value)    # we get the latest data point and append it to our array
        lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
        lineValueText.set_text('[' + lineLabel + '] = ' + str(value))

    def backgroundThread(self):    # retrieve data
        time.sleep(1.0)  # give some buffer time for retrieving data
        self.serialConnection.reset_input_buffer()
        while (self.isRun):
            self.serialConnection.readinto(self.rawData)
            self.isReceiving = True

    def close(self):
        self.isRun = False
        self.thread.join()
        self.serialConnection.close()
        print('Disconnected...')


def makeFigure(xLimit, yLimit, title):
    xmin, xmax = xLimit
    ymin, ymax = yLimit
    fig = plt.figure()
    ax = plt.axes(xlim=(xmin, xmax), ylim=(int(ymin - (ymax - ymin) / 10), int(ymax + (ymax - ymin) / 10)))
    ax.set_title(title)
    ax.set_xlabel("Time")
    ax.set_ylabel("Force/Distance")
    return fig, ax


def main():
    # portName = 'COM5'
    portName = '/dev/ttyACM0'
    baudRate = 38400
    maxPlotLength = 100     # number of points in x-axis of real time plot
    dataNumBytes = 4        # number of bytes of 1 data point
    numPlots = 4            # number of plots in 1 graph
    s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots)   # initializes all required variables
    s.readSerialStart()                                               # starts background thread

    # plotting starts below
    pltInterval = 50    # Period at which the plot animation updates [ms]
    lineLabelText = ['W', 'X', 'Y', 'Z']
    title = ['Strain Gauge 1 Force', 'Strain Gauge 2 Force', 'ToF 1 Distance', 'ToF 2 Distance']
    xLimit = [(0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength)]
    yLimit = [(-1, 1), (-1, 1), (-1, 1), (-1, 1)]
    style = ['y-', 'r-', 'g-', 'b-']    # linestyles for the different plots
    anim = []
    for i in range(numPlots):
        fig, ax = makeFigure(xLimit[i], yLimit[i], title[i])
        lines = ax.plot([], [], style[i], label=lineLabelText[i])[0]
        timeText = ax.text(0.50, 0.95, '', transform=ax.transAxes)
        lineValueText = ax.text(0.50, 0.90, '', transform=ax.transAxes)
        anim.append(animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabelText[i], timeText, i), interval=pltInterval))  # fargs has to be a tuple
        plt.legend(loc="upper left")
    plt.show()

    s.close()


if __name__ == '__main__':
    main()
Ashby S.
  • 1
  • 2

2 Answers2

1

I think something like this can be useful.

A similar issue was addressed in this post too.

Here we can use the backend class of matplotlib namely FigureCanvasTkAgg. It works like a tkinter canvas but with the additional ability to be able to plot figures into it.

This means that we can initialize multiple matplotlib figures, plot graphs on them and then plot those figures onto the canvas. This allows us to plot multiple graphs on the same tkinter window.

To import this class -:

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

Then a figure object of matplotlib can be used to plot a graph on the canvas like so -:

from matplotlib.figure import Figure
fig = Figure(...) # Initializing the figure object.
canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
tk_canvas.pack() # Packing it into it's master window.
canvas.draw() # Drawing the canvas onto the screen.

Similarly multiple canvases can be initialized and packed into the tk window, thus giving multiple plotted graphs. The plotting of the figure object can be done using matplotlib methods.

The full code for two such figures will become -:

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

# FOR FIRST GRAPH

fig = Figure(...) # Initializing the figure object.
canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
tk_canvas.pack() # Packing it into it's master window.
canvas.draw() # Drawing the canvas onto the screen.

# FOR SECOND GRAPH

fig_2 = Figure(...) # Initializing the second figure object.
canvas_2 = FigureCanvasTkAgg(fig_2, master=root) # Initializing the second FigureCanvasTkAgg Class Object.
tk_canvas_2 = canvas_2.get_tk_widget() # Getting the second Figure canvas as a tkinter widget.
tk_canvas_2.pack() # Packing it into it's master window.
canvas_2.draw() # Drawing the second canvas onto the screen.

# CAN BE REPEATED FOR MULTIPLE SUCH GRAPHS....
typedecker
  • 1,351
  • 2
  • 13
  • 25
  • Also I would suggest looking into [this](https://stackoverflow.com/questions/4073660/python-tkinter-embed-matplotlib-in-gui) post answered by @BryanOakley – typedecker May 05 '21 at 08:25
0

EDIT: Misunderstood question, but will still leave this here as it'll be useful for you in terms of manipulating the graphs

Here is some code that I use to generate 5 listboxes, and append them to a dictionary so I can reference them later in the code.

        self.listboxes = []
        for i in range(5):
            self.lb = tk.Listbox(self.modeSelect)
            self.lb.configure(background='#2f2a2d', exportselection='false', font='{Arial} 12 {}', foreground='#feffff', height='23')
            self.lb.configure(relief='flat', width='9', justify='center', selectbackground='#feffff', selectforeground='#000000')
            self.lb.pack(side='left')
            self.listboxes.append(self.lb)
            self.lb.bind("<Double-1>", self.getIndexLB)

You can then assign functions or call attributes by using

self.listboxes[0].get() #for example

That way you can assign points to each graph, it will also allow you to control all the graphs simultaneously by doing something like:

for child in self.modeSelect.winfo_children():
      if isinstance(child, tk.Listbox):
          child.delete(index)
  • What do you mean by dynamically generating the 4 graphs? I do have a code that can make 4 separate graphs each with one plot. Although the graphs are not within a Tkinter GUI with a text box and button to go along with them, which I want as well. I'll add that code for you to see. – Ashby S. May 05 '21 at 02:03
  • The code written does not have proper indentation. – typedecker May 05 '21 at 08:08
  • Are the graphs embedded within a tkinter object? @AshbyS. – Giuseppe Marziano May 05 '21 at 08:08
  • @MatrixProgrammer Im a newbie to Stack, forgive me – Giuseppe Marziano May 05 '21 at 08:11
  • No problem, sorry I did not mean to offend you was just saying as a suggestion. – typedecker May 05 '21 at 08:18
  • Neither me nor you are here to let anyone down, no one is perfect and that is why we all help each other. I am just here so I can help as much people as I can. – typedecker May 05 '21 at 08:19
  • Actually the OP does not have the problem of not being able to generate the graphs but rather faces problem in being able to place those graphs on the same tkinter window. – typedecker May 05 '21 at 08:21