JacobNeill
Posts: 3
Joined: Mon May 27, 2019 8:15 am

Dynamic PID Temperature Controller Using Raspberry Pi 3 Model B and Arduino Nano

Mon May 27, 2019 8:40 am

Hi there, to preface i'm an undergraduate student studying chemistry. My past experience prior to chemistry was in IT and so i have some minimal coding experience but much of the python I have learnt has been for the project over the past 3-4 months. I've hit a wall in a project i'm working on and need some guidance/direction in where to head next. The end goal is to design a melting point apparatus (MPA) similar to the Mettler Toledo MP50 to measure the melting point of various chemical compounds in a capillary tube.
The plan has been to design a Tkinter GUI on the Raspberry Pi to readout temperature data, display a camera view inside the MPA and dynamically alter the PID temperature control input to the furnace based on a method design, i.e. Ramp to start temp?, Start Temp=x, End Temp=y, waiting time=z, degrees C /min=r.

So far, i have designed a GUI in tkinter that functions as intended and has most all of the functionality inbuilt, currently the method variables are stored in a .csv file on the raspberry pi where each method is separated by a line space, i.e each method is printed on a new line. I have also made a USB add-on module that has an arduino nano connected to a max6675 chip to read temperature and a Keyes sr1y 5v relay module.

I have been able to design a PID temperature controller that functions on the arduino nano as a sketch file, however it is required to be sent to the nano via the arduino IDE and has a static input variable set in the arduino code itself, this variable would have to be dynamic rather than static as the furnace temperature would be required to change over the course of the method. I have also managed to read temperature from the arduino into the GUI on the raspberry pi via a sketch code (arduino IDE) that reads temp and outputs it as serial data, python then reads the usb serial data and saves it to file before plotting on a graph with matplotlib.

Needless to say, all of these solutions result in uploading a code to the arduino, then executing code to save the serial data to file before executing the GUI on the raspberry pi, ideally i would like a more seamless integration of these elements. I have looked at nanpy and pyFirmata to use the arduino as a slave device, allowing for direct coding in python. but i'm not really sure what the applications/benefits/cons of each are and so i'm putting it to the RaspPi forums! i hope that someone out there has had some similar experience with temperature controllers on the pi and can help me out.

below is the code for the GUI i'm using on my raspberry pi:

Code: Select all

from tkinter import *
from datetime import datetime
import time
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.animation as animation
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2TkAgg)
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
from matplotlib import pyplot as plt 
import csv
import tkinter
import RPi.GPIO as GPIO
#import picamera
from matplotlib import rcParams
rcParams.update({'figure.autolayout': True})
import serial
from gpiozero import CPUTemperature
from time import sleep, strftime, time

#camera = picamera.PiCamera()    
       

class DataOutput(Frame):
    def __init__(self, root):
        Frame.__init__(self, root)
        title = Label(self, text="Data Output", font="BOLD 18").grid(row=0, column=0, sticky=W)
        temperature = Label(self, text="CPU Temperature :", font="15").grid(row=1, column=0, sticky=W)       
        tempout = Label(self, text="20°C", font="BOLD 10").grid(row=2, column=0, sticky=W)
        temperature2 = Label(self, text="Ambient Temperature :", font="15").grid(row=3, column=0, sticky=W)       
        self.tempout2 = Label(self, text="20°C", font="BOLD 10").grid(row=4, column=0, sticky=W)
        time = Label(self, text="Time [MM:SS] :", font="15").grid(row=5, column=0, sticky=W)
        clock = SmlClk(self).grid(row=6, column=0, sticky=W)
        
class Clock(Frame):
    def __init__(self, root):
        Frame.__init__(self, root)
        clock_frame = Label(self)
        clock_frame['text'] = '19:12:09'
        clock_frame.grid(row=0, column=0)
        def tic():
         clock_frame['text'] = datetime.now().strftime("%d %B %Y %I:%M:%S %p")

        def tac():
         tic()
         clock_frame.after(1, tac)
        tac()

class SmlClk(Frame):
    def __init__(self, root):
        Frame.__init__(self, root)
        clock_frame = Label(self)
        clock_frame['text'] = '19:12:09'
        clock_frame['font'] = 'BOLD 10'
        clock_frame.grid(row=0, column=0)
        def tic():
         clock_frame['text'] = datetime.now().strftime("%M:%S")

        def tac():
         tic()
         clock_frame.after(1, tac)
        tac()
    
