#!/usr/bin/env python #-*- coding: iso8859-15 -*- ########################################################## # MODBUS_TCP v1.0 # ########################################################## # Autor: Juan Miguel Taboada Godoy # # Fecha: Szczecin, 19 de Julio de 2006 # # Descripción: Adquisitor de datos mediante MODBUS bajo # # el protocolo TCP # # Versión: 2006120700 # # # # Codigo fuente bajo licencia GNU/GPL # # Centrologic (Computational Logistic Center) # # http://www.centrologic.com - info@centrologic.com # ########################################################## # Librerías que voy a usar {{{1 import socket from MATH_BIN import hex2bin, bin2hex, hextobin, zeros # }}}1 # ### TELEMANDO ### ################################### ## \internal ## MODBUS_TCP: Libreria para acceder a un servidor MODBUS mediante TCP ## \version 07/12/2006 0025 Szczecin class MODBUS_TCP: # Constructor {{{1 ## Constructor ## \param self - ## \param host Servidor MODBUS ## \param port Puerto MODBUS (por defecto: 502) ## \param vpp Numero de valores por paquete (por defecto: 10) ## \exception ConnectionError Cuando falla el intento de conexion def __init__(self,host,port=502,vpp=10,unitid=0): # Inicializa la memoria de la clase self.__send_id=None self.__send_function=None self.__send_start=None # Unit id self.__unit_id=unitid # Guarda los datos de recombinacion de paquetes self.__vpp=vpp # Inicia la conexion con el servidor try: self.__stream=socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.__stream.connect((host,port)) self.__connected=1 except Exception,e: self.__connected=0 raise ConnectionError,"Connecting process error: %s" % (e) # }}}1 # Destructor {{{1 ## Destructor, ordena la desconexion del servidor antes de destruir el objeto ## \param self - def __del__(self): if (self.__connected): self.DISCONNECT() # }}}1 # Method to DISCONNECT (disconnect from server) {{{1 ## Desconecta del servidor FTP ## \param self - ## \exception ConnectionError Si falla el intento de desconexion def DISCONNECT(self): if (self.__connected): # Finish connection with the FTP server try: self.__stream.close() self.__connected=0 except Exception,e: raise ConnectionError,"Disconnecting process error: %s" % e # }}}1 # Method to SEND (send a message) {{{1 ## Envia el mensaje indicado al servidor MODBUS ## \param self - ## \param message Mensaje a enviar ## \exception TransferError Si se produce un error durante el envío del mensaje ## \exception ConnectionError Si no estamos conectados al servidor def _SEND(self,mensaje): if (self.__connected): # Transforma el mensaje a binario binario=hex2bin(mensaje) # Envía el mensaje self.__stream.send(binario) else: raise ConnectionError,"No active connection" # }}}1 # Method to SEND (send a message, throught an easy to use interface) {{{1 ## Envia el mensaje indicado al servidor MODBUS mediante el interfaz dado por los argumentos ## \param self - ## \param id ID del mensaje ## \param function Funcion a usar en el mensaje ## \param start Posicion de memoria desde la que comenzar la lectura ## \param size Numero de posiciones de memoria a leer desde la posicion de comienzo ## \param unitid Unit number we are talking with (Default: 0) ## \warning Shouldn't be used. I discovered that Advantech ADAM 5000 TCP ask to have unitid seted up to '1' instead to work properly) def SEND(self,id,function,start,size,unitid=None): # Get unitid if (unitid==None): unitid=self.__unit_id # Salvo el id, la funcion, la direccion de inicio y el numero de posiciones a descargar self.__send_id=id self.__send_function=function self.__send_start=start self.__send_unitid=unitid # Genero el mensaje MODBUS mensaje=self.msg2modbus(id,function,start,size,unitid) # Mando el mensaje MODBUS self._SEND(mensaje) # }}}1 # Method to RECEIVE (receive a message) {{{1 ## Recibe un mensaje del servidor MODBUS ## \param self - ## \param size Tamano del paquete a esperar (por defecto: 300) ## \return Devuelve el mensaje recibido en hexadecimal ## \exception TransferError Si se produce un error durante la recepcion del mensaje ## \exception ConnectionError Si no estamos conectados al servidor def _RECEIVE(self,size): if (self.__connected): # Espera el mensaje del servidor recibido=self.__stream.recv(size) # Comprueba el resultado if (len(recibido)<=0): raise IOError,"Connection closed unexpectedly" # Transforma el mensaje a hexadecimal=bin2hex(recibido) # Devuelve el resultado return hexadecimal else: raise ConnectionError,"No active connection" # }}}1 # Method to RECEIVE (receive a message, return an easy to use tuple) {{{1 ## Recibe un mensaje del servidor MODBUS, devuelve los resultados en una tupla ## \param self - ## \param send_id Se usa para indicar el ID del paquete que se espera recibir (por defecto: None=ID del ultimo paquete enviado) ## \param send_function Se usa para indicar el numero de funcion del paquete que se espera recibir (por defecto: None=numero de funcion del ultimo paquete enviado) ## \param size Tamano del paquete a esperar (por defecto: None=300) ## \return Tupla de 3 elementos (id del mensaej, funcion, datos en formato hexadecimal) def RECEIVE(self,send_id=None,send_function=None, size=300): # Carga los datos en send_id y send_function si no lo indicó el usuario if (send_id==None): send_id=self.__send_id if (send_function==None): send_function=self.__send_function # Recoge los datos de un servidor MODBUS recibido=self._RECEIVE(size) # Descompone el mensaje (id,funct,datos)=self.modbus2msg(recibido) # Control de que el ID coincide if (send_id!=id): raise IOError,"El servidor MODBUS ha respondido con un paquete inesperado, se esperaba el ID %s y se ha recibido %s" % (send_id,id) # Control del numero de funcion #if (send_function!=funct): #raise IOError,"El servidor MODBUS ha respondido con un numero de funcion incorrecto, se esperaba '%s' y se ha recibido '%s'" % (send_function,funct) # Compruebo el resultado del paquete recibido if (funct==132): # Calculo el mensaje de error if (datos==1): error="Codigo de funcion ilegal en la solicitud" elif (datos==2): error="Direccion ilegal en la solicitud" elif (datos==3): error="Valor ilegal en el campo de datos de la solicitud" elif (datos==4): error="Error en el servidor" elif (datos==5): error="ACK" elif (datos==6): error="Servidor ocupado" elif (datos==10): error="Problema en la puerta de enlace" elif (datos==11): error="Problema en la puerta de enlace" else: error="???" # Genero el error raise IOError,"RECEIVE Error: El servidor MODBUS ha respondido con un codigo de error: %s (%s)" % (error,datos) # Devuelve el mensaje recibido en una tupla formateada return (id,funct,datos) # }}}1 # Recombina la lista de direcciones dadas segun las restricciones (se usa para optimizar las consultas de red) {{{1 ## Recombina la lista de direcciones para optimizar las consultas de red ## \param self - ## \param lista Lista de direcciones de memoria a obtener, puede venir en tuplas, siempre y cuando la primera posicion contenga la posicion ## \param vpp Se usa para indicar el numero de valores (validos o no) que se puede solicitar por paquete (por defecto se usa el configurado al crear el objeto) ## \return Devuelve una lista de tuplas que contienen: (posicion de inicio, numero de valores a obtener) ## \todo Esta funcion no es optima del todo, por ejemplo en el caso de pedir: (1,10,11,15,21) con vpp=10, el programa indica que la mejor secuencia es [(1,10),(11,5),(21,1)] = 3 paquetes y 16 posiciones, pero realmente la mejor secuencia es [(1,1),(10,6),(21,1)] = 3 paquetes y 8 posiciones de memoria. En anadido el mejor comparador a mi parecer entre soluciones es: numero_posiciones^numero_paquetes, de tal modo que 8 posiciones en 3 paquetes da 512, mientras que 3 posiciones de memoria en 8 paquetes da 6561 (lo que ademas deja claro que es peor solucion enviar muchos paquetes mal usados que pocos paquetes bien aprovechados) def compute_send(self,lista,vpp=None): # Recojo el vpp general si el usuario no lo ha indicado if (vpp==None): vpp=self.__vpp # Ordena la lista lista.sort() # Guardo la lista self.__compute_send_lista=lista # Inicia el particionamiento nueva=[] primero=True inicio=0 fin=0 posible=0 abierta=False # Procesa la lista completa for paquete in lista: # Recojo los datos del paquete if (type(paquete)==type(())): posicion=paquete[0] else: posicion=paquete # Si es el primer valor de un paquete (inicializo los valores) if (primero): # Activo inicio inicio=posicion # Activo posible posible=posicion # Activo fin fin=posicion # Indico que lo que venga no es el primer dato de un paquete primero=False # Indico que la cadena esta abierta abierta=True else: leer_siguiente=False usado=False while (not leer_siguiente): # Incremento posible posible+=1 # Compruebo si el siguiente es fantasma o real if (posible==posicion): # Actualizo fin fin=posicion # El elemento se ha usado y busco otro leer_siguiente=True # Indico que el elemento ha sido usado usado=True # Compruebo como de grande es es el paquete if ((posible-inicio+1)==vpp): # Hemos llegado al limite posible del paquete (limite VPP) nueva.append((inicio,fin-inicio+1)) # Indico que comienza una nueva secuencia if (not usado): # Inicializo los datos inicio=posicion posible=posicion fin=posicion # Indico que la cadena esta abierta abierta=True # Indico que lo que viene no es el primer dato de un paquete primero=False else: # Indico que la cadena no esta abierta abierta=False # Indico que lo que venga es el primer dato de un paquete primero=True # El elemento se ha usado y busco otro leer_siguiente=True # Añado la secuencia pendiente if (abierta): nueva.append((inicio,fin-inicio+1)) # Devuelvo el resultado return nueva # }}}1 # Filtra el resultado recibido (se usa para filtrar las soluciones) {{{1 ## Filtra el resultado recibido del servidor ## \param self - ## \param hexadecimal Cadena hexadecimal recibida del servidor ## \param start Posicion de comienzo indicada en el paquete MODBUS de solicitud ## \param lista Lista de direcciones de memoria que se deseaba obtener (se usara para concer que resultados son validos y cuales no), puede estar formada por tuplas, siempre y cuando el primer elemento de la tupla sea la posicion. ## \param palabra Longitud en bytes de cada palabra de la memoria (por defecto 4) ## \return Devuelve una lista de tuplas que contienen: (posicion de memoria, cadena con el valor en hexadecimal) o bien devuelve la tupla con el numero hexadecimal al final de la misma. def compute_received(self,hexadecimal,start=None,lista=None,palabra=4): # Si no se indica la posicion de inicio se usa la enviada en el ultimo SEND() if (start==None): start=self.__send_start # Si no se indica la lista de posiciones se usa la misma del ultimo compute_send() if (lista==None): lista=self.__compute_send_lista # Ordena la lista lista.sort() # Inicializa los datos nueva=[] i=0 indice=start # Para toda la cadena while (i