Todos sabemos que Ruby no es muy rápido que digamos. Hay quienes opinan que pudiera ser al menos tan rápido como Python, pero el caso es que esto todavía no es así. Quizás esto del "duck typing" hace que el interpretador se comporte como una verdadera tortuga. Pero como toda tortuga es lento pero hace el trabajo y llega a la meta.

En estos últimos días he estado usando Ruby para escribir un prototipo de un futuro software. Es más bien algo como una prueba de concepto. La idea se circunscribe al ámbito del tiempo real, aunque de verdad para lo que queremos el tiempo no es tan crítico. No es tiempo real como el que toman en cuenta los que escriben software para visualizar video, o reproducir mp3, o para controlar el vuelo de un avión. No, no es para eso que lo quiero. Mi idea es usar un programa escrito en Ruby para leer datos en tiempo real de un dispositivo remoto de adquisición de datos. Estos dispositivos son pequeñas computadoras que recogen información de un proceso industrial y la colocan en diversos sitios de una red para su visualización, almacenamiento o posterior análisis. Son conocidos en la jerga de la automatización industrial como PLC's (controladores lógicos programables) o RTU's (unidades terminales remotas).

La forma en que se leen los datos de estos dispositivos es usando TCP/IP con un protocolo muy sencillo llamado Modbus. Así que me dispuse a crear una clase, muy preliminar, para que interrogara al PLC y leyera lo que éste responde.

La lógica es muy simple. Utilicé por supuesto las recomendaciones que la misma gente que escribió el protocolo sugiere. Primero se crea un socket TCP para conectarnos al PLC. Para esto instanciamos un objeto TCPSocket de la biblioteca estándar de Ruby. Este objeto lo instanciamos con la dirección IP y el puerto 502 que es el que usa Modbus. Luego se prepara la pregunta al PLC. Esto consiste en armar una cadena de caracteres que contenga un encabezado, la dirección del PLC, la función que queremos que ejecute el PLC y los registros de memoria que queremos leer. Algo como esto:

RUBY:
  1. request = sprintf("%c%c%c%c%c",0,0,0,0,0) # Encabezado de la solicitud
  2. request += sprintf("%c%c",6,@devaddress) # No. de bytes y dirección del PLC
  3. request += sprintf("%c",@funcode) # Código de función Modbus
  4. request += sprintf("%c%c",byte8,byte9) # Bytes de la dirección inicial
  5. request += sprintf("%c%c",byte10,byte11) # Bytes del número de registros a leer

Luego de armar la solicitud enviamos esto al socket en cuestión:

RUBY:
  1. @socket.send(request, 0)

Luego calculamos el número de bytes que se supone nos deba devolver el PLC. Esto para dimensionar el buffer de lectura. Después de hacer esto simplemente leemos lo que está en ese buffer.

RUBY:
  1. @nobytes = 9 + (@norecords.to_f/8.0).round
  2. result = @socket.recv(@nobytes)

El resultado pues lo podemos imprimir en pantalla en formato hexadecimal:

RUBY:
  1. print "Rx ="
  2. lon = result.length
  3. lon.times do |i|
  4.     printf("%02x ",result[i])
  5. end
  6. print "\n"

Por supuesto que no todo termina acá. Esto es apenas el comienzo de la construcción de una verdadera clase que tome en cuenta todo lo que pudiera ocurrir mientras nuestra computadora lee los datos de este PLC. Por lo tanto la clase para que esté completa hay que agregarle el manejo de las excepciones y algo muy importante, el que hacer si no se reciben datos una vez que ya se ha iniciado el proceso de lectura. Para esta última parte utilicé la clase Thread de Ruby la cual es muy fácil de usar y es bien poderosa. Lo que hago es crear un hilo donde leo el buffer del socket y otro donde voy contando el tiempo. Si no hay nada en x segundos considero que se interrumpió la comunicación y mando un mensaje. Se puede mejorar de muchas formas, pero este código me funciona para mí:

RUBY:
  1. @socket.send(request, 0)
  2. @nobytes = 9 + (@norecords.to_f/8.0).round
  3. begintime = Time.now
  4. result = nil
  5. t1 = Thread.new {result = @socket.recv(@nobytes)}
  6. t2 = Thread.new{ while true
  7.             endtime = Time.now
  8.             if (endtime - begintime)> 2 and result == nil then
  9.                 t1.kill
  10.                 result = "0"
  11.                 @exc = "Time out"
  12.                 break
  13.             elsif result != nil
  14.                 @exc = "Reading fine!"
  15.                 break
  16.             end
  17.         end
  18.         }
  19. t2.join

El resto de la clase son varias funciones para decodificar los datos que provienen de la variable result. Algunas de ellas bien interesantes como por ejemplo la que convierte de hexadecimal a un float en precisión sencilla usando el estándar IEEE 754 y lo contrario, de decimal a hexadecimal. Todavía me falta codificar un montón de cosas y algo muy importante que me gustaría hacer que es una verdadera prueba de carga, ver con cuantos datos es que se atora nuestro lector. Pero todo esto es materia para varias notas más.


