Node-RED – Tela de Sinópticos

Node-RED – Tela de Sinópticos


O que é um sinóptico

Um sinóptico é uma representação gráfica e esquemática de um processo físico que permite ao operador visualizar os estados completos de uma planta em tempo real a partir de uma única tela. O termo tem origem nos antigos painéis sinópticos das salas de controle, que consistiam em grandes murais físicos, com lâmpadas, indicadores e o desenho esquemático de tubulações, tanques e equipamentos, onde cada lâmpada acesa ou cada ponteiro refletia uma condição real do processo (veja um exemplo na ).
Painel de sinópticos da usina hidroelétrica de Kegum, Letônica, 1939.
Fig. 1
Painel de sinópticos da usina hidroelétrica de Kegum Letônia, ano 1939.
Com a digitalização da operação, esses painéis migraram para a tela do computador (vide exemplo na ), mas a ideia central permaneceu intacta, que é oferecer ao operador um mapa completo do processo e seus estados em tempo real. A própria palavra, derivada do grego synoptikós ("que se vê em conjunto"), descreve a função de prover uma visão de conjunto daquilo que, no campo, poder estar disperso geograficamente.
descrição para leitores de tela
Fig. 2
Tela de sinóptico moderna da usina hidroelétrica de Itaipú, Brasil/Paraguai, 2020.

Características de um bom sinóptico

Uma tela de sinóptico é constituída por um desenho estático e elementos dinâmicos. O mapa do processo é estático e renderizado ao fundo, representando os diversos elementos do processo e como eles se conectam entre si. Já os elementos dinâmicos são renderizados sobrepostos ao mapa e representam os estados das variáveis do processo em tempo real. Estratégias comuns para demonstrar as variáveis são, por exemplo, fazer o visual de nível de um reservatório subir e descer, alterar a cor de preenchimento de um atuador enquanto ligado ou desligado, girar uma válvula ao abrir, alterar um valor numérico a cada leitura, etc.
É importante entender que um bom sinóptico não é um desenho bonito, e sim uma ferramenta de consciência situacional. As boas práticas correntes de projeto de interfaces homem-máquina (e.g., ISA-101, filosofia High Performance HMI) recomendam certos cuidados na implementação destas telas, como por exemplo o emprego assertivo de cores, a ausência de excesso decorativo e destaque para as anomalias e não para o estado normal. O objetivo é permitir que o operador perceba um desvio de forma rápida, sem precisar procurar a informação relevante em meio ao ruído visual. Além disto, o sinóptico também pode comandar, como por exemplo ligar/desligar uma bomba ao clicá-la, ajustar um setpoint, reconhecer um alarme, etc. Essa via de mão dupla, em que o processo informa a tela e a tela comanda o processo, é o cerne da operação das telas de supervisão.

Sinópticos no Node-RED: as possibilidades

Por ser uma plataforma de fluxo orientada a eventos e profundamente ligada ao ecossistema de IoT, o Node-RED é um ambiente natural para construir a camada de visualização de um supervisório didático. Entretanto, no Node-RED existem caminhos distintos para desenhar sinópticos, sendo que cada método carrega uma filosofia diferente:
  • ui-svg do Dashboard 1.0: Node legado que permite importar um SVG e se enviar comandos imperativos aos elementos desenhados (e.g. "altere o atributo X do elemento Y"). É o modelo mais próximo do que fazem os pacotes SCADA comerciais, em que o desenho é preparado em um editor e as tags são amarradas a ele.
  • template do Dashboard 2.0: o desenho SVG do processo é embutido diretamente no node e seus atributos são vinculados a variáveis de maneira reativa, com o apoio do Vue.js. Assim, em vez de comandar a tela, declara-se a relação entre o dado e o desenho, e o framework cuida da atualização.
  • Há ainda motores dedicados, como o SCADAvis.io, integrados ao Node-RED via UIBuilder, que trazem bibliotecas de símbolos industriais e um modelo de tags completo, aproximando-se bastante de um SCADA comercial.
