viernes, 27 de marzo de 2020

MONITOREO DE MULTIPLES HILOS EN PYTHON 3


En este programa se ejemplifica cómo monitorear varios hilos y mostrar el progreso de cada uno en la interfase gráfica. Para saber mas sobre hilos, procesos y demás. puedes leer el post del link.


El programa utiliza unas barras de progreso (gauge) para representar cada hilo en la interfase gráfica. Cada hilo tiene un proceso, el cual consiste en contar de 0 a 100, con un retraso aleatorio de tiempo de entre 5 y 10 mili segundos en cada iteración para simular la variación de tiempos de proceso. Puedes  cambiar el numero de procesos en la variable num_procesos.  Corre este programa una vez y observa como las barras de progreso se llenan casi al mismo tiempo.

semáforo desactivado

En el código mostrado mas abajo hay 2 funciones comentadas. Estas funciones son threadLock.acquire() y threadLock.release(). Estas funciones funcionan (valga la redunancia) como un semáforo que pausa un hilo determinado mientras otro continua. Esto es útil para evitar corromper datos cuando 2 o mas hilos intentan acceder a la misma información para lectura o escritura al mismo tiempo. Des-comenta estas 2 funciones como experimento para observar como las barras de progreso se pausan entre si de modo que ya no se llenan de de forma uniforme.

Semáforo activado

En el código fuente te dejo comentarios comprender lo que sucede. Nesecitas la librería wxpython para correr este programa. Este programa fué probado en Python 3.7.


import wx
from time            import sleep
from random          import randint
from threading       import Thread
from threading       import Lock
from threading       import current_thread
from wx.lib.newevent import NewEvent
 
threadLock = Lock() # descomenta threadLock.acquire() y threadLock.release()
                    # (mas abajo) como experimento para ver que sucede.
MandaGaugeDatos, EVT_ACTUAL_GAUG = NewEvent() # evento que se usara para monitorear hilos y actualizar gauges
num_procesos = 5   # numero de hlos (y por ende, de gauges) pon aqui los que quieras

class TestThread(Thread): # Esto es un hilo.
    def __init__(self,parent,threadID):
        Thread.__init__(self)     # llamar al constructor de la clase
        self.threadID = threadID  # variable para identificar el hilo
        self.parent   = parent    # padre de el hilo (se usa para matar hilo cunando se sierre el programa)

    def run(self): # EL PROCESO
        #--------------------------------------------------
        #   El roceso consiste en contar de 1 a 100.
        #   se generan retardos aleatorios para simular tiempos variantes
        #--------------------------------------------------
        for i in range(1,100): # Cuenta de 1 a 100%
            
            #threadLock.acquire() # DESCOMENTA ESTO A VER QUE PASA.

            slp = randint(5, 10)/1000  # generar tiempo aleatorio entre 5 y 10ms
            sleep(slp)                 # realizar el retraso aleatorio
            #-------------------------------------------------------------
            # Enviar datos resultantes de cada iteracion del hilo a la GUI
            #  wx.postevent(parent, function) #generar evento
            #  MandaGaugeDatos(datos nombrados que quieras regresar)
            #--------------------------------------------------
            wx.PostEvent(self.parent,
                         MandaGaugeDatos(dato   = i,
                                         gauge  = self.threadID,
                                         thread = current_thread()))
            
            #threadLock.release() # DESCOMENTA ESTO A VER QUE PASA.
     
class MainFrame(wx.Frame):
    def __init__(self, parent, title =""):
        super().__init__(parent, title = title , size = (500,250))
        self.panel  = wx.Panel(self)
        
        self.gauges = []  # Arreglo de Gauges
        threads     = []  # arreglo de hilos
        #---------------------------------------------
        #                WIDGETS
        #---------------------------------------------
        #       llenar el arreglo de gauges
        #--------------------------------------------
        for gauge in range(num_procesos):
            self.gauges.append(wx.Gauge(self.panel, size=(300, 20)))
        #---------------------------------------------
        #                 BINDS
        #---------------------------------------------
        # Conectar señal del evento emitida con la funcion correspondiente
        #--------------------------------------------
        self.Bind(EVT_ACTUAL_GAUG, self.on_actualizar)
        #---------------------------------------------
        #                 LAYOUT
        #---------------------------------------------
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        main_sizer.Add(self.panel,1, wx.CENTER|wx.ALL|wx.EXPAND)
        panel_sizer = wx.BoxSizer(wx.VERTICAL)
        self.panel.SetSizer(panel_sizer)
        for gauge in self.gauges:
            panel_sizer.Add(gauge , 0, wx.CENTER|wx.ALL|wx.EXPAND)
        self.SetSizer(main_sizer)
        #---------------------------------------------
        #  llenar arreglo de hilos e  Iniciarlos todos
        #---------------------------------------------
        for x in range(num_procesos):
            threads.append(TestThread(self, x))
            threads[x].daemon = True
            threads[x].start()
            
    def on_actualizar(self, evt):
        #Actualizar el gauge correspondiente en la GUI
        self.gauges[evt.gauge].SetValue(evt.dato)
 