class SavedMethodButtons(Frame):
    def __init__(self, root):
        Frame.__init__(self, root)
        self.sm1 = Button(self, height=3, width=10, text='Btn').grid(row=0, column=0, sticky='')
        self.sm2 = Button(self, height=3, width=10,  text='Btn').grid(row=0, column=1, sticky='')
        self.sm3 = Button(self, height=3, width=10,  text='Btn').grid(row=1, column=0, sticky='')
        self.sm4 = Button(self, height=3, width=10,  text='Btn').grid(row=1, column=1, sticky='')
        self.sm5 = Button(self, height=3, width=10,  text='Btn').grid(row=2, column=0, sticky='')
        self.sm6 = Button(self, height=3, width=10,  text='Btn').grid(row=2, column=1, sticky='')
    
        
def CameraON():
    camera.preview_fullscreen=False
    camera.preview_window=(148,-50, 400, 345)
    camera.resolution=(390,200)
    camera.start_preview()
    
def CameraOFF():
    camera.stop_preview()
    
def EXIT():
    #camera.stop_preview()
    #camera.close()
    app.destroy()

def UpdateBrightness(value):
    camera.brightness = int(value)
    
def UpdateContrast(value):
    camera.contrast = int(value)
    
def UpdateSharpness(value):
    camera.sharpness = int(value)
    
def UpdateSaturation(value):
    camera.saturation = int(value)

def SetAWB(var):
    camera.awb_mode = var

def SetEFFECTS(var):
    camera.image_effect = var

def Zoom(var):
    x = float("0."+var)
    camera.zoom = (0.5,0.5,x,x)

class CameraButtonFrame(Frame):
    def __init__(self, root):
        Frame.__init__(self, root)
        Scale(self, from_=30, to=100, orient=HORIZONTAL, label = "Brightness", width=15, command=UpdateBrightness).grid(row=0, padx=10, column=0)
        Scale(self, from_=-100, to=100, orient=HORIZONTAL, label = "Contrast", width=15, command=UpdateContrast).grid(row=0, padx=10, column=1)
        Scale(self, from_=-100, to=100, orient=HORIZONTAL, label = "Sharpness", width=15, command=UpdateSharpness).grid(row=1, padx=10, column=0)
        Scale(self, from_=-100, to=100, orient=HORIZONTAL, label = "Saturation", width=15, command=UpdateSaturation).grid(row=1, padx=10, column=1)
        Scale(self, from_=10, to=99, orient=HORIZONTAL, label = "Zoom", width=15, command=Zoom).grid(row=2, column=0,  padx=10, columnspan=2)
        Button(self, text='Cam ON', command=CameraON).grid(row=3, column=0)
        Button(self, text='Cam OFF', command=CameraOFF).grid(row=3, column=1)

def animate(i):
    csv_reader = csv.reader(open('/home/pi/ambient_temp.csv'))
    csv_reader2 = csv.reader(open('/home/pi/cpu_temp.csv'))

    xList = []
    yList = []
    xList2 = []
    yList2 = [] 

    for line in csv_reader2:
        #xList2.append(datetime.strptime(line[0], '%H:%M:%S'))
        yList2.append(float(line[1]))
        
    for line in csv_reader:
        #xList.append(datetime.strptime(line[0], '%H:%M:%S'))
        yList.append(float(line[1]))
        
    a.clear()
    a.plot(yList, 'g-', linewidth=2, label='Ambient Temp')
    a.plot(yList2, 'r-', linewidth=2, label='CPU Temp')
    #a.plot(xList, yList, 'g-', linewidth=2, label='Ambient Temp')
    #a.plot(xList, yList2, 'r-', linewidth=2, label='CPU Temp')
    a.legend()
    a.set_title('Current CPU Temperature / Time')
    a.set_xlabel("Time[Seconds]")
    a.set_ylabel("Temperature[°C]")
 
f = Figure(figsize=(5.9, 3.3), dpi=75)
a = f.add_subplot(111)


class SampleApp(Tk):
    def __init__(self):
        Tk.__init__(self)
        self._frame = None
        self.fullscreen = True
        self.switch_frame(MeltPoint)
        ani = animation.FuncAnimation(f, animate, interval=100)
        
    def switch_frame(self, frame_class):
        """Destroys current frame and replaces it with a new one."""
        #camera.stop_preview()
        new_frame = frame_class(self)
        if self._frame is not None:
            self._frame.destroy()
        self.wm_attributes("-fullscreen", True)
        self.fullscreen = True
        self._frame = new_frame
        self._frame.grid(row=1, column=0)
        self.fst = Button(self, height='2', text="Toggle Fullscreen",font='BOLD 10', command=self.fullscreen_toggle)
        self.fst.grid(row=0, column=0, rowspan=2, sticky='NW') 
        
    def fullscreen_toggle(self):
        if self.fullscreen == False:
            self.wm_attributes("-fullscreen", True)
            self.fullscreen = True
        else:
            self.wm_attributes("-fullscreen", False)
            self.fullscreen = False

