Proyecto UWS (7)

publicado en: Base de datos, Programación, SCADA | 0

Esta entrada es continuación de Proyecto UWS (6).

Entrada de datos
Entrada de datos

Mi proyecto personal de entorno de desarrollo SCADA ha tomado nuevos derroteros. Tenía pendientes bastantes temas (usuarios, otros drivers, más controles para la interfaz…) pero se ha apropiado de él una necesidad que ha orientado la nueva versión. Pongo en situación: quiero almacenar una serie de valores históricos que se van a recoger manualmente, para poder analizarlos con posterioridad: sacar tendencias, exportar tablas, hacer cálculos… Podría recurrir a una hoja de cálculo, pero no es lo ideal si la tienen que manejar varias manos y el volumen de información es grande. Lo adecuado es una base de datos, pero hay que proporcionar un método de entrada sencillo, como una página web. Y aquí es donde he pensado añadir un nuevo controlador a UWS. Puede parecer forzar algo el sentido original del SCADA, pero bien pensado, ¿qué diferencia hay entre intercambiar información con un servidor físico a hacerlo con una base de datos?
El nuevo módulo, DBPLCModule hace de driver para este tipo de comunicación. La clase derivada de PLC controla la conexión, y parece natural que una tabla se amolde a una clase Memory y cada variable Tag represente una columna. La reinterpretación de los métodos existentes es también directa. ¿Qué devuelve get()? El valor más reciente. Para eso, hay que poder ordenarlos temporalmente, y eso requiere una columna en cada tabla de tipo datetime. Es consistente con la necesidad de almacenar valores históricos. ¿Qué almacena set()? El valor actual. O uno antiguo, aprovechando el polimorfismo. ¿Qué hace el método connect() del PLC? Conectarse a la base de datos y actualizar en el motor el valor de las variables de acuerdo a su contenido. También se añade un escaneo periódico, porque otra aplicación podría estar actualizando por su cuenta la información. Aquí he añadido una funcionalidad interesante: cuando llamamos a este método, DBPLC tiene ya toda la información necesaria sobre la base de datos, tablas y columnas, así que, ¿por qué no crear la estructura si no existe previamente? Hecho, ni siquiera debemos molestarnos en levantar todo este andamiaje, basta con la clásica llamada a ensemble.deploy() para tenerlo todo funcionando.

Hasta aquí lo directo, pero sigue siendo insuficiente para responder a las necesidades iniciales. Para ello es necesario añadir algunas funciones extra:

  • Datapicker
    Datapicker

    Tenemos que insertar nuevos datos. Y no nos vale Tag.set(), porque debemos poder agregar un registro completo. Se resuelve con el método Memory.set_row(), al que se le pasa como argumento un diccionario con los valores de las columnas y, opcionalmente, la fecha y hora de registro. Esta función clama a gritos un nuevo control en la interfaz, que tiene que ser un formulario. He adoptado el mismo criterio de simplicidad que otras veces: el atributo data-table lo liga a nuestro control, y nos podemos olvidar de código extra.
    El código en j.js busca todas las variables (atributo data-dir) en su interior, actualiza los valores iniciales y añade los eventos necesarios. El campo con atributo data-datetime es opcional, y se convertirá en un calendario para seleccionar fecha y hora de inserción. Para quien se lo pregunte, no necesitamos hacer referencia a la tabla, ya que está implícita en la memoria a la que pertenecen las variables.

  • Es necesario también un método para reclamar los valores de una variable entre dos fechas. Éste es Tag.get_data(), al que se le pasan los instantes de inicio y el de fin, y devuelve un listado de pares [tiempo,valor]. Esto va a ser útil para, por ejemplo, trazar una tendencia. También va a ser sencillo para el desarrollador, basta incluir un div con las variables dentro del atributo data-trend, y UWS lo transformará en una gráfica.

    Tendencia
    Tendencia

    En este caso va a ser importante añadir un id, para en el futuro discriminar acciones sobre diferentes tendencias. Para este elemento he recurrido a la estupenda librería Flot. De momento, las tendencias de UWS están en una fase muy inicial, y necesitan controles extra para moverse, añadir y eliminar variables, ampliar y reducir, usar varias escalas, etc.