Nesta disciplina, adotaremos o node template do Dashboard 2.0 pois este permite mostrar de forma didática e simples como o back-end das telas de supervisão funcionam, além ser de fácil implementação e naturalmente integrado ao Dashboard 2.0 e Node-RED que estamos trabalhando. Entretanto, caso no futuro tenha que implementar uma tela de supervisão profissional e comercial, pode ser mais interessante utilizar as ferramentas dedicadas que apresentamos acima.

Desenhando a tela de supervisão

Para começar nosso fluxo de trabalho, primeiro precisaremos desenhar a tela de supervisão em algum software que permita exportá-la em formato SVG (scalable vectorial graphics), que é desenho vetorial baseado em arquivo XML. Uma excelente opção open-source é o Inkscape (link a seguir), que adotaremos nesta disciplina.

Inkscape

Free design tool

Ao abrir o Inkscape você verá canvas em branco ao centro, com ferramentas de desenho no painel à esquerda, diversas ferramentas no painel superior e uma paleta seletora de cores na parte inferior.
A primeira ação necessária é configurar o tamanho do canvas. Para isto, vá em File > Document Properties. Na janela que abrir, configure o Format para px (pixels) e ajuste os tamanhos Width e Height para o desejado. Como recomendação, mantenha as dimensões próximas a px, com proporção 3W:2H para telas widescreen ou 1W:2H para telas mobile.
Agora desenhe a tela de sinóptico, representado o mapa do processo e os componentes principais que se deseja supervisionar. Pode-se utilizar ferramentas como Pen Tool para o desenho de linhas, Rectangle tool para retângulos e Ellipse/Arc Tool para círculos/arcos. Elementos visuais que são uma combinação de formas elementares (vide a Bomba #1 ou Válvula #1 na ) devem ser agrupados: selecione todos os desenhos elementares do grupo de clique em Object > Group.
descrição para leitores de tela
Fig. 3
Desenho no Inskcape da tela de sinóptico de um tanque simples ilustrativo.
Todos os elementos que serão dinâmicos devem receber um identificador único para ser posteriormente animado pelo Node-RED. Para isto, selecione o elemento (ou grupo) que será dinâmico, clique em Object > Object Properties.... Na tela que se abre à direita, insira no campo ID uma string com o identificador único deste elemento (recomenda-se uma string simples, sem espaços ou acentos, e.g., bomba-1, tanque1) e clique em Set.
Agora salvamos o arquivo SVG, primeiro clicando em File > Save As.... Na tela que se abre, dê o nome para o arquivo e altere o formato para Plain SVG (.svg); clique save.
O arquivo .svg pode ser aberto em qualquer editor de texto, verificando-se assim sua estrutura em formato de marcação XML. Veja abaixo o exemplo do XML gerado para o exemplo do sinóptico desenhado na .
xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="1500"
   height="1000"
   viewBox="0 0 396.87499 264.58333"
   version="1.1"
   id="svg1"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs1" />
  <g
     id="layer1">
    <g
       id="layer1-6"
       transform="matrix(1.6415909,0,0,1.6415909,-2.7252544,3.646178)">
      <rect
         style="fill:#7398ff;fill-opacity:1;stroke:none;stroke-width:1.62089"
         id="tank-liquid"
         width="94.399651"
         height="134.66446"
         x="89.927414"
         y="11.343787"
         rx="2.1066425"
         ry="2.1066425" />
      <rect
         style="fill:none;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.62089;stroke-opacity:1"
         id="rect1"
         width="94.399651"
         height="134.66446"
         x="89.927414"
         y="11.343787"
         rx="2.1066425"
         ry="2.1066425" />
      <g
         id="g13">
        <path
           style="fill:none;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.755"
           d="M 89.96995,27.413319 H 50.769136 c 0,0 -9.383658,0.938366 -9.383658,9.383655 V 158.94082"
           id="path1" />
        <path
           style="fill:none;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.755;stroke-opacity:1"
           d="M 90.22112,33.386164 H 53.115724 c 0,0 -4.98458,1.319446 -4.98458,4.984581 V 158.99325"
           id="path2" />
      </g>
      <g
         id="pump-1"
         transform="matrix(1.6100848,0,0,1.6100848,-88.385608,-22.008851)"
         style="fill:#76ff76;fill-opacity:1;stroke:#666666">
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#666666;stroke-width:1;stroke-dasharray:none"
           d="m 74.62181,67.992148 -5.699296,3.886787 V 55.402317 l 6.026033,3.307273"
           id="path5" />
        <circle
           style="fill:#76ff76;fill-opacity:1;stroke:#666666;stroke-width:1.00004;stroke-dasharray:none"
           id="path3"
           r="6.6486397"
           cy="63.716843"
           cx="79.568748" />
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#666666;stroke-width:0.996714;stroke-dasharray:none;stroke-opacity:1"
           d="M 86.30731,64.015485 V 51.785232 l -7.083859,-0.121091 -0.06055,5.917692"
           id="path8" />
      </g>
      <g
         id="g7"
         transform="matrix(1.4575545,0,0,1.4575545,-74.229555,-59.18125)"
         style="fill:#999999;stroke:#b3b3b3;stroke-opacity:1">
        <path
           style="fill:#999999;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.42315;stroke-dasharray:none;stroke-opacity:1"
           d="m 144.9523,149.63601 v -20.2475"
           id="path6" />
        <path
           style="fill:#999999;fill-opacity:0.973475;stroke:#b3b3b3;stroke-width:0.426945;stroke-dasharray:none;stroke-opacity:1"
           d="m 144.74079,129.34691 c 0,0 21.55047,6.2257 21.55047,0 0,-6.22569 -21.55047,0 -21.55047,0 z"
           id="path7" />
        <path
           style="fill:#999999;fill-opacity:0.973475;stroke:#b3b3b3;stroke-width:0.426945;stroke-dasharray:none;stroke-opacity:1"
           d="m 145.04713,129.34691 c 0,0 -21.55047,6.2257 -21.55047,0 0,-6.22569 21.55047,0 21.55047,0 z"
           id="path7-3" />
      </g>
      <text
         xml:space="preserve"
         style="font-size:6.554px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.07707;stroke-dasharray:none;stroke-opacity:1"
         x="20.11138"
         y="62.037308"
         id="text8"><tspan
           id="tspan8"
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.07708"
           x="20.11138"
           y="62.037308">Bomba #1</tspan></text>
      <text
         xml:space="preserve"
         style="font-size:6.554px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.07707;stroke-dasharray:none;stroke-opacity:1"
         x="218.41583"
         y="107.67558"
         id="text8-6"><tspan
           id="tspan8-1"
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.07708"
           x="218.41583"
           y="107.67558">Válvula #1</tspan></text>
      <text
         xml:space="preserve"
         style="font-size:6.554px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.07707;stroke-dasharray:none;stroke-opacity:1"
         x="154.81863"
         y="155.75458"
         id="text8-0"><tspan
           id="tspan8-5"
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.07708"
           x="154.81863"
           y="155.75458">Agitador</tspan></text>
      <text
         xml:space="preserve"
         style="font-size:8.2187px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.35064;stroke-dasharray:none;stroke-opacity:1"
         x="137.78539"
         y="7.0295162"
         id="text8-0-6"><tspan
           id="tspan8-5-2"
           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.35065"
           x="137.78539"
           y="7.0295162">Tanque #1</tspan></text>
      <g
         id="g12">
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
           d="m 183.85461,132.73335 h 23.38262"
           id="path12" />
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
           d="m 183.85461,127.98561 h 23.38262"
           id="path12-7" />
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
           d="m 232.53516,132.73335 10.88768,0"
           id="path12-2" />
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
           d="m 232.53516,127.98561 10.88768,0"
           id="path12-7-6" />
      </g>
      <g
         id="valve-1"
         transform="translate(11.005867,7.282557)">
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
           d="m 195.81183,113.54802 v 18.42664 l 12.55382,-9.31684 z"
           id="path9" />
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
           d="m 221.82636,113.54802 v 18.42664 l -12.55382,-9.31684 z"
           id="path9-9" />
        <path
           style="fill:none;fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
           d="M 208.77395,122.6488 V 111.68203"
           id="path10" />
        <path
           style="fill:#76ff76;fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
           id="path11"
           d="m 200.78603,111.67303 a 7.7919927,7.7919927 0 0 1 7.79199,-7.792 7.7919927,7.7919927 0 0 1 7.79199,7.792 h -7.79199 z" />
      </g>
    </g>
  </g>
</svg>
Note no código acima que cada elemento visual é declarado com uma tag específica (e.g., <text>, <rect>, <path>) e que os grupos são definidos pela tag <g>). Cada elemento possui seus próprios parâmetros definidos nestas tags (e.g., fill= para cor de preenchimento, x= para posição x no canvas). Perceba também os parâmetros id= que recebem o valor dos IDs que foram definidos; este parâmetro será útil para identificarmos os elementos que queremos animar. Todo o desenho que criamos visualmente é então definido nesta marcação SVG que começa em <svg> e termina em </svg>.

