3

I'm trying to plot a graph with python using the canvas widget, I'm currently sending data in from an arduino sensor sketch ..does anyone have an idea on how I can plot this graph in real time on Python using canvas. I've read up on it but I'm having problems with unpacking the x , y and z axis data and looping it to updating in real time. Below is the arduino sketch.And also the skeleton of the Tkinter canvas sketch. Thanks!!

AcceleroMMA7361 ski;
int x;
int y;
int z;

void setup(){
    Serial.begin(115200);
    ski.begin(13, 12,11,10, A0, A1, A2);
    ski.setARefVoltage(3.3);             
    ski.setSensitivity(HIGH);
    ski.calibrate();
}

void loop(){
    x = ski.getXRaw();
    y = ski.getYRaw();
    z = ski.getZRaw();
    Serial.print("\nx: ");
    Serial.println(x);
    Serial.print("\ty: ");
    Serial.print(y);
    Serial.print("\tz: ");
    Serial.print(z);
    delay(10);                                     //(make it readable)
}//End of Arduino Sketch



  from Tkinter import *
  import serial #import Serial Library
  import numpy as np # Import numpy

  ardoData = serial.Serial('COM4', 115200) 
  accelX1 = []     #to hold the incoming axis data from the arduino
  accelY1 = []
  accelZ1 = []


  class GraphException(Exception):
      def __init__(self, string):
         Exception.__init__(self, string)

  class Smoking(Canvas):
     def __init__(self, master, FrameTitle, Col, Row, Height):
        Canvas.__init__(self, master)
          self.FrameTitle = FrameTitle
          self.x1Axis = range(1001)
          self.y1Axis = range(1001)
          self.z1Axis = range(1001)
          self.Line1 = range(1001)
          self.Line2 = range(1001)
          self.Line3 = range(1001)
          self.configure(height=Height, width=750, bg='grey', bd=3, relief=GROOVE)
         self.grid()
         self.grid_propagate(0)
         self.Col = Col
         self.Row = Row
         self.place(y=Col, x=Row)
         self.create_text(380, 20, text=self.FrameTitle, fill = 'black')
         for i in range(0, 1001):
            self.Line1[i] = self.create_line(-10+(i*10), 90, 0+(i*10), 90, fill='blue', width=0)
            self.Line2[i] = self.create_line(-10+(i*10), 90, 0+(i*10), 90, fill='red', width=0)
            self.Line3[i] = self.create_line(-10+(i*10), 90, 0+(i*10), 90, fill='yellow', width=0)

     def LiveValues(self,accelX1, accelY1, accelZ1, Centerline = False, Dritter = False):
          for x in range(1, 59):
             for i in np.arange(1, 1001, 1): 
                  while(ardoData.inWaiting() == 0):  
                      pass #do nothing
                  ardoString = ardoData.readline()              
                  dataArray = ardoString.strip().strip('\n')   #Split into an array called dataArray and strip off any spaces
                # Ensure that you are not working on empty line
               if ardoString:
                   dataArray = ardoString.split(",")
               if len(dataArray) > 1:
                   self.x1Axis = int(dataArray[0])
                   self.y1Axis = int(dataArray[1])
                   self.z1Axis = int(dataArray[2])
                   accelX1.append(self.x1Axis)
                   accelY1.append(self.y1Axis)
                   accelZ1.append(self.z1Axis)
              for x in range(0, 1001):
                 self.coords(self.Line1[x], -10+(x*10), self.accelX1[x], (x*10), self.accelX1[x+1])
                 self.coords(self.Line2[x], -10+(x*10), self.accelY1[x], (x*10), self.accelY1[x+1])
                 self.coords(self.Line3[x], -10+(x*10), self.accelZ1[x], (x*10), self.accelZ1[x+1])


   # Create and run the GUI
   root = Tk()
   app = Smoking(root, "Smooth Sailing", 1000, 1000, 1000)
   app.mainloop()

So after working with the answer given on the post I ended up with a Line graph that is not reactive, I understand that I have to tweak the code to suit the range of values I am getting which is shown below. I increased the limit as indicated by @patthoyts but I still couldn't get either lines to show up on the canvas and to even be reactive ( what i mean by that is to respond to the movement of the sensor). Also, do bear with me Editors!, if I'm going about editing this question the wrong way, is just that I'm quite desperate to have this issue resolved. Does anyone have any clue as to how I could resolve this issue. Thanks PS: I added both images , that of my data stream and the image on the canvas. enter image description here enter image description here

2
  • You should show at least part of your python code. Really this is not about arduino but about plotting a graph from a stream of 3 decimal numbers, 1 per line, output every 10 ms. Create a canvas and put a line on it. Read 3 lines and parse the numbers then convert into canvas coords and update the line configuration coords property.
    – patthoyts
    Commented Nov 5, 2015 at 7:58
  • @patthoyts.....please do bear in mind THIS is a NEWBIE alert!!!!! I have limited idea on how to go about it...The idea I had going was simply inculcated in this sketch below
    – dada
    Commented Nov 6, 2015 at 0:03

1 Answer 1

1

To generate a useful example I've setup an Arduino Nano with a magnetic orientation sensor and have it output 3 values per line for the X, Y and Z field strengths. It prints 1 line every 50ms and the following python tkinter script plots this. It started as your python code above but it has been reworked to be more pythonic. The result looks like this: screenshot of tkinter plot

Call the script with the comport and speed. eg: python magplot.py COM21 19200