Faltan todavía más cosas. Es necesario poder exportar datos, consultar valores en una fecha determinada, quizás editar la información… Además de la gestión de usuarios. Será en próximas entregas.
Recursos asociados:
SCADA UWS (versión 1.4) y código de ejemplo.

Proyecto UWS (5)

publicado en: Programación, SCADA | 0

Esta entrada es continuación de Proyecto UWS (4).
Hora de cambiarlo todo. Era necesario para que UWS avanzase en las líneas que expuse hace poco. Las principales novedades son:

  • Se vuelca todo el peso en la clase Tag, y se eliminan mapeos de memoria de controladores. Por supuesto, debe seguir habiendo referencias a direcciones, pero a partir de ahora las memorias se gestionan como un diccionario de variables. ¿Cómo se determinan las tramas de un PLC Modbus, por buscar una pega? Sencillo, las memorias pueden ser de dos tipos: ordenadas y no ordenadas. En las primeras, el índice es numérico, y las tramas de lectura se pueden resolver con facilidad a partir de sus valores. Pero el objeto PLC no copia toda la memoria del controlador, sólo contiene las variables que se necesitan.
    Una de las consecuencias de esta decisión es que se eliminan los direccionamientos absolutos. Desde el interfaz ya no se puede acceder a áreas aleatorias de la memoria, lo cual podía ser un foco de inseguridades. A partir de ahora, las referencias sólo podrán hacerse a las variables dadas de alta en el motor.
  • Se eliminan los escaneos para actualizar valores. En vez de ello, cada variable mantiene una lista de suscriptores que son avisados ante sus cambios de valor. Un suscriptor es cualquier objeto derivado de la clase Subscriptor, y debe poseer un método update. Esto mejora la eficiencia y hace que las actualizaciones sean todo lo ágiles que se puede. Cada cambio de valor se notifica inmediatamente a todos los elementos que de ella dependen: alarmas, históricos, interfaz, cálculos… Por otro lado, muchos protocolos de comunicación con controladores funcionan de este modo, así que era una transición inevitable.
  • La interfaz es un suscriptor más de una lista de variables. Esto demanda poderosamente introducir los websockets de HTML5. La interfaz ahora usa dos puertos: uno para solicitar ficheros y otro nuevo para establecer una conexión bidireccional para el envío y recepción de información. Como antes, se gana en eficicencia, velocidad y se reduce el ancho de banda consumido por las comunicaciones.

Aunque hay algún cambio más, me voy a centrar en describir estos puntos. Respecto al primero, mi intención es que todo lo que contenga un valor derive de la clase Tag. Se me ocurre en un primer momento esta jerarquía de clases:

Jerarquía de variables
Jerarquía de variables

En bordes punteados se marcan las clases que todavía no están definidas. La intención es que existan al menos estos tipos de variables:

  • IOTag. Se lee de un controlador, y por tanto contiene una referencia a dicho equipo, el área de memoria donde se ubica y su dirección. La principal particularidad es que redefine el método set para enviar por comunicaciones los cambios de valor.
  • MemoryTag. Se aloja en memoria. Lo adecuado sería establecer un mecanismo para que algunas de estas variables preserven su valor en cada arranque de la aplicación.
  • Expression. Como he comentado, se trata como variable cualquier elemento que arroje un valor, dotado de una lista de suscriptores para advertirlos de su modificación. De acuerdo a este criterio, una expresión debe ser considerada un tipo de variable. La clase Expression permite definir un cálculo dependiente de otras variables. Tiene herencia múltiple por tanto, de Tag y Subscriptor. Por el momento sólo contempla expresiones básicas: suma, resta, multiplicación, división, negación, igualdad, inferioridad, superioridad y anidamientos mediante paréntesis.
  • Alarm. Definida como una expresión, una alarma debería llevar a cabo ciertas acciones de notificación cuando se activa: almacenarse en un fichero, mostrarse en los HMI, enviar un correo…
  • TrendTag. Nuevamente, es una expresión (aunque su contenido puede ser una variable pura) cuyo valor se almacena periódicamente en base de datos o archivos.