Node template do Dashboard 2.0

Agora voltamos para o Node-RED e consideramos que o módulo @flowfuse/node-red-dashboard já está instalado.
O Dashboard 2.0 disponibiliza o node "curinga" ui-template, que permite renderizar um widget customizável implementado via código em suas propriedades. . Por baixo, o template é um componente Vue.js (na versão 3), com acesso à biblioteca de componentes Vuetify. Quem já viu desenvolvimento web moderno vai reconhecer a estrutura de um Single-File Component: três seções, <template>, <script> e <style>, cada uma com um papel claro. Quem nunca viu não precisa se assustar, pois a lógica é simplificada.

Para que serve

O node template serve para tudo aquilo que os widgets prontos não cobrem: painéis customizados, formulários com regras próprias, visualizações sob medida e sinópticos. Por via de regra, sempre que precisar de um widget que o Dashboard 2.0 não possui nativamente ou via comunidade, recorre-se ao template para criar o widget customizado.
Dois recursos o conectam o template ao flow do Node-RED:
  • toda mensagem que chega ao nó fica disponível na variável msg de forma reativa;
  • a função send() envia uma mensagem de volta para o fluxo.

A estrutura: três seções

A lógica Vue.js implementada no campo Template nas propriedades do node templatepossui a seguinte estrutura básica:
  • a seção <template> descreve o que se vê (o HTML e os componentes na tela);
  • a seção <script> descreve a lógica (variáveis, cálculos, ações e reações);
  • a seção <style> descreve a aparência (CSS).