class Chart(Frame):
    def __init__(self, master):
        Frame.__init__(self, master)
        canvas = FigureCanvasTkAgg(f, self)
        canvas.draw_idle()
        canvas.get_tk_widget().pack(side=BOTTOM, fill=BOTH, expand=True)     
        toolbar = NavigationToolbar2TkAgg(canvas, self)
        toolbar.update()
        canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=True)
        b1 = Button(self, height=4, width=12, text='Back', command=lambda: master.switch_frame(ManMeth)).pack(side=LEFT, anchor="w", fill=BOTH, expand=True)
        b2 = Button(self, height=4, width=12,  text='Home', command=lambda: master.switch_frame(MeltPoint)).pack(side=LEFT, anchor="w", fill=BOTH, expand=True)
        b4 = Button(self, height=4, width=12,  text='Add Method', command=lambda: master.switch_frame(AddMethod)).pack(side=LEFT, anchor="w", fill=BOTH, expand=True)

class MeltPoint(Frame):
    def __init__(self, master):
        Frame.__init__(self, master)
        #camera = picamera.PiCamera
        #CameraON()
        title = Label(self, text="Melting Point Determination").grid(row=0, column=1)
        clock  = Clock(self).grid(row=0, column=2, columnspan=2)        
        b1 = Button(self, height=4, width=12, text='Add Method', command=lambda: master.switch_frame(AddMethod)).grid(row=2, column=0, sticky='')
        b2 = Button(self, height=4, width=12,  text='Manual Method', command=lambda: master.switch_frame(ManMeth)).grid(row=3,column=0, sticky='')
        b3 = Button(self, height=4, width=12,  text='Results', command=lambda: master.switch_frame(Chart)).grid(row=4,column=0, sticky='')
        b4 = Button(self, height=4, width=12,  text='Setup').grid(row=5,column=0, sticky='')
        b5 = Button(self, height=4, width=12,  text='Exit', command=EXIT).grid(row=6,column=0, sticky='')
        tl = Frame(self, height=210, width=410, bd=1, relief=SUNKEN).grid(row=1, rowspan=3, column=1, sticky='')            
        savdmeth = SavedMethodButtons(self).grid(row=1, column=2, rowspan=3)    
        dato = DataOutput(self).grid(row=4, rowspan=3, column=2, columnspan=2)
        canvas = FigureCanvasTkAgg(f, self)
        canvas.draw_idle()
        canvas.get_tk_widget().grid(row=4, column=1, rowspan=3)
        #toolbar = NavigationToolbar2TkAgg(canvas, self)
        #toolbar.update()
        #canvas._tkcanvas.grid(row=8,column=1)

