UNPKG

wow-another-ethernet-ip

Version:

Uma biblioteca para comunicação com dispositivos com o protocolo CIP via Ethernet/IP

1,200 lines (1,020 loc) 65.3 kB
import { EmissorEvento } from "../../../Utils/EmissorEvento.js"; import { CompactLogixRockwell } from "./CompactLogix.js"; /** * Gerencia a conexão com um CompactLogix e permite ler/escrever tags, além de observar alterações de tags. ** Essa classe é uma camada de abstração com melhorias no desempenho de leitura e escritas de tags, além de permitir observações de tags. (Coisa que a classe base não tem) ** Se preferir ter mais controle sobre lógica de leituras/escritas/observações, utilize a classe `CompactLogix.js` diretamente. */ export class CompactLogixV2 { #configuracoes = { /** * Se os logs devem ser mostrados no console */ isMostrarConsoleLogs: false, /** * Se em caso de perda de conexão, será tentado reconectar automaticamente */ isAutoReconectar: false, /** * Endereço IP do CompactLogix */ ip: '', /** * Porta de comunicação com o CompactLogix */ porta: 44818, /** * Configuraçoes do comportamento da leitura de tags */ leitura: { /** * Tempo em millisegundos que deve ocorrer as leituras das tags pendentes de leituras */ tempoMinimoEntreLeituras: 1400 }, /** * Configurações do comportamento das escritas de tags */ escrita: { /** * Tempo em millisegundos que deve ocorrer as escritas das tags pendentes de escritas */ tempoMinimoEntreEscritas: 1400 }, /** * Configurações do comportamento de observações das tags */ observacoes: { /** * O tempo em millisegundos que as leituras das tags observadas devem ser realizadas */ tempoEntreLeituras: 1000 } } /** * @callback CallbackConfirmaLeituraTag * @param {String} tag - Tag que foi lido * @param {DataTypeTag} dataType - Informações do Data Type da tag * @param {DataValorTag} valor - Informações do valor lido da tag * @param {Boolean} isLeituraSucesso - Se o retorno da tag retornou sucesso. Caso contrario, objeto erro deve conter os detalhes do erro * @param {Object} erro - Se ocorreu algum erro, contém detalhes do erro * @param {String} erro.descricao - Descrição ocorrida do erro */ /** * Informações do Data Type de uma tag * @typedef DataTypeTag * @property {Boolean} dataType.isAtomico - Se o Data Type é atomico * @property {Object} dataType.atomico - Se o Data Type for atomico, contém os detalhes do Data Type * @property {Number} dataType.atomico.codigoAtomico - O código especifico desse tipo de tag atomico * @property {String} dataType.atomico.descricao - Descrição do tipo atomico * @property {Boolean} dataType.atomico.isSigned - Se o valor é um número com sinal * @property {Number} dataType.atomico.tamanho - Tamanho em bytes do número atómico * @property {Boolean} dataType.isStruct - Se o Data Type é uma Struct * @property {Object} dataType.struct - Se o Data Type for uma Struct, contém os detalhes da Struct * @property {Number} dataType.struct.codigoStruct - O código especifico desse tipo de tag Struct * @property {String} dataType.struct.descricao - Descrição do tipo Struct */ /** * Informações do valor de uma tag * @typedef DataValorTag * @property {Boolean} valor.isAtomico - Se o valor for atomico, contém o valor atomico * @property {Object} valor.atomico - Detalhes do tipo atomico * @property {Number} valor.atomico.numero - Valor atomico da tag(no caso um número) * @property {Boolean} valor.isStruct - Se o valor for uma struct * @property {Object} valor.struct - Se o valor for uma struct, contém os detalhes do Struct * @property {Boolean} valor.struct.isASCIIString82 - Se o valor for uma Struct do tipo ASCII String 82 * @property {Object} valor.struct.ASCIIString82 - Detalhes do valor da Struct do tipo ASCII String 82 * @property {String} valor.struct.ASCIIString82.string - String que foi lida na tag */ /** * @typedef LeituraTagResultado * @property {String} tag - Tag que foi lido * @property {DataTypeTag} dataType - Detalhes do Data Type da tag * @property {DataValorTag} valor - Valor lido da tag. Esse valor é dinamico dependendo do Data Type da tag. * @property {Object} erro - Se ocorreu algum erro, contém detalhes do erro * @property {String} erro.descricao - Descrição ocorrida do erro * @property {Boolean} isLeituraRecebida - Se essa tag já foi confirmado o recebimento da leitura(sendo ele sucesso ou não) * @property {Boolean} isTagValida - Se a leitura da tag é valida e não ocorreu nenhum erro. */ /** * @typedef PromiseLeituraTag * @property {Number} id - ID unico incremental * @property {LeituraTagResultado[]} tags - Tags da leitura * @property {CallbackConfirmaLeituraTag} confirmaTag - Função a ser chamada para confirmar a leitura de uma tag * @property {Number} idTimeout - ID do setTimeout caso demore demais para a leitura das tags */ /** * @callback CallbackConfirmaEscritaTag * @param {String} tag - A tag que foi escrita * @param {Boolean} isEscritaSucesso - Se a escrita foi confirmada no CompactLogix * @param {Object} erro - Se ocorreu um erro, contém os detalhes do erro * @param {String} erro.descricao - A descrição do erro */ /** * @typedef EscritaTagResultado * @property {String} tag - Tag no CompactLogix * @property {*} valor - Valor a ser escrito na tag * @property {Boolean} isEscritoSucesso - Se a escrita foi confirmada no CompactLogix * @property {Boolean} isRecebidoConfirmacao - Se essa tag já foi confirmado o recebimento pelo dispositivo(sendo ele sucesso ou não) * @property {Object} erro - Se não foi possível escrever a tag, contém os detalhes do erro * @property {String} erro.descricao - Descrição do erro */ /** * @typedef PromiseEscritaTag * @property {Number} id - ID unico incremental * @property {EscritaTagResultado[]} tags - Tags a serem escritas * @property {CallbackConfirmaEscritaTag} confirmaEscrita - Confirma a escrita de uma tag * @property {Number} idTimeout - ID do setTimeout caso demore demais para a escrita das tags */ /** * @typedef CacheDataType * @property {String} tag - Nome da tag * @property {Boolean} isAtomico - Se o Data Type é atomico * @property {Object} atomico - Se o Data Type for atomico, contém os detalhes do Data Type * @property {Number} atomico.codigoAtomico - O código especifico desse tipo de tag atomico * @property {Boolean} isStruct - Se o Data Type é uma Struct * @property {Object} struct - Se o Data Type for uma Struct, contém os detalhes da Struct * @property {Number} struct.codigoStruct - O código especifico desse tipo de tag Struct */ /** * @callback CallbackObservaTagValorAlterado * @param {DataValorTag} valorAntigo - O valor anterior da tag * @param {DataValorTag} valorNovo - O novo valor da tag */ /** * @typedef CallbackObservacaoTag * @property {Number} id - ID unico desse callback * @property {CallbackObservaTagValorAlterado} onAlterado - Função a ser chamada quando o valor da tag mudar */ /** * @typedef ObservacaoDeTag * @property {String} tag - Nome da tag * @property {DataTypeTag} dataType - Informações do Data Type da tag * @property {DataValorTag} valor - Valor atual da tag * @property {CallbackObservacaoTag[]} callbacks - Callbacks que desejam receber notificações de alterações dessa tag */ /** * Controle de estado com o CompactLogix */ #estado = { /** * Instancia do controlador CompactLogix da minha biblioteca que comunica via EtherNet IP * @type {CompactLogixRockwell} */ controlador: undefined, /** * Estado de leituras de tags */ leituras: { /** * Tags que estão na fila de pendentes para serem lidas * @type {PromiseLeituraTag[]} */ pendentesDeLeitura: [], /** * O totalizador de lote é um ID incremental que é utilizado para identificar um lote de leituras. */ totalizadorLoteId: 0, /** * Se existe uma chamada de leitura pendente para executar */ isLeituraPendente: false, /** * Se a leitura está sendo executada, ou seja aguardando a resposta do CompactLogix */ isLeituraExecutando: false, /** * Data da última vez que foi executado a leitura das tags no CompactLogix * @type {Date} */ dataUltimaExecucao: undefined }, /** * Estado de escrita de tags */ escritas: { /** * Tags que estão na fila para serem escritas * @type {PromiseEscritaTag[]} */ pendentesDeEscrita: [], /** * O totalizador de lote é um ID incremental que é utilizado para identificar um lote de escritas. */ totalizadorLoteId: 0, /** * Se já existe uma chamada pendente que será executada */ isEscritaPendente: false, /** * Se esta atualmente executando a escrita das tags no CompactLogix */ isEscritaExecutando: false, /** * Data em que ocorreu a ultima execução de escrita de tags */ dataUltimaExecucao: undefined }, /** * Estado de observações de tags */ observacoes: { /** * Tags observadas * @type {ObservacaoDeTag[]} */ tags: [], /** * Se a leitura das tags está sendo realizada para disparar as observações */ isRealizandoLeitura: false, /** * ID do setInterval criado que realiza o trigger de leitura das tags observadas */ IDSetInterval: -1 }, /** * O cache de Data Types armazena os tipos das últimas tags que foram escritas/lidas. * * O comportamento desse cache é definido assim: * - Se a tag não estiver no cache e for uma escrita, a chamada da função ao Compact informara para auto-reconhecer a tag, o que resultará em duas requisições (busca tipo da tag e depois escreve). Se retornar sucesso, a tag será adicionada ao cache. * - Se a tag não estiver no cache e for uma leitura, se o retorno da leitura for sucesso, a tag será adicionada ao cache. * - Se a tag existir no cache, o tipo dela será informado na requisição ao Compact se necessario(no caso de escritas), evitando overhead de 2 requisições. * - Se por uma rara chance o controlador retornar que o Data Type tá incorreto, é provavél que a tag no controlador foi alterado o tipo dela. Nesses casos, o cache identificara que esse erro ocorreu e atualizará o cache com o novo Data Type. * * @type {CacheDataType[]} */ cacheDataTypes: [], /** * Emissor de eventos customizado * @type {EmissorEvento} */ emissorEventos: undefined } /** * Instanciar uma nova conexão com um CompactLogix * @param {Object} construtorOpcoes - Propriedades de conexão * @param {Boolean} construtorOpcoes.isMostrarConsoleLogs - Se deve mostrar logs no console * @param {Object} construtorOpcoes.conexao - Propriedades de conexão com o controlador CompactLogix * @param {String} construtorOpcoes.conexao.ip - Endereço IP do CompactLogix * @param {Number} construtorOpcoes.conexao.porta - Porta de comunicação com o CompactLogix * @param {Boolean} construtorOpcoes.isAutoReconectar - Se em caso de perca de conexão, será tentado reconectar automaticamente */ constructor(construtorOpcoes) { if (construtorOpcoes == undefined) throw new Error('As propriedades do construtor precisam ser informadas.'); if (construtorOpcoes.isMostrarConsoleLogs) this.#configuracoes.isMostrarConsoleLogs = construtorOpcoes.isMostrarConsoleLogs; if (construtorOpcoes.conexao == undefined) throw new Error('As propriedades de conexão com o CompactLogix precisam ser informadas.'); if (construtorOpcoes.conexao.ip == undefined) throw new Error('O endereço IP do CompactLogix precisa ser informado.'); if (construtorOpcoes.isAutoReconectar) this.#configuracoes.isAutoReconectar = construtorOpcoes.isAutoReconectar; // Se informado a porta opcional, eu utilizo ela, senão eu utilizo a padrão 44818 if (construtorOpcoes.conexao.porta != undefined) this.#configuracoes.porta = construtorOpcoes.conexao.porta; this.#configuracoes.ip = construtorOpcoes.conexao.ip; this.#estado.emissorEventos = new EmissorEvento(`CompactLogix ${this.#configuracoes.ip}`); this.#estado.controlador = new CompactLogixRockwell({ ip: this.#configuracoes.ip, porta: this.#configuracoes.porta, habilitaLogs: false, autoReconectar: this.#configuracoes.isAutoReconectar }) this.#estado.controlador.onLog((msgLog) => { this.log(msgLog); }) } /** * Iniciar a conexão com o CompactLogix */ async conectar() { const retornoConexao = { /** * Se conectou com sucesso */ isConectado: false, /** * Se não conectou, contém detalhes do erro */ erro: { descricao: '' } } const retornoConectar = await this.#estado.controlador.conectar(); if (retornoConectar.isConectou) { retornoConexao.isConectado = true; } else { retornoConexao.erro.descricao = retornoConectar.erro.descricao; } return retornoConexao; } /** * Encerrar a conexão com o CompactLogix. Independente se está conectado ou não, o Socket será destruido, leituras e escritas pendentes serão removidas. */ desconectar() { this.#estado.controlador.desconectar(); } getParametrosConexao() { return { ip: this.#configuracoes.ip, porta: this.#configuracoes.porta } } /** * @typedef RetLerTags * @property {Boolean} isSucesso - Se a leitura foi sucesso * @property {Object} sucesso - Tags lidas com sucesso * @property {LeituraTagResultado[]} sucesso.tags - Tags lidas com sucesso * @property {Object} erro - Descrição do erro ocorrido * @property {String} erro.descricao - Descrição do erro ocorrido */ /** * Ler tags no CompactLogix * @param {Array<String>} tags - Array de tags string a serem lidas * @returns {Promise<RetLerTags>} */ async lerTags(tags) { /** * @type {RetLerTags} */ let retornoLeitura = { isSucesso: false, sucesso: { tags: [] }, erro: { descricao: '' } } // Precisa ser um array de tags if (tags == undefined || Array.isArray(tags) == false) { throw new Error('As tags a serem lidas não foram informadas ou não são um array.'); } // Tags que vão ser adicionados na leitura let tagsDesejadas = []; for (const tagString of tags) { if (typeof tagString != 'string') { throw new Error(`As tags a serem lidas precisam ser strings. Foi informado um valor ${tagString} do tipo ${typeof tagString}`); } // Não adicionar tags repetidas if (tagsDesejadas.indexOf(tagString) != -1) continue tagsDesejadas.push(tagString); } /** * @type {PromiseLeituraTag} */ let novaLeitura = { id: this.#estado.leituras.totalizadorLoteId++, tags: [], confirmaTag: () => { }, idTimeout: -1 } // Adicionar o status das tags pro retorno for (const tagString of tagsDesejadas) { novaLeitura.tags.push({ tag: tagString, valor: {}, erro: { descricao: '' }, isLeituraRecebida: false, isTagValida: false }) } const logLeitura = (string) => { this.log(`[Leitura Tag] Lote #${novaLeitura.id}: ${string}`); } const promiseAguarda = new Promise((resolve) => { // Essa função é utilizada para confirmar a leitura de uma tag /** * @type {CallbackConfirmaLeituraTag} */ const confirmaLeituraTag = (tag, datatype, valor, isLeituraSucesso, detalhesErro) => { // Encontrar a tag confirmada na lista de tags pendentes const tagObj = novaLeitura.tags.find(t => t.tag == tag); if (tagObj != undefined) { // Se a tag já foi recebida, eu só ignoro if (tagObj.isLeituraRecebida) { logLeitura(`A tag ${tag} já foi confirmada anteriormente. Ignorando...`); isTodosConfirmados(); return; } // Se teve sucesso na leitura, algumErro será nullo if (isLeituraSucesso) { logLeitura(`Tag ${tag} confirmada a leitura com sucesso.`); tagObj.isTagValida = true; // Adicionar o valor e o Data Type da tag tagObj.valor = valor; tagObj.dataType = datatype; } else { if (detalhesErro == undefined) { detalhesErro = { descricao: 'Erro da leitura não informado.' } } logLeitura(`Tag ${tag} confirmada a leitura com erro: ${detalhesErro.descricao}`); tagObj.erro.descricao = detalhesErro.descricao; } tagObj.isLeituraRecebida = true; // Verificar se todas as tags já foram confirmadas e se sim, resolver a promise isTodosConfirmados(); } else { logLeitura(`Tag ${tag} não foi encontrada na lista de tags pendentes.`); } } // Valida se todas as tags foram lidas const isTodosConfirmados = () => { const tagsAguardandoConfirmacao = novaLeitura.tags.filter(t => t.isLeituraRecebida == false); if (tagsAguardandoConfirmacao.length == 0) { retornoLeitura.isSucesso = true; // Cancelar o timeout de leitura clearTimeout(novaLeitura.idTimeout); // Remover da lista de leituras pendentes this.#estado.leituras.pendentesDeLeitura = this.#estado.leituras.pendentesDeLeitura.filter(l => l.id != novaLeitura.id); logLeitura(`Todas as tags foram confirmadas. Resolvendo a promise...`); retornoLeitura.sucesso.tags = novaLeitura.tags; return resolve(retornoLeitura); } else { logLeitura(`Aguardando confirmações das tags ${tagsAguardandoConfirmacao.map(t => t.tag).join(', ')}...`); } } // Inicia um setTimeout para retornar caso demore na leitura das tags novaLeitura.idTimeout = setTimeout(() => { const tagsAguardandoConfirmacao = novaLeitura.tags.filter(t => t.isLeituraRecebida == false); tagsAguardandoConfirmacao.forEach(t => { t.erro.descricao = 'O CompactLogix demorou para retornar a leitura da tag.'; t.isLeituraRecebida = true; }) // Se pelo menos alguma tag ocorreu sucesso, eu considero que a leitura foi sucesso if (tagsAguardandoConfirmacao.length != novaLeitura.tags.length) { retornoLeitura.isSucesso = true; } else { // Se todas as tags deram erro de leitura, não devo considerar como sucesso. retornoLeitura.erro.descricao = `Timeout ocorrido para todas as leituras de tags`; } logLeitura(`Timeout de leitura ocorrido para as tags: ${tagsAguardandoConfirmacao.map(t => t.tag).join(', ')}. Resolvendo promise...`); retornoLeitura.sucesso.tags = novaLeitura.tags.map(t => { return { valor: t.valor, erro: t.erro, isLeituraRecebida: t.isLeituraRecebida, tag: t.tag, isTagValida: t.isTagValida } }) return resolve(retornoLeitura); }, 5000); novaLeitura.confirmaTag = confirmaLeituraTag; }) // Adicionar essa leitura pendente a lista para ser lida quando possível this.#estado.leituras.pendentesDeLeitura.push(novaLeitura); this.#triggerLeituraTags(); return promiseAguarda; } /** * @typedef RetEscreverTags * @property {Boolean} isSucesso - Se a solicitação de escrita foi enviada e recebido com sucesso(não garante que tags individuais tenham sido sucesso, apenas que o dispositivo remoto retornou alguma resposta) * @property {Object} sucesso - Se a solicitação deu sucesso, contém os detalhes das tags solicitadas para escritas * @property {EscritaTagResultado[]} sucesso.tags - Tags que foram solicitadas para escrita com seus status * @property {Object} erro - Descrição do erro ocorrido * @property {String} erro.descricao - Descrição do erro ocorrido */ /** * Escrever tags no CompactLogix * @param {Object[]} tags - Tags para escrever no CompactLogix * @param {String} tags[].tag - Tag a ser escrita * @param {*} tags[].valor - Valor a ser escrito na tag * @returns {Promise<RetEscreverTags>} */ async escreverTags(tags) { /** * @type {RetEscreverTags} */ let retornoEscrita = { isSucesso: false, sucesso: { tags: [] }, erro: { descricao: '' } } // Se não foi informado um array válido de tags, retornar erro if (tags == undefined || Array.isArray(tags) == false) { throw new Error('As tags a serem escritas não foram informadas ou não são um array.'); } /** * @type {PromiseEscritaTag} */ let novaEscrita = { id: this.#estado.escritas.totalizadorLoteId++, tags: [], confirmaEscrita: () => { } } // Adicionar as tags a serem escritas for (const tag of tags) { novaEscrita.tags.push({ tag: tag.tag, valor: tag.valor, erro: { descricao: '' }, isEscritoSucesso: false, isRecebidoConfirmacao: false }) } const logEscrita = (string) => { this.log(`[Escrita Tag] Lote #${novaEscrita.id} (${novaEscrita.tags.map(t => t.tag).join(', ')}) ${string}`); } const promiseAguardaEscrita = new Promise((resolve) => { // Vincular a função de confirmação de escrita de tag novaEscrita.confirmaEscrita = (tag, isEscritaSucesso, erro) => { const tagObj = novaEscrita.tags.find(t => t.tag == tag); if (tagObj != undefined) { // Se a tag já foi recebida if (tagObj.isRecebidoConfirmacao) { // logEscrita(`A tag ${tag} já foi confirmada anteriormente. Ignorando...`); isTudoOk(); return; } if (isEscritaSucesso) { // logEscrita(`Tag '${tag}' confirmada a escrita com sucesso.`); tagObj.isEscritoSucesso = true; } else { if (erro == undefined) { erro = { descricao: 'Erro da escrita não informado.' } } // logEscrita(`Tag '${tag}' confirmada com erro. Erro: ${erro.descricao}`); tagObj.erro.descricao = erro.descricao; } tagObj.isRecebidoConfirmacao = true; isTudoOk(); } else { // logEscrita(`Tag ${tag} não foi encontrada na lista de tags pendentes de escrita.`); } } const isTudoOk = () => { // Valida se todas as escritas foram realizadas com sucesso pra retornar a mensagem const escritasAguardando = novaEscrita.tags.filter(t => t.isRecebidoConfirmacao == false); if (escritasAguardando.length == 0) { retornoEscrita.isSucesso = true; // Cancelar o timeout de escrita clearTimeout(novaEscrita.idTimeout); // Remover da lista pendente de escritas this.#estado.escritas.pendentesDeEscrita = this.#estado.escritas.pendentesDeEscrita.filter(e => e.id != novaEscrita.id); // logEscrita(`Todas as tags foram confirmadas. Resolvendo a promise...`); retornoEscrita.sucesso.tags = novaEscrita.tags.map(t => { return { tag: t.tag, valor: t.valor, isEscritoSucesso: t.isEscritoSucesso, erro: t.erro, isRecebidoConfirmacao: t.isRecebidoConfirmacao } }) resolve(retornoEscrita); } else { // logEscrita(`Aguardando confirmações das tags ${escritasAguardando.map(t => t.tag).join(', ')}...`); } } // Definir o timeout se demorar para receber as confirmações novaEscrita.idTimeout = setTimeout(() => { const escritasAguardando = novaEscrita.tags.filter(t => t.isRecebidoConfirmacao == false); escritasAguardando.forEach(t => { t.erro.descricao = 'O CompactLogix demorou para retornar a escrita da tag.'; t.isRecebidoConfirmacao = true; }) // Se pelo menos alguma tag ocorreu sucesso, eu considero que a leitura foi sucesso if (escritasAguardando.length != novaEscrita.tags.length) { retornoEscrita.isSucesso = true; } else { // Se todas as tags deram erro de leitura, não devo considerar com sucesso. retornoEscrita.erro.descricao = `Timeout ocorrido para todas as escritas de tags`; } // logEscrita(`Timeout de escrita ocorrido para as tags: ${escritasAguardando.map(t => t.tag).join(', ')}. Resolvendo promise...`); retornoEscrita.sucesso.tags = novaEscrita.tags.map(t => { return { tag: t.tag, valor: t.valor, isEscritoSucesso: t.isEscritoSucesso, erro: t.erro, isRecebidoConfirmacao: t.isRecebidoConfirmacao, } }) return resolve(retornoEscrita); }, 5000); }) // Adicionar ao array de escritas pendentes this.#estado.escritas.pendentesDeEscrita.push(novaEscrita); // Triggar a escritura de tags se possível this.#triggerEscreverTags(); return promiseAguardaEscrita; } /** * Adicionar uma tag para ser observada e invocar seu callback quando houver alteração * * **A tag precisa exitir e ser suportada pela blblioteca.** * @param {String} tag - Nome da tag * @param {Object} callbacks - Definir seus callbacks para executar quando certos eventos ocorrerem * @param {CallbackObservaTagValorAlterado} callbacks.onAlterado - Função a ser chamada quando o valor da tag mudar */ async observarTag(tag, callbacks) { if (tag == undefined || typeof tag != 'string') { throw new Error('A tag a ser observada precisa ser informada e ser uma string.'); } const retObservar = { /** * Se ocorreu sucesso em iniciar o processo de observação da tag. Sucesso é retornado se: * * - A tag existe * - É suportada pela biblioteca */ isSucesso: false, /** * Se sucesso em observar, contém detalhes do observador gerado */ sucesso: { /** * ID de callback unico para permitir cancelar depois */ idCallback: undefined }, /** * Se ocorreu algum erro, contém detalhes adicionais do erro */ erro: { descricao: '' } } const requisitaLeituraTag = await this.lerTags([tag]); if (!requisitaLeituraTag.isSucesso) { retObservar.erro.descricao = requisitaLeituraTag.erro.descricao; return retObservar; } const tagEncontrada = requisitaLeituraTag.sucesso.tags.find(t => t.tag == tag); if (tagEncontrada == undefined) { retObservar.erro.descricao = `A tag ${tag} não foi encontrada na solicitação de leitura ao controlador, não será possível observar.`; return retObservar; } if (!tagEncontrada.isTagValida) { retObservar.erro.descricao = `A tag ${tag} não foi lida com sucesso, motivo: ${tagEncontrada.erro.descricao}.`; return retObservar; } // Se for valida, adicionar as observações let tagObservada = this.#estado.observacoes.tags.find(t => t.tag == tag); if (tagObservada == undefined) { tagObservada = { tag: tag, dataType: tagEncontrada.dataType, valor: tagEncontrada.valor, callbacks: [], } this.#estado.observacoes.tags.push(tagObservada); } /** * @type {CallbackObservacaoTag} */ const novoCallback = { id: tagObservada.callbacks.length + 1, onAlterado: callbacks.onAlterado } tagObservada.callbacks.push(novoCallback); retObservar.isSucesso = true; retObservar.sucesso.idCallback = novoCallback.id; if (this.#estado.observacoes.IDSetInterval == -1) { this.iniciarObservadorInterval(); } return retObservar; } /** * Excluir um callback adicionado de observação para uma tag * @param {String} tag - Tag original que foi observada * @param {Number} id - ID do callback a ser removido */ pararCallbackObservacaoTag(tag, id) { const exclusaoCallback = { isExcluido: false, erro: { descricao: '' } } const tagAlvo = this.#estado.observacoes.tags.find(t => t.tag == tag); if (tagAlvo == undefined) { exclusaoCallback.erro.descricao = `A tag ${tag} não está sendo observada.`; return exclusaoCallback; } const callbackAlvo = tagAlvo.callbacks.find(c => c.id == id); if (callbackAlvo == undefined) { exclusaoCallback.erro.descricao = `O callback com ID ${id} não foi encontrado na tag ${tag}.`; return exclusaoCallback; } tagAlvo.callbacks = tagAlvo.callbacks.filter(c => c.id != id); if (tagAlvo.callbacks.length == 0) { this.pararObservacaoTag(tag); } exclusaoCallback.isExcluido = true; return exclusaoCallback; } /** * Excluir todos os callbacks de uma tag observada * @param {String} tag */ pararObservacaoTag(tag) { const exclusaoTag = { isExcluido: false, erro: { descricao: '' } } const tagAlvo = this.#estado.observacoes.tags.find(t => t.tag == tag); if (tagAlvo == undefined) { exclusaoTag.erro.descricao = `A tag ${tag} não está sendo observada.`; return exclusaoTag; } tagAlvo.callbacks = []; exclusaoTag.isExcluido = true; this.#estado.observacoes.tags = this.#estado.observacoes.tags.filter(t => t.tag != tag); return exclusaoTag; } /** * Verificar se uma tag está sendo observada * @param {String} tag - Nome da tag a ser verificada */ isTagObservada(tag) { return this.#estado.observacoes.tags.find(t => t.tag == tag) != undefined } /** * Inicia o setInterval que verifica por alterações nas tags */ iniciarObservadorInterval() { if (this.#estado.observacoes.IDSetInterval != -1) { clearInterval(this.#estado.observacoes.IDSetInterval); this.#estado.observacoes.IDSetInterval = -1; } this.#estado.observacoes.IDSetInterval = setInterval(() => { this.#triggerObservacaoTags(); }, this.#configuracoes.observacoes.tempoEntreLeituras); this.log(`Iniciado observador de tags com intervalo de ${this.#configuracoes.observacoes.tempoEntreLeituras}ms.`); } /** * Inicia a leitura das tags que estão pendentes */ async #triggerLeituraTags() { // Se já tiver uma chamada de leitura pendente, não deixar iniciar outra if (this.#estado.leituras.isLeituraPendente) { this.log(`Já existe uma chamada de leitura pendente. Ignorando...`); return; } // Se já tiver uma chamada de leitura em execução e aguardando a resposta do CompactLogix if (this.#estado.leituras.isLeituraExecutando) { this.log(`Já existe uma chamada de leitura em execução no CompactLogix. Ignorando...`); return; } if (this.#estado.leituras.pendentesDeLeitura.length == 0) { this.log(`Não existem leituras pendentes para serem lidas.`); return; } let dataAgra = new Date(); const diffMsUltimaExecucao = dataAgra.getTime() - (this.#estado.leituras.dataUltimaExecucao != undefined ? this.#estado.leituras.dataUltimaExecucao.getTime() : this.#configuracoes.leitura.tempoMinimoEntreLeituras + 1); // Se o tempo desde a ultima leitura ainda não atingiu o tempo minimo de espera if (this.#estado.leituras.dataUltimaExecucao != undefined && (diffMsUltimaExecucao < this.#configuracoes.leitura.tempoMinimoEntreLeituras)) { this.#estado.leituras.isLeituraPendente = true; let diffRestante = this.#configuracoes.leitura.tempoMinimoEntreLeituras - diffMsUltimaExecucao; setTimeout(() => { this.#estado.leituras.isLeituraPendente = false; this.#triggerLeituraTags(); }, diffRestante); return; } this.#estado.leituras.dataUltimaExecucao = dataAgra; this.log(`Iniciando leitura de tags pendentes...`); this.#estado.leituras.isLeituraExecutando = true; this.#estado.emissorEventos.disparaEvento('trigger-leitura-tags-executando'); /** * Array de string contendo as tags para ler * @type {String[]} */ const listaDeTagsParaLer = []; // Pra cada leitura pendente, coletar as tags que preciso adicionar no TagGroup pra solicitar for (const leituraTag of this.#estado.leituras.pendentesDeLeitura) { leituraTag.tags.forEach(tag => { if (listaDeTagsParaLer.indexOf(tag.tag) == -1) { listaDeTagsParaLer.push(tag.tag); } }); } // Iniciar a solicitação de leitura const retornoLeituraTags = await this.#estado.controlador.lerMultiplasTags(listaDeTagsParaLer); // Passar por todas as tags solicitadas e verificar no retorno da leitura multipla for (const leituraDeTag of listaDeTagsParaLer) { // Encontrar os lotes de leituras pendentes que possuem essa tag no meio const loteLeiturasDessaTag = this.#estado.leituras.pendentesDeLeitura.filter(l => l.tags.find(t => t.tag == leituraDeTag) != undefined); this.log(`Leitura da tag ${leituraDeTag} tem ${loteLeiturasDessaTag.length} callbacks pendentes.`); // Se a leitura múltipla não deu certo, nenhuma tag foi lida. Retorno o erro if (!retornoLeituraTags.isSucesso) { // Passar em cada lote, e confirmar para o lote que a leitura retornou erro for (const loteLeitura of loteLeiturasDessaTag) { loteLeitura.confirmaTag(leituraDeTag, undefined, undefined, false, { descricao: retornoLeituraTags.erro.descricao }) } continue; } // Beleza, se a leitura multipla deu certo, isso me diz que pelo menos o dispositivo retornou o resultado das tags lidas const resultadoTagLida = retornoLeituraTags.sucesso.tags.find(t => t.tag == leituraDeTag); // Por algum motivo a tag lida não foi retornado no retorno da leitura multipla(não devia acontecer) if (resultadoTagLida == undefined) { for (const loteLeitura of loteLeiturasDessaTag) { loteLeitura.confirmaTag(leituraDeTag, undefined, undefined, false, { descricao: `Não foi retornado nenhuma informação da tag na solicitação de leitura do controlador.` }) } continue; } // Se a leitura da tag retornado não deu sucesso if (!resultadoTagLida.isSucesso) { for (const loteLeitura of loteLeiturasDessaTag) { loteLeitura.confirmaTag(leituraDeTag, undefined, undefined, false, { descricao: resultadoTagLida.erro.descricao }) } continue; } // Atualizar o cache com as informações do Data Type da tag lida if (resultadoTagLida.sucesso.isAtomico) { this.atualizarCacheTagDataType(leituraDeTag, { isAtomico: true, atomico: { codigoAtomico: resultadoTagLida.sucesso.atomico.dataType.codigo } }) } else if (resultadoTagLida.sucesso.isStruct) { this.atualizarCacheTagDataType(leituraDeTag, { isStruct: true, struct: { codigoStruct: resultadoTagLida.sucesso.struct.dataTypeStruct.codigoTipoStruct } }) } // Leitura confirmada com sucesso for (const loteLeitura of loteLeiturasDessaTag) { if (resultadoTagLida.sucesso.isAtomico) { loteLeitura.confirmaTag(leituraDeTag, { isAtomico: true, atomico: { codigoAtomico: resultadoTagLida.sucesso.atomico.dataType.codigo, descricao: resultadoTagLida.sucesso.atomico.dataType.descricao, isSigned: resultadoTagLida.sucesso.atomico.dataType.isSigned, tamanho: resultadoTagLida.sucesso.atomico.dataType.tamanho } }, { isAtomico: true, atomico: { numero: resultadoTagLida.sucesso.atomico.valor } }, true); } else if (resultadoTagLida.sucesso.isStruct) { // Verificar o tipo da Struct switch (resultadoTagLida.sucesso.struct.dataTypeStruct.codigoTipoStruct) { case this.#estado.controlador.getDataTypes().structs.ASCIISTRING82.codigoTipoStruct: { loteLeitura.confirmaTag(leituraDeTag, { isStruct: true, struct: { codigoStruct: resultadoTagLida.sucesso.struct.dataTypeStruct.codigoTipoStruct, descricao: resultadoTagLida.sucesso.struct.dataTypeStruct.descricao } }, { isStruct: true, struct: { isASCIIString82: true, ASCIIString82: { string: resultadoTagLida.sucesso.struct.valor.stringConteudo } } }, true) break; } // Se for um Data Type Struct não suportado, retorno como erro default: { loteLeitura.confirmaTag(leituraDeTag, undefined, undefined, false, { descricao: `Data Type Struct não suportado para leitura: ${resultadoTagLida.sucesso.struct.dataTypeStruct}.` }) break; } } } else { loteLeitura.confirmaTag(leituraDeTag, undefined, undefined, false, { descricao: `Data Type não suportado para leitura da tag` }) } } } this.#estado.leituras.isLeituraExecutando = false; this.#estado.emissorEventos.disparaEvento('trigger-leitura-tags-finalizado'); } /** * Inicia a escrita das tags que estão pendentes */ async #triggerEscreverTags() { if (this.#estado.escritas.isEscritaPendente) { this.log(`Já existe uma chamada de escrita pendente. Ignorando...`); return; } if (this.#estado.escritas.isEscritaExecutando) { this.log(`Já existe uma chamada de escrita em execução no CompactLogix. Ignorando...`); return; } if (this.#estado.escritas.pendentesDeEscrita.length == 0) { this.log(`Não existem escritas pendentes para executar o trigger de escrita.`); return; } let dataAgra = new Date(); const diffMsUltimaExecucao = dataAgra.getTime() - (this.#estado.escritas.dataUltimaExecucao != undefined ? this.#estado.escritas.dataUltimaExecucao.getTime() : this.#configuracoes.escrita.tempoMinimoEntreEscritas + 1); // Se o tempo desde a ultima escrita ainda não atingiu o tempo minimo de espera if (this.#estado.escritas.dataUltimaExecucao != undefined && (diffMsUltimaExecucao < this.#configuracoes.escrita.tempoMinimoEntreEscritas)) { this.#estado.escritas.isEscritaPendente = true; let diffRestante = this.#configuracoes.escrita.tempoMinimoEntreEscritas - diffMsUltimaExecucao; setTimeout(() => { this.#estado.escritas.isEscritaPendente = false; this.#triggerEscreverTags(); }, diffRestante); return; } this.#estado.escritas.dataUltimaExecucao = dataAgra; this.log(`Iniciando escrita de tags pendentes...`); this.#estado.escritas.isEscritaExecutando = true; this.#estado.emissorEventos.disparaEvento('trigger-escreve-tags-executando') /** * @typedef TagParaEscrever * @property {String} tag - Nome da tag * @property {*} valor - Valor a ser escrito na tag */ /** * Tags para adicionar no grupo * @type {TagParaEscrever[]} */ const tagsParaEscrever = [] for (const tagEscrever of this.#estado.escritas.pendentesDeEscrita) { tagEscrever.tags.forEach(tag => { tagsParaEscrever.push({ tag: tag.tag, valor: tag.valor }) }); } this.log(`Iniciando escrita de ${tagsParaEscrever.length} tags...`); const retornoEscritaTags = await this.#estado.controlador.escreveMultiplasTags(tagsParaEscrever.map(t => { // Verificar a existencia do DataType no cache const existeDataTypeCache = this.#estado.cacheDataTypes.find(cT => cT.tag == t.tag); let dataTypePadrao = { isResolverAutomaticamente: true, resolverAutomaticamente: { valor: t.valor } } // Se existir, definir no objeto o data type com seu valor correto if (existeDataTypeCache != undefined) { this.log(`Tag ${t.tag} encontrada no cache de Data Types.`); // Tipo atomicos if (existeDataTypeCache.isAtomico) { dataTypePadrao = { isAtomico: true, atomico: { codigoAtomico: existeDataTypeCache.atomico.codigoAtomico, valor: t.valor } } } else if (existeDataTypeCache.isStruct) { // Structs dataTypePadrao = { isStruct: true, struct: { codigoStruct: existeDataTypeCache.struct.codigoStruct, classeStruct: {} } } // Identificar o tipo da Struct switch (existeDataTypeCache.struct.codigoStruct) { // Se for uma String ASCII 82 case this.#estado.controlador.getDataTypes().structs.ASCIISTRING82.codigoTipoStruct: { const classeStructASCIIString82 = { string: t.valor } dataTypePadrao.struct.classeStruct = classeStructASCIIString82; break; } default: { break; } } } } return { tag: t.tag, dataType: dataTypePadrao } })) // Passar por cada tag solicitada e verificar se houve retorno na resposta da solicitação de escrita for (const tagParaEscrever of tagsParaEscrever) { // Encontrar os lotes de escritas pendentes que possuem essa tag no meio const loteEscritasDessaTag = this.#estado.escritas.pendentesDeEscrita.filter(l => l.tags.find(t => t.tag == tagParaEscrever.tag) != undefined); this.log(`Escrita da tag ${tagParaEscrever.tag} tem ${loteEscritasDessaTag.length} callbacks pendentes.`); // Se a escrita múltipla não deu certo, nenhuma tag foi escrita. Retorno o erro