A regra de ouro é manter cada coisa no seu lugar: marcação no <template>, comportamento no <script>, estética no <style>.

Comentando o código-base

Ao adicionar o template no flow, você notará que um código-base de exemplo é automaticamente adicionado. Este código renderiza um widget reativo de exemplo no Dashboard; este widget () renderiza um botão que, ao ser clicado, incrementa um contador interno e altera o visual do widget em função de certas condições.
descrição para leitores de tela
Fig. 4
Widget de exemplo automaticamente inserido pelo node template.
A seguir, comentamos este código de exemplo. Vamos por partes.

A seção <template>

html
<template>
    <div>
        <h2>Counter</h2>
        <p :style="{'color' : (count > 5 ? 'red' : 'green' )}">Current Count: {{ count }}</p>
        <p class="my-class">Formatted Count: {{ formattedCount }}</p>
        <b v-if="count > 5">Too many!</b>
        <v-btn @click="increase()">Increment</v-btn>
    </div>
</template>
Linha a linha, este trecho concentra quase toda a sintaxe do Vue que usaremos:
  • O <h2>Counter</h2> é HTML comum e estático.
  • O {{ count }} é uma interpolação de texto: as chaves duplas inserem o valor de uma variável dentro do conteúdo da tag. Quando count muda, esse texto se atualiza sozinho. É o mesmo mecanismo que, no sinóptico, exibiria um valor numérico vindo do processo.
  • O :style="{...}" é uma ligação de atributo (attribute binding). Os dois-pontos são um atalho para v-bind, e significam "o valor deste atributo é uma expressão JavaScript, não um texto fixo". Aqui, a cor do parágrafo é verde ou vermelha conforme count ultrapasse 5 — uma estilização condicional. Em uma tela de sinóptico, esta estrutura poderia ser utilizad para trocar a cor de algum equipamento (e.g., bomba, válvula) em função de seu estado operacional.
  • O v-if="count > 5" é uma renderização condicional: o elemento só existe na página quando a condição é verdadeira. Diferente de apenas escondê-lo, o v-if remove o elemento do DOM. Esta estrutura é útil para mensagens de alarme que só devem aparecer em certas condições.
  • O @click="increase()" é uma ligação de evento. A arroba é atalho para v-on, e conecta um evento do navegador (o clique) a um método do componente. No sinóptico, esta funcionalidade permite fazer com que a tela seja interativa (e.g., o operador clica em uma bomba e ela desliga).
  • Por fim, <v-btn> não é HTML puro: é um componente do Vuetify (um botão estilizado). Os componentes que começam com v- vêm dessa biblioteca e estão à disposição dentro do template (veja a biblioteca de componentes aqui).