En lo que toca al desarrollo web, se mantiene la misma estructura del fichero html y la página tiene las idénticas funcionalidades. Sin embargo, al usar websockets, se elimina el método de refresco periódico de los valores, y se minimiza el tiempo de actualización de la información. Por comentar brevemente el código en Javascript: cuando el documento está listo, tras asociar los eventos a la página, se instancia un websocket y se le asocian eventos.

El primero, onopen, registra (se suscribe) a las variables contenidas en la página. Como decíamos en entradas previas, el nombre de la variable se indica en el atributo data-dir. La suscripción se lleva a cabo enviando un JSON que especifica la acción (subscribe) y el listado de variables:

UWS responderá con un mensaje (que se gestiona con el evento onmessage) que contiene los valores de dichas variables:

Por último, los envíos de consignas u operaciones se hacen mediante el método send de websocket, también codificados en JSON:

En próximas entradas avanzaremos en las alarmas e históricos, e incluiré algún driver más de comunicaciones.
Esta entrada continúa en Proyecto UWS (6).
Recursos asociados:
SCADA UWS (versión 1.2) y código de ejemplo.

Proyecto UWS (4)

publicado en: Programación, SCADA | 0

En la entrada previa sobre UWS terminaba proponiendo unas líneas de evolución del proyecto. Una de ellas era permitir la utilización de nombres simbólicos, en lugar de direccionamiento absoluto. Como se trata de algo casi directo de hacer (basta añadir un diccionario al ensemble, un método de importación y pequeñas modificaciones del servidor web), me he puesto manos a la obra. El uso de simbólicos permite al programador abstraerse de la estructura interna de los controladores, hace el desarrollo más inteligible, y con ello reduce las posibilidades de equivocación. En la nueva versión es posible agregar un símbolo de la siguiente manera, en Python:

Nota: Aprovecho para decir que he renombrado las áreas del controlador Modbus, y he dejado sus nombres en singular (coil, input, holding y register).
Aunque es posible incorporar los nombres simbólicos picando código, es obvio que no resulta eficiente. Lo ideal es trabajar con ellos en una hoja de cálculo, e importarlos al ensemble. Por ejemplo, podemos crear el siguiente archivo csv, en el que la primera línea contiene los encabezados y las siguientes, en orden, el nombre de la variable, controlador, área de memoria y posición. El separador usado es el punto y coma, para evitar ciertos problemas clásicos con el punto decimal en Windows y Excel.

Datos en csv
Datos en csv

Incorporar estos datos es tan sencillo como hacer una llamada al método importtags del ensemble, pasándole la ruta:

Hecho esto, la lectura y escritura de una variable se puede efectuar a través de los métodos get y set de Tag:

En este punto, existe una posibilidad de mejora del proyecto que reclama nuestra atención. En la versión previa de UWS existía el método fast_deploy, que permitía crear automáticamente los grupos de escaneo de la memoria de los controladores. Esta función permitía desentenderse de la tarea de definir una por una las zonas con información relevante. No obstante, si resolvía un problema, generaba otro, y es que, al desconocer qué datos son de interés para el usuario, se opta por mapear toda la memoria. Sin embargo ahora, una vez hemos importado todas las variables que va a usar la aplicación, se hace evidente que conocemos dónde se ubica la información útil. Sólo hay que revisar las direcciones de la tabla que hemos importado, determinar qué zonas de memoria se demandan, y dentro de cada una de ellas, cuál es el primer y último valor. Con esto, es inmediato definir las áreas y grupos de escaneo. Así, el nuevo método mejorado para lanzar las comunicaciones es smart_deploy:

Hasta aquí lo que toca al motor de comunicación con los controladores. Respecto al servidor web, las peticiones GET /?r y /?w se han redefinido para atender a nombres simbólicos. He mantenido el direccionamiento absoluto, pero con llamadas /?dr y /?dw. Es decir, podemos tener cinco respuestas a una URL:

  • http://[host]:[puerto]/?r:[símbolo] devuelve el valor de la variable con dicho símbolo.
  • http://[host]:[puerto]/?w:[símbolo][valor] escribe el valor de la variable con dicho símbolo.
  • http://[host]:[puerto]/?dr:[plc][memoria][elemento] devuelve el valor de dicho elemento en la memoria de su controlador.
  • http://[host]:[puerto]/?dw:[plc][memoria][elemento] escribe el valor de dicho elemento en la memoria de su controlador.
  • http://[host]:[puerto]/[fichero] entrega un fichero.