class AddMethod(Frame):
    def __init__(self, master):
        Frame.__init__(self, master)
        title = Label(self, text="Method Settings").grid(row=0, column=1)
        clock  = Clock(self).grid(row=0, column=2, columnspan=2)

        a1 = Button(self, text='Back', height=5, width=20, command=lambda: master.switch_frame(ManMeth)).grid(row=1, rowspan=2, column=0, sticky='')
        a2 = Button(self, text='Save and Return to Analysis', height=5, width=20, command=lambda:[self.writeToFile, master.switch_frame(ManMeth)]).grid(row=3, rowspan=2, column=0, sticky='')
        a3 = Button(self, text='Save Method', height=5, width=20, command=self.writeToFile).grid(row=5, rowspan=2, column=0, sticky='')   

        mthtit = Label(self, text="Method Title", font="12").grid(row=2, column=1, sticky=W)
        sttemp = Label(self, text="Start Temperature", font="12").grid(row=3, column=1, sticky=W)
        wttm = Label(self, text="Waiting Time", font="12").grid(row=4, column=1, sticky=W)
        sktm = Label(self, text="Soaking Time", font="12").grid(row=5, column=1, sticky=W)
        endtemp = Label(self, text="End Temperature", font="12").grid(row=6, column=1, sticky=W)
        htrt = Label(self, text="Heating Rate", font="12").grid(row=7, column=1, sticky=W)
        self.checkbutton = Checkbutton(self)
        self.checkbutton.grid(row=1, column=2)
        self.checkbutton.configure(height=3)
        self.chklabel = Label(self)
        self.chklabel.grid(row=1, column=1, sticky=W)
        self.chklabel.configure(text="Ramp to Starting Temperature", font="12")      

        self.mthtite = Entry(self, relief=SUNKEN, bd=5)
        self.mthtite.grid(row=2, column=2)
        self.sttempe = Entry(self, relief=SUNKEN, bd=5)
        self.sttempe.grid(row=3, column=2)
        self.wttme = Entry(self, relief=SUNKEN, bd=5)
        self.wttme.grid(row=4, column=2)
        self.sktme = Entry(self, relief=SUNKEN, bd=5)
        self.sktme.grid(row=5, column=2)
        self.endtempe = Entry(self, relief=SUNKEN, bd=5)
        self.endtempe.grid(row=6, column=2)
        self.htrte = Entry(self, relief=SUNKEN, bd=5)
        self.htrte.grid(row=7, column=2)       

        sttemp1 = Label(self, text="°C", font="BOLD 12").grid(row=3, column=3, sticky=W)
        wttm1 = Label(self, text="Sec", font="BOLD 12").grid(row=4, column=3, sticky=W)
        sktm1 = Label(self, text="MM:SS", font="BOLD 12").grid(row=5, column=3, sticky=W)
        endtemp1 = Label(self, text="°C", font="BOLD 12").grid(row=6, column=3, sticky=W)
        htrt1 = Label(self, text="°C/Min", font="BOLD 12").grid(row=7, column=3, sticky=W)
                 
        buttons = [
            '~','`','!','@','#','$','%','^','&','*','(',')','-','_','Clear',
            'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P','0','7','8','9','Back',
            'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L','[',']','4','5','6','Tab',
            'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.','?','/','1','2','3','Space',
            ]

        def select(value):

            
            if value =="Clear":
                Entry.focus_get(self).delete(0, END)
            elif value == "Back":
                widget = Entry.focus_get(self)
                if widget:
                    widget.delete(widget.index("end")-1)
            elif value == "Space":
                Entry.focus_get(self).insert(END, ' ')
            elif value == "Tab":
                Entry.focus_get(self).insert(END, '    ')
            else :
                Entry.focus_get(self).insert(END, value)
                
        class Keyboard(Frame):
            def __init__(self, root):
                Frame.__init__(self, root)

                def HosoPop():

                        varRow = 2
                        varColumn = 0

                        for button in buttons:

                                command = lambda x=button: select(x)
                                
                                if button == "Clear" or button == "Space" or button == "Tab" or button == "Back":
                                        Button(self,text= button,width=4, height=2, bg="white", fg="black", relief=RAISED,
                                                activebackground = "#ffffff", activeforeground="#3c4987",
                                                command=command).grid(row=varRow,column=varColumn)

                                else:
                                        Button(self,text= button,width=3, height=2, bg="white", fg="black", relief=RAISED,
                                                activebackground = "#ffffff", activeforeground="#3c4987",
                                                command=command).grid(row=varRow,column=varColumn)


                                varColumn +=1 

                                if varColumn > 14 and varRow == 2:
                                        varColumn = 0
                                        varRow+=1
                                if varColumn > 14 and varRow == 3:
                                        varColumn = 0
                                        varRow+=1
                                if varColumn > 14 and varRow == 4:
                                        varColumn = 0
                                        varRow+=1
                HosoPop()
        
        keys  = Keyboard(self).grid(row=8, column=0, columnspan=4)
        
    def writeToFile(self):
        with open('Working_MethodFile.csv', 'a') as f:
            w=csv.writer(f, quoting=csv.QUOTE_ALL)
            w.writerow([self.mthtite.get(), self.sttempe.get(), self.wttme.get(), self.sktme.get(), self.endtempe.get(), self.htrte.get()])

        
class ManMeth(Frame):
    def __init__(self, master):
        Frame.__init__(self, master)
        #CameraON()
        title = Label(self, text="Manual Method").grid(row=0, column=1)
        clock  = Clock(self).grid(row=0, column=2, columnspan=4)
        b1 = Button(self, height=4, width=12, text='Home', command=lambda: master.switch_frame(MeltPoint)).grid(row=2, column=0)
        b2 = Button(self, height=4, width=12,  text='Method Settings', command=lambda: master.switch_frame(AddMethod)).grid(row=3, column=0)
        b3 = Button(self, height=4, width=12,  text='Start Analysis').grid(row=4, rowspan=2, column=0)
        b4 = Button(self, height=4, width=12,  text='Data Handling', command=lambda: master.switch_frame(Chart)).grid(row=6,column=0)
        b5 = Button(self, height=4, width=12,  text='Save Method').grid(row=7,column=0)    
        vidfrm = Frame(self, height=210, width=410, bd=1, relief=SUNKEN).grid(row=1, column=1, columnspan=2, rowspan=4)
        cambtnfrm = CameraButtonFrame(self).grid(row=1, column=3, rowspan=4)
        dato = DataOutput(self).grid(row=5, rowspan=3, column=2, columnspan=3)
        canvas = FigureCanvasTkAgg(f, self)
        canvas.draw_idle()
        canvas.get_tk_widget().grid(row=5, column=1, rowspan=3, sticky='')