A Tabela a seguir consolida a sintaxe utilizada como uma referência rápida para uso futuro:
SintaxeNomeO que faz
{{ x }}InterpolaçãoInsere o valor de x como texto
:attr="expr"v-bindLiga um atributo a uma expressão javascript
@evento="metodo"v-onDispara um método ao ocorrer o evento
v-if="cond"CondicionalRenderiza o elemento só se cond for verdadeira
v-for="i in lista"RepetiçãoRepete o elemento para cada item (não está no exemplo, mas também é bem útil)

A seção <script>

js
export default {
    data() {
        return { count: 0 }
    },
    watch: {
        count: function () {
            if (this.count % 5 === 0) {
                this.send({payload: 'Multiple of 5'})
            }
        }
    },
    computed: {
        formattedCount: function () {
            return this.count + ' Apples'
        }
    },
    methods: {
        increase: function () {
            this.count++
        }
    },
    mounted() { /* ... */ },
    unmounted() { /* ... */ }
}
O export default { ... } entrega ao Vue um objeto com as "opções" do componente. Cada chave dentro dele tem um propósito bem definido, e entender qual usar quando é metade do aprendizado.
O data() devolve as variáveis reativas do componente (seu estado). Aqui há uma só, count, iniciada em 0. "Reativa" quer dizer que, ao alterá-la, tudo que depende dela na tela se atualiza automaticamente; não se mexe no DOM à mão. Note que msg não precisa ser declarada aqui: o nó já a injeta sozinho.
O computed define variáveis derivadas, recalculadas automaticamente quando aquilo de que dependem muda. formattedCount é sempre count + ' Apples'. A diferença em relação a um método é que o computed é cacheado: só recalcula quando uma dependência muda, ao passo que um método roda toda vez que é chamado.
O methods reúne as ações que o componente pode executar, normalmente disparadas por eventos. increase() apenas incrementa count.
O watch observa uma variável e reage às mudanças dela. Aqui ele vigia count e, sempre que o valor for múltiplo de 5, chama this.send(...) — disparando uma mensagem para a saída do nó. Repare que é justamente no watch que o componente fala com o fluxo do Node-RED. A diferença entre computed e watch também vale fixar: use computed quando precisa calcular um valor a partir de outros; use watch quando precisa executar uma ação (um efeito colateral, como enviar uma mensagem) em resposta a uma mudança.
Os mounted() e unmounted() são hooks de ciclo de vida. O primeiro roda uma vez, quando o componente é carregado na tela — bom lugar para inicializações, como pedir ao fluxo o estado atual do processo. O segundo roda quando o componente é retirado, por exemplo quando o operador navega para outra página — lugar para limpeza, como parar um temporizador. Num sinóptico de produção, são onde se liga e desliga o que precisa começar e terminar junto com a tela.

