Interface and application programming

Objectives of the week

  • Individual assignment
    • Write an application that interfaces with an input and/or output device that you made
  • Group assignment
    • Compare as many tool options as possible

Fun week coming! My objective here is to develop an interface for my pressure controller, and, if possible, that could be integrated with the motion tracking!

What I did

  • Used python and several libraries to make an interface and plot data
  • Interfaced my pressure sensors and PWM outputs with an interfaced

What I learned

  • A lot! How to make an interface, and to receive and send formatted data

If I had the time to go further...

  • As usual now, my final project would be finished!

Files and resources

Step 1: planning what I’ll do

First thing (I am used to it now): planning my week! I planned to develop an interface for my final project anyway, so this week’s project comes with a good timing for me! Basically, I want to plot data, and to control outputs. A plus would be to integrate the image processing part in the same window, so I want my interface to be modular.

Since I fall in love with Python, and that I started the image processing using python, I will use this langage for my interface. I am quite novice, so I will use Tkinter for the interface itself, and Pyserial for the communication with the board. On the other side, I have now all the Arduino resources!

Step 2: discovering Tkinter and Pyserial

First thing: the Tkinter Hello world. I followed this french tutorial

Concerning Pyserial, I followed this introduction. After several tries, the first tests were (almost) successful:

Note that all the source code is given at the top of the page!

Step 3: making a nice (but far from fancy) interface

Difficult to do a good documentation here, I mostly invite you to go check the source code! However, I will develop the key parts of the code below. To do this code, here are the libraries that I used:

And the tutorials that I followed:

After several developments, here is the working interface:

Ok, I agree, it is not very fancy, but it does the job! It plots and saves the data, and allows to communicate with the board! Note that neither the PWM values nor the pressure values are calibrated: they still mean nothing now! More, the pneumatic circuit behind it is not fully connected, so the window showed here is just an example! However, you can see that the pressure reacts to the PWM, that can be adjusted with the sliders: good! I will detail a little bit further the ley parts of the interface below. Note that the final source code can be downloaded from the top of the page.

Tkinter and widgets

To add elements to a tkinter interface, widgets must be defined. Widgets can be label, sliders, buttons, canves, … Here I used Frames, to place and arrange the widgets on the window, Buttons, Labels, and Slider, and a Canvas for the plot. Here is an example of how I used those elements. Variable labels of text can be set by using the StringVar elements: the text will be automatically Updated when the StringVar is changed.

Setting up the interface:

# set up GUI
root = Tk()
root.title('Presure control interface (q to exit)')
root['bg']='white'
root.bind('q','exit')

# inputs
fr_input=Frame(root,borderwidth=2, relief=GROOVE, width=400)
fr_input.pack(side=TOP)

w1 = Scale(fr_input, from_=0, to=200, orient=HORIZONTAL, resolution=1, tickinterval=100, length=120, label='PWM channel 1')
w1.set(PWM1)
w1.pack(side=LEFT)

# labels
fr_legends=Frame(root,borderwidth=2,relief=GROOVE)
fr_legends.pack(side=LEFT)

fr_labels=Frame(fr_legends,borderwidth=2,relief=GROOVE)
fr_labels.pack(side=TOP)

fr_label_p1=Frame(fr_labels,borderwidth=2,relief=GROOVE)
fr_label_p1.pack()
label_p1=Label(fr_label_p1, text="Pressure channel 1 =")
label_p1.pack(side=LEFT)
v_p1 = StringVar()
v_p1.set("0")
label_v_p1=Label(fr_label_p1, textvariable=v_p1 )
label_v_p1.pack(side=LEFT)

fr_legend_graph=Frame(fr_legends,borderwidth=2,relief=GROOVE)
fr_legend_graph.pack()
l=Label(fr_legend_graph, text="Channel 1", fg="blue")
l.pack(side=TOP)
l=Label(fr_legend_graph, text="Channel 2", fg="green")
l.pack(side=TOP)

# buttons
fr_button=Frame(fr_legends,borderwidth=2,relief=GROOVE)
fr_button.pack(side=TOP)

button_reinit=Button(fr_button, text='Reinitialize data')
button_reinit.bind("<Button-1>", reinit_b)
button_reinit.pack(side=TOP)

And in the idle loop:

#Update parameters
PWM1=w1.get() #Reads the value from the slider

#Update the variable labels of the interface (p1 is received from the arduino)
v_p1.set(p1)

Serial communication

This part was the key challenge of this week: how to send and receive data from the arduino. This works sufficiently good to be able to communicate with the bard, but this could still be improved: the connection is still sometimes unstable and can lead to crashing the interface.

Initializing the communication

# open serial port
port = '/dev/ttyUSB0'
baudRate=9600
ser = serial.Serial(port,baudRate)

Defined functions

def decode_int():
    """Reads an int sent by the arduino as a string and terminated by a '\n'"""
    global serial
    try:
        ser_bytes = ser.readline()
        decoded_bytes = (ser_bytes[0:len(ser_bytes)-2].decode("utf-8"))
        test =int(decoded_bytes)/1 # The value is effectively an int
        return(int(decoded_bytes))
    except:
        print("An error occured on the int serial transmission")
        return(-1)

def decode_float():
    """Reads a float sent by the arduino as a string and terminated by a '\n'"""
    global serial
    try:
        ser_bytes = ser.readline()
        decoded_bytes = (ser_bytes[0:len(ser_bytes)-2].decode("utf-8"))
        test =float(decoded_bytes)/1 # The value is effectively an int
        return(float(decoded_bytes))
    except:
        print("An error occured on the float serial transmission")
        return(-1.0)