if __name__ == '__main__':
    app = wx.App(0)
    frame = MainFrame(None)
    app.SetTopWindow(frame)
    frame.Show()
    app.MainLoop()

jueves, 19 de marzo de 2020

MEDICION MULTIPLE DE TIEMPOS EN PYTHON 3



Imagina que tienes varias estaciones donde se procesa algún tipo de  pieza y quieres medir cuanto tiempo permanece cada pieza en cada estación. Supongamos también que cada estación tiene su tiempo de ciclo (unas inician antes otras despues, otras al mismo tiempo).

En el siguiente programa muestro como medir una cantidad determinada de tiempos con un solo temporizador y un solo hilo (thread). El programa calcula minutos, segundos y centésimas de segundo de cada estacion. La interfase de usuario es dinámica, es decir, se puede determinar el numero de registros de tiempo que se deseen (yo puse 20 pero lo puedes cambiar la variable numtimers al numero que quieras), el programa genera los elementos nesesarios. Este programa apenas consume CPU (se podría usar en un Raspberry Pi).El programa inicia el conteo al presionar un botón correspondiente, cuando se presiona de nuevo detiene el conteo de tiempo y lo guarda en un archivo de texto.

Programa en windows

El programa tiene comentarios en la mayoría de las líneas de código para que se pueda entender lo que sucede.

AVISO: Cada temporizador crea un archivo de texto, si pones 50, se crean 50 archivos de texto en el lugar donde esta el script. Se recomienda correrlo dentro de su propio folder.

Este programa fue probado con python 3.7. También nesecitas wxpython para correr este programa.

import wx
from   time                 import sleep
from   threading            import Thread
from   wx.lib.scrolledpanel import ScrolledPanel

class TimeReg():   #clase para registrar conteo de tiempo y estado del conteo.
    def __init__(self):
        self.tiempo = [0,0,0]  #registro de minutos, segundos y decasegundos
        self.activado = False  #iniciar desactivado

    def update(self):
        self.tiempo[2] += 1         # incremento cada centisegundo (1/100)

        if (self.tiempo[2] >= 100): # Han pasado 100 centisegundos (1 segundo)?
            self.tiempo[2] =  0     # resetear cuenta de centisegundos.
            self.tiempo[1] += 1     # incrementar segundero
                
        if (self.tiempo[1] >= 60):  # Han pasado 60 segundos (1 minuto)?
            self.tiempo[0] += 1     # incrementar minutero
            self.tiempo[1] =  0     # resetear segundero

    def reset(self):
        self.tiempo = [0, 0, 0]     # 0 minutos, 0 segundos, 0 centisegundos
        