A seção <style>

css
<style>
    .my-class {
        color: red;
    }
</style>
Aqui vai CSS puro. A classe .my-class, aplicada no <template> ao segundo parágrafo, deixa o texto vermelho. É o canto da estética: cores, espaçamentos, fontes. Em sinópticos, costuma ser pouco usado, porque a maior parte da aparência vem dos próprios atributos do SVG — mas está à disposição quando necessário.
Exemplo 19·Sinóptico de planta com tanque
Este exemplo mostra como fazer uma tela de sinóptico de exemplo que representa uma planta com tanque e dois atuadores.
No Node-RED, crie um flow como ilustrado na figura abaixo. O slider injeta um payload simples com um Number entre $[0,100].
O node function 1 adequa a mensagem do slider para o código do template, com o seguinte algoritmo:
javascript
msg.payload = {nivelTanque1: msg.payload}
return msg;
O node template recebe o seguinte código em seu campo Template:
javascript
<template>
    <svg width="500" height="333" viewBox="0 0 396.87499 264.58333" version="1.1" id="svg1"
        xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
        <defs id="defs1" />
        <g id="layer1">
            <g id="layer1-6" transform="matrix(1.6415909,0,0,1.6415909,-2.7252544,3.646178)">
                <rect style="fill:#7398ff;fill-opacity:1;stroke:none;stroke-width:1.62089" id="tank-liquid"
                    width="94.399651" :height="tanque1liquidH" x="89.927414" :y="tanque1liquidY" rx="2.1066425"
                    ry="2.1066425" />
                <text xml:space="preserve"
                    style="font-size:13.4917px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:2.2172;stroke-dasharray:none;stroke-opacity:1"
                    x="300" y="170" id="text-nivel-tanque1">
                    <tspan id="tspan8-5-2-0"
                        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:2.21721"
                        x="140" y="90">Nível: {{nivelTanque1}}%</tspan>
                </text>
                <rect style="fill:none;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.62089;stroke-opacity:1"
                    id="rect1" width="94.399651" height="134.66446" x="89.927414" y="11.343787" rx="2.1066425"
                    ry="2.1066425" />
                <g id="g13">
                    <path style="fill:none;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.755"
                        d="M 89.96995,27.413319 H 50.769136 c 0,0 -9.383658,0.938366 -9.383658,9.383655 V 158.94082"
                        id="path1" />
                    <path style="fill:none;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.755;stroke-opacity:1"
                        d="M 90.22112,33.386164 H 53.115724 c 0,0 -4.98458,1.319446 -4.98458,4.984581 V 158.99325"
                        id="path2" />
                </g>
                <g id="pump-1" @click="toggleBomba1" transform="matrix(1.6100848,0,0,1.6100848,-88.385608,-22.008851)"
                    style="fill:#76ff76;fill-opacity:1;stroke:#666666">
                    <path style="fill-opacity:1;stroke:#666666;stroke-width:1;stroke-dasharray:none" :fill="corBomba1"
                        d="m 74.62181,67.992148 -5.699296,3.886787 V 55.402317 l 6.026033,3.307273" id="path5" />
                    <circle style="fill-opacity:1;stroke:#666666;stroke-width:1.00004;stroke-dasharray:none"
                        :fill="corBomba1" id="path3" r="6.6486397" cy="63.716843" cx="79.568748" />
                    <path
                        style="fill-opacity:1;stroke:#666666;stroke-width:0.996714;stroke-dasharray:none;stroke-opacity:1"
                        :fill="corBomba1" d="M 86.30731,64.015485 V 51.785232 l -7.083859,-0.121091 -0.06055,5.917692"
                        id="path8" />
                </g>
                <g id="g7" transform="matrix(1.4575545,0,0,1.4575545,-74.229555,-59.18125)"
                    style="fill:#999999;stroke:#b3b3b3;stroke-opacity:1">
                    <path
                        style="fill:#999999;fill-opacity:0.496021;stroke:#b3b3b3;stroke-width:1.42315;stroke-dasharray:none;stroke-opacity:1"
                        d="m 144.9523,149.63601 v -20.2475" id="path6" />
                    <path
                        style="fill:#999999;fill-opacity:0.973475;stroke:#b3b3b3;stroke-width:0.426945;stroke-dasharray:none;stroke-opacity:1"
                        d="m 144.74079,129.34691 c 0,0 21.55047,6.2257 21.55047,0 0,-6.22569 -21.55047,0 -21.55047,0 z"
                        id="path7" />
                    <path
                        style="fill:#999999;fill-opacity:0.973475;stroke:#b3b3b3;stroke-width:0.426945;stroke-dasharray:none;stroke-opacity:1"
                        d="m 145.04713,129.34691 c 0,0 -21.55047,6.2257 -21.55047,0 0,-6.22569 21.55047,0 21.55047,0 z"
                        id="path7-3" />
                </g>
                <text xml:space="preserve"
                    style="font-size:6.554px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.07707;stroke-dasharray:none;stroke-opacity:1"
                    x="20.11138" y="62.037308" id="text8">
                    <tspan id="tspan8"
                        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.07708"
                        x="20.11138" y="62.037308">Bomba #1</tspan>
                </text>
                <text xml:space="preserve"
                    style="font-size:6.554px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.07707;stroke-dasharray:none;stroke-opacity:1"
                    x="218.41583" y="107.67558" id="text8-6">
                    <tspan id="tspan8-1"
                        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.07708"
                        x="218.41583" y="107.67558">Válvula #1</tspan>
                </text>
                <text xml:space="preserve"
                    style="font-size:6.554px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.07707;stroke-dasharray:none;stroke-opacity:1"
                    x="154.81863" y="155.75458" id="text8-0">
                    <tspan id="tspan8-5"
                        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.07708"
                        x="154.81863" y="155.75458">Agitador</tspan>
                </text>
                <text xml:space="preserve"
                    style="font-size:8.2187px;line-height:1;font-family:Comfortaa;-inkscape-font-specification:Comfortaa;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#666666;fill-opacity:1;stroke:#666666;stroke-width:1.35064;stroke-dasharray:none;stroke-opacity:1"
                    x="137.78539" y="7.0295162" id="text8-0-6">
                    <tspan id="tspan8-5-2"
                        style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1.35065"
                        x="137.78539" y="7.0295162">Tanque #1</tspan>
                </text>
                <g id="g12">
                    <path
                        style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
                        d="m 183.85461,132.73335 h 23.38262" id="path12" />
                    <path
                        style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
                        d="m 183.85461,127.98561 h 23.38262" id="path12-7" />
                    <path
                        style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
                        d="m 232.53516,132.73335 10.88768,0" id="path12-2" />
                    <path
                        style="fill:#76ff76;fill-opacity:1;stroke:#b3b3b3;stroke-width:1.64475;stroke-dasharray:none;stroke-opacity:1"
                        d="m 232.53516,127.98561 10.88768,0" id="path12-7-6" />
                </g>
                <g id="valve-1" @click="toggleValvula1" transform="translate(11.005867,7.282557)">
                    <path
                        style="fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
                        :fill="corValvula1" d="m 195.81183,113.54802 v 18.42664 l 12.55382,-9.31684 z" id="path9" />
                    <path
                        style="fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
                        :fill="corValvula1" d="m 221.82636,113.54802 v 18.42664 l -12.55382,-9.31684 z" id="path9-9" />
                    <path
                        style="fill:none;fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
                        d="M 208.77395,122.6488 V 111.68203" id="path10" />
                    <path
                        style="fill-opacity:1;stroke:#666666;stroke-width:0.609165;stroke-dasharray:none;stroke-opacity:1"
                        :fill="corValvula1" id="path11"
                        d="m 200.78603,111.67303 a 7.7919927,7.7919927 0 0 1 7.79199,-7.792 7.7919927,7.7919927 0 0 1 7.79199,7.792 h -7.79199 z" />
                </g>
            </g>
        </g>
    </svg>