if __name__ == "__main__":
    app = SampleApp()
    ani = animation.FuncAnimation(f, animate, interval=100)
    app.mainloop()
Should i use nanpy? Should i use pyFirmata? Should i ditch the arduino nano and buy relay and thermocouple shields directly compatible with the raspberry pi?

Thanks in advance i know its a long post and in depth project but i'm at a loss

scotty101
Posts: 3958
Joined: Fri Jun 08, 2012 6:03 pm

Re: Dynamic PID Temperature Controller Using Raspberry Pi 3 Model B and Arduino Nano

Tue May 28, 2019 9:20 am

You haven't shared any information about your Arduino code and how you communicate with it.

It is possible to connect to an Arduino via it's serial port using python and send/receive information from it. Look at the pySerial library.
The arduino is then coded to receive messages, process these and update the variables as appropriate.
For example you could send the start temperature setting via the serial port as

Code: Select all

st=25\r\n
The arduino would recognise the "st=" as a command to set the "st" variable and use the next few characters before the end of the line (\r\n) as the temperature.
You could also have it respond to commands like

Code: Select all

st?\r\n
which would be "what is your current start temperature?" and it would respond "25\r\n" or something like that.

firmata works well in you only want to read basic information from the Arduino or set GPIO pins. It's not designed to set variable values.
Electronic and Computer Engineer
Pi Interests: Home Automation, IOT, Python and Tkinter

JacobNeill
Posts: 3
Joined: Mon May 27, 2019 8:15 am

Re: Dynamic PID Temperature Controller Using Raspberry Pi 3 Model B and Arduino Nano

Wed May 29, 2019 3:37 pm

Thanks for the reply, i'll have a look over pySerial library. The current code i'm using on the arduino is as follows:

Code: Select all

#include "max6675.h"

int ktcSO = 8;
int ktcCS = 9;
int ktcCLK = 10;

MAX6675 ktc(ktcCLK, ktcCS, ktcSO);

  
void setup() {
  Serial.begin(9600);
  // give the MAX a little time to settle
  delay(500);
}

void loop() {
  // basic readout test
  
   Serial.print("Deg C = "); 
   Serial.println(ktc.readCelsius());

 
   delay(500);
}
I don't have access to the code at the moment but I run a separate python script to take the readout and save it to file with a timestamp. So essentially nanpy and firmata are useless in this case? and i'm assuming by giving advice on the arduino, you think its the 'correct' route to take in terms of design, i.e i shouldn't bother buying shields/chips that interface the raspberry pi?

EDIT: After watching a few videos and doing a little bit of research, I have quickly realised the power of pySerial in this application! thanks for the pointer i think i'll be using this to implement the communication between the RaspPi and Arduino, it means my code can be condensed to one file for the pi and one for the nano. i'll post a reply when i get some code running!

scotty101
Posts: 3958
Joined: Fri Jun 08, 2012 6:03 pm

Re: Dynamic PID Temperature Controller Using Raspberry Pi 3 Model B and Arduino Nano

Wed May 29, 2019 4:15 pm

JacobNeill wrote:
Wed May 29, 2019 3:37 pm
and i'm assuming by giving advice on the arduino, you think its the 'correct' route to take in terms of design, i.e i shouldn't bother buying shields/chips that interface the raspberry pi?
From looking at the code you have, it seems that you are only really using the Arduino as a way to connect to the MAX6675. You should be able to connect the MAX6675 directly to the Pi.

Using both the Arduino and Pi could be a design choice depending on which device you want various functions to run.
For example you might chose to have the Pi just as a user interface for the PID controller and some simple data logging but the Arduino deals read the temperature, calculates the PID output and sets the heater output.
OR
You could have the Pi doing all of these things.

Advantage of the Arduino over the Pi is that it will resume it's function in a much shorter time after a power reset than the Pi will. If this function is at all safety critical, you might want to use both.
Electronic and Computer Engineer
Pi Interests: Home Automation, IOT, Python and Tkinter

Return to “Troubleshooting”