@@ -4,20 +4,51 @@ Ce logiciel est distribué sous license MIT. | |||
Poeles-Dragon-Calculator (PoDoCor) est un logiciel d'expérimentation développé pour les besoins du Low-Tech Bordeaux. | |||
Pour l'instant il n'existe pas d'interface graphique, de plus il n'existe qu'en français. Cela sera développé plus tard. | |||
Pour l'instant il n'existe pas d'interface graphique et s'utilise donc avec un terminal, de plus il n'existe qu'en français. Le GUI et les autres langues seront développées plus tard. | |||
Ce logiciel est développé pour Linux avec python3 et Arduino. Les autres plateformes viendront plus tard. | |||
## Utilisation et fonctionnement | |||
Au démarrage, le logiciel crée un dossier "data" à la racine du dossier de PoDoCor, où se trouve le script principal; dans ce dossier se trouveront toutes les données au format CSV. | |||
Pour lancer le programme, ouvrez un terminal dans le dossier "PoDoCor", dans le répertoire où vous l'avez téléchargé (et dézippé si besoin), ou utilisez la commande "cd /votre/dossier/dextraction/PoDoCor". | |||
Avant de pouvoir utiliser le programme, lancez-le en tapant "python3 main.py" dans le terminal. | |||
Si aucun Arduino n'est relié au PC, le programme s'arrêtera immédiatement. | |||
Si aucun fichier n'est déjà créé, ou s'il ne contient aucune déclaration de capteur, le logiciel en demandera la création au démarrage. | |||
### Premier démarrage | |||
Au premier démarrage, le logiciel crée plusieurs dossiers et fichiers dans votre dossier PoDoCor: | |||
- un dossier "data", où vous pourrez retrouver tous les fichiers .csv horodatés créés par le logiciel lors de l'enregistrement des données; | |||
- un fichier "parameters.txt", où se trouve pour l'instant la seule ligne du chemin Serial où se trouve l'Arduino. Par défaut, après démarrage, ce chemin est "/dev/ttyUSB0", mais il peut être changé (notamment grâce au logiciel Arduino, le chemin est indiqué tout en bas à droite) en remplaçant cette valeur par une autre; | |||
- un fichier "captors.txt", où sont déclarés les capteurs (nom, unité de mesure, etc.). | |||
### Menus | |||
Si aucun fichier "captors.txt" n'est déjà créé, ou s'il ne contient aucune déclaration de capteur, le logiciel en demandera la création au démarrage. | |||
Le menu principal propose de commencer l'enregistrement des données ou de gérer les capteurs; le sous-menu permet d'afficher les capteurs enregistrés, d'en ajouter ou supprimer, et de changer l'ordre des capteurs pour correspondre à l'ordre des données envoyées par l'Arduino | |||
Pour que le logiciel fonctionne, il est nécessaire de déclarer les capteurs utilisés: leur nom, l'unité dans laquelle il mesure, et une variable (0 ou 1) pour savoir si l'Arduino envoie des données brutes de ce capteur (i.e. une simple conversion analogique-numérique, sans mapping des données acquises), ou si ce sont les données normalisées que renvoie l'Arduino (une température par exemple). Lors de cette création de capteurs, ne JAMAIS utiliser de virgule (","), sans quoi le logiciel s'arrêterait pour ses futures initialisations. | |||
### Arduino et synchronisation | |||
Ce programme ne permet ni de programmer un Arduino, ni d'écrire le code à votre place. Vous devrez écrire le code Arduino vous-même (ou vous faire aider/vous aider de tutos, nombreux sur Internet). Un exemple (complet et détaillé) sera posté prochainement avec le code de PoDoCor. | |||
Le principe est le suivant: l'ordinateur effectue seul la synchronisation de l'envoi des données. Pour celà, il envoie 2 signaux: le signal "1" pour signifier à Arduino que ce dernierpeut récolter les données des capteurs, et les empaqueter en une ligne de données à envoyer. Après quelques millisecondes, PoDoCor envoie le signal "2" pour signifier qu'il souhaite récolter les données. Arduino envoie alors les données à PoDoCor, qui les traite. Ainsi, PoDoCor n'envoie que des données de synchronisation à Arduino, et Arduino n'envoie que des données issues des capteurs. | |||
Ce processus se répète toutes les secondes, selon l'horloge de l'ordinateur utilisé. Il est néanmoins possible d'avoir un très léger retard d'envoi du bit de synchronisation "1" (quelques millisecondes) lors de l'affichage des données; mais ce retard n'est jamais cumulé. L'ordre de l'interrogation des capteurs étant toujours le même, et la synchronisation "1" variant normalement (selon un gaussienne de 3*sigma = 1ms environ), les données, prises selon un échantillon suffisamment grand (plus de 1000 points de mesure), sont donc théoriquement prises à un intervalle régulier de précisément 1 seconde. | |||
### Capteurs | |||
Concernant les données simplement converties en numérique (si l'Arduino extraie la valeur 342 à la suite d'une conversion analogique-numérique par exemple), ce sera cette valeur qui sera transmise. Le CSV contient donc à la fois des données brutes et normalisées; les données brutes nécessiteront donc un traitement, en particulier si les capteurs utilisés nécessitent l'utilisation d'une table de conversion ou d'un étalonnage, ces opérations devront être effectuées avant de lancer PoDoCor. Il est prévu néanmoins, dans une future version, d'inclure la phase d'initialisation dans le processus. | |||
Pour que le logiciel fonctionne, il est nécessaire de déclarer les capteurs utilisés: leur nom, l'unité dans laquelle il mesure, et une variable (0 ou 1) pour savoir si l'Arduino envoie des données brutes de ce capteur (i.e. une simple conversion analogique-numérique, sans mapping des données acquises), ou si ce sont les données normalisées ou converties dans l'unité de destination que renvoie l'Arduino (une température par exemple). Lors de cette création de capteurs, ne JAMAIS utiliser de virgule (","), sans quoi le logiciel s'arrêterait pour ses futures initialisations. | |||
Concernant les données simplement converties en numérique (si l'Arduino extraie la valeur 342 à la suite d'une conversion analogique-numérique par exemple), ce sera cette valeur qui sera transmise. Le CSV contient donc à la fois des données brutes et normalisées; les données brutes nécessiteront donc un traitement, en particulier si les capteurs utilisés nécessitent l'utilisation d'une table de conversion ou d'un étalonnage, ces opérations concernant uniquement l'Arduino devront donc être effectuées avant de lancer PoDoCor. Il est prévu néanmoins, dans une future version, d'inclure la phase d'initialisation dans le processus. | |||
Il est PRIMORDIAL de respecter l'ordre des données envoyées par l'Arduino dans PoDoCor: pour cela, une fois vos capteurs déclarés, vous pouvez changer leur ordre pour qu'ils correspondent exactement à l'ordre des données envoyées par Arduino. | |||
### Enregistrement | |||
Une fois le logiciel démarré, le démarrage du processus d'enregistrement des données démarre en utilisant Shift+P; de même, ce processus s'arrête en utilisant Shift+P. | |||
Une fenêtre s'afiche pour afficher les données transmises par l'Arduino en format graphique. Cette fenêtre ne permet pas, pour l'instant, de visualiser la valeur réelle des données (notamment pour les données raw), elle affiche simplement les données (raw ou réelles) reçues par PoDoCor. | |||
### TODO | |||
Il est prévu, dans une future version, d'ajouter des calculs simples sur certains capteurs, pour convertir les données raw en données réelles. |
@@ -125,7 +125,7 @@ class Arduino_data(): | |||
coma_places.append(len(raw_data_txt)) | |||
for i in range(min(len(coma_places)-1, self.nb_captor_signals)): | |||
self.raw_data_temp[0,i] = int(raw_data_txt[coma_places[i]+1:coma_places[i+1]]) | |||
np.append(self.data_array, self.raw_data_temp, axis=1) | |||
self.data_array = np.append(self.data_array, self.raw_data_temp, axis=0) | |||
def create_data_file(self): | |||
self.data_file_path = self.data_path_dir + "/" + time.asctime()+".csv" | |||
@@ -0,0 +1,92 @@ | |||
import matplotlib.pyplot as plt | |||
import numpy as np | |||
from multiprocessing import Process, Queue | |||
class Figures(): | |||
def __init__(self, nb_signals): | |||
self.nb_curves = nb_signals #nombre de courbes | |||
self.plots_grid = (1,1) # (y,x) | |||
self.total_subplots = self.plots_grid[0]*self.plots_grid[1] # nombre de fenetres de plot | |||
self.repartition = [] # repartition de chaque courbe dans les fenetres | |||
self.plots_grids(nb_signals) # determination de la grille de fenetres à l'init | |||
self.fig = None # objet Figure() | |||
self.axes = None # liste d'objets subplots(), suivant la grille plots_grid definie | |||
self.time = None # axe x des figures (pas dans data_array car reextractible dans un tableur) | |||
def plots_grids(self, nb_plots): | |||
""" Determine la grille (x,y) de fenêtres de plots dans la figure affichée. | |||
Dans tous les cas, pour voir quelque chose des mesures, le nombre de fenêtres | |||
de plots est < 6; on pourra afficher 2 graphiques dns une même fenêtre | |||
lorsqu'il y a plus de courbes à afficher que de fenêtres dans la grille, | |||
dans l'ordre de parcours " de gauche à droite, puis de haut en bas" .""" | |||
if(nb_plots == 1): | |||
self.plots_grid = (1,1) | |||
elif(nb_plots == 2): | |||
self.plots_grid = (1,2) | |||
elif(nb_plots == 3): | |||
self.plots_grid = (1,3) | |||
elif(nb_plots == 4 or nb_plots == 5): | |||
self.plots_grid = (2,2) | |||
elif(nb_plots == 6): | |||
self.plots_grid = (2,3) | |||
else: | |||
if(type(nb_plots/2) == type(int())): | |||
self.plots_grids(nb_plots/2) | |||
elif(nb_plots >= 9): | |||
self.plots_grids(6) | |||
else: | |||
self.plots_grids(nb_plots-1) | |||
self.total_subplots = self.plots_grid[0]*self.plots_grid[1] | |||
# definition de la repartition sous forme d'un nombre à 3 chiffres yXnb | |||
if(self.plots_grid == (1,1)): | |||
self.repartition = [111] | |||
elif(self.plots_grid == (1,2)): | |||
self.repartition = [111, 122] | |||
elif(self.plots_grid == (1,3)): | |||
self.repartition = [111, 122, 133] | |||
elif(self.plots_grid == (2,2)): | |||
self.repartition = [111, 122, 213, 224] | |||
elif(self.plots_grid == (2,3)): | |||
self.repartition = [111, 122, 133, 214, 225, 236] | |||
if(nb_plots > 6): | |||
for i in range(6,nb_plots): | |||
# parcours des cases de la figure de droite à gauche puis de haut en bas, suivant la grille | |||
self.repartition.append(self.repartition[i%self.nb_curves]) | |||
def create_figure(self): | |||
"""Creation de la fenêtre contenant les plots. | |||
Mode interactif enclenché pour réactualiser à chaque fois que plot_data est | |||
appelée. Utilisation de plt.pause pour laisser la fenêtre s'afficher et | |||
actualiser les données.""" | |||
plt.ion() | |||
self.fig, self.axes = plt.subplots(self.plots_grid[0], self.plots_grid[1]) | |||
if(self.plots_grid[0] < 2): # si 1 seule ligne, empaqueter dans une liste pour | |||
# y acceder de la meme facon dans tous les cas | |||
self.axes = [self.axes] | |||
plt.show() | |||
plt.pause(0.05) | |||
def plot_data(self, data_array): | |||
"""Appel pour réactualistion des données. Utilisation de plt.pause pour laisser | |||
afficher les données.""" | |||
data_array_t = np.transpose(data_array) | |||
self.time = np.linspace(0,data_array_t.shape[1]-1,data_array_t.shape[1]) | |||
for i in self.repartition: | |||
self.axes[i//100-1][(i//10)%10-1].clear() | |||
for i in range(self.nb_curves): | |||
self.axes[self.repartition[i]//100-1][(self.repartition[i]//10)%10-1].scatter(self.time, data_array_t[i]) | |||
plt.show() | |||
plt.pause(0.05) | |||
def reinit(self): | |||
"""Remise à zéro (défaut) de l'objet Figures(), permet aussi de fermer la figure | |||
et d'en réouvrir une nouvelle lors de la réutilisation de l'objet.""" | |||
plt.close(self.fig) | |||
self.plots_grid = (1,1) | |||
self.total_subplots = self.plots_grid[0]*self.plots_grid[1] | |||
self.repartition = [] | |||
self.fig = None | |||
self.axes = None |
@@ -1,23 +1,22 @@ | |||
import os | |||
from multiprocessing import Process, Queue | |||
if(os.name == "posix"): | |||
import getch as gt #clavier | |||
import getch as gt #clavier linux | |||
else: | |||
import msvcrt as gt | |||
import msvcrt as gt #clavier linux; à tester! | |||
def press_key_timeout(caracters, numbers, actual, timeout = 0.1): | |||
"""Utilisation de press_key en insérant un timeout grâce à un thread exécuté | |||
en parallèle de l'exécution principale.""" | |||
queue = Queue() | |||
time_thread = Process(target = press_key, args=(caracters, numbers, actual, queue)) | |||
time_thread.start() | |||
time_thread.start() # demarre le thread | |||
time_thread.join(timeout=timeout) | |||
if(time_thread.is_alive()): | |||
time_thread.terminate() | |||
time_thread.terminate() #termine le thread pour pouvoir le relancer (thread exclusif) | |||
return actual | |||
else: | |||
return queue.get() | |||
#main_thread.join(timeout=timeout) | |||
#actual = queue.get() | |||
#main_thread.terminate() | |||
return queue.get() # pas besoin de terminer le thread, il s'est termine en quittant press_key | |||
def press_key(caracters, numbers, actual, queue = None): | |||
""" Cette fonction sert à détecter si un caractère est pressé, pour passer d'un | |||
@@ -26,13 +25,31 @@ def press_key(caracters, numbers, actual, queue = None): | |||
pressed_key = gt.getch() #detection de touche pressee (blocant) | |||
place = caracters.find(pressed_key) | |||
if(queue != None): | |||
if(queue != None): #si dans un thread (press_key_timeout) | |||
if(place == -1): | |||
queue.put(actual) | |||
else: | |||
queue.put(numbers[place]) | |||
else: | |||
else: #si pas dans un thread | |||
if(place == -1): | |||
return actual | |||
else: | |||
return numbers[place] | |||
def read_line_file(path): | |||
""" Lecture des paramètres du logiciel et de l'Arduino, mis à part les capteurs | |||
définis dans un autre fichier. Pour l'instant 1 seul paramètre, donc une fonction | |||
très simple, mais peut se complexifier si besoin en 'serial_path = dev/ttyUSB0' | |||
par exemple.""" | |||
try: | |||
file = open(path, "r") #lecture du fichier si existant | |||
except: | |||
file = open(path,"w") #creation du fichier si pas existant | |||
file.write("/dev/ttyUSB0") #valeur par defaut | |||
file.close() | |||
file = open(path, "r") #reouverture fichier pour lecture | |||
finally: #execute dans tous les cas | |||
line = file.readline() #lis la 1e ligne | |||
file.close() #fermer avant de renvoyer la valeur | |||
return line |
@@ -1,16 +1,17 @@ | |||
import serial | |||
import numpy as np | |||
import matplotlib.pyplot as plt | |||
import os | |||
import time | |||
import bin.functions as fc | |||
import bin.data as data | |||
import bin.figures as fig | |||
#Variables principales | |||
serial_path = '/dev/ttyUSB0' | |||
parameters_path = os.path.abspath(os.path.dirname(__file__)) + "/parameters.txt" | |||
data_nb = 10 | |||
captors_path = os.path.abspath(os.path.dirname(__file__)) + "/captors.txt" | |||
serial_path = fc.read_line_file(parameters_path) | |||
#Variables cachees | |||
@@ -29,8 +30,9 @@ flag_state = 1 # etat neutre | |||
try: | |||
arduino_ser = serial.Serial(serial_path, 9600) #communication avec Arduino | |||
except serial.serialutil.SerialException: | |||
print("SerialException : la carte Arduino n'est pas connectee ou est connectee sur au autre port.") | |||
print("SerialException : la carte Arduino n'est pas connectee ou est connectee sur au autre port (actuel : "+ serial_path+").") | |||
flag_state = -1 | |||
exit() | |||
else: | |||
arduino_data = data.Arduino_data() | |||
arduino_data.add_captors_from_file(captors_path) | |||
@@ -40,6 +42,7 @@ while(flag_state > 0): | |||
if(flag_state == 1): #afficher le menu | |||
print("_________________________________\n\nMAJ + P : Enregistrement des donnees\nMAJ + N : Gerer les capteurs\nMAJ + X : Arrêt du programme (ne fonctionne pas pendant l'enregistrement)\n_________________________________\n") | |||
flag_state = 4 | |||
time.sleep(2) # attente de la bonne communication avec Arduino | |||
elif(flag_state == 4): # appui clavier pour les choix du menu principal | |||
flag_state = fc.press_key('XPN', [0, 2, 3], flag_state) #detection de touche pressee (demarrage procedure acquisition) | |||
@@ -62,6 +65,8 @@ while(flag_state > 0): | |||
flag_state = 3 | |||
elif(flag_state == 2): # enregistrement des données | |||
figures = fig.Figures(arduino_data.nb_captor_signals) | |||
figures.create_figure() | |||
data_buff_nb = 0 | |||
arduino_data.create_data_file() | |||
time_0 = time.time()-1 | |||
@@ -73,7 +78,7 @@ while(flag_state > 0): | |||
arduino_ser.write(bytes('2','utf-8')) #envoi de '2' (réception donnees) | |||
time.sleep(0.05) | |||
raw_data_txt = arduino_ser.readline() #format des donnees: data1,data2,data3\n | |||
print(str(raw_data_txt)[2:-5]) | |||
#print(str(raw_data_txt)[2:-5]) | |||
if(raw_data_txt == ''): # si aucune donnée reçue | |||
print("Arduino n'envoie aucune donnee, veuillez verifier le code televerse.") | |||
flag_state = -1 | |||
@@ -84,11 +89,14 @@ while(flag_state > 0): | |||
#procedure extraction donnees pour affichage | |||
arduino_data.extract_data_to_array(str(raw_data_txt)[2:-5]) | |||
# procedure d'ecriture fichier | |||
if(data_buff_nb == data_buff_max): | |||
# procedure d'ecriture fichier | |||
arduino_data.append_data_to_file(data_buff) | |||
data_buff = "" #remise a zero | |||
data_buff_nb = 0 | |||
# plot | |||
figures.plot_data(arduino_data.data_array) | |||
flag_state = fc.press_key_timeout('P', [1], flag_state) | |||
time.sleep(max(time_0+1-time.time(), 0)) | |||
@@ -98,6 +106,7 @@ while(flag_state > 0): | |||
arduino_data.append_data_to_file(data_buff) | |||
data_buff = "" | |||
data_buff_nb = 0 | |||
figures.reinit() | |||