Al renombrar la sintaxis de las peticiones para asociar ?r y ?w a los nombres simbólicos, no es necesario actualizar j.js, que ahora trabaja directamente con símbolos en lugar de direccionamientos absolutos.
Otra tarea pendiente era mejorar la parte gráfica. En realidad esto no es una evolución de UWS propiamente hablando, ya que queda en el lado del desarrollo web. Sin embargo, he querido introducir algunas modificaciones para mostrar el potencial de un SCADA de este tipo. No me voy a entretener en describir cómo funciona el nuevo j.js (que será bastante evidente para quien se maneje con Javascript), pero sí debo comentar qué hace con los atributos que localiza en la página:

  • data-dir: Es el atributo que define un elemento como dinámico, y contiene el nombre simbólico que se va a representar. Por defecto, j.js decide qué hacer según la etiqueta que lo contenga:
    • Los campos de texto se interpretan como un espacio para introducir un valor numérico. Se usarán por tanto en la práctica para consignas.
    • Los checkbox permiten modificar valores digitales (un marcha/paro, automático/manual, etc.).
    • Si el atributo pertenece a una etiqueta p o spam, se entiende que su contenido es un valor que se debe visualizar (puede ser numérico o booleano).
    • Por último, si se aplica a una capa o div, el refresco se hace sobre su visibilidad.
  • data-css: Si está presente, las modificaciones no se hacen conforme a lo anterior, sino sobre el elemento de estilo contenido en este atributo. Podemos por tanto, a través de este atributo, alterar el tamaño de un elemento, su visibilidad de nuevo, color, etc.
  • data-transform: Permite realizar transformaciones sobre el valor. Se pueden anidar varias, separadas por punto y coma:
    • invert invierte el valor de una señal digital.
    • scale:[x0],[x1],[y0],[y1] lleva a cabo un escalado, conforme a la recta (x0,y0)-(x1,y1).

    Mediante estas nuevas funcionalidades, se puede hacer un SCADA perfectamente funcional como el que se muestra en la siguiente imagen:

    SCADA gráfico UWS
    SCADA gráfico UWS

    Para quien desee hacer pruebas, dejo enlace al código, como otras veces.
    Esta entrada continúa en Proyecto UWS (5).
    Recursos asociados:
    SCADA UWS (versión 1.1) y código de ejemplo.

Proyecto UWS (3)

publicado en: Programación, SCADA | 0

En la entrada previa mostré cómo se configuraba una red de controladores en UWS, y concluía con el arranque del servidor web. Quedó pendiente cómo desarrollar la interfaz del SCADA. Aunque al acometer la parte gráfica no es necesario comprender bien cómo funciona el propio servidor, resulta conveniente para conocer sus posibilidades. Cuando lo instanciamos se le pasan tres parámetros: una tupla con la interfaz y puerto de escucha, el manejador y el ensemble:

Hecho esto, una hebra se encargará de atender las peticiones HTTP que lleguen por dicho puerto con el método GET. En román paladino, cuando escribamos la dirección del servidor en un navegador recibiremos una respuesta, en función de la URL que se esté demandando. El formato de una petición sería como sigue:

Respecto a la dirección del servidor no creo que haya lugar a dudas. Si hemos arrancado el UWServer con la línea antedicha y queremos acceder al SCADA desde el propio equipo, escribiríamos en el navegador http://127.0.0.1:80 (o http://localhost:80, o simplemente http://localhost). La parte interesante está en la cadena recurso. Tenemos tres alternativas:

  • Lectura de un valor. Para conocer un valor de un controlador, el recurso debe adoptar la forma ?r:[plc]:[memoria]:[elemento]. Por ejemplo, si en la situación descrita en la entrada previa queremos conocer la entrada analógica 2 (la tercera, ya que empezamos a contar por 0), la URL que debemos escribir sería http://localhost:8080/?r:plc1:registers:2. El navegador nos devuelve directamente su valor en HTTP. Sencillo, ¿no?
  • Escritura de un valor. De la misma forma, podríamos modificar un valor en uno de los controladores. La cadena de recurso en este caso sería ?w:[plc]:[memoria]:[elemento]:[valor]. En la misma situación anterior, si quisiésemos escribir un 50 en el primer registro de retención (dirección 0), lo haríamos pidiendo desde el navegador http://localhost:8080/?w:plc1:holdings:0:50. Podemos ver cómo en el controlador, dicho registro se pone en 50.
  • En cualquier otro caso, se nos devolverá, como un servidor web al uso, el fichero localizado en la ruta relativa, que por defecto es www. Por ejemplo, podemos crear una página de bienvenida index.html, situarla dentro de www, y verla con http://localhost:8080/index.html (o de forma más simple, http://localhost:8080).