def decode_string():
    """Reads an string sent by the arduino and terminated by a '\n'"""
    global serial
    try:
        ser_bytes = ser.readline()
        decoded_bytes = (ser_bytes[0:len(ser_bytes)-2].decode("utf-8"))
        return(decoded_bytes)
    except:
        print("An error occured on the String serial transmission")
        return("-1")

In the idle loop

#Detects the framing
start=decode_string()
if(start=="P1"):
        p1=decode_int()
        p2=decode_int()
        p3=decode_int()
        timeInt=decode_int()
        time=float(timeInt/1000)

        # Add the received values to the lists
        if(p1>=0 and p2>=0 and p3>=0 and time>=0): #Ensure that all the data were cotrrectly received
            pressure1.append(p1)
            pressure2.append(p2)
            pressure3.append(p3)
            PWM1_L.append(PWM1)
            PWM2_L.append(PWM2)
            PWM3_L.append(PWM3)
            timeL.append(time)
            save_csv()#Save at each step the values in a csv file
        else:
            print("Data missed")

#Format data to be sent to the arduino using the serail communication
str_PWM1 = str(PWM1)+'\n'
str_PWM2 = str(PWM2)+'\n'
str_PWM3 = str(PWM3)+'\n'

#Send data to the arduino
ser.write(b'PWM\n')
ser.write(str_PWM1.encode("utf-8"))
ser.write(str_PWM2.encode("utf-8"))
ser.write(str_PWM3.encode("utf-8"))

And in the Arduino loop, every 100 loops

//Serial communication with the python interface: sending data
Serial.println("P1");
Serial.println(sensorAvg1);
Serial.println(sensorAvg2);
Serial.println(sensorAvg3);
Serial.println(millis());
}

//Serial communication with the python interface: receiving data
if(Serial.available()) {
  String incomingString = Serial.readStringUntil('\n');
  if(incomingString=="PWM"){
    String pwm1=Serial.readStringUntil('\n');
    String pwm2=Serial.readStringUntil('\n');
    String pwm3=Serial.readStringUntil('\n');

    dutyCycle1=pwm1.toInt();
    dutyCycle2=pwm2.toInt();
    dutyCycle3=pwm3.toInt();
  }

Plot

This was the other key part of the interface! The challenge here was to be able to update the plot without having a full replot. In the first time, this took too much time, so it interfered with the serial communication, and lead to 2 problems: the program crashed often, and it induced a shift between the PWM values (not received) and the pressure values (received). Below is the final code:

Initializing the plot

# canvas and figures
plt.ion() #Enable interactive plot
fig = Figure(figsize=(5, 4), dpi=100)
a =fig.add_subplot(111)
a.set_ylim(0, 255)
a.set_xlabel('Time (s)')
a.set_ylabel('Pressure')
#a.grid()
l_p1, =a.plot(0,0, 'b')
l_p2, =a.plot(0,0, 'g')
l_p3, =a.plot(0,0, 'r')
l_pwm1, =a.plot(0,0, 'b')
l_pwm2, =a.plot(0,0, 'g')
l_pwm3, =a.plot(0,0, 'r')

canvas = FigureCanvasTkAgg(fig, master=root)  # A tk.DrawingArea.
canvas.draw()
canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)

toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)

Defined plot functions

def update_graphs(a,l):
    """ Update the graph real time """
    global pressure1, pressure2, pressure3, timeL, p1, time

    update_single_graph(timeL, pressure1, l_p1)
    update_single_graph(timeL, pressure2, l_p2)
    update_single_graph(timeL, pressure3, l_p3)
    update_single_graph(timeL, PWM1_L, l_pwm1)
    update_single_graph(timeL, PWM2_L, l_pwm2)
    update_single_graph(timeL, PWM3_L, l_pwm3)
    a.set_xlim(timeL[-1]-60, timeL[-1]+10) #number of seconds before and after the last element of the time list

    canvas.draw()


def update_single_graph(x,y,l):
    """x, y: python lists of the same size"""
    X=np.array(x)
    Y=np.array(y)
    l.set_data(X, Y)

And in the idle loop

if(plot_pressure==True):
  try:
    update_graphs(a,l)
  except:
    print("Reinitialisation of the data required")

Saving the data

Finally, I implemented 2 ways of saving the data: saving them as Numpy lists as the action of a button, and saving them automatically in a csv file, at each idle loop.

Saving data as numpy lists:

def save_b(event):
    """Saves the numpy lists (action of the  button)"""
    global timeL, pressure1, pressure2, pressure3
    try:
        os.mkdir('results')
    except OSError:
        pass
    now=datetime.now()
    folder_name=now.strftime("%Y-%m-%d_%H-%M")
    path='results/'+str(folder_name)
    try:
        os.mkdir(path)
    except OSError as e:
        print(e.errno, e.strerror, e.filename)
    np.save(path+'/timeL', timeL)
    np.save(path+'/pressure1', pressure1)
    np.save(path+'/pressure2', pressure2)
    np.save(path+'/pressure3', pressure3)
    print('Results saved in folder', path)

Saving data in csv file:

# open csv file
now=datetime.now()
folder_name=now.strftime("%Y-%m-%d_%H-%M")
path='results/'+str(folder_name)
f_csv=open(path+"test_data.csv","a")
def save_csv():
    """Save the data in the csv file f_csv"""
    global f_csv, p1, p2, p3, PWM1, PWM2, PWM3, time
    writer = csv.writer(f_csv,delimiter=",")
    writer.writerow([time,PWM1,PWM2,PWM3,p1, p2, p3])

Update week 18: making my own board for the final project.

After some prototyping steps using my satshakit, I made my own board, including among other the outputs and inputs chains. This board is fully documented in the final project’s page. Now, the interface is effectively for a board that I designed and made!

Step 4: group assignment

In the group, we tested mainly 2 options:

Comparing the results, we can highlight several points: