#!/usr/bin/python3 # Bertrand Vandeportaele 2019 # https://pymotw.com/2/socket/udp.html import socket import sys import fcntl, errno, os #pour threads from threading import Thread, Lock #à cause de la GIL, ce n'est pas du vrai multithread, lire https://stackoverflow.com/questions/3310049/proper-use-of-mutexes-in-python #il faudrait utiliser multiprocessing au lieu de threading #from multiprocessing import Process, Lock # https://stackoverflow.com/questions/22885775/what-is-the-difference-between-lock-and-rlock import _thread import time # https://docs.python.org/fr/3/library/time.html #création d'une thread par connexion TCP #https://www.geeksforgeeks.org/socket-programming-multi-threading-python/ '''idée pour identification des messages: chaque capteur envoie dans le message son adresse mac (et les simulateurs en génère une par capteur simulé) si la trame passe par un routeur, il n'est pas possible d'avoir l'adresse mac du capteur autrement... plusieurs messages différents possible par capteur en ajoutant 16 bits en suffixe à l'adresse mac (equivalent numero de port) requete au serveur de capteur de 2 types possibles par annuaire de fonctionnalité par numero d'adresse mac requete pour récupérer les infos capteur requete pour récupérer les infos d'identification d'un capteur TODO: au démarage (et régulièrement) une carte capteur s'identifie dans l'annuaire en indiquant son @MAC + une chaine de caractère qui décrit ce que fournit le capteur champs, unité date et heure de compilation du programme auteur du code l'annuaire est sauvé dans un fichier texte en local et se reconstruit au fûr et a mesure on y stocke notamment la date et l'heure de dernière reception du capteur gérer les changements de formats de données capteur ou le remplacement d'une fonctionnalité @mac carte joystick: bc:dd:c2:fe:6f:f0 ''' '''todo: identifier si une donnée capteur est -seule la dernière est utile -peut elle être consommée plusieurs fois la même ou pas? -est au client de vérifier si la donnée a déjà été lue? (auquel cas il faudrait un indice) -toutes sont utiles en séquence, auquel cas il faut mettre en place une gestion de fifo pour les données qui ne tolèrent pas de pertes (encore que l'UDP entre le capteur et le serveur ne soit pas forcement adapté pour ca... timestamper les données recues depuis les capteurs lors d'une requette ''' ######################################################################################################################## class DataBuffer: def __init__(self,nbinit): self.mutex = Lock() self.nb=nbinit; self.dataBuffer=[] self.nbdata=0 #initialisation avec des valeurs en "rampe" de 0 à 1 with self.mutex: for ix in range(1, self.nb+ 1): self.dataBuffer.append([ ix/self.nb]) def setData(self,i,val): #print("tentative d'écriture à indice %s de la valeur %s" % (i , val)) if (i>=self.nb): print("tentative d'écriture à indice %s trop grand" % (i)) return 0 else: with self.mutex: self.nbdata+=1 self.dataBuffer[i]=val def getData(self,i): if (i>=self.nb): print("tentative de lecture à indice %s trop grand" % (i)) return 0 else: with self.mutex: return self.dataBuffer[i] # question: est ce que c'est bien la valeur qui est copiée lors du return, et non pas une référence, auquel cas hors # du mutex, la valeur référencée pourrait devenir incohérente ######################################################################################################################## #réception des données depuis les différents capteurs en UDP sur port 10000 #https://riptutorial.com/fr/python/example/4985/recevoir-des-donnees-via-udp class CommSensor(Thread): def __init__(self, sharedDataInit): Thread.__init__(self) self.sharedData=sharedDataInit; self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind(('', 10000)) # premier champ vide pour recevoir depuis n'importe quelle adresse,sinon IP en chaine de caractères #self.sock.setblocking(0) self.oldti=0 #self.displayIncommingFromSensor=False self.displayIncommingFromSensor=True def run(self): while True: #print('thread CommSensor '); msg, addr = self.sock.recvfrom(8192) # This is the amount of bytes to read at maximum if self.displayIncommingFromSensor: print("Got message from %s: %s" % (addr, msg )) change=1; #pour un seul champ # self.sharedData.dataBuffer[0]=int(msg) # print("valeur recue: " + str(self.sharedData.dataBuffer[0])) #pour plusieurs champs ti=time.time() chaine=str(self.sharedData.nbdata) + " valeurs recues: "; chaine+=str(ti)+ " " +str(ti-self.oldti) + " " listfields=msg.split() if (len(listfields)>1): # if False: #pour plus tard quand je gérerai l'identification des messages if True: adresseMAC=listfields[0] chaine += " @MAC: "+ adresseMAC.decode('utf-8') + " " try: numero_data=int(listfields[1]) chaine += "num: " + str(numero_data) +" " for i in range(0, len(listfields)-2): # self.sharedData.dataBuffer[0+i]=int(listfields[i]) self.sharedData.setData(numero_data+i,float(listfields[i+2])) chaine+=str(numero_data+i) + " -> " + str(self.sharedData.getData(numero_data+i)) + " , " ; if self.displayIncommingFromSensor: print(chaine) except ValueError: if self.displayIncommingFromSensor: print("erreur parsing") continue self.oldti=ti #TODO tuer ce thread lorsque la fenêtre principale ferme ######################################################################################################################## #chaque client voulant lire des données capteurs se connecte en TCP, ce qui crée une thread à chaque connexion #https://riptutorial.com/fr/python/example/4985/recevoir-des-donnees-via-udp class CommServer(Thread): ##########################################################"" def __init__(self, sharedDataInit): Thread.__init__(self) self.sharedData=sharedDataInit; #self.displayIncommingFromClient=False #self.displayResponseToClient=False self.displayIncommingFromClient=True self.displayResponseToClient=True ##########################################################"" def run(self): print("New Thread server") # http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/ #utilisation du context manager pour libérer les ressources socket quand on quitte l'appli #https://stackoverflow.com/questions/16772465/how-to-use-socket-in-python-as-a-context-manager #https://hg.python.org/cpython/file/e57c8a90b2df/Lib/socket.py#l87 #oui la methode __exit__( de socket ferme bien la socket! with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: host = "" port = 30000 s.bind((host, port)) # premier champ vide pour recevoir depuis n'importe quelle adresse,sinon IP en chaine de caractères print("socket binded to port", port) # put the socket into listening mode s.listen(5) print("socket is listening") # a forever loop until client wants to exit while True: # establish connection with client c, addr = s.accept() # lock acquired by client # print_lock.acquire() print('Connected to :', addr[0], ':', addr[1]) # Start a new thread and return its identifier _thread.start_new_thread(self.threaded, (c,)) #s.close() ##########################################################"" # thread fuction def threaded(self,c): while True: # data received from client data = c.recv(1024) if self.displayIncommingFromClient: print("receving " + str(data)) if not data: print('Bye') # lock released on exit # print_lock.release() break # sort du while -> déconnexion # décodage de la requete: format r n1 n2... listfields = data.split() if self.displayIncommingFromClient: print(str(listfields)) reponse ="" if listfields[0]==b'r': for i in range(1, len(listfields)): #saute le premier champ nbdatarequise=int(listfields[i]) reponse+= str(self.sharedData.getData(nbdatarequise)) + " "; else: print("probleme formatage requete") # conn.sendall(data) #reponse = str(self.sharedData.dataBuffer[0]) + chr(13) # + chr(10) : ne pas mettre le 10 sinon VAL3 interprete comme un caracatère débutant la chaine suiavnte... reponse+= chr(13) # + chr(10) : ne pas mettre le 10 sinon VAL3 interprete comme un caracatère débutant la chaine suiavnte... if self.displayResponseToClient: print("sending " + str(reponse)) c.sendall(reponse.encode('utf-8')) # connection closed c.close() ''' s1, s2 = socket.socketpair() b1 = bytearray(b'----') b2 = bytearray(b'0123456789') b3 = bytearray(b'--------------') s2.recvmsg_into([b1, memoryview(b2)[2:9], b3]) (22, [], 0, None) [b1, b2, b3] ''' #time.sleep(1) # TODO tuer tous ces thread thread lorsque la fenêtre principale ferme # https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread # on ne doit pas tuer les threads à la bourrin (par contre possible avec process # facon propre : https://dzone.com/articles/understanding ######################################################################################################################## if __name__ == '__main__': try: sharedData=DataBuffer(10000); #10000 données capteur différentes au max thread_1 = CommSensor(sharedData); thread_1.start() thread_2 = CommServer(sharedData); thread_2.start() # Attend que les threads se terminent #thread_1.join() #thread_2.join() while True: time.sleep(1) #print("...") except KeyboardInterrupt: #print("attente fin thread 1") #thread_1.join() print("bye") #dans pyCharm, cliquer sur la tête de mort rouge pour bien tuer tout les processus et libérer les numeros de ports (envoi de SIGKILL) #NE PAS SE CONNECTER AU WIFI ROBOT AIP SI J'UTILISE PC RAPID avec IP 1.49 #ajouter un mutex sur les données partagées