miércoles, 24 de octubre de 2012

Escribir y leer en autómatas (PLC) con Java mediante ModbusTCP

Según la Wikipedia, Modbus es un protocolo cliente/servidor empleado para la conexión de autómatas (PLC). Se trata de un protocolo abierto y de fácil implementación, cosas que han hecho que esté ampliamente extendido. Podemos consultar en la web modbus.org tanto el estándar como mucha otra documentación oficial. Modbus se encuentra dentro del nivel 7 de la pila de protocolos OSI, por lo que se establece dentro de lo que se conoce como nivel de aplicación.
El nacimiento de Modbus se remonta al año 1979 por lo que ha llovido bastante ;-) y durante estos años han surgido versiones del protocolo para poder conectar los autómatas (PLCs) en una red junto con mas autómatas o más ordenadores. Estas versiones del protocolo se conocen como Modbus/RTU si se conecta el autómata al ordenador a través de un puerto serie o Modbus/TCP si se conecta mediante Ethernet. Me centraré en Modbus/TCP que es el que he tenido que usar.

Modbus/TCP usa la trama Ethernet para encapsular las tramas de Modbus. Por lo tanto la parte de la trama Ethernet empleada para transportar datos se encarga de transportar la trama Modbus. Vamos a meternos en faena y vamos a contaros como se escribe y se lee en la memoria del autómata mediante Java.
La clase principal que se encarga de abrir las conexiones, cerrarlas, leer del autómata y escribir en él la llamaremos ClienteModbusTCP.java y la escribo a continuación:

