Componentes de página web SVG para IoT y fabricantes (Parte 2)
Publicado: 2022-03-10Entonces, ya tenemos formas de cargar dinámicamente un menú de íconos SVG hechos para reaccionar cargando paneles si así lo deseamos, pero los íconos no eran componentes reales. Pudimos usar un truco simple de traer el SVG para cada ícono y pasarlo a la aplicación Vue. Fue lo suficientemente simple como para generar una lista de íconos, y cada ícono reaccionó de manera similar excepto por pequeñas diferencias de datos. La diferencia de datos hizo posible vincular el nombre de un panel a cada ícono de tal manera que el controlador para el clic del botón del ícono pudiera transmitirlo.
Cuando se carga un panel en forma de componente Vue, se debe cargar todo lo relacionado con el panel y sus componentes, plantillas, JavaScript y más. Por lo tanto, el trabajo de solo administrar la carga del panel es más grande que lo que hemos encontrado hasta ahora en esta discusión.
Veamos la forma en que Vue proporciona un gancho para la carga asíncrona. El siguiente fragmento es de la guía de Vue.
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // Pass the component definition to the resolve callback resolve({ template: '<div>I am async!</div>' }) }, 1000) })
La guía nos dice que la función setTimeout es un ejemplo de cómo usar la sincronicidad con los componentes de Vue. Tenga en cuenta que donde antes había un objeto como segundo parámetro de Vue.component
, ahora hay una función, que se conoce como función de fábrica. Dentro de la devolución de llamada de resolve
hay una definición de componente, que habría sido el segundo parámetro de Vue.component
antes.
Entonces, tuve que mirar este ejemplo un rato antes de que tuviera sentido para mí. Aquí hay otro ejemplo, que me queda mejor:
Vue.component('async-example', function (resolve, reject) { // Vue will call this function and promise itself to handle // it when it gets back with data. // this function can then call a promising object loader // here the 'loader' function is some abstract function. // Most likely the application will use 'fetch' // but it could be something else. loader('/my/resource/on/server.json'). then(function (JSON_data) { var object = transformJSONToJSObject(JSON_data); resolve(object) }).catch( (error) => { handle it } );
Parece que lo correcto es hacer que una función más general rodee este formulario.
function componentLoader(c_name,resource_url) { Vue.component(c_name, function (resolve, reject) { loader(resource_url). then(function (JSON_data) { var object = transformJSONToJSObject(JSON_data); resolve(object) }).catch( (error) => { handle it } ); }
Entonces, en general, para cargar un componente, solo necesitaríamos una línea como la siguiente:
componentLoader('ThermoPanel','./JSON/thermo-panel.json');
Entonces, ¿cuál es el JSON que se está cargando? Puede incluir todo sobre el componente. En este caso, como componente del panel, puede incluir termómetros, interruptores de máquinas, controles deslizantes, indicadores y más. Si bien parecía mejor mantener las partes de los componentes en la página web, en realidad puede funcionar mejor usar el campo de subcomponente que se encuentra en el ejemplo más largo para el 'panel térmico' que hicimos antes y también para los otros paneles construidos de manera similar. El JSON contendrá una estructura de panel completa.
Sin embargo, si el lector nota la inclusión de la llamada de función a transformJSONToJSObject
, comprenderá que JSON podría estar codificado de alguna manera para facilitar el transporte y facilitar que un servidor maneje la definición. Después de todo, la definición incluirá plantillas SVG completas, definiciones de funciones y otras expresiones de JavaScript. Además, el objeto JSON puede contener más que solo la definición del panel porque alguna información puede simplemente ayudar en la contabilidad o la validación. Por lo tanto, uno puede esperar que haya algún tratamiento del objeto al recibirlo.
En cuanto a la codificación, los datos que llegan del servidor se pueden codificar de varias maneras. Tal vez sea simplemente una URL codificada. O más seguro, podría estar cifrado. Para esta discusión, solo podemos usar la codificación de URL.
Algunas de las herramientas que están disponibles para crear aplicaciones Vue sin duda se encargan de la transformación JSON. Pero, esta discusión hasta ahora ha evitado el uso de herramientas de línea de comandos. Esta omisión no es tan mala ya que también hemos usado Vue con el mínimo de recursos, usando solo una etiqueta de secuencia de comandos para hacer referencia a la CDN. Sin embargo, ciertamente recomiendo buscar en las herramientas de línea de comando, especialmente para organizar proyectos.
Cuando el JSON llega a la página, dado que el componente está completamente ensamblado con subcomponentes, no es necesario realizar más trabajo para obtener las partes. Podemos suponer que todos los componentes estarán completamente definidos para el resto de esta discusión. Sin embargo, ensamblar jerarquías de componentes completas requerirá herramientas de línea de comandos en algún momento.
El proceso de edición de SVG también requerirá algo de trabajo. Los procesos de edición de SVG permiten que un diseñador dibuje un panel y todos los componentes en él. Pero, cada subcomponente debe identificarse, llamarse en un grupo o asignarse un marcador de posición. Cualquier enfoque para usar el dibujo requiere algún tratamiento del SVG para que las etiquetas de los componentes de Vue puedan reemplazar los grupos o elementos gráficos. De esta manera, cualquier interpretación de artista puede convertirse en una plantilla. Y los subcomponentes dibujados deberán desmontarse en plantillas para los subcomponentes de Vue.
Este tipo de parsimonia es contraria al flujo de trabajo de la mayoría de los marcos de JavaScript. Los marcos se tratan de ensamblar páginas. Pero, editar o dibujar, da como resultado algo ya ensamblado por un artista. En la práctica, el resultado de la edición no proporciona un archivo de texto que se corresponda directamente con la definición de un componente del marco.
Se puede considerar más sobre el proceso de edición en alguna otra discusión. Hay mucho para eso. Pero, por ahora, tenemos las herramientas que necesitamos para cargar componentes jerárquicos y darles vida.
La aplicación perezosa
Para la construcción de nuestro panel IoT, ya contamos con una barra de selección que responde a las búsquedas. Y tenemos una forma de cargar componentes cuando los necesitamos. Sólo tenemos que conectar estas partes. Y, por último, tenemos que asegurarnos de que los paneles aparecen y que empiezan a funcionar cuando lo hacen.
La carga diferida de paneles realizada por el código asíncrono anterior proporciona un esbozo de una idea. Pero, afortunadamente, algunas personas han experimentado para encontrar formas de asegurarse de que se puedan cargar todo tipo de componentes. Hay una entrada de codepen que muestra cómo actualizar las aplicaciones de Vue con nuevos componentes de diferentes tipos. Ese es el mecanismo que se necesita para actualizar una parte designada de la página con diferentes tipos de panel.
Con la capacidad de agregar diferentes tipos de paneles y con un mecanismo simple para cargar sus definiciones, podemos, por fin, tener nuestra página de búsqueda de paneles.
Aquí está el HTML que necesitamos en nuestra página para que la aplicación Vue pueda colocar componentes dinámicamente:
<template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name"></component> </template>
La etiqueta del component
es una metaetiqueta de Vue. Consulte la referencia de los componentes dinámicos. Las propiedades, atributos especiales, usados para la etiqueta del component
en este caso son es y clave. El atributo is
existe para los componentes dinámicos. Y, la key
asegura que los nuevos niños tendrán identidades diferentes entre sí y ayuda a Vue a decidir qué dibujar.
“Los hijos del mismo padre común deben tener claves únicas. Las claves duplicadas provocarán errores de representación”.
La etiqueta de la template
recorrerá los componentes que se proporcionan en el campo de datos panelList
de la aplicación.
Entonces, comenzando con la definición de Vue a nivel de aplicación para la aplicación de ícono, podemos hacer cambios para incluir panelList en los elementos de datos. (Llamémoslo ahora panelApp).
var panelApp = new Vue({ el: '#PanelApp', data: { iconList: [ // Where is the data? Still on the server. ], panelList: [ ], queryToken : "Thermo Batches" // picked a name for demo }, methods : { goGetPanel: function (pname) { // var url = panelURL(pname); // this is custom to the site. fetch(url).then((response) => { // this is now browser native response.text().then((text) => { var newData = decodeURIComponent(text); eval(pHat); // widgdef = object def, must be assignment pHat = widgdef; var pnameHat = pname + pcount++; pHat.name = pnameHat; // this is needed for the key this.panelList.push(pHat); // now it's there. }).catch( error => { /* handle it */ }); } } });
Además de agregar el panel, goGetPanel
ahora tiene la forma requerida para obtener una definición de componente de una base de datos u otra tienda. El lado del servidor debe tener cuidado al entregar el código JavaScript en el formato correcto. En cuanto a cómo se ve el objeto desde el servidor, ya lo hemos visto. Es el tipo de objeto utilizado como parámetro para Vue.component
.
Aquí está el cuerpo completo de la aplicación Vue que proporciona un menú como resultado de búsqueda y un lugar para colocar paneles obtenidos del servidor cuando el usuario hace clic en un icono.
<div> <!-- Recognize the name from the Vue doc --> <div> <h2 itemprop="name">Request MCU Groups</h2> <p itemprop="description">These are groups satistfying this query: {{queryToken}}.</p> <button>Find All</button> <button>Find 5 Point</button> <button>Find 6 Point</button> </div> <!-- Here is a Vue loop for generating a lit --> <div class="entryart"> <button v-for="iconEntry in iconList" @click="goGetPanel(iconEntry.name)" > <div v-html="iconEntry.icon"> </div> </button> </div> <div class="entryart" > <template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name" :ref="panel.name" ></component> </template> </div> </div>
En el último div
, la etiqueta del component
ahora tiene un parámetro ref
vinculado al nombre del panel. El parámetro ref permite que la aplicación Vue identifique qué componente actualizar con datos y mantiene los componentes separados. Los parámetros ref
también permiten que nuestra aplicación acceda a los nuevos componentes cargados dinámicamente.
En una versión de prueba de la aplicación del panel, tengo el siguiente controlador de intervalos:
setInterval(() => { var refall = panelApp.$refs; // all named children that panels for ( var pname in refall ) { // in an object var pdata = refall[pname][0]; // off Vue translation, but it's there. pdata.temp1 = Math.round(Math.random()*100); // make thermos jump around. pdata.temp2 = Math.round(Math.random()*100); } },2000)
El código proporciona una pequeña animación, cambiando los termómetros al azar. Cada panel tiene dos termómetros y la aplicación permite al usuario seguir agregando paneles. (En la versión final, algunos paneles deben desecharse). Se accede a las referencias mediante panelApp.$refs
, un campo que Vue crea dada la información de las refs
en la etiqueta del component
.
Entonces, así es como se ven los termómetros que saltan al azar en una instantánea:
Conexión del panel al dispositivo IoT
Entonces, la última pieza de código es una prueba setInterval
que actualiza los termómetros con valores aleatorios cada dos segundos. Pero, lo que queremos hacer es leer datos reales de máquinas reales. Para hacer eso, necesitaremos alguna forma de comunicación.
Hay una variedad de formas. Pero usemos MQTT, que es un sistema de mensajes de publicación/suscripción. Nuestra SPWA puede suscribirse a mensajes de dispositivos en cualquier momento. Cuando recibe esos mensajes, la SPWA puede dirigir cada mensaje al controlador de datos apropiado para el panel asignado al dispositivo identificado en el mensaje.
Entonces, básicamente, lo que debemos hacer es reemplazar setInterval
con un controlador de respuesta. Y, eso será para un panel. Probablemente queramos asignar paneles a controladores a medida que se cargan. Y depende del servidor web asegurarse de que se entregue la asignación correcta.
Una vez que el servidor web y el SPWA tienen la página lista para funcionar, el servidor web ya no necesita encargarse de la mensajería entre la página y el dispositivo. el protocolo MQTT especifica un servidor de enrutamiento para manejar pub/sub. Se han creado varios servidores MQTT. Algunos de ellos son de código abierto. Uno muy popular es Mosquito , y hay algunos desarrollados sobre Node.js.
El proceso para la página es simple. La SPWA se suscribe a un tema. Una buena versión de un tema es un identificador para una MCU, como una dirección MAC o un número de serie. O bien, la SPWA podría suscribirse a todas las lecturas de temperatura. Pero, entonces la página tendría que hacer el trabajo de filtrar los mensajes de todos los dispositivos. La publicación en MQTT es esencialmente una transmisión o multidifusión.
Echemos un vistazo a cómo el SPWA interactuará con MQTT.
Inicializando MQTT en el SPWA
Hay varias bibliotecas de clientes para elegir. Uno, por ejemplo, es un MQTT.js. Otro es eclipse paho. Hay más, por supuesto. Usemos Eclipse Paho ya que tiene una versión almacenada en CDN. Solo necesitamos agregar la siguiente línea a nuestra página:
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
El cliente MQTT debe conectarse a un servidor antes de poder enviar y recibir mensajes. Por lo tanto, las líneas que configuran la conexión también deben incluirse en el JavaScript. Podemos agregar una función MQTTinitialize
que configura el cliente y las respuestas para la administración de la conexión y la recepción de mensajes.
var messagesReady = false; var mqttClient = null; function MQTTinitialize() { mqttClient = new Paho.MQTT.Client(MQTTHostname, Number(MQTTPort), "clientId"); mqttClient.onMessageArrived = onMessageArrived; // connect the client mqttClient.connect({ onSuccess: () => { messagesReady = true; } }); // set callback handlers mqttClient.onConnectionLost = (response) => { // messagesReady = false; // if (response.errorCode !== 0) { console.log("onConnectionLost:"+response.errorMessage); } setTimeout(() => { MQTTinitialize() },1000); // try again in a second }; }
Configuración de suscripción
Con la conexión lista, el cliente puede suscribirse a los canales de mensajes, enviar mensajes en ellos, etc. Solo unas pocas rutinas pueden hacer la mayor parte del trabajo necesario para conectar los paneles con las rutas MQTT.
Para el panel SPWA, el momento de la suscripción se puede utilizar para establecer la asociación entre el panel y el tema, el identificador de la MCU.
function panelSubcription(topic,panel) { gTopicToPanel[topic] = panel; gPanelToTopic[panel] = topic; mqttClient.subscribe(topic); }
Dado que una MCU está publicando sobre su tema, la SPWA recibirá un mensaje. Aquí se desglosa el mensaje de la OPS. Y, luego, el mensaje se transmite a la mecánica de la aplicación.
function onMessageArrived(pmessage) { // var topic = pmessage.destinationName; var message = pmessage.payloadString; // var panel = gTopicToPanel[topic]; deliverToPanel(panel,message); }
Entonces, ahora todo lo que tenemos que hacer es crear deliverToPanel
, que debería ser algo así como el controlador de intervalo que teníamos antes. Sin embargo, el panel está claramente identificado y solo se pueden actualizar los datos clave enviados en el mensaje en particular.
function deliverToPanel(panel,message) { var refall = panelApp.$refs; // all named children that panels var pdata = refall[panel][0]; // off Vue translation, but it's there. var MCU_updates = JSON.parse(message); for ( var ky in MCU_updates ) { pdata[ky] = MCU_updates[ky] } }
Esta función deliverToPanel
es lo suficientemente abstracta como para permitir cualquier definición de panel con cualquier cantidad de puntos de datos para la animación.
Enviando mensajes
Para completar el ciclo de la aplicación entre la MCU y la SPWA, definimos una función para enviar un mensaje.
function sendPanelMessage(panel,message) { var topic = gPanelToTopic[panel]; var pmessage = new Paho.MQTT.Message(message); pmessage.destinationName = topic; mqttClient.send(pmessage); }
La función sendPanelMessage
no hace más que enviar el mensaje en la misma ruta de tema a la que se suscribe la SPWA.
Como planeamos hacer que los botones de íconos sean responsables de traer una cierta cantidad de paneles para un solo grupo de MCU, habrá más de un panel para cuidar. Pero, tengamos en cuenta que cada panel corresponde a una sola MCU, por lo que tenemos un mapeo uno a uno, para lo cual podemos usar dos mapas de JavaScript para el mapa y el inverso.
Entonces, ¿cuándo enviamos mensajes? Por lo general, la aplicación del panel enviará un mensaje cuando quiera cambiar el estado de la MCU.
Mantener el estado de View (Vue) sincronizado con los dispositivos
Una de las mejores cosas de Vue es que es muy fácil mantener el modelo de datos sincronizado con la actividad del usuario, que puede editar campos, hacer clic en botones, usar controles deslizantes, etc. reflejarse inmediatamente en los campos de datos de los componentes.
Pero queremos que los cambios envíen mensajes a la MCU tan pronto como ocurran los cambios. Por lo tanto, buscamos hacer uso de los eventos de la interfaz que puede gobernar Vue. Buscamos responder a tal evento, pero solo después de que el modelo de datos de Vue esté listo con el valor actual.
Creé otro tipo de panel, este con un botón de aspecto bastante artístico (quizás inspirado en Jackson Pollock). Y lo convertí en algo cuyo clic informa el estado al panel que lo contiene. Ese no fue un proceso tan simple.
Una cosa que me desconcertó es que había olvidado algunas de las rarezas en la gestión de SVG. Primero intenté cambiar la cadena de estilo para que el campo de display
del estilo CSS fuera "Ninguno" o "algo". Pero, el navegador nunca reescribió la cadena de estilos. Pero, como eso era engorroso, intenté cambiar la clase CSS. Eso tampoco tuvo efecto. Pero, existe el atributo de visibility
, que la mayoría de nosotros recordamos del antiguo HTML (quizás la versión 1.0), pero que está muy actualizado en SVG. Y eso funciona bien. Todo lo que tenía que hacer era conseguir que el evento de clic de botón se propagara.
Vue ha diseñado propiedades para propagarse en una dirección, de padre a hijo. Entonces, para cambiar datos en la aplicación o en el panel, debe enviar un evento de cambio al padre. Luego, puede cambiar los datos. El cambio del elemento de datos que controla el botón hace que Vue actualice la propiedad que afecta la visibilidad del elemento SVG que hemos elegido para indicar el estado. Aquí hay un ejemplo:
Cada instancia del panel de botones ondulados es independiente. Entonces, algunos están ENCENDIDOS y otros están APAGADOS.
Este fragmento de SVG contiene el indicador amarillo de aspecto extraño:
<path :visibility="stateView" d="m -36.544616,12.266886 c 19.953088,17.062165 5.07961,-19.8251069 5.317463,8.531597 0.237853,28.356704 13.440044,-8.847959 -3.230451,10.779678 -16.670496,19.627638 14.254699,-2.017715 -11.652451,3.586456 -25.90715,5.60417 10.847826,19.889979 -8.095928,-1.546575 -18.943754,-21.436555 -1.177383,14.210702 -4.176821,-12.416207 -2.999438,-26.6269084 -17.110198,8.030902 2.14399,-8.927709 19.254188,-16.9586105 -19.075538,-8.0837048 9.448721,-5.4384245 28.52426,2.6452804 -9.707612,-11.6309807 10.245477,5.4311845 z" transform="translate(78.340803,6.1372042)" />
La visibilidad se completa con stateView
, una variable calculada que asigna el estado booleano a una cadena para SVG.
Aquí está la plantilla de definición de componentes del panel:
<script type="text/x-template"> <div> <control-switch :state="bstate" v-on:changed="saveChanges" ></control-switch> <gauge :level="fluidLevel" ></gauge> </div> </script>
Y esta es la definición de JavaScript del panel Vue con sus elementos secundarios como subcomponentes:
var widgdef = { data: function () { var currentPanel = { // at the top level, values controlling children bstate : true, fluidLevel : Math.round(Math.random()*100) } // return currentPanel }, template: '#mcu-control-panel-template', methods: { saveChanges: function() { // in real life, there is more specificity this.bstate = !this.bstate relayToMCU(this.name,"button",this.bstate) // to be defined } }, components: { 'control-switch' : { // the odd looking button props: ['state'], template: '#control-switch-template', // for demo it is in the page. computed: { // you saw this in the SVG above. stateView : function() { return ( this.state ) ? "visible" : "hidden" } }, methods : { // the button handler is in the SVG template at the top. stateChange : function () { // can send this.$emit('changed'); // tell the parent. See on the template instance } } }, 'gauge' : { // some other nice bit of SVG props: ['level'], template: '#gauge-template' } } }
Entonces, ahora se ha presentado el mecanismo para un solo botón incrustado en un panel. Y tiene que haber un gancho para decirle a la MCU que algo ha sucedido. Debe llamarse inmediatamente después de que se haya actualizado el estado de los datos del componente del panel. Vamos a definirlo aquí:
function relayToMCU(panel,switchName,bstate) { var message = switchName + ':' + bstate // a on element parameter string. sendPanelMessage(panel,message) }
Existe el cambio de estado en su camino hacia el hardware en solo dos líneas de código.
Pero, este es un caso bastante simple. Cualquier interruptor puede verse como una llamada de función a una pieza de hardware en el mundo. Por lo tanto, la cadena puede contener el nombre del conmutador y varios otros elementos de datos. Por lo tanto, el método del componente que registra el cambio tendrá que tener algún manejo personalizado para que pueda reunir todos los conjuntos de datos en el panel y enviarlos en una cadena de comando. Incluso la cadena de comandos es un poco simple. Si la MCU es bastante pequeña, es posible que la cadena de comando deba traducirse a un código. Si la MCU tiene una gran capacidad, la cadena de comando podría ser en realidad una estructura JSON o quizás todos los datos que aloja el panel.
En esta discusión, los botones en el panel de íconos contienen el nombre del panel a buscar. Eso también puede simplificarse bastante. Parece tener sentido que ese parámetro pueda representar cualquier panel que pueda estar almacenado en las bases de datos de una empresa. Pero, tal vez sea alguna fórmula. Tal vez, la información sobre el panel debería incluirse en la definición del panel que recibimos del servidor. En cualquier caso, los conceptos básicos se pueden ampliar fácilmente una vez que se eliminan ciertos dolores de cabeza, como hacer que el SVG responda correctamente a los clics.
Conclusión
Esta discusión ha establecido algunos pasos básicos y decisiones que conducen a la realización de una aplicación web de página única (SPWA) que puede interactuar con dispositivos IoT. Ahora sabemos cómo obtener paneles de un servidor web y convertirlos en una interfaz MCU.
Hay mucho más en esta discusión con bastantes otras discusiones que pueden seguir. Comenzar con Vue es algo en lo que pensar. Pero, luego está toda la historia de MCU, que solo hemos mencionado brevemente.
En particular, al seleccionar MQTT como sustrato de comunicación, asumimos que los dispositivos IoT en el otro extremo pueden ser gobernados por MQTT de alguna manera. Pero, ese puede no ser siempre el caso. A veces, se necesitan puertas de enlace si MQTT debe obtener acceso a un dispositivo con enlaces en serie o Bluetooth. O, tal vez, todo lo que uno necesita de la página web es WebSockets. Sin embargo, usamos MQTT como ejemplo para mostrar cómo Vue podría recibir y enviar datos mientras mantiene su estado de datos sincronizado con los dispositivos.
Una vez más sólo tenemos una parte de la historia. Esta vez es para la sincronización porque la página debería poder manejar alertas y molestar al usuario si está sucediendo algo crítico. A veces los mensajes se pueden perder. Entonces, tenemos que tener un mecanismo para los reconocimientos.
Finalmente, es mi opinión que Vue hace que la actualización de datos al recibirlos sea bastante elegante. Pero enviar los cambios de estado no es tan sencillo. No parece hacer el trabajo mucho más simple de lo que se puede hacer con JavaScript estándar. Pero, hay una manera y tiene sentido.
Tal vez se pueda construir una biblioteca limpia para hacer un conjunto universal de componentes para todos los paneles. Se han mencionado brevemente los elementos para hacer tales bibliotecas y tenerlas almacenadas en una base de datos. Es posible que se deban desarrollar herramientas que vayan más allá de la simple creación de imágenes SVG. En cualquier caso, es probable que se puedan hacer muchas cosas para los próximos pasos.