Componentes de página da Web SVG para IoT e criadores (parte 2)
Publicados: 2022-03-10Então, já temos maneiras de carregar dinamicamente um menu de ícones SVG feitos para reagir carregando painéis se assim o desejarmos, mas os ícones não eram componentes reais. Conseguimos usar um truque simples de trazer o SVG para cada ícone e passá-lo para o aplicativo Vue. Foi simples o suficiente para gerar uma lista de ícones, e cada ícone reagiu de maneira semelhante, exceto por pequenas diferenças de dados. A diferença de dados tornou possível vincular o nome de um painel a cada ícone de forma que o manipulador do clique do botão do ícone pudesse transmiti-lo.
Quando um painel é carregado na forma de componente Vue, tudo sobre o painel e seus componentes devem ser carregados, templates, JavaScript e muito mais. Portanto, o trabalho de apenas gerenciar o carregamento do painel é maior do que encontramos até agora nesta discussão.
Vejamos a maneira do Vue fornecer um gancho para carregamento assíncrono. O trecho a seguir é do guia 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) })
O guia nos diz que a função setTimeout é um exemplo de como usar a sincronicidade com componentes Vue. Observe que onde antes havia um objeto como segundo parâmetro para Vue.component
, agora existe uma função, que é chamada de função de fábrica. Dentro do callback de resolve
está uma definição de componente, que teria sido o segundo parâmetro para Vue.component
antes.
Então, eu tive que olhar para este exemplo um pouco antes que fizesse sentido para mim. Aqui está outro exemplo, que me convém melhor:
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 ser a coisa certa a fazer uma função mais geral para contornar essa forma.
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 } ); }
Então, em geral, para carregar um componente, precisaríamos apenas de uma linha como a seguinte:
componentLoader('ThermoPanel','./JSON/thermo-panel.json');
Então agora, qual é o JSON que está sendo carregado? Pode incluir tudo sobre o componente. Nesse caso, como componente de painel, pode incluir termômetros, interruptores de máquina, controles deslizantes, medidores e muito mais. Embora tenha parecido melhor manter as partes dos componentes na página da web, pode funcionar melhor usar o campo de subcomponentes que está no exemplo mais longo para o 'termo-painel' que fizemos antes e também para os outros painéis construídos de forma semelhante. O JSON conterá uma estrutura de painel completa.
No entanto, se o leitor perceber a inclusão da chamada de função para transformJSONToJSObject
, ele entenderá que o JSON pode ser codificado de alguma forma para facilitar o transporte e facilitar o manuseio da definição por um servidor. Afinal, a definição incluirá modelos SVG completos, definições de funções e outras expressões JavaScript. Além disso, o objeto JSON pode conter mais do que apenas a definição do painel porque algumas informações podem simplesmente ajudar na contabilidade ou validação. Assim, pode-se esperar que haja algum tratamento do objeto no recebimento.
Quanto à codificação, os dados vindos do servidor podem ser codificados de várias maneiras. Talvez seja simplesmente URL codificado. Ou mais seguro, pode ser criptografado. Para esta discussão, podemos usar apenas a codificação de URL.
Algumas das ferramentas disponíveis para a criação de aplicativos Vue, sem dúvida, cuidam da transformação JSON. Mas, até agora, essa discussão evitou o uso de ferramentas de linha de comando. Essa omissão não é tão ruim, pois também usamos o Vue com o mínimo de recursos, usando apenas uma tag de script para a referência ao CDN. No entanto, eu certamente recomendo olhar para as ferramentas de linha de comando especialmente para organizar projetos.
Quando o JSON chega à página, dado que o componente está completamente montado com subcomponentes, não há mais trabalho a ser feito para buscar as partes. Podemos assumir que todos os componentes virão totalmente definidos para o restante desta discussão. Mas, montar hierarquias de componentes completas exigirá ferramentas de linha de comando em algum momento.
O processo de edição SVG também exigirá algum trabalho. Os processos de edição SVG permitem que um designer desenhe um painel e todos os componentes nele. Mas, cada subcomponente deve ser identificado, chamado em um grupo ou dado um espaço reservado. Qualquer abordagem de uso do desenho requer algum tratamento do SVG para que as tags dos componentes Vue possam substituir os grupos ou elementos gráficos. Dessa forma, qualquer renderização de artista pode se tornar um modelo. E, os subcomponentes desenhados terão que ser desmontados em templates para subcomponentes Vue.
Esse tipo de parcimônia é contrário ao fluxo de trabalho da maioria dos frameworks JavaScript. Os frameworks são sobre a montagem de páginas. Mas, editar ou desenhar, resulta em algo já montado por um artista. Na prática, o resultado da edição não fornece um arquivo de texto que corresponda diretamente a uma definição de componente do framework.
Mais sobre o processo de edição pode ser considerado em alguma outra discussão. Há muito para isso. Mas, por enquanto, temos as ferramentas necessárias para carregar componentes hierárquicos e torná-los vivos.
O aplicativo preguiçoso
Para a construção do nosso painel IoT, já temos uma barra de seleção que responde às pesquisas. E temos uma maneira de carregar componentes quando precisamos deles. Só precisamos conectar essas partes. E, por fim, temos que garantir que os painéis apareçam e comecem a funcionar quando aparecerem.
O carregamento lento de painéis feito pelo código assíncrono acima fornece um esboço de uma ideia. Mas, felizmente, algumas pessoas experimentaram encontrar maneiras de garantir que todos os tipos de componentes possam ser carregados. Há uma entrada de codepen que mostra como atualizar aplicativos Vue com novos componentes de vários tipos. Esse é o mecanismo necessário para atualizar uma parte designada da página com diferentes tipos de painel.
Com a possibilidade de adicionar diferentes tipos de painéis e com um mecanismo simples para carregar suas definições, podemos, finalmente, ter nossa página de busca de painéis.
Aqui está o HTML que precisamos em nossa página para que o aplicativo Vue possa colocar componentes dinamicamente:
<template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name"></component> </template>
A tag de component
é uma meta tag Vue. Consulte a referência para componentes dinâmicos. As propriedades, atributos especiais, usados para a tag do component
neste caso são is e key. O atributo is
existe para componentes dinâmicos. E, a key
garante que as novas crianças tenham identidades diferentes umas das outras e ajuda o Vue a decidir o que desenhar.
“Filhos do mesmo pai comum devem ter chaves exclusivas. Chaves duplicadas causarão erros de renderização.”
A tag de template
percorrerá os componentes fornecidos no campo de dados panelList
do aplicativo.
Assim, começando com a definição do Vue no nível do aplicativo para o aplicativo de ícone, podemos fazer alterações para incluir o panelList nos elementos de dados. (Vamos agora chamá-lo de 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 */ }); } } });
Além de adicionar no painel, goGetPanel
agora está em um formulário necessário para obter uma definição de componente de um banco de dados ou outra loja. O lado do servidor deve ter cuidado ao entregar o código JavaScript no formato correto. Quanto à aparência do objeto vindo do servidor, já o vimos. É o tipo de objeto usado como parâmetro para Vue.component
.
Aqui está o corpo completo do aplicativo Vue que fornece um menu como resultado de pesquisa e um local para colocar os painéis obtidos do servidor quando o usuário clica em um ícone.
<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>
Na última div
, a tag do component
agora tem um parâmetro ref
vinculado ao nome do painel. O parâmetro ref permite que o aplicativo Vue identifique qual componente atualizar com dados e mantenha os componentes separados. Os parâmetros ref
também permitem que nosso aplicativo acesse os novos componentes carregados dinamicamente.
Em uma versão de teste do aplicativo de painel, tenho o seguinte manipulador de intervalo:
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)
O código fornece uma pequena animação, alterando os termômetros aleatoriamente. Cada painel possui dois termômetros, e o aplicativo permite que o usuário continue adicionando painéis. (Na versão final, alguns painéis devem ser descartados.) As refs estão sendo acessadas usando panelApp.$refs
, um campo que o Vue cria a partir das informações de refs
na tag do component
.
Então, é assim que os termômetros pulando aleatoriamente se parecem em um instantâneo:
Conectando o painel ao dispositivo IoT
Assim, o último pedaço de código é um teste setInterval
atualizando termômetros com valores aleatórios a cada dois segundos. Mas, o que queremos fazer é ler dados reais de máquinas reais. Para isso, precisaremos de alguma forma de comunicação.
Há uma variedade de maneiras. Mas, vamos usar o MQTT, que é um sistema de mensagens pub/sub. Nosso SPWA pode assinar mensagens de dispositivos a qualquer momento. Ao receber essas mensagens, o SPWA pode direcionar cada mensagem para o manipulador de dados apropriado para o painel mapeado para o dispositivo identificado na mensagem.
Então, basicamente o que precisamos fazer é substituir o setInterval
por um manipulador de resposta. E isso será para um painel. Provavelmente queremos mapear painéis para manipuladores à medida que são carregados. E cabe ao servidor web verificar se o mapeamento correto é entregue.
Uma vez que o servidor web e o SPWA tenham a página pronta para operação, o servidor web não precisa mais cuidar das mensagens entre a página e o dispositivo. o protocolo MQTT especifica um servidor de roteamento para manipular pub/sub. Vários servidores MQTT foram criados. Alguns deles são de código aberto. Um muito popular é o Mosquito , e existem alguns desenvolvidos em cima do Node.js.
O processo para a página é simples. O SPWA assina um tópico. Uma boa versão de um tópico é um identificador para um MCU, como um endereço MAC ou um número de série. Ou, o SPWA pode assinar todas as leituras de temperatura. Mas, então, a página teria que fazer o trabalho de filtrar as mensagens de todos os dispositivos. A publicação em MQTT é essencialmente um broadcast ou multicast.
Vamos dar uma olhada em como o SPWA irá interagir com o MQTT.
Inicializando o MQTT no SPWA
Existem várias bibliotecas de cliente para escolher. Um, por exemplo, é um MQTT.js. Outro é o eclipse paho. Há mais claro. Vamos usar o Eclipse Paho, pois ele possui uma versão armazenada em CDN. Só precisamos adicionar a seguinte linha à nossa página:
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
O cliente MQTT precisa se conectar a um servidor antes de enviar e receber mensagens. Portanto, as linhas que configuram a conexão também precisam ser incluídas no JavaScript. Podemos incluir uma função MQTTinitialize
que configura o cliente e as respostas para gerenciamento de conexão e recebimento de mensagens.
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 }; }
Configurando a assinatura
Com a conexão pronta, o cliente pode se inscrever nos canais de mensagens, enviar mensagens neles, etc. Apenas algumas rotinas podem fazer a maior parte do trabalho necessário para conectar os painéis com os caminhos MQTT.
Para o painel SPWA, o momento da assinatura pode ser utilizado para estabelecer a associação entre o painel e o tópico, o identificador MCU.
function panelSubcription(topic,panel) { gTopicToPanel[topic] = panel; gPanelToTopic[panel] = topic; mqttClient.subscribe(topic); }
Dado que um MCU está publicando sobre seu tópico, o SPWA receberá uma mensagem. Aqui, a mensagem Paho é descompactada. E, então, a mensagem é passada para a mecânica do aplicativo.
function onMessageArrived(pmessage) { // var topic = pmessage.destinationName; var message = pmessage.payloadString; // var panel = gTopicToPanel[topic]; deliverToPanel(panel,message); }
Então, agora tudo o que precisamos fazer é criar deliverToPanel
que deve ser um pouco como o manipulador de intervalo que tínhamos antes. No entanto, o painel é claramente identificado e apenas os dados digitados enviados na mensagem específica podem ser atualizados.
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] } }
Essa função deliverToPanel
é abstrata o suficiente para permitir qualquer definição de painel com qualquer número de pontos de dados para animação.
Enviando mensagens
Para completar o loop de aplicação entre o MCU e o SPWA, definimos uma função para enviar uma mensagem.
function sendPanelMessage(panel,message) { var topic = gPanelToTopic[panel]; var pmessage = new Paho.MQTT.Message(message); pmessage.destinationName = topic; mqttClient.send(pmessage); }
A função sendPanelMessage
não faz mais do que enviar a mensagem no mesmo caminho de tópico que o SPWA assina.
Como planejamos tornar os botões de ícone responsáveis por trazer algum número de painéis para um único cluster de MCUs, haverá mais de um painel para cuidar. Mas, lembramos que cada painel corresponde a um único MCU, então temos um mapeamento um-um, para o qual podemos usar dois mapas JavaScript para o mapa e o inverso.
Então, quando enviamos mensagens? Normalmente, o aplicativo do painel enviará uma mensagem quando desejar alterar o estado do MCU.
Mantendo o estado de visualização (Vue) em sincronia com os dispositivos
Uma das grandes vantagens do Vue é que é muito fácil manter o modelo de dados sincronizado com a atividade do usuário, que pode editar campos, clicar em botões, usar sliders, etc. ser refletido imediatamente nos campos de dados dos componentes.
Mas queremos que as alterações disparem mensagens para o MCU assim que as alterações ocorrerem. Assim, procuramos fazer uso dos eventos de interface que o Vue pode governar. Buscamos responder a tal evento, mas somente depois que o modelo de dados Vue estiver pronto com o valor atual.
Criei outro tipo de painel, este com um botão bem artístico (talvez inspirado em Jackson Pollock). E fui transformando-o em algo cujo clique reporta o estado de volta ao painel que o contém. Esse não foi um processo tão simples.
Uma coisa que me surpreendeu é que eu tinha esquecido algumas das estranhezas no gerenciamento de SVG. Primeiro tentei alterar a string de estilo para que o campo de display
do estilo CSS fosse "Nenhum" ou "algo". Mas, o navegador nunca reescreveu a string de estilos. Mas, como isso era complicado, tentei mudar a classe CSS. Isso também não surtiu efeito. Mas, há o atributo de visibility
, que a maioria de nós lembra do HTML antigo (versão 1.0 talvez), mas que está muito atualizado em SVG. E, isso funciona bem. Tudo o que eu tinha que fazer era fazer com que o evento de clique do botão se propagasse.
O Vue projetou propriedades para propagar em uma direção, de pai para filho. Portanto, para alterar dados no aplicativo ou no painel, você deve enviar um evento de alteração para o pai. Em seguida, você pode alterar os dados. A mudança do elemento de dados que controla o botão faz com que o Vue atualize a propriedade que afeta a visibilidade do elemento SVG que escolhemos para indicar o estado. Aqui está um exemplo:
Cada instância do painel de botões squiggly é independente. Assim, alguns estão LIGADOS e outros DESLIGADOS.
Este trecho de SVG contém o indicador amarelo de aparência estranha:
<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)" />
A visibilidade é preenchida por stateView
, uma variável computada que mapeia o estado booleano para uma string para SVG.
Aqui está o modelo de definição do componente do painel:
<script type="text/x-template"> <div> <control-switch :state="bstate" v-on:changed="saveChanges" ></control-switch> <gauge :level="fluidLevel" ></gauge> </div> </script>
E, esta é a definição JavaScript do painel Vue com seus filhos 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' } } }
Então, agora o mecanismo para um único botão embutido em um painel foi definido. E tem que haver um gancho para dizer ao MCU que algo aconteceu. Ele deve ser chamado imediatamente após a atualização do estado dos dados do componente do painel. Vamos definir aqui:
function relayToMCU(panel,switchName,bstate) { var message = switchName + ':' + bstate // a on element parameter string. sendPanelMessage(panel,message) }
Há a mudança de estado no caminho para o hardware em apenas duas linhas de código.
Mas, este é um caso bastante simples. Qualquer switch pode ser visto como uma chamada de função para uma peça de hardware no mundo. Portanto, a string pode conter o nome do switch e vários outros elementos de dados. Assim, o método do componente que registra a mudança terá que ter algum tratamento personalizado para que ele possa reunir todos os conjuntos de dados no painel e enviá-los em uma string de comando. Até mesmo a string de comando é um pouco simples. Se o MCU for muito pequeno, a string de comando pode ter que ser traduzida em um código. Se o MCU tiver muita capacidade, a string de comando pode ser na verdade uma estrutura JSON ou talvez todos os dados que o painel hospeda.
Nesta discussão, os botões no painel de ícones contêm o nome do painel a ser buscado. Isso também pode ser bastante simplificado. Parece fazer sentido que esse parâmetro possa representar qualquer painel que possa ser armazenado em bancos de dados corporativos. Mas, talvez seja alguma fórmula. Talvez as informações sobre o painel devam ser agrupadas em torno da definição do painel que recebemos do servidor. De qualquer forma, o básico pode ser facilmente expandido quando certas dores de cabeça estiverem fora do caminho, como fazer com que o SVG responda aos cliques corretamente.
Conclusão
Esta discussão apresentou algumas etapas e decisões básicas que levam à realização de um aplicativo Web de página única (SPWA) que pode interagir com dispositivos IoT. Agora sabemos como obter painéis de um servidor web e transformá-los em interface MCU.
Há muito mais nesta discussão com algumas outras discussões que podem seguir. Começar com o Vue é uma coisa a se pensar. Mas, depois, há toda a história do MCU, que abordamos apenas brevemente.
Em particular, ao selecionar o MQTT como substrato de comunicação, assumimos que os dispositivos IoT na outra extremidade podem de alguma forma ser governados pelo MQTT. Mas, isso pode não ser sempre o caso. Às vezes, os gateways são necessários para que o MQTT tenha acesso a um dispositivo com links seriais ou Bluetooth. Ou, talvez, tudo o que alguém precise na página da Web seja o WebSockets. No entanto, usamos o MQTT como exemplo para mostrar como o Vue pode receber e enviar dados, mantendo seu estado de dados sincronizado com os dispositivos.
Mais uma vez, temos apenas parte da história. Desta vez é para sincronização porque a página deve ser capaz de lidar com alertas e incomodar o usuário caso algo crítico esteja acontecendo. Às vezes, as mensagens podem ser perdidas. Então, temos que ter um mecanismo de reconhecimento.
Finalmente, é minha opinião que o Vue torna a atualização dos dados no recebimento bastante elegante. Mas, enviar as mudanças de estado não é tão simples. Não parece tornar o trabalho muito mais simples do que pode ser feito com o JavaScript vanilla. Mas, há uma maneira e faz sentido.
Talvez uma biblioteca limpa possa ser construída para criar um conjunto universal de componentes para todos os painéis. Os elementos para fazer tais bibliotecas e armazená-las em um banco de dados foram brevemente mencionados. Ferramentas que vão além de apenas fazer imagens SVG podem ter que ser desenvolvidas. De qualquer forma, provavelmente há muitas coisas que podem ser feitas para as próximas etapas.