No necesitamos nada más para elaborar la interfaz. Con esto es suficiente para crear una página web, y un poco de Javascript nos debería permitir el refresco de la representación y el envío de las escrituras. Se puede objetar que el código que habría que elaborar es engorroso, pero aquí es donde entra en juego la magia de jQuery Ajax. En el ejemplo que hemos dado, he incluido el fichero j.js, que permite olvidarse de toda esta tarea (en realidad, de todo lo que he explicado hasta el momento). Su uso es simple, y sólo hay que hacer dos cosas:

  • En la página que desarrollemos hay que incluir una referencia a jQuery y a j.js:
  • A cada elemento dinámico hay que agregarle el atributo data-dir, cuyo valor será la dirección del dato que se desea representar. La sintaxis es la misma antes descrita ([plc]:[memoria]:[elemento]). Es decir, para mostrar el estado de la bobina número 1, el código HTML sería el siguiente:
    Si además nos interesa poder modificarlo, la etiqueta adecuada es sin duda un input:

Con estos atributos data-dir, podemos desentendernos de la programación asociada a la página. El código en j.js hace lo necesario. Por un lado asocia a las etiquetas susceptibles de ser modificadas (input de tipo checkbox y text) los eventos correspondientes, de modo que ante un cambio envíen la escritura al servidor. Por otro, se lanza un temporizador para refrescar periódicamente los valores, tanto de las etiquetas anteriores como de los div, que no se pueden modificar. El tiempo de refresco, que es de 1000ms por defecto, se puede determinar mediante el siguiente elemento oculto HTML:

Por supuesto, todo lo descrito hasta aquí es muy mejorable. Así a bote pronto se me ocurren las siguientes líneas de desarrollo para las próximas versiones de UWS:

  • Utilizar los websockets de HTML5 para el refresco de la página, en vez de solicitar periódicamente los datos, mediante un modelo de suscripción. Con esto se gana agilidad y se disminuye el tráfico de red.
  • Añadir nuevas funcionalidades a j.js, como alterar la visibilidad de elementos, dimensiones, etc. Mediante atributos adicionales, también se puede agregar transformaciones sobre valores, como inversión o escalado.
  • Introducir en el ensemble un diccionario, para referenciar los datos mediante nombres simbólicos. Lo ideal sería cargar dichos nombres desde fichero o base de datos, en lugar de picarlos en Python.
  • Agregar nuevos protocolos de comunicación con controladores. También otras formas de conectividad con el ensemble.
  • Por último, un SCADA no puede estar completo sin alarmas e históricos.

Esta entrada continúa en Proyecto UWS (4).
Recursos asociados:
SCADA UWS (versión 1.0) y código de ejemplo.

Proyecto UWS (2)

publicado en: Modbus, Programación, SCADA | 0

He terminado la fastidiosa tarea de documentar el Proyecto UWS. Y de camino me ha servido para corregir un par de errores. Como comentaba, el UWS es un esbozo de SCADA muy en pañales. Corre en Python (yo uso concretamente la versión 3.5) y la interfaz es HTML estándar. De momento sólo comunica por Modbus TCP, pero todo se andará.
Los módulos y clases de los que se compone son los siguientes:

  • PLCModule:
    • PLC: Representación de un controlador genérico.
    • Memory: Representación de una de las zonas de memoria de un PLC. En el caso de un equipo Modbus, van a ser bobinas, entradas digitales, registros de retención y registros de entradas. Pero otros controladores exigirán otra distribución.
    • Area: Segmento de memoria. Su uso es interno, normalmente no va a requerir que el programador la ataque.
    • Poll: Conjunto de áreas (información) que se piden con una misma frecuencia de muestreo. Por ejemplo, valores que cambian muy rápido o son críticos requieren refresco con intervalo reducido.
  • MBPLCModule:
    • MBPLC: Controlador con comunicación Modbus. Deriva de la clase PLC.
  • EnsembleModule:
    • Ensemble: Vendría a ser una red de controladores. Pero en realidad no hay exigencia real de que se encuentren en la misma red. En el fondo es un conjunto de controladores y de grupos de muestreo.
  • UWServerModule:
    • UWServer: El SCADA propiamente dicho. En principio sólo integra los componentes.
    • WebServer: El servidor web.

Me va a perdonar el lector por el revuelto de idiomas: clases, atributos, métodos, etc. están nombrados usando el inglés, pero tengo poca soltura y mucha pereza para usar la misma lengua para los docstrings, de modo que los he redactado en español.
El siguiente gráfico muestra los componentes de un proyecto:

Componentes de UWServer
Componentes de UWServer

En la otra entrada puse un ejemplo de cómo se llevaría a cabo un proyecto, sin apenas dar explicaciones. Me gustaría repasarlo de nuevo, explicando más en detalle los pasos. Básicamente el desarrollo se realiza en dos espacios. En Python se define la estructura del proyecto (controladores, conexiones, muestreos…) y en HTML se compone la interfaz del SCADA. Comencemos por la primera, que nos va a llevar el resto de la entrada. Por lo usual, en primer lugar debemos instanciar un Ensemble, es decir, una red de controladores.

En nuestro ejemplo sólo vamos a requerir que el ensemble tenga un controlador, al que vamos a llamar plc1. Como comunicamos con él en Modbus TCP, hay que indicarle su dirección IP. También cuánto ocupan sus distintas áreas de memoria (coils, inputs, holdings y registers). La clase nos permite aportar más parámetros, como puerto de conexión (típicamente 502), método, unidad (número de esclavo), reintentos caso de fallo de comunicación. También atributos para saber si se ha establecido la comunicación, métodos para leer o escribir datos, e incluso podríamos definir áreas adicionales de memoria con un sentido algo peregrino. Pero para nuestro ejemplo no nos vamos a complicar: la longitud de todas sus áreas de memoria va a ser de 20 elementos y el resto de parámetros quedan por defecto. También aprovechamos para agregarlo al ensemble en el mismo paso:

Ahora vendría la tarea fastidiosa de definir diferentes muestreos, según periodo de refresco, y las áreas que se leen. Un ejemplo de cómo se crearía uno de estos grupos es el siguiente:

Hago notar que propiamente hablando, el tiempo de muestreo es “entre muestreos”. Es decir, el código espera a que se hayan completado las comunicaciones (o hayan devuelto error) antes de empezar a contar para las siguientes peticiones. Estamos hablando comunicación desde un SCADA, no en un bus de campo, con lo que podemos ser algo laxos en este sentido. Se evita así además saturar las comunicaciones por solapamientos. Para continuar con el ejemplo, ahora habría que iniciar las comunicaciones con cada uno de los grupos de muestreos definidos.

Aunque de esta forma se controla de forma fina cómo se van a llevar a cabo las comunicaciones, he querido incorporar un método más ágil. fast_deploy crea un único grupo de muestreo con el tiempo por defecto, añade todas las áreas de memoria al completo de todos los PLC que posea el ensemble, e inicia sus comunicaciones de una tacada:

En este momento tenemos el motor de comunicación en marcha, y podemos leer y escribir de la forma siguiente:

En estas circunstancias, tan solo nos falta lanzar el servidor web. Lo normal sería usar el puerto 80, pero se puede usar cualquier otro disponible, como el 8080. Por supuesto, tenemos libertad para emplear más de un puerto con diferentes servidores, y el mismo ensemble. Puede ser interesante conjugado con otro de los parámetros, la ruta relativa de los ficheros HTML, que por defecto es www.

Queda discutir la segunda parte, donde entraremos en el desarrollo de la interfaz web. Eso será en una próxima entrega.
Esta entrada continúa en Proyecto UWS (3).
Recursos asociados:
SCADA UWS (versión 1.0) y código de ejemplo.