import java.io.* ; import java.net.* ; public class ModbusTCP { private final int PUERTO = 502 ; private Socket socket = null; private OutputStream output = null; private BufferedInputStream input = null; private byte buffer[] = new byte [261]; public ClienteModbusTCP(){} // Rutina para abrir conexión de MODBUS public boolean AbrirConexion (String dns) { boolean resultado=false; try { // Crear el socket y establecer las conexiones de flujo respectivas socket = new Socket (dns, PUERTO); output = socket.getOutputStream(); input = new BufferedInputStream(socket.getInputStream()); resultado=true; } catch (Exception e) { System.out.println (e.getMessage( ) ); resultado=false; } return resultado; } // Rutina para cerrar conexión de MODBUS public boolean CerrarConexion () { boolean resultado=false; try { // Cerrar la conexi�n socket socket.close( ); resultado=true; } catch (Exception e) { System.out.println (e.getMessage()); resultado=false; } return resultado; } // Rutina para función leer de MODBUS. Código de función 03. public int []Leer_Multiples_Registros ( int unidad, // Identificador de unidad (nº esclavo - PC) int direccion, // Direccion de memoria int cantidad) { // Cantidad de registros a leer (max 126) int registros[] = new int [126]; // Buffer para colocar los valores leídos int c, i; try { // Construir la trama Modbus/TCP ?leer registros? for ( i=0; i<5; i++ ) buffer[i] = 0; buffer[6] = (byte) unidad; buffer[7] = 3; //3: leer; 16: escribir buffer[8] = (byte) (direccion >> 8); // MSB desplaza 8 bits a la derecha buffer[9] = (byte) (direccion & 0xFF); // LSB resto buffer[10] = 0; buffer[11] = (byte) cantidad; buffer[5] = 6; // nº de bytes que siguen // Enviar la solicitud al servidor output.write(buffer, 0, buffer[5] + 6); // Esperar y leer la respuesta c = input.read(buffer, 0, buffer.length); // Verificar la respuesta y extraer los valores le�dos if (c == (9+2*cantidad) && buffer[7] == 3) { for (i=0;i<cantidad;i++) { // Construir el valor de registro de los bytes alto y bajo /* * buffer de 261 bytes [0..260] * Sólo admite 256 [0..255] bytes de envío => * 126 integer [0..125] */ registros[i] = (((int) buffer[9+2*i]) << 8) & 0xFF00 | ((int) buffer[10+2*i] & 0xFF); System.out.print(String.valueOf(registros[i]) + " "); } } else { System.out.println ("Respuesta recibida erronea leer" + "\n"); registros[0] = 0; // Hacemos que no coincida el primer elemento } } catch (Exception e) { System.out.println ( e.getMessage( ) ); registros[0] = 0; // Hacemos que no coincida el primer elemento } return registros; } // Rutina para función escribir de MODBUS. Código de función 16. public boolean Escribir_Multiples_Registros ( int unidad, // Identificador de unidad (nº esclavo - PC) int direccion, // Dirección de memoria int cantidad, // Cantidad de registros a escribir (max 126) int registros[]) { // Buffer valores a escribir int c, i; boolean resultado=false; try { // Construir la trama Modbus/TCP (leer registros) for ( i=0; i<5; i++ ) buffer[i] = 0; buffer[6] = (byte) unidad; buffer[7] = 16; //3: leer; 16: escribir buffer[8] = (byte) (direccion >> 8); // desplaza 8 bits a la derecha buffer[9] = (byte) (direccion & 0xFF); // resto buffer[10] = 0; buffer[11] = (byte) cantidad; buffer[12] = (byte) (cantidad * 2); for ( i=0; i<cantidad; i++ ) { buffer[13 + 2*i] = (byte) (registros[i] >> 8); buffer[13 + 2*i + 1] = (byte) (registros[i] & 0xFF); } buffer[5] = (byte) (7 + cantidad*2); // nº de bytes que siguen // Enviar la solicitud al servidor output.write(buffer, 0, buffer[5] + 6); //System.out.println ("write"); // Esperar y leer la respuesta c = input.read(buffer, 0, buffer.length); //System.out.println ("read"); // Verificar la respuesta y extraer los valores leídos if (c == 12 && buffer[7] == 16) { // Número de bytes recibidos System.out.println (c + " ok" + "\n"); resultado=true; } else { System.out.println ("Respuesta recibida erronea" + "\n"); resultado=false; } } catch (Exception e) { System.out.println ( e.getMessage( ) ); System.out.println ("excepcion"); resultado=false; } return resultado; } }
Ahora ya podemos explicarla un poco más. Principalmente tenemos un método para abrir la conexión con el autómata mediante un socket, otro método para cerrar la conexión y luego dos métodos de lectura y escritura.
Al método que se encarga de abrir la conexión se le pasa la dirección IP al que queremos hace la conexión y se establece un socket.OutPutStream() de salida y un BufferedInputStream(socket.getInputStream()) de entrada mediante los que escribiremos y leeremos.

// Rutina para abrir conexión de MODBUS public boolean AbrirConexion (String dns) { boolean resultado=false; try { // Crear el socket y establecer las conexiones de flujo respectivas socket = new Socket (dns, PUERTO); output = socket.getOutputStream(); input = new BufferedInputStream(socket.getInputStream()); resultado=true; } catch (Exception e) { System.out.println (e.getMessage( ) ); resultado=false; } return resultado; }

El método que se encarga del cierre simplemente hace un socket.close() y si no hay problema socket cerrado.

public boolean CerrarConexion () { boolean resultado=false; try { // Cerrar la conexión socket socket.close( ); resultado=true; } catch (Exception e) { System.out.println (e.getMessage()); resultado=false; } return resultado; }
El método que se encarga de la lectura se le indica cual es la dirección IP del autómata que queremos leer, la dirección de memoria en la que queremos leer y por último la cantidad de registros que queremos que se lean. La posición de buffer[7] indica que estamos realizando una lectura o una escritura. Al indicarle un 3 estamos diciendo que procedemos a una lectura. Luego en el inputStream del socket le indicamos los parámetros de la lectura para terminar devolviendo lo leído.

// Rutina para función leer de MODBUS. Código de función 03. public int []Leer_Multiples_Registros ( int unidad, // Identificador de unidad (nº esclavo - PC) int direccion, // Direccion de memoria int cantidad) { // Cantidad de registros a leer (max 126) int registros[] = new int [126]; // Buffer para colocar los valores leídos int c, i; try { for ( i=0; i<5; i++ ) buffer[i] = 0; buffer[6] = (byte) unidad; buffer[7] = 3; //3: leer; 16: escribir buffer[8] = (byte) (direccion >> 8); // MSB desplaza 8 bits a la derecha buffer[9] = (byte) (direccion & 0xFF); // LSB resto buffer[10] = 0; buffer[11] = (byte) cantidad; buffer[5] = 6; // nº de bytes que siguen // Enviar la solicitud al servidor output.write(buffer, 0, buffer[5] + 6); // Esperar y leer la respuesta c = input.read(buffer, 0, buffer.length); // Verificar la respuesta y extraer los valores le�dos if (c == (9+2*cantidad) && buffer[7] == 3) { for (i=0;i<cantidad;i++) { // Construir el valor de registro de los bytes alto y bajo /* * buffer de 261 bytes [0..260] * Sólo admite 256 [0..255] bytes de envío => * 126 integer [0..125] */ registros[i] = (((int) buffer[9+2*i]) << 8) & 0xFF00 | ((int) buffer[10+2*i] & 0xFF); System.out.print(String.valueOf(registros[i]) + " "); } } else { System.out.println ("Respuesta recibida erronea leer" + "\n"); registros[0] = 0; // Hacemos que no coincida el primer elemento } } catch (Exception e) { System.out.println ( e.getMessage( ) ); registros[0] = 0; // Hacemos que no coincida el primer elemento } return registros; }

Y por último el método que se encarga de la escritura, es similar al anterior, pero en este caso en la posición 7 de buffer tenemos el valor 16 lo que indica que estamos realizando una operación de escritura:

// Rutina para función escribir de MODBUS. Código de función 16. public boolean Escribir_Multiples_Registros ( int unidad, // Identificador de unidad (nº esclavo - PC) int direccion, // Dirección de memoria int cantidad, // Cantidad de registros a escribir (max 126) int registros[]) { // Buffer valores a escribir int c, i; boolean resultado=false; try { // Construir la trama Modbus/TCP (leer registros) for ( i=0; i<5; i++ ) buffer[i] = 0; buffer[6] = (byte) unidad; buffer[7] = 16; //3: leer; 16: escribir buffer[8] = (byte) (direccion >> 8); // desplaza 8 bits a la derecha buffer[9] = (byte) (direccion & 0xFF); // resto buffer[10] = 0; buffer[11] = (byte) cantidad; buffer[12] = (byte) (cantidad * 2); for ( i=0; i<cantidad; i++ ) { buffer[13 + 2*i] = (byte) (registros[i] >> 8); buffer[13 + 2*i + 1] = (byte) (registros[i] & 0xFF); } buffer[5] = (byte) (7 + cantidad*2); // nº de bytes que siguen // Enviar la solicitud al servidor output.write(buffer, 0, buffer[5] + 6); //System.out.println ("write"); // Esperar y leer la respuesta c = input.read(buffer, 0, buffer.length); //System.out.println ("read"); // Verificar la respuesta y extraer los valores leídos if (c == 12 && buffer[7] == 16) { // Número de bytes recibidos System.out.println (c + " ok" + "\n"); resultado=true; } else { System.out.println ("Respuesta recibida erronea" + "\n"); resultado=false; } } catch (Exception e) { System.out.println ( e.getMessage( ) ); System.out.println ("excepcion"); resultado=false; } return resultado; }

Espero que os sirva y si tenéis alguna sobre como llamar alguno de los métodos o como implementarlo intentaré ayudaros. 
Un último comentario, ninguna de las cosas que os pongo aquí se hubieran podido hacer sino es por la inestimable ayuda del compañero y amigo @pablesite

11 comentarios :

  1. Jose, te pongo el enlace de este post en mi blog. Gracias por la mención!

    ResponderEliminar
  2. Que es la direccion de memoria, me puedes aclarar eso gracias !!!!

    ResponderEliminar
  3. Hola Jorge,
    cuando se trabaja con autómatas, se trabaja con direcciones de memoria. Se trata de acceso a variables mediante la posición de memoria en la que se encuentra. Esto suele ser de esta forma en lenguajes de bajo nivel como ensamblador o lenguaje autómata.
    Espero que te ayude!

    ResponderEliminar
  4. Gran trabajo, queria saber a que te refieres en la funcion leer_multiples_registros, que significa la unidad, a que te refieres con eso?

    ResponderEliminar
    Respuestas
    1. Cuando pones varios autómatas en la red a cada uno se le establece un identificador para poder conectarse a ellos.

      Eliminar
  5. Gran trabajo, podrias poner un ejemplo sencillo como implementar la clase..

    gracias

    ResponderEliminar
  6. Muy buen trabajo, podrías poner un ejemplo de cómo implementarlo. Gracias

    ResponderEliminar
  7. Pondré el enlace a BitBucket con el proyecto! En cuanto saque un rato jejej

    ResponderEliminar
  8. hola espero y todabvia estaes al tanto de este blog... estoy realizando un proyecto de residencia . y requiero conectar un programa hecho en java con un PLC ALLEN BRADLEY.. HASTA DONDE VEO ES POSIBLE.. pero la duda es si asi como tu realizas aqui la conexion pudiera yo enviar datos (bits ) para encender ciertos leds que cumplan con una consulta que se realize a un BD. es mas o menso el esquema general del sistemas gracias y espero respondas mi pregunta

    ResponderEliminar
  9. hola jose
    pudiers poner algun ejemplo de modbusRTU

    ResponderEliminar