</template>

<script>
    export default {
        data() {
            // define variables available component-wide
            // (in <template> and component functions)
            return {
                nivelTanque1: 50,        // 0 a 100 (%)
                bomba1Ligada: false,
                valvula1Ligada: false,
            }
        },
        watch: {
            // Toda mensagem que chega no node template vira a variavel msg
            msg(m) {
                const p = m && m.payload; // curto-circuito logico para receber o valor de payload com segurança
                if (p === undefined || null) return
                if (typeof p === 'object') {
                    if (p.nivelTanque1 !== undefined) this.nivelTanque1 = Math.max(0, Math.min(Number(p.nivelTanque1), 100)) // limita o valor de nivelTanque1 entre 0 e 100
                    if (p.bomba1Ligada !== undefined) this.bomba1Ligada = !!p.bomba1Ligada // garante que p.bomba1Ligada recebe um booleano válido
                    if (p.valvula1Ligada !== undefined) this.valvula1Ligada = !!p.valvula1Ligada
                }
            }
        },
        computed: {
            // Cor da bomba1 conforme sua variavel de estado
            corBomba1() {
                return this.bomba1Ligada ? '#00ff00' : '#9e9e9e'
            },
            //Cor da valvula1 conforme sua varivel de estado
            corValvula1(){
                return this.valvula1Ligada ? '#00ff00' : '#9e9e9e'
            },
            // Líquido do tanque no SVG: y=11.343787, height=134.66446 (cheio).
            // Fundo interno = 11.343787 + 134.66446 = 146.008247.
            // Para crescer de baixo para cima: aumenta height E reduz y simultaneamente, proporcionalmente.
            tanque1liquidH() {
                return (134.66446 * this.nivelTanque1 / 100).toFixed(3)
            },
            tanque1liquidY() {
                return (146.008247 - 134.66446 * this.nivelTanque1 / 100).toFixed(3)
            }
        },
        methods: {
            toggleBomba1() {
                //this.bomba1Ligada = !this.bomba1Ligada
                this.send({topic: 'comandoBomba1', payload: this.bomba1Ligada})
            },
            toggleValvula1() {
                //this.valvula1Ligada = !this.valvula1Ligada
                this.send({topic: 'comandoValvula', payload: this.valvula1Ligada})
            }
            
        },
        mounted() {
            // code here when the component is first loaded
        },
        unmounted() {
            // code here when the component is removed from the Dashboard
            // i.e. when the user navigates away from the page
        }
    }
</script>

<style>
    /* define any styles here - supports raw CSS */
</style>
O node function 2 recebe os comandos de acionamento dos atuadores provindos da tela de sinóptico e envia um comando adequado para a tela de sinóptico simulando a atualização de estado dos atuadores:
javascript
if (msg.topic === 'comandoBomba1'){
    return { payload: {bomba1Ligada: !msg.payload}}
}
if (msg.topic === 'comandoValvula') {
    return { payload: { valvula1Ligada: !msg.payload } }
}
Salve os componentes e realize Deploy. O resultado esperado é um Dashboard com tela de sinóptico interativa, como representada no vídeo a seguir. Clique altere o slide e clique nos atuadores para verificar se está tudo funcionando de forma adequada.
Problema 6·Alterando o sinóptico do tanque
Considerando a tela de sinóptico apresentada no Exemplo 1 faça:
  • Sendo o tanque repleto de ácido, altere a cor de preenchimento para #B3B826.
  • Considerando que o tanque possui capacidade de L, o texto interno do tanque deve indicar esta quantidade em volume (e.g. Volume: <X> L).
  • Os atuadores devem assumir a cor de preenchimento #9CEFFF quando estiverem acionados e #CDCDCD quando estiverem desacionados.
  • A mensagem de saída dos atuadores quando pressionados deve ser {topic: 'comandoBomba1', payload: {valorAtual: >true/false>}

Autor: FILIPE A. S. ROCHA

Publicado em 09 de junho de 2026· Atualizado em 29 de junho de 2026