#!/usr/bin/env python #-*- coding: iso8859-15 -*- ########################################################## # MOMENTUM v1.0 # ########################################################## # Autor: Juan Miguel Taboada Godoy # # Fecha: Szczecin, 07 de diciembre de 2006 # # Descripción: Adquisitor de datos para MOMENTUM # # Versión: 2006120800 # # # # Codigo fuente bajo licencia GNU/GPL # # Centrologic (Computational Logistic Center) # # http://www.centrologic.com - info@centrologic.com # ########################################################## # Librerías que voy a usar {{{1 from lib.MODBUS_TCP import MODBUS_TCP from lib.MATH_BIN import bin_complementoA2, hextofloat, hextobin, bintohex from lib.HERENCIA import BASE,DEFAULTCONFIG # }}}1 # Funciones generales {{{1 # Anade un texto a una mascara {{{2 def enmascarar(mascara,cadena): # Calcula la longitud de la cadena cad=len(str(cadena)) # Obtiene la cadena con la mascara cargada result="%s%s" % (mascara[:-cad],cadena) # Devuelve el resultado return result # }}}2 # Función para extrapolar un valor físico a uno lógico {{{2 def fis2log(ili,ilf,ifi,iff,ivalor): li=float(ili) lf=float(ilf) fi=float(ifi) ff=float(iff) valor=float(ivalor) return (((lf-li)*(valor-fi)/(ff-fi))+li) # }}}2 # Funcion para interpretar los datos de una lista {{{2 ## Lee los datos de una lista: unificando las posiciones de memoria de los float ## \param lista Lista de datos [(posicion,tipo,dato),...] def leelista(lista): # Inicializo la lista de salida NEW=[] float=False olddato=None oldpos=None oldtipo=None for elemento in lista: # Extraigo el elemento (pos,tipo,dato)=elemento # Si estabamos leyendo un float if (float): # Control de posicion if (pos-1==oldpos): # Lo termino de leer y lo cargo en la lista NEW.append((oldpos,oldtipo,"%s%s" % (olddato,dato))) float=False else: raise IOError,"Se ha encontrado un valor float cuya segunda palabra no esta consecutiva en memoria" else: # Si no estamos leyendo un float, analizo el tipo de dato if (tipo=="float"): # Si es float, guardo todo oldpos=pos oldtipo=tipo olddato=dato # E indico que estamos leyendo un float float=True else: # Si no es un float, guardo en memoria NEW.append((pos,tipo,dato)) # Devuelvo la lista resultante return NEW # }}}2 # Hace el complemento a 2 de una cadena que contiene un número binario {{{2 def str_complementoA2(binario_str): # Rompe la cadena binario=[] for i in range(0,len(binario_str)): if (binario_str[i]=='0'): binario.append(0) else: binario.append(1) # Hace el complemento a 2 binario=bin_complementoA2(binario) # Junta la cadena binario_str="" for i in range(0,len(binario)): binario_str+=str(binario[i]) # Devuelvo el resultado return binario_str # }}}2 # }}}1 # ### CONFIG ### ###################################### ## Acceso a Telemando mediante el protocolo MOMENTUM: configuracion por defecto class CONFIG(DEFAULTCONFIG): # Constructor {{{1 def __init__(self): # Cargo las acciones del padre DEFAULTCONFIG.__init__(self,"MOMENTUM") # }}}1 # Config Telemando {{{1 # vpp {{{2 ## VPP (Número de valores por paquete) ## \param self - ## \param arg Total de valores por paquete def vpp(self,arg=None): if (arg!=None): self._vpp=arg return self._vpp # }}}2 # port {{{2 ## Puerto de acceso al servidor MODBUS ## \param self - ## \param arg Nombre o direccion IP del servidor def port(self,arg=None): if (arg!=None): self._port=arg return self._port # }}}2 # }}}1 # Checker {{{1 ## Comprueba que el conjunto minimo de datos requeridos por la clase han sido declarados por el usuario: \n ## - debug() ## - port() ## - vpp() ## \param self - def check(self): self._check("debug") self._check("port") self._check("vpp") self.check_default() # }}}1 # ### TELEMANDO ### ################################### ## Acceso a Telemando mediante el protocolo MOMENTUM: Telemando class TELEMANDO(BASE): # Constructor {{{1 ## Constructor ## \param self - ## \param id Identificador del Telemando (ALFANUMERICO) ## \param registrador Registrador de datos ## \param senales Objeto senales que contiene todas las senales (opcional) ## \warning En el nombre de fichero no indique la extension del mismo (.signal.dat), esto ya lo hace la clase como obligacion. def __init__(self,id,registrador,senales=None): # Cargo las acciones del padre BASE.__init__(self,id,"MOMENTUM") # Lista de senales a descargar self.__senales=[] # Base de datos de senales self.__bdsenales=senales # Incializo las listas de direcciones de memoria self.__ED=[] # Empiezan por 0 self.__SD=[] # Empiezan por 1 self.__EA=[] # Empiezan por 3 self.__SA=[] # Empiezan por 4 # Inicio el lector de ficheros self.__REGISTRADOR=registrador # Datos pendientes self.__pendiente=False # }}}1 # Guarda la config {{{1 ## Guarda la configuracion del Telemando ## \param self - ## \param config Configuración por defecto obtenida de la clase CONFIG ## \param ip Direccion IP o host del servidor MODBUS def config(self,config,ip): # Configuración por defecto self.default_config(config) # Cargo los datos en la config self.ip(ip) # Chequeo la config self.check() # }}}1 # Guarda la IP {{{1 ## Servidor MODBUS ## \param self - ## \param arg Direccion IP o nombre del servidor MODBUS def ip(self,arg=None): if (arg!=None): self._ip=arg return self._ip; # }}}1 # Guarda el VPP (Valores por paquete) {{{1 ## VPP (Número de valores por paquete) ## \param self - ## \param arg Total de valores por paquete def vpp(self,arg=None): if (arg!=None): self._vpp=arg return self._vpp # }}}1 # Devuelve el listado de registradores {{{1 ## Devuelve el listado de registradores ## \param self - def registradores(self): return self.__REGISTRADOR.list() # }}}1 # Checker {{{1 ## Comprueba que el conjunto minimo de datos requeridos por la clase han sido declarados por el usuario: \n ## - ip() ## \param self - def check(self): self._check("ip") self.check_default() # }}}1 # Carga la lista de señales a descargar {{{1 ## Carga la lista de senales a descarga evitando el uso repetitivo de addsignal y aprovechando el conocimiento obtenido mediante el Objeto senales ## \param self - ## \param referencia Nombre del telemando del que obtener senales o listado de senales def load(self,referencia): # Compruebo que tengo base de datos de senales if (self.__bdsenales==None): raise IOError,"Al crear el objeto no se ha entregado una base de datos de senales" # Obtengo el listado de senales si no fue entregado aun if (type(referencia)==type("abc")): senales=self.__bdsenales.search_tele(referencia) if (senales==[]): raise IOError,"No se ha encontrado ninguna senal para la referencia: %s" % (referencia) elif (type(referencia)==type([])): senales=referencia else: raise IOError,"El argumento de referencia solo puede ser el nombre de un Telemando o un listado de senales" # Proceso la lista de senales a cargar for senal in senales: # Reviso si es el nombre de una senal o si es una senal compuesta if (type(senal)==type("abc")): # Busco la señal en la lista de senales (senalid,name,telemando,descripcion,tiposenal,conector,param1,param2)=self.__bdsenales.search(None,senal) else: # Descompongo la senal (senalid,name,telemando,descripcion,tiposenal,conector,param1,param2)=senal # Comprueba el resultado if (name==None): raise IOError,"No se ha encontrado la senal %s en el listado de senales" % (senal) # Descompongo el tipo tipopos=conector.split(":") if (len(tipopos)==2): (tipo,posicion)=tipopos tipodato=None elif (len(tipopos)==3): (tipo,posicion,tipodato)=tipopos else: raise IOError,"Error descomponiendo el conector" posicion=int(posicion) # Decido el tipo de senal if (tipo=="sd"): # Entradas digital self.addsignal(0,posicion,"bit") elif (tipo=="ed"): # Entradas digital self.addsignal(1,posicion,"bit") elif (tipo=="sa"): # Salidas analógicas self.addsignal(3,posicion,"int") elif (tipo=="ea"): # Comprueba si hay que leerlo como un real if (tipodato=="float"): # Entradas analogicas (Grupo 2) - REALES self.addsignal(4,posicion,"float") else: # Entradas analogicas (Grupo 1) self.addsignal(4,posicion,"int") # }}}1 # Anade una senal a la lista de descarga {{{1 ## Insertar una senal en la lista de senales a descargar ## \param self - ## \param funcion Primer digito de las posiciones de memoria (especifica la funciona a usar para recoger los datos) ## \param posiciones Posiciones de memoria a descargar ## \param tipo Tipo de datos que se va a recoger: int, float o bit def addsignal(self,funcion,posiciones,tipo=None): # Detecta el tipo de funcion if (funcion==0): tipof="SD" lista=self.__SD elif (funcion==1): tipof="ED" lista=self.__ED elif (funcion==3): tipof="SA" lista=self.__SA elif (funcion==4): tipof="EA" lista=self.__EA else: raise IOError,"Valor %s como funcion es desconocido. Use: 0, 1, 3 o 4" % (funcion) # Detecta el tipo de datos a procesar if (tipo=="bit"): if ((tipof=="ED") or (tipof=="SD")): tipod="bit" else: raise IOError,"Valor %s como tipo es incompatible con la funcion %s. Use: float o int" % (tipo,tipof) elif (tipo=="int"): if ((tipof=="EA") or (tipof=="SA")): tipod="int" else: raise IOError,"Valor %s como tipo es incompatible con la funcion %s. Use: float o int" % (tipo,tipof) elif (tipo=="float"): if ((tipof=="EA") or (tipof=="SA")): tipod="float" else: raise IOError,"Valor %s como tipo es incompatible con la funcion %s. Use: float o int" % (tipo,tipof) else: raise IOError,"Valor %s como tipo es desconocido. Use: bit, float o int" % (tipo) # Comprueba si es una lista if (type([])==type(posiciones)): # Ordena la lista posiciones.sort() # Procesa la lista oldpos="NULL" for posicion in posiciones: # Reconfiguro la direccion de memoria if (tipod!="float"): posicion=posicion-1 # Busco si la posicion ya esta en la lista for elemento in lista: # (temp1,temp2)=elemento temp1=elemento[0] if (temp1==posicion): # Se ha solicitado 2 veces la misma posicion (mensaje adaptado para el usuario) if (tipod!="float"): raise IOError,"La posicion de memoria %s para la lista %s se ha solicitado 2 veces en la configuracion" % (posicion+1,tipof.upper()) else: raise IOError,"La posicion de memoria %s para la lista %s se ha solicitado 2 veces en la configuracion" % (posicion,tipof.upper()) # Reviso que no estoy solapando direcciones con los float if (posicion==oldpos): raise IOError,"En la configuracion hay 2 valores que se solapan, bien porque esten repetidos, o bie porque uno sea un real (2 palabras) y la segunda palabra esta siendo solicitada tambien por separado" # Control de float (para leer 2 palabras) lista.append((posicion,tipod)) oldpos=posicion if (tipod=="float"): lista.append((posicion+1,tipod)) oldpos=posicion+1 else: # Reconfiguro la direccion de memoria if (tipod!="float"): posiciones=posiciones-1 # Busco si la posicion ya esta en la lista for elemento in lista: #(temp1,temp2)=elemento temp1=elemento[0] if (temp1==posiciones): # Se ha solicitado 2 veces la misma posicion (mensaje adaptado para el usuario) if (tipod=="int"): raise IOError,"La posicion de memoria %s para la lista %s se ha solicitado 2 veces en la configuracion" % (posiciones+1,tipof.upper()) else: raise IOError,"La posicion de memoria %s para la lista %s se ha solicitado 2 veces en la configuracion" % (posiciones,tipof.upper()) # Anade la senal lista.append((posiciones,tipod)) if (tipod=="float"): lista.append((posiciones+1,tipod)) # }}}1 # Descarga los datos del Telemando {{{1 ## Descarga los datos del Telemando ## \param self - def execute(self): # Recojo los datos que necesito para conectar target=self.valor("ip") port=self.valor("port") # Control de errores try: # Genera el objeto MODBUS y conecta self.debug("Conectando al automata %s con IP %s..." % (self.internal_id(),target)) modbus=MODBUS_TCP(target,port,self.valor("vpp")) # Descarga de datos self.debug("Descargando datos del automata...") ED=modbus.AUTOSR(2,self.__ED) SD=modbus.AUTOSR(1,self.__SD) EA=modbus.AUTOSR(3,self.__EA) SA=modbus.AUTOSR(4,self.__SA) # Desconectando del automata self.debug("Desconecto del automata...") modbus.DISCONNECT() # Anoto que no hubo error de red error_red=None except Exception,e: error_red=e # Compruebo si hubo error de red if (error_red==None): # Regenero las cadenas hexadecimales haciendo crecer los que contengan como tipo "double" y añado el tipo en la cadena EA=leelista(EA) SA=leelista(SA) # Transformo los datos hexadecimales a valores útiles # 1) Digitales (transformo a 0/1) - ED y SD {{{2 digitales=[(ED,"ed"),(SD,"sd")] for tupla in digitales: # Extraigo la tupla (lista,tipolista)=tupla # Proceso la lista for elemento in lista: # Extraigo el dato (posicion,tipo,valor)=elemento # Reescribo la posicion posicion=posicion+1 # Control de tipo if (tipo!="bit"): raise IOError,"Se ha detectado que uno de los datos en %s, no tiene el tipo 'bit'. La posicion de memoria es: %s" % (tipolista.upper(),posicion) # Registro el elemento en la clase de salida self.__REGISTRADOR.register(posicion,tipolista,valor) # }}}2 # 2) Analogicas (interpreto los INT y los FLOAT) - EA y SA {{{2 analogicas=[(EA,"ea"),(SA,"sa")] for tupla in analogicas: (lista,tipolista)=tupla for elemento in lista: # Extraigo el dato (posicion,tipo,dato)=elemento # Compruebo el tipo del dato if (tipo=='float'): # Si es float, comprueba que la longitud de la cadena es 8 if (len(dato)!=8): raise IOError,"Se ha dado una posicion de tipo float, pero la longitud del hexadecimal recibido no es de 2 palabras" # Transforma el hexadecimal a float valor=hextofloat(dato) elif (tipo=='int'): # Reescribo la posicion posicion=posicion+1 # Si es int, convertir a binario binario=hextobin(dato) # Obtener el signo del binario signo=(-1)**int(binario[0]) # Comprobamos el signo if (signo==-1): # Si el signo es negativo, hacer el complemento a 2 binario=str_complementoA2(binario) # Obtenemos el hexadecimal en complemento a 2 hexa=bintohex(binario) # Computo el valor y le añado el signo valor=signo*eval("0x%s" % (hexa)) # Compruebo el valor recibido if (valor==-32768): # Asigno un valor NULO valor=None # Si el valor vale -32768 significa cable roto (el mensaje de error muestra la posicion de memoria apta para el usuario) self.debug("Detectado CABLE ROTO para la lista de senales %s en la posicion %s con tipo de dato %s" % (tipolista.upper(),posicion,tipo)) else: raise IOError,"Se ha detectado que uno de los datos en %s, no tiene el tipo 'int' o 'float'. La posicion de memoria es: %s" % (tipolista.upper(),posicion) # Registro el valor en la clase de salida self.__REGISTRADOR.register(posicion,tipolista,valor) # }}}2 else: # Hubo un error de red, almaceno lo que teníamos a disco self.__REGISTRADOR.register_error("ERROR_NET") # Nueva línea self.debug("") # }}}1 # Escribe el resultado en un fichero de registros {{{1 ## Escribe el resultado a un fichero de registros en formato SIGNALS ## \param self - ## \Exception IOError Si ocurre un error en el proceso def write(self): self.__REGISTRADOR.write() # }}}1 # ### EXCEPTIONS ### ################################## # EXCEPTION CLASSES {{{1 # Excepciones básicas # Except (General Exception) {{{2 class Except(Exception): def __init__(self,string): self.string=string def __str__(self): return self.string #}}}2 # ConnectionError (Conexión errónea al servidor) {{{2 class ConnectionError(Exception): def __init__(self,string): self.string=string def __str__(self): return self.string # }}}2 # IOError (Error de entrada/salida) {{{2 class IOError(Exception): def __init__(self,string): self.string=string def __str__(self): return self.string # }}}2 # ExecutionError (Error de ejecucción) {{{2 class ExecutionError(Exception): def __init__(self,string): self.string=string def __str__(self): return self.string # }}}2 # Excepciones graves # TerminalError (Error irrecuperable del programa) {{{2 class TerminalError(Exception): def __init__(self,string): self.string=string def __str__(self): return self.string # }}}2 # }}}1