class MyForm(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Timers")#llamar a la funcion constructor de Frame
        #---------------------------------------------------
        #                 VARIABLES
        #---------------------------------------------------
        self.numtimers       = 20        # cuantos timers quieres 20, 50, 200?
        self.CronometrArr    = []        # Arreglo de registro de tiempos
        self.TimeRegePattern = '{0:02d}:{1:02d}:{2:02d}'       # Darle formato al tiempo
        self.thread          = Thread(target = self.WrkrThred) # poner funcion WrkrThred en un hilo
        self.thread.daemon   = True      # Poner hilo en modo daemon, si el programa termina, tambien el hilo
        self.exitFlag        = False     # Bandera para parar el ciclo del hilo
        self.LblTimeRegeArr  = []        # Arreglo de etiquetas de tiempos
        self.BtnTimeRegrArr  = []        # Arreglo de botones.
        FntLblTimeRegrs      = wx.Font(16, wx.MODERN, wx.NORMAL, wx.BOLD)  #fuente a usar
        self.TiempoStr       = ""

        for i in range(self.numtimers):
            self.CronometrArr.append(TimeReg()) #Arreglo de objetos de registro de tiempos

        self.createFiles() #crear los archivos de texto nesesarios
        #---------------------------------------------------
        #                 WIDGETS
        #---------------------------------------------------
        PnlTimeRegpos = ScrolledPanel(self) #Panel de lecturas de tiempos
        
        for i in range(self.numtimers):
            # crear etiquetas y llenar arreglo
            self.LblTimeRegeArr.append(wx.StaticText(PnlTimeRegpos,
                                                -1,
                                                style = wx.ALIGN_CENTER|wx.ST_ELLIPSIZE_MIDDLE))
            self.LblTimeRegeArr[i].SetFont(FntLblTimeRegrs)     #asignar fuente
            self.LblTimeRegeArr[i].SetBackgroundColour('black') #asignar color de fondo
            self.LblTimeRegeArr[i].SetForegroundColour('white') #asignar color de letra
            self.LblTimeRegeArr[i].SetLabel("00:00:00")         #texto inicial

            # crear botones y llenar arreglo de botones
            self.BtnTimeRegrArr.append(wx.ToggleButton(PnlTimeRegpos ,
                                                  -1,
                                                  str(i),
                                                  size=(50,40)))
        #--------------------------------------------------
        #                 BINDS
        #--------------------------------------------------
        self.Bind(wx.EVT_CLOSE, self.onClosed) # trabajo de limpieza al salir de la aplicacion
        
        for i in range(self.numtimers):
            # poner bind a todos los botones a self.onTggleBtn
            self.BtnTimeRegrArr[i].Bind(wx.EVT_TOGGLEBUTTON,self.onTggleBtn)
 
        #---------------------------------------------------
        #                 MAIN LAYOUT
        #--------------------------------------------------
        MainSizer = wx.BoxSizer(wx.VERTICAL)
        #--------- Panel de lecturas ------
        TimeRegposSizrArr = [] #Arreglo de sizers
        PnlTimeRegposSizr = wx.BoxSizer(wx.VERTICAL)
        
        for i in range(self.numtimers):
            TimeRegposSizrArr.append(wx.BoxSizer(wx.HORIZONTAL))
            TimeRegposSizrArr[i].Add(self.BtnTimeRegrArr[i],0,wx.EXPAND|wx.ALL,2)
            TimeRegposSizrArr[i].Add(self.LblTimeRegeArr[i],1,wx.EXPAND|wx.ALL,2)
            PnlTimeRegposSizr.Add(TimeRegposSizrArr[i],1,wx.EXPAND|wx.ALL,0)
        PnlTimeRegpos.SetSizer(PnlTimeRegposSizr)
        PnlTimeRegpos.SetupScrolling() #calcular espacio para scroll bar
        #--------------------------------------------------
        MainSizer.Add(PnlTimeRegpos, 1,wx.EXPAND|wx.ALL,5)
        self.SetSizer(MainSizer)
        #---------------------------------------------------
        #            INICIAR HILOS AL ULTIMO
        #---------------------------------------------------         
        self.thread.start()       # Iniciar hilo
        
    def WrkrThred(self):
        while True: #loop "infinito"
            if self.exitFlag == False: # programa sigue corriendo?
                sleep(0.01)            # reterdo de 1 centi sedundo
                for i in range(self.numtimers):                    #iterar en los registros
                    if self.CronometrArr[i].activado == True:      #si esta activado
                        self.CronometrArr[i].update()              #actualizar registro i
                        self.TiempoStr = self.TimeRegePattern.format(self.CronometrArr[i].tiempo[0],
                                                                     self.CronometrArr[i].tiempo[1],
                                                                     self.CronometrArr[i].tiempo[2]) #Dar formato a los tiempos
                        wx.CallAfter(self.LblTimeRegeArr[i].SetLabel,self.TiempoStr)                 #Actualizar Texto correspondiente
                        #wx.callafter es un modo seguro de actualizar GUI desde un hilo.
            else:
                break #Romper el ciclo infinito

    def onTggleBtn(self, evt):
        BTN      = evt.GetEventObject()  #obtener el objeto boton que emite la señal
        BtnID    = BTN.GetLabelText()    #obtener el texto del boton
        BtnIdInt = int(BtnID)            #convertir el texto del boton en entero.
        if BTN.GetValue():
            self.CronometrArr[BtnIdInt].reset()          #reestablecer conteo
            self.CronometrArr[BtnIdInt].activado = True  #iniciar conteo
        else:
            self.savetimefile(BtnID)                     #Guardar conteo
            self.CronometrArr[BtnIdInt].activado = False #detener conteo

    def savetimefile(self,timer):
        f = open(timer+".txt", "a+")
        f.write(self.TiempoStr+"\n")
        f.close()
        
    def createFiles(self):
        for x in range(self.numtimers):
            open(str(x)+".txt","w+").close()

    def onClosed(self, evt):
        self.exitFlag = True #Salir del ciclo while del hilo
        self.Destroy()       #Destruir TODO

if __name__ == "__main__":
    app = wx.App()
    frame = MyForm().Show()
    app.MainLoop()