#!/usr/bin/python
#
# Read stream of lines from an Arduino with a magnetic sensor. This
# produces 3 values per line every 50ms that relate to the orientation
# of the sensor. Each line looks like:
# MAG   1.00    -2.00   0.00
# with each data line starting with "MAG" and each field separated by
# tab characters. Values are floating point numbers in ASCII encoding.
#
# This script supports both Python 2.7 and Python 3

from __future__ import print_function, division, absolute_import
import sys
if sys.hexversion > 0x02ffffff:
    import tkinter as tk
else:
    import Tkinter as tk
from serial import Serial

class App(tk.Frame):
    def __init__(self, parent, title, serialPort):
        tk.Frame.__init__(self, parent)
        self.serialPort = serialPort
        self.npoints = 100
        self.Line1 = [0 for x in range(self.npoints)]
        self.Line2 = [0 for x in range(self.npoints)]
        self.Line3 = [0 for x in range(self.npoints)]
        parent.wm_title(title)
        parent.wm_geometry("800x400")
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.bind("<Configure>", self.on_resize)
        self.canvas.create_line((0, 0, 0, 0), tag='X', fill='darkblue', width=1)
        self.canvas.create_line((0, 0, 0, 0), tag='Y', fill='darkred', width=1)
        self.canvas.create_line((0, 0, 0, 0), tag='Z', fill='darkgreen', width=1)
        self.canvas.grid(sticky="news")
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
        self.grid(sticky="news")
        parent.grid_rowconfigure(0, weight=1)
        parent.grid_columnconfigure(0, weight=1)

    def on_resize(self, event):
        self.replot()

    def read_serial(self):
        """
        Check for input from the serial port. On fetching a line, parse
        the sensor values and append to the stored data and post a replot
        request.
        """
        if self.serialPort.inWaiting() != 0:
            line = self.serialPort.readline()
            line = line.decode('ascii').strip("\r\n")
            if line[0:3] != "MAG":
                print(line) # line not a valid sensor result.
            else:
                try:
                    data = line.split("\t")
                    x, y, z = data[1], data[2], data[3]
                    self.append_values(x, y, z)
                    self.after_idle(self.replot)
                except Exception as e:
                    print(e)
        self.after(10, self.read_serial)

    def append_values(self, x, y, z):
        """
        Update the cached data lists with new sensor values.
        """
        self.Line1.append(float(x))
        self.Line1 = self.Line1[-1 * self.npoints:]
        self.Line2.append(float(y))
        self.Line2 = self.Line2[-1 * self.npoints:]
        self.Line3.append(float(z))
        self.Line3 = self.Line3[-1 * self.npoints:]
        return

    def replot(self):
        """
        Update the canvas graph lines from the cached data lists.
        The lines are scaled to match the canvas size as the window may
        be resized by the user.
        """
        w = self.winfo_width()
        h = self.winfo_height()
        max_X = max(self.Line1) + 1e-5
        max_Y = max(self.Line2) + 1e-5
        max_Z = max(self.Line3) + 1e-5
        max_all = 200.0
        coordsX, coordsY, coordsZ = [], [], []
        for n in range(0, self.npoints):
            x = (w * n) / self.npoints
            coordsX.append(x)
            coordsX.append(h - ((h * (self.Line1[n]+100)) / max_all))
            coordsY.append(x)
            coordsY.append(h - ((h * (self.Line2[n]+100)) / max_all))
            coordsZ.append(x)
            coordsZ.append(h - ((h * (self.Line3[n] + 100)) / max_all))
        self.canvas.coords('X', *coordsX)
        self.canvas.coords('Y', *coordsY)
        self.canvas.coords('Z', *coordsZ)

def main(args = None):
    if args is None:
        args = sys.argv
    port,baudrate = 'COM4', 115200
    if len(args) > 1:
        port = args[1]
    if len(args) > 2:
        baudrate = int(args[2])
    root = tk.Tk()
    app = App(root, "Smooth Sailing", Serial(port, baudrate))
    app.read_serial()
    app.mainloop()
    return 0

if __name__ == '__main__':
    sys.exit(main())

Update

Looking at the updated question it is clear that the new Tkinter code is not updating the display. In all windowing systems it is essential that the application thread is allowed to process events regularly. This is why the example code above uses the Tk after() method to add calls to the event queue. The read_serial() function checks to see if any data is available and if not schedules a retry later. This is done by using after() to request the same method is called again in 10ms time. During that 10ms delay the Tk application processes all the other windowing system events like mouse movement, keyboard input and crucially expose and painting events.

The same method is used to call the replot() method once we have updated the list of data items. In this case after_idle() has been used which means schedule this function call as soon as nothing else is happening.

The act of changing the canvas line coordinates will cause further events to be raised by Tk which affect the display only once they are processed by the event loop. If we never process the events, nothing will appear on screen.

2
  • 1
    I suggest adding some print() statements in read_serial to check you are reading values and splitting them right. Then check the scaling in replot. I've offset by 100 and scaled to 200 so the values must be -100 < x < 100. If your values lie outside that range they'll be offscreen. You could use max_all = max([max_X, max_Y, max_Z]) to range dynamically.
    – patthoyts
    Commented Nov 11, 2015 at 13:30
  • @pattyhoyts from your update, I garner that even though the structure of the sketch is correct, it's compilation/execution is the problem . Would you advise I continue to find ways for it to work on my system (Windows) or ditch it for something else?
    – dada
    Commented Nov 12, 2015 at 21:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.