9 Respuestas a “Ruby para leer datos en tiempo real”

  1. 1 Kodegeek

    Muy interesante la aplicación, al fin veo algo escrito en RUby que no sea Rails :)

    El ejemplo está genial, espero ver más código como este por el blog. Lo que no me quedó muy claro es como sincronizas a las hebras de ejecución y evitas condiciones de carrera...

    Saludos.

  2. 2 caronte

    mejor usar esto para controlar el timeout?

    require 'timeout'
    .
    .
    Timeout::timeout(2) do
    begin
    result = @socket.recv(@nobytes)
    @exc = "Reading fine!"
    rescue Exception
    @exc = "Time out"
    end
    end

  3. 3 Rómulo Rodríguez

    caronte, eso fue lo primero que pensé, pero la lectura del buffer no levanta ninguna excepción. Al parecer pasa por ahí y devuelve un nil nada más, por eso fue que se me ocurrió abrir un hilo que contara un par de segundos. Pero por supuesto que el código ni de broma está terminado, la versión que tengo ahora es ligeramente diferente ya que incluye un poco de más control sobre la ejecución de los hilos. Otra cosa que no se ve en esta muestra que publiqué es que todo esto corre dentro de un hilo maestro que está continuamente corriendo en un lazo infinito haciendo barridos. Allí tuve que considerar el manejo de las excepciones que ocurren si se interrumpe la comunicación, o si el PLC se apaga y el socket se destruye, cosas así. Supongo que falta mucho para que sea óptimo, pero al menos funciona como prototipo.

  4. 4 Markus Sorensson

    Lo que no veo en todo esto es donde está el tiempo real. Ruby no es concurrente, por eso que digas que es tiempo real no me parece adecuado . Los datos están generándose en tiempo real, cierto, pero no se están leyendo en tiempo real. Cómo garantizas eso con Ruby?

  5. 5 Caronte

    Rómulo, a mi me funciona:

    Prueba este fragmento tal y como está:
    ------------------------
    require 'timeout'
    require 'socket'
    HH='www.google.es'
    sock = TCPSocket.new(HH, 80)
    Timeout::timeout(2) do
    begin
    sock.print("GET / HTTP/1.1\r\nHost: #{HH}\r\n\r\n")
    #sock.print("GET / HTTP/1.1\r\nHost: #{HH}\r\n\r")
    puts sock.gets #solo recupera el status: HTTP/1.1 200 OK
    sock.close
    rescue Exception => bum
    puts "Time out: " + bum.to_s
    end
    end
    ----------------------
    Despues, prueba comentando la linea:
    sock.print("GET / HTTP/1.1\r\nHost: #{HH}\r\n\r\n")
    y descomentando:
    #sock.print("GET / HTTP/1.1\r\nHost: #{HH}\r\n\r")
    El protocolo, en este segundo caso es incorrecto (el servidor web se queda esperando \r\n\r\n indefinidamente), y se sale por 'Time out: execution expired'.

    De todas formas, la clase Timeout supongo que en la práctica hace lo que haces tú; ejecutar en una hebra el bloque y lanzar una excepción si la hebra no ha terminado en el tiempo dado.

  6. 6 Luis Lavena

    @Markus Sorensson:

    Lo que creo que se refiere es obtener los datos de un dispositivo que SI los obtiene en tiempo real.

    Generalmente esos dispositivos poseen un buffer circular y puedes consultar las ultimas N operaciones (o registros de operaciones).

    Estaciones meteorologicas o redes de sensores funcionan de esa manera.

    Algunos consideran eso tiempo real, otros no. Yo por mi parte hago control de flujos MPEG2 e inserción de publicidad de video en tiempo real (pero no con ruby).

    @Rómulo:

    No pensaste usar el método pack de la clase Array? "ruby-doc":http://www.ruby-doc.org/core/classes/Array.html#M002272, es mas al estilo de Ruby que el codigo estás empleando (sprintf... malos recuerdos de C).

    Saludos,

    Luis

  7. 7 Rómulo Rodríguez

    Markus, tal como dice Luis a eso es lo que los ingenieros de automatización llamamos tiempo real. Lo interesante es que estos dispositivos pudieran etiquetar los datos con marcas de tiempo que al ser interrogados por un programita como este permitiría analizar los datos tal como ocurrieron. Otra cosa interesante es la resolución a la que se muestrean los datos. Evidentemente dado lo lento de Ruby nunca se podrá obtener una resolución de tiempo lo suficientemente buena como para ciertos procesos críticos, pero siguen habiendo muchas aplicaciones donde los procesos son lentos y quizás no te interese saber en que milisegundo ocurrió el evento sino nada más en que segundo o en que minuto.

    Caronte, voy a probar a ver.

    Luis, en algún momento pensé en serializar los datos usando pack pero no lo he probado.

  8. 8 Rómulo Rodríguez

    Con el método pack de la clase array todo el primer chorizo de armar el arreglo request quedaría reducido a esta línea:

    RUBY:
    1. request = [0, 0, 0, 0, 0, 6, @devaddress, @funcode, byte8, byte9, byte10, byte11].pack("cccccccccccc")

    Más bonito como lo recomienda Luis.

  9. 9 Luis Lavena

    Aún podes hacerlo más bonito:


    request = [0, 0, 0,
    0, 0, 6,
    @devaddress, @funcode, byte8,
    byte9, byte10, byte11].pack("c12")

    #pack permite agrugar series de tipos de datos, asi que en lugar de decir N veces un tipo byte (o char), le indicas "un tipo char 12 veces".

    Además, como estas trabajando con un Array, podes expandirte en más de una linea si es necesario para darle claridad.

    Un ejemplo seria si los primeros bytes (que ahora estan en cero) fueran otras variables de tu instancia de clase.

    Pero bueno, siempre se puede refinar todo un poquito más, esa es la magia del refactoring :-)

Añade un Comentario





RSS feeds

Suscríbete a nuestros RSS Feeds