# # Python Interface for Oxymeter WTW # Tested with intruments of the series # Multi 34XX # Written by Jonathan Flye-Sainte-Marie # July 2013 # from Tkinter import * import serial, threading, time, math, os def ReadOxy(): global Arret, pO2, O2_id, Temp, UnitOxy, SerialPort, OxyNewVal, TimeOxyNewVal # ouverture du port serie Oxym=serial.Serial(SerialPort, "4800", timeout = 2) new_probe= False while Arret == False : line = [] line = Oxym.readline() # Check for sensor id # do something if it is an oxygen probe if line[0:6] == 'SC-FDO': # Split the line and extract the sensor id SplitedLine = line.split(' ') SplitedLine = filter(None, SplitedLine) O2_id = SplitedLine[2] # Remove the last end of line character by taking # only the 8 first characters O2_id = O2_id[0:8] new_probe = True if line[0:2] == 'Ox' and new_probe : SplitedLine = line.split(' ') SplitedLine = filter(None, SplitedLine) pO2 = SplitedLine[1] UnitOxy = SplitedLine[2] Temp = SplitedLine[3] ToPrint = 'Id '+O2_id+' O2: '+pO2+' Temp: '+Temp pO2 = float(pO2) OxyNewVal = True TimeOxyNewVal = time.time() Temp = float(Temp) print ToPrint new_probe = False def AffichDials(): global Arret Canevas.delete(ALL) Canevas.create_line(0,30, 480,30) Canevas.create_line(125,0, 125,60) Canevas.create_line(250,0, 250,60) Canevas.create_text(62, 20, text='Id sonde :' ) Canevas.create_text(62, 45, text=O2_id) Canevas.create_text(185, 20, text='Conc. O2 :') Canevas.create_text(185, 45, text=str(pO2) + ' ' + UnitOxy, font = ( 'Arial 14 normal bold')) Canevas.create_text(320, 20, text='Temp :' ) Canevas.create_text(320, 45, text=str(Temp)+ u'\u00B0' +'C', font = ( 'Arial 14 normal bold')) if Arret == False: Mafenetre.after(1000,AffichDials) def DrawAxis(): global LargCan2, HautCan2, XAxisOffset, YAxisOffset Can2.create_line(XAxisOffset - 5, HautCan2 - YAxisOffset , LargCan2 - XAxisOffset +5, HautCan2 - YAxisOffset) Can2.create_line(XAxisOffset, HautCan2 - YAxisOffset , XAxisOffset, 0, arrow=LAST) Can2.create_line(LargCan2 - XAxisOffset , HautCan2 - YAxisOffset , LargCan2 - XAxisOffset, 0, arrow=LAST) Can2.create_text(YAxisOffset/4, YAxisOffset/2, text = "Oxy", anchor =NW, fill = 'blue') Can2.create_text(LargCan2 - YAxisOffset*1.75, YAxisOffset/2, text = "Temp.", anchor =NW, fill='red') Can2.create_text(LargCan2/2, HautCan2 - YAxisOffset/2, text = "Temps", anchor = CENTER) def SaveData(): global pO2, O2_id, Temp, Arret, OxyNewVal # on ne sauve que si on a une nouvelle valeur if OxyNewVal == True : toWrite = time.strftime('%x %X')+';'+str(pO2)+';'+str(Temp)+';' +str(O2_id)+ '\n' OutFile = open(SaveFileName,"a") OutFile.write(toWrite) OutFile.close() OxyNewVal = False if Arret == False: Mafenetre.after(1000*int(SaveTimeStep),SaveData) def MapInCanavas(Value, haut, YAxisOffset, Min, Max): MappedValue = (haut-YAxisOffset)-(((float(Value) - Min)/(Max-Min))*(haut-YAxisOffset)) return MappedValue def DrawCurve(): global haut, YAxisOffset, YAxisOffset, MinOxy, MaxOxy, HautCan2, MinTemp, MaxTemp global LargCan2, OxyCurve, TempCurve, TempScale1, TempTic, TempScale2 global OxyCurve, TempCurve, OxyScale1, OxyLineCenter, OxyTic, OxyScale2 global NbValGrap DataOxyCurve=[] DataTempCurve=[] # On compte le nombre le lignes du fichier SaveFile = open(SaveFileName,"r") nbLines = 0 for line in SaveFile: nbLines = nbLines + 1 SaveFile.close() # On ne prende que les NbValGraph dernieres valeurs du fichier NumLineDebut = nbLines - (NbValGraph) SaveFile = open(SaveFileName,"r") i=0 x = float(XAxisOffset) for SaveLine in SaveFile : # On ne commence que quand on arrive a la ligne NumLineDebut if i >= NumLineDebut: SaveLineSplit = SaveLine.split(';') # On teste si la ligne est complete if len(SaveLineSplit) ==4 and i>0 : # On traite l'oxygene Oxy = SaveLineSplit[1] Oxy = float(Oxy) if math.isnan(Oxy): Oxy =0 Oxy = MapInCanavas(Oxy, HautCan2, YAxisOffset, MinOxy, MaxOxy) # On traite la temperature Temp = SaveLineSplit[2] Temp = float(Temp) if math.isnan(Temp): Temp =0 Temp = MapInCanavas(Temp, HautCan2, YAxisOffset, MinTemp, MaxTemp) # Stockage des valeurs DataOxyCurve.append((int(x),int(Oxy))) DataTempCurve.append((int(x),int(Temp))) x = float(x) + (float(LargCan2) - float(XAxisOffset*2)) /float(NbValGraph) i +=1 # On efface le try: OxyCurve except NameError : pass else: Can2.delete(OxyCurve) Can2.delete(OxyScale1) Can2.delete(OxyLineCenter) Can2.delete(OxyTic) Can2.delete(OxyScale2) try: TempCurve except NameError : pass else: Can2.delete(TempCurve) Can2.delete(TempScale1) Can2.delete(TempTic) Can2.delete(TempScale2) # On en trace une nouvelle OxyCurve = Can2.create_line(DataOxyCurve, fill='blue', smooth=1) OxyScale1 =Can2.create_text(XAxisOffset-8 ,HautCan2-YAxisOffset, font= ((),8), text = str(MinOxy), anchor =E) OxyLineCenter = Can2.create_line(((XAxisOffset,(HautCan2-YAxisOffset)/2), (LargCan2-XAxisOffset,(HautCan2-YAxisOffset)/2)), fill='grey') OxyTic = Can2.create_line(((XAxisOffset-5,(HautCan2-YAxisOffset)/2), (XAxisOffset,(HautCan2-YAxisOffset)/2)), fill='black') OxyScale2 = Can2.create_text(XAxisOffset-8 ,(HautCan2-YAxisOffset)/2, font= ((),8), text = str(((MaxOxy-MinOxy)/2)+MinOxy), anchor =E) TempCurve = Can2.create_line(DataTempCurve, fill='red', smooth=1) TempScale1 = Can2.create_text(LargCan2 -XAxisOffset + 8 ,HautCan2-YAxisOffset, font= ((),8), text = str(int(MinTemp)), anchor =W) TempTic = Can2.create_line(((LargCan2-XAxisOffset ,(HautCan2-YAxisOffset)/2), (LargCan2-XAxisOffset + 5 ,(HautCan2-YAxisOffset)/2)), fill='black') TempScale2 = Can2.create_text(LargCan2-XAxisOffset + 8 ,(HautCan2-YAxisOffset)/2, font= ((),8), text = str(int(((MaxTemp-MinTemp)/2)+MinTemp)), anchor =W) def UpdateCurve(): if os.path.isfile(SaveFileName) == True : # On compte le nombre le lignes du fichier SaveFile = open(SaveFileName,"r") nbLines = 0 for line in SaveFile: nbLines = nbLines + 1 SaveFile.close() # On lance l'affichage de la courbe s'il y a plus de deux valeurs # dans le fichier if nbLines > 2 : DrawCurve() if Arret == False: Mafenetre.after(1000,UpdateCurve) def SaveWin(): global SaveTimeStep, SaveFileName SaveFen = Toplevel() SaveFen.title('Configuration de la sauvegrade') LabSaveFileName= Label(SaveFen,text='Nom du fichier de sauvegarde',width=30, anchor = W) LabSaveFileName.grid(column=1, row=0) EntSaveFileName=Entry(SaveFen, width=25) EntSaveFileName.insert(0, SaveFileName) EntSaveFileName.grid(column=2, row=0) LabSaveTimeStep= Label(SaveFen,text='Pas de temps enregistrement (sec.)',width=30, anchor = W) LabSaveTimeStep.grid(column=1, row=2) EntSaveTimeStep=Entry(SaveFen, width=25) EntSaveTimeStep.insert(0, str(SaveTimeStep)) EntSaveTimeStep.grid(column=2, row=2) def ReadSaveSettings(): global SaveFileName, SaveTimeStep SaveFileName = EntSaveFileName.get() SaveTimeStep = EntSaveTimeStep.get() SaveTimeStep = int(SaveTimeStep) SaveFen.destroy() def AnnulReadSaveSettings(): SaveFen.destroy() # Bouton de lecture BoutonValid = Button(SaveFen, text ='Valider', command = ReadSaveSettings) BoutonValid.grid(column=2, row=3, sticky=W) # Bouton annuler BoutonAnnul = Button(SaveFen, text ='Annuler', command = AnnulReadSaveSettings) BoutonAnnul.grid(column=1, row=3, sticky=E) def SelectSerialPort(): global SerialPort # Fonction por selectionner le port serie SerialFen = Toplevel() SerialFen.title('Configuration du port s'+u'\u00E9'+'rie:') LabSerialPort= Label(SerialFen,text='Adresse du port s'+u'\u00E9'+'rie:',width=25, anchor = W) LabSerialPort.grid(column=1, row=0) LabNotif = Label(SerialFen,text='',width=25, anchor = W) LabNotif.grid(column=1, columnspan=2, row=1 ) EntSerialPort=Entry(SerialFen, width=25) EntSerialPort.insert(0, SerialPort) EntSerialPort.grid(column=2, row=0) def ReadSerialPort(): global SerialPort, Arret Arret = True SerialPort = EntSerialPort.get() if os.path.exists(SerialPort) == False : LabNotif = Label(SerialFen,text='Attention ! Port s'+u'\u00E9'+'rie invalide',width=25,fg="red") LabNotif.grid(column=1, columnspan=2, row=1) else : SerialFen.destroy() def AnnulReadSerial(): SerialFen.destroy() # Bouton de lecture BoutonValid = Button(SerialFen, text ='Valider', command = ReadSerialPort) BoutonValid.grid(column=2, row=2, sticky=W) # Bouton annuler BoutonAnnul = Button(SerialFen, text ='Annuler', command = AnnulReadSerial) BoutonAnnul.grid(column=1, row=2, sticky=E) def ScalesWin(): # Boite de dialogue pour la configuration des axes ScalesFen = Toplevel() ScalesFen.title('Configuration des axes') # Nombre de valeurs a tracer LabNbValGraph= Label(ScalesFen,text='Nombre de valeurs '+u'\u00E0'+' tracer :',width=25, anchor = W) LabNbValGraph.grid(column=1, row=0) EntNbValGraph=Entry(ScalesFen, width=10) EntNbValGraph.insert(0, str(NbValGraph)) EntNbValGraph.grid(column=2, row=0) # Echelle de l'oxy LabMaxOxy= Label(ScalesFen,text='Valeur max. oxyg'+u'\u00E8'+'ne :',width=25, anchor = W) LabMinOxy= Label(ScalesFen,text='Valeur min. oxyg'+u'\u00E8'+'ne :',width=25, anchor = W) LabMaxOxy.grid(column=1, row=3) LabMinOxy.grid(column=1, row=4) EntMaxOxy =Entry(ScalesFen, width=10) EntMinOxy =Entry(ScalesFen, width=10) EntMaxOxy.insert(0, str(MaxOxy)) EntMinOxy.insert(0, str(MinOxy)) EntMaxOxy.grid(column=2, row=3) EntMinOxy.grid(column=2, row=4) #Echelle de la temperature LabMaxTemp= Label(ScalesFen,text='Valeur max. temp'+u'\u00E9'+'rature :',width=25, anchor = W) LabMinTemp= Label(ScalesFen,text='Valeur min. temp'+u'\u00E9'+'rature :',width=25, anchor = W) LabMaxTemp.grid(column=1, row=5) LabMinTemp.grid(column=1, row=6) EntMaxTemp =Entry(ScalesFen, width=10) EntMinTemp =Entry(ScalesFen, width=10) EntMaxTemp.insert(0, str(MaxTemp)) EntMinTemp.insert(0, str(MinTemp)) EntMaxTemp.grid(column=2, row=5) EntMinTemp.grid(column=2, row=6) def ReadScales(): global NbValGraph, NbValGraph, MinFluo, MaxFluo, MinOxy, MinDebit global MaxOxy, MinTemp, MaxTemp, MaxDebit NbValGraph = EntNbValGraph.get() NbValGraph = int(NbValGraph) MaxOxy = EntMaxOxy.get() MinOxy = EntMinOxy.get() MaxOxy = float(MaxOxy) MinOxy = float(MinOxy) MaxTemp = EntMaxTemp.get() MinTemp = EntMinTemp.get() MaxTemp = float(MaxTemp) MinTemp = float(MinTemp) ScalesFen.destroy() def AnnulReadScales(): ScalesFen.destroy() # Bouton de lecture BoutonValid = Button(ScalesFen, text ='Valider', command = ReadScales) BoutonValid.grid(column=2, row=7, sticky=W) # Bouton annuler BoutonAnnul = Button(ScalesFen, text ='Annuler', command = AnnulReadScales) BoutonAnnul.grid(column=1, row=7, sticky=E) def WatchDog(): global WarnTime, pO2, Temp, O2_id, WarnTimeSerial # Fonction de verification de l'arrivee de nouvelles # valeurs try: WarnTime except NameError : # Afficher un warning seulement si il n'y en a pas if time.time() > TimeOxyNewVal + TimeSecu : # Afficher le warning si temps critique depasse WarnTime = Can2.create_text(LargCan2/2, HautCan2/2, text = 'Pas de nouvelles valeurs sur le port s'+u'\u00E9'+'rie depuis plus de ' +str(TimeSecu)+' secondes', fill='red') pO2 = float('NaN') Temp = float('NaN') O2_id = '----' WarnTimeSerial = True else : # On ne depasse pas le temps critique pass else: # S'il le warning existe pas if time.time() > TimeOxyNewVal + TimeSecu : # et que le temps critique est depasse # on passe notre tour pass else : #sinon on efface la label Can2.delete(WarnTime) del WarnTime WarnTimeSerial = False if Arret == False: Mafenetre.after(1000,WatchDog) # Petites fonctions accociees aux boutons : def Arreter(): global Arret Arret = True def ArreterTout(): Arreter() Mafenetre.destroy() def DemarrerReadOxy(): global Arret, TimeOxyNewVal # On ne demmarre que si l'on est arrete if Arret == True: Canevas.delete(ALL) Can2.delete(ALL) Arret = False TimeOxyNewVal = time.time() TreadReadOxy = threading.Thread(None, ReadOxy, None, (), {}) TreadReadOxy.start() WatchDog() AffichDials() DrawAxis() SaveData() UpdateCurve() #La fonction DrawCurve() n'est activee qu'une fois que la premiere valeur est #sauvegardee ### Valeurs par default : NbValGraph=200 pO2 = 0 O2_id=0 Temp =0 MinOxy= 0 MaxOxy=12 MinTemp=10 MaxTemp=30 SerialPort = "/dev/ttyUSB0" TimeSecu = 5 SaveFileName = 'OutFile.txt' SaveTimeStep = 1 ### Initialisations Arret = True OxyNewVal = False UnitOxy='--' ### Creation de la fenetre principale (main window) Mafenetre = Tk() Mafenetre.title('GUI pour Oxym'+u'\u00E8'+'tre WTW') ### Creation des canevas # Creation d'un widget Canvas Largeur = 375 Hauteur = 60 Canevas = Canvas(Mafenetre, width = Largeur, height =Hauteur, bg='#FFFFCC') Canevas.pack(padx =5, pady =5) # Creation d'un widget Canvas LargCan2 = 500 HautCan2 = 200 XAxisOffset =40 YAxisOffset =20 Can2 = Canvas(Mafenetre, width = LargCan2, height =HautCan2, bg ='white') Can2.pack(padx =5, pady =5) ### Creation de boutons # Creation d'un widget Button (bouton Demarrer lecture) BoutonGo = Button(Mafenetre, text ='D'+u'\u00E9'+'marrer lecture', command = DemarrerReadOxy) BoutonGo.pack(side = LEFT, padx = 10, pady = 10) # Creation d'un widget Button (bouton Arreter) BoutonArreter = Button(Mafenetre, text ='Arr'+u'\u00EA'+'ter lecture', command = Arreter) BoutonArreter.pack(side = RIGHT, padx = 5, pady = 5) ### Creation d'une barre de Menu MenuBar = Menu(Mafenetre) # menu "fichier" MenuFile = Menu(MenuBar, tearoff=0) MenuFile.add_command(label="Quitter", command=ArreterTout) MenuBar.add_cascade(label="Fichier", menu=MenuFile) # menu "config" MenuConfig = Menu(MenuBar, tearoff=0) MenuConfig.add_command(label='Port s'+u'\u00E9'+'rie', command=SelectSerialPort) MenuConfig.add_command(label='Sauvegarde', command=SaveWin) MenuConfig.add_command(label="Echelle des axes", command=ScalesWin) MenuBar.add_cascade(label="Configuration", menu=MenuConfig) Mafenetre.config(menu=MenuBar) Mafenetre.mainloop()