#!/usr/bin/env python #-*- coding: iso8859-15 -*- ########################################################## # ADVANTECH ADAM 5000 TCP v1.0 # ########################################################## # Autor: Juan Miguel Taboada Godoy # # Fecha: Szczecin, 20 de agosto de 2007 # # Descripción: Adquisitor de datos para ADVANTECH # # Modelo: ADAM 5000 TCP # # Versión: 2007082000 # # # # 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,"ADVANTECH") # }}}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 evento Lista de eventos ## \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,eventos,senales=None): # Cargo las acciones del padre BASE.__init__(self,id,"ADVANTECH") # Lista de senales a descargar self.__senales=[] # Base de datos de senales self.__bdsenales=senales # Incializo las listas de direcciones de memoria self.__RD=[] # Empiezan por 1 (Read digital) self.__RA=[] # Empiezan por 4 (Read analogic) # Inicio resolutor de eventos self.__EVENTS=eventos # Inicio el registrador self.__REGISTRADOR=registrador # Datos pendientes self.__pendiente=False # Modbus socket self.__modbus=None # }}}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 # Connect {{{1 ## Connect ## \param self - def connect(self): # Get the data we need to connect target=self.valor("ip") port=self.valor("port") # Make the MODBUS object and connect self.debug("Conecting to the RTU %s with IP %s..." % (self.internal_id(),target)) self.__modbus=MODBUS_TCP(target,port,self.valor("vpp"),1) # }}}1 # Disconnect {{{1 ## Disconnect ## \param self - def disconnect(self): # Disconnecting from the RTU self.debug("Disconnect from RTU...") self.__modbus.DISCONNECT() # }}}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 slotpin=posicion.split(",") if (len(slotpin)==2): (slot,pin)=slotpin posicion=int(self.position(slot,pin,(tiposenal=='a'))) tipodato=None elif (len(tipopos)==3): (tipo,posicion,tipodato)=tipopos slotpin=posicion.split(",") if (len(slotpin)==2): (slot,pin)=slotpin posicion=int(self.position(slot,pin,(tiposenal=='a'))) else: raise IOError,"Error descomponiendo el conector" # Decido el tipo de senal if (tipo=="rd"): # Lectura de datos self.addsignal(1,posicion,"bit") elif (tipo=="ra"): # 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==1): tipof="RD" lista=self.__RD analogic=False elif (funcion==4): tipof="RA" lista=self.__RA analogic=True 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=="RD"): 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=="RA"): 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=="RA"): 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)): # Interpreta la lista de posiciones newposiciones=[] for posicion in posiciones: if (type(posicion)==type(())): (slot,pin)=posicion posicion=self.posicion(slot,pin,analogic) newposiciones.append(posicion) posiciones=newposiciones # 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 bien 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: # Reviso la posicion if (type(posiciones)==type(())): (slot,pin)=posiciones posiciones=self.posicion(slot,pin,analogic) # 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 # Execute all the actions for this RTU {{{1 ## Execute all the actions for this RTU ## \param self - def execute(self): # Download all data data=self.getall() # Review events and add new possible data data+=self.__EVENTS.process(data) # Register all data for (position,kind,value) in data: self.__REGISTRADOR.register(position,kind,value) # }}}1 # Download the information from this RTU {{{1 ## Download the information from this RTU ## \param self - def get(self,kind,position): # Control of connected if (self.__modbus==None): raise IOError,"Network error during GET: we are not connected" # Get the function if (kind=='rd'): function=1 datatype="bit" analogic=False elif (kind=='ra'): function=4 datatype="int" analogic=True elif (kind=='rf'): function=4 datatype="float" analogic=True else: raise IOError,"This module doesn't have support fot the type of data '%s'" % (kind) # Position if (type(position)==type(())): (slot,pin)=position position=self.position(slot,pin,analogic) # Control for errors return_data=None try: # Download data self.debug("Downloading data from RTU...") result=self.__modbus.AUTOSR(function,[position])[0] # Write we didn't have any error error_red=None except Exception,e: error_red=e # Check if was some error if (error_red==None): # Remake the hexadecimal strings making to grow up every one of type 'double' and addint type to the string result=leelista([(result[0],datatype,result[1])]) # Transform the hexadecimal data to usefull values # 1) Digitals (transform to 0/1) {{{2 if (kind=="rd"): # Get the data (position,kind,value)=result[0] # Rewrite the position position=position+1 # Rewrite the value value=int(value) # Type control if (kind!="bit"): raise IOError,"Detected an error from taken data, doesn't have type 'bit'. The position of memory is: %s" % (position) # Return the element return_data=value # }}}2 # 2) Analogic (interpretate INT and FLOAT) {{{2 else: # Get the data (position,kind,data)=result[0] # Check the kind of data if (kind=='float'): # If if is float, check that the length of the string is 8 if (len(data)!=8): raise IOError,"You gave a position of type 'float', but the lenght of the hexadecimal gotten is not 2 words" # Transform the hexadecimal to float valor=hextofloat(data) elif (kind=='int'): # If it is int, convert to binary binary=hextobin(data) # Get the sign of the binary sign=1 #sign=(-1)**int(binary[0]) ## Check the sign #if (sign==-1): # # If the sign is negative, make complement-2 # binary=str_complementoA2(binary) # We get the hexadecimal in complment-2 hexa=bintohex(binary) # Compute the value and add the sign value=sign*eval("0x%s" % (hexa)) # Check the value gotten if (value==-32768): # Asign a NULL value value=None # If the value is -32768 means broken cable (the message of error shows the memory position ready for the user) self.debug("Detected BROKEN CABLE for the signal at position %s and type %s" % (posicion,tipo)) else: raise IOError,"I have detected that the data doesn't have type 'int' or 'float'. The position of memory is: %s" % (posicion) # Return the value return_data=value # }}}2 else: # Happened and error, store what we had on the disk self.debug("Network error during GET: %s" % (e)) # New line self.debug("") return return_data # }}}1 # Download all the information from this RTU {{{1 ## Download all the information from this RTU ## \param self - def getall(self): # Control of connected if (self.__modbus==None): raise IOError,"Network error during GETALL: we are not connected" # Datos recogidos datos=[] # Control de errores try: # Descarga de datos self.debug("Descargando datos del automata...") RD=self.__modbus.AUTOSR(1,self.__RD) RA=self.__modbus.AUTOSR(4,self.__RA) # 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 RD=leelista(RD) RA=leelista(RA) # Transformo los datos hexadecimales a valores útiles # 1) Digitales (transformo a 0/1) - RD {{{2 # Proceso la lista for elemento in RD: # Extraigo el dato (posicion,tipo,valor)=elemento # Reescribe el valor valor=int(valor) # Control de tipo if (tipo!="bit"): raise IOError,"Se ha detectado que uno de los datos en RD, no tiene el tipo 'bit'. La posicion de memoria es: %s" % (posicion) # Registro el elemento en la clase de salida datos.append((posicion,"rd",valor)) # }}}2 # 2) Analogicas (interpreto los INT y los FLOAT) - RA {{{2 for elemento in RA: # Extraigo el dato (posicion,tipo,dato)=elemento # Compruevo 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'): # Si es int, convertir a binario binario=hextobin(dato) # Obtener el signo del binario signo=1 #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 RA en la posicion %s con tipo de dato %s" % (posicion,tipo)) else: raise IOError,"Se ha detectado que uno de los datos en RA, no tiene el tipo 'int' o 'float'. La posicion de memoria es: %s" % (posicion) # Registro el valor en la clase de salida datos.append((posicion,"ra",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("") return datos # }}}1 # Put a value on this RTU {{{1 ## Put a value on this RTU ## \param self - def put(self,kind,position,value): # Control of connected if (self.__modbus==None): raise IOError,"Network error during PUT: we are not connected" # Get the function if (kind=='wd'): raise IOError,"This module doesn't support the funciont 'wd', not implemented yet" function=2 analogic=False elif (kind=='wa'): function=6 analogic=True elif (kind=='wr'): function=5 analogic=False if (value>0): value=65280 else: raise IOError,"This module doesn't have support fot the type of data '%s'" % (kind) # Position if (type(position)==type(())): (slot,pin)=position position=self.position(slot,pin,analogic) # Control for errors try: # Put data self.debug("Putting data in RTU...") result=self.__modbus.SEND(1,function,position,value) x=self.__modbus.RECEIVE() except Exception,e: # Happened and error, store what we had on the disk self.debug("Network error during PUT: %s" % (e)) raise IOError,"Network error during PUT: %s" % (e) # New line 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 # Action {{{1 ## Action over itself ## \param self - ## \param values ## \param action def start(self,values,kind,position,variable): # Break the position if necesary if (type(position)==type(())): if ((kind=="wd") or (kind=="wr")): analogic=False else: analogic=True (slot,pin)=position position=self.position(slot,pin,analogic) # Get the value if (variable[0]=='%'): value=values.getvalue(variable[1:]) else: value=variable # Execute the order self.put(kind,int(position),int(value)) # }}}1 # Conversion of positions of memory {{{1 ## Conversion of positions of memory ## \param self - ## \param slot Slot number ## \param pin Pin number ## \param da Is analogic? (Default: False) def position(self,slot,pin,analogic=False): # Get the number of pin per slot if (analogic): slot_pins=8 else: slot_pins=16 # Calculate the position of memory return (slot_pins*int(slot)+int(pin)) # }}}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