UWS con alarmero e histórico

Proyecto UWS (6)

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

UWS con alarmero e histórico
UWS con alarmero e histórico

El anidamiento de clases en Python no tiene otra función que la de organizar el código. ¡Pero el orden es importante! En la última versión de UWS he reestructurado el código para darle una estructura menos difusa. También he renombrado algunos métodos para hacer la nomenclatura más coherente. En lo que toca a funcionalidad, hay muchas posibilidades nuevas: alarmas, driver OPC, registro en ficheros, base de datos, etc. Voy a ir comentando de forma resumida:

  • La clase Tag ya no está aislada, sino dentro de la clase Memory, que a su vez está en PLC. Por supuesto, se puede crear una variable sin asociarla a un controlador (de hecho, las alarmas van a tener ese tratamiento), pero creo que esta estructura es más natural. Este cambio organizativo afecta varios aspectos. En primer lugar, se funden los módulos PLCModule con TagModule dentro del último. Los diferentes drivers, en módulos independientes, se expresan como derivados de la clase PLC, y redefinen si es preciso los atributos y métodos de Memory y Tag. En segundo lugar, desaparece la clase IOTag. Bien pensado no tenía mucho sentido: todas las variables se pueden escribir y leer, ¿por qué habría que hablar de variables de entrada/salida? Si es preciso, como se ha dicho, se derivará la clase Tag en el correspondiente módulo del driver.
  • Aunque no tengo espacio para detenerme a comentar todas las clases, creo que Tag merece cierta atención por su relevancia. Cada variable tiene un identificador, que debe ser único dentro del Ensemble. Puede pertenecer a un controlador, en cuyo caso formará parte de una memoria, dentro de la cual tendrá una dirección; o estar disociada. También posee una descripción, útil para ciertos registros, que comentaré más adelante. El valor de una variable se lee con get() y se modifica con set(). Al principio comencé usando métodos mágicos de Python (__get__(), __set__(), __getitem__(), __setitem__()…), pero lo he abandonado, porque interfieren con el uso de listas o diccionarios. Por poner un ejemplo, si hacemos listadetag[«tag1»]=5, posiblemente estamos deseando llamar al método __set()__ de la variable listadetag[«tag1»], pero en realidad lo que hacemos es asignar un 5 al elemento «tag1» de la lista, lo que es un destrozo mayúsculo. Para evitar confusiones, me he desecho de casi todos los métodos mágicos. Sólo he guardado una excepción con la asignación de controladores al Ensemble, donde no cabe solapamiento. Hecho este largo inciso, comentar que la clase Tag tiene también un método update(), de uso interno para los drivers. Así, una clase derivada puede redefinir set(), y llamar posteriormente a update() para las obligadas gestiones, como advertir a todos los objetos suscritos de los cambios de valores. ¿Qué es una suscripción? Lo comento en el siguiente punto.
  • Cuando queremos lanzar una serie de acciones ante los cambios de una variable, creamos un objeto derivado de la clase Subscriptor, se lo pasamos a la variable mediante Tag.subscribe() y redefinimos el método Subscriptor.update() para programar dichas acciones. El sistema es rápido, eficiente y enormemente versátil. Tanto que todo el SCADA se ha redefinido para funcionar de esta guisa. Las actualizaciones de la interfaz gráfica se hacen por suscripción; también las alarmas, las expresiones calculadas, los registros… Vayamos por partes.
  • Como comenté en la pasada entrada, una expresión es una evaluación de operaciones sobre variables; por ejemplo (nivel<5)+(nivel>90)+boya_minimo+boya_maximo. La clase Expression aprovecha la herencia múltiple para derivar tanto de Subscriptor como Tag. Esto quiere decir que será advertida de la modificación de los valores que la conforman, pero que también, al constituir una variable en sí misma, puede advertir a otros elementos de sus cambios. En concreto, el ejemplo anterior sería una alarma; pero también podríamos configurar escalados, fórmulas complejas, históricos, etc. En la inicialización de la expresión hay que pasarle un diccionario de variables, que puede cambiar a posteriori, pero que debe estar completo en el momento de llamar a su método analyze(), que es el que lanza las suscripciones.
  • Una alarma (clase Alarm) es un tipo de expresión (y por tanto, también tiene funciones de variable). Idealmente, debería asociarse a un grupo de alarmas (AlarmGroup), que contiene una lista de salidas o registros de sus cambios. Estos registros son derivados de la clase Output, que define cómo deben ser gestionados. Igual que se añadió al Ensemble un método import_tags() para importar las variables de un csv, se le ha incorporado un import_alarms() para importar las alarmas. Éste es un ejemplo de definición de alarmas:

    CSV de alarmas
    CSV de alarmas
  • La clase Output está definida en el módulo OutputModule. Su función es registrar expresiones de cualquier tipo, no necesariamente alarmas. Su método transform hace algunas sustituciones automáticas. Por ejemplo, {n.description} en el texto de la expresión es reemplazado por la descripción de la variable n, {n.value} se sustituye por su valor… Para dar un poco de juego, he incorporado varias clases derivadas que pueden ser de utilidad: LogOutput envía los cambios a la depuración de Python, SimpleFileOutput lleva a cabo el registro en un fichero y DataBaseOutput los escribe en base de datos.

    Log
    Log
  • El driver Modbus TCP se simplifica. Al instanciarlo se crean automáticamente sus diferentes memorias, y conforme se le van agregando variables, se redefinen automáticamente sus áreas de escaneo. ¡Ah, lo había olvidado! He jubilado las clases para gestionar áreas y grupos de escaneo. Que cada driver maneje sus comunicaciones como mejor convenga. MBPLC lanza una hebra independiente con ese fin. Consecuencia de ello, el método deploy() del Ensemble ha quedado reducido al análisis de expresiones y la conexión con los controladores.
  • El nuevo driver OPCPLC permite la comunicación con un servidor OPC UA. El encapsulado hace que tenga poco que contar de su funcionamiento. Tan solo en lo tocante al direccionamiento de las variables, que ataca a los nodos del protocolo, y describe la ruta a partir del nodo de objetos usando el carácter \ como separador. Como los nodos también se diferencian por tipo, es preciso anteponerles su identificador correspondiente, separado por :. Así, por ejemplo, una dirección válida puede ser 2:Data\2:Static\2:Scalar\2:DateTimeValue. Para ayudar a determinar el direccionamiento he incluido el método print_tree(), que imprime la estructura completa de un servidor OPC una vez que el driver se ha conectado.

    Driver OPC UA
    Driver OPC UA
  • Toda la gestión de alarmas que se ha definido clamaba con ansia dos elementos nuevos en la interfaz. El primero es un visualizador de alarmas. Se hace de una forma tan sencilla como insertar una tabla con el atributo data-alarmgroup=»nombre_del_grupo_de_alarmas». El código en Javascript se encarga solo de asociarle los eventos correspondientes para ir mostrando en todo momento las alarmas presentes, con aspecto definido en la hoja de estilos. El segundo elemento es un histórico de alarmas. Su definición es idéntica, salvo que añade el atributo data-historical. A continuación se muestra cómo se insertan estos dos nuevos objetos en la interfaz:

Para mostrar cómo se levanta un proyecto con los nuevos cambios, a continuación muestro el código necesario para definir un Ensemble con un PLC Modbus y un servidor OPC, importar sus variables y alarmas, registrar estas últimas en un fichero, en el depurador de Python y en una base de datos MySQL, y por último lanzar la interfaz web:

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

Facebooktwitterlinkedin