UNPKG

wow-another-ethernet-ip

Version:

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

1,023 lines (867 loc) 185 kB
import { CIPGeneralStatusCodes } from "../../../EtherNetIP/Utils/CIPRespondeCodes.js"; import { EtherNetIPSocket } from "../../../EtherNetIP/EtherNetIP.js"; import { EmissorEvento } from "../../../Utils/EmissorEvento.js"; import { SingleServicePacketServiceBuilder } from "../../../EtherNetIP/Builder/Layers/EtherNetIP/CommandSpecificDatas/SendRRData/CIP/Servicos/CIPConnectionManager/SingleServicePacket/SingleServicePacket.js"; /** * Comunicação com o controlador CompactLogix da Rockwell. ** Essa classe oferece os recursos básicos para: leitura, escrita e obtenção de tags. ** Não há nenhum controle e otimização para leituras/escritas consecutivas de tags, muito menos observações de tags, justamente para permitir sua própria customização desejada. * Se preferir ter mais controle sobre lógica de leituras/escritas/observações, utilize a classe `CompactLogixV2.js` diretamente. */ export class CompactLogixRockwell { /** * @type {EtherNetIPSocket} - Socket de comunicação com o dispositivo remoto no protocolo EtherNet/IP */ #ENIPSocket; #configuracao = { ip: '', porta: 0, logs: { habilitarLogsConsole: false } } /** * @typedef DataTypeTagAtomico * @property {Number} codigo - Codigo do DataType no controlador * @property {String} descricao - Descrição do DataType * @property {Number} tamanho - Tamanho do tipo do DataType em bytes * @property {Boolean} isSigned - Se o DataType é um número com sinal */ /** * @typedef DataTypeTagStruct * @property {Number} codigoTipoStruct - O código de identificação desse tipo de Struct. Todas as tags Struct possuem o Data Type Code 672, o codigoStruct é um identificador único para cada tipo de Struct * @property {String} descricao - Descrição do DataType */ /** * @typedef DataTypeTagStructASCIIString82 * @property {String} stringConteudo - Conteudo da string * @property {Number} tamanho - Tamanho total da string em bytes */ #estado = { /** * Emissor de eventos do CompactLogix */ emissorEvento: new EmissorEvento(), } /** * Parametros de conexão com o controlador CompactLogix * @param {Object} parametros * @param {String} parametros.ip - Endereço IP do controlador CompactLogix * @param {Number} parametros.porta - Porta de comunicação com o controlador CompactLogix * @param {Boolean} parametros.habilitaLogs - Se os logs devem ser exibidos no console * @param {Boolean} parametros.autoReconectar - Se deve tentar reconectar automaticamente em caso de: 1 - Perda de conexão com o controlador, 2 - Desautenticação com o dispositivo por algum motivo(acho raro acontecer '-') */ constructor(parametros) { if (parametros == undefined || typeof parametros != 'object') throw new Error('Parâmetros de conexão não informados'); if (parametros.ip == undefined) throw new Error('Endereço IP não informado'); if (parametros.porta == undefined) throw new Error('Porta de comunicação não informada'); this.#configuracao.ip = parametros.ip; this.#configuracao.porta = parametros.porta; let isHabLogs = false; if (parametros.habilitaLogs) isHabLogs = true; this.#configuracao.logs.habilitarLogsConsole = isHabLogs; this.#ENIPSocket = new EtherNetIPSocket({ isHabilitaLogs: false, conexao: { ip: this.#configuracao.ip, porta: this.#configuracao.porta }, isAutoReconnect: parametros.autoReconectar }); this.#ENIPSocket.onLog((msgLog) => { this.log(msgLog); }); } /** * Conectar o Socket EtherNet/IP. */ async conectar() { /** * @typedef conectarRetorno * @property {Boolean} isConectou - Se a conexão foi bem sucedida * @property {Object} erro - Se não conectou, contém os detalhes do motivo * @property {String} erro.descricao - Descrição do erro ocorrido * @property {Boolean} erro.isSemConexao - Se o erro foi causado por não conseguir conectar ao controlador * @property {Boolean} erro.isAutenticar - Se o erro foi causado devido ao erro de tentar estabelecer a conexão com o RegisterSession * @property {Boolean} erro.isDispositivoRecusou - Se o erro foi causado devido ao dispositivo recusar a conexão */ /** * @type {conectarRetorno} */ const retornoConexao = { isConectou: false, erro: { descricao: '', isAutenticar: false, isDispositivoRecusou: false, isSemConexao: false } } let retornoConectar = await this.#ENIPSocket.conectar(); // Se o EtherNet/IP não conseguiu se conectar if (!retornoConectar.isConectou) { if (retornoConectar.erro.isSemConexao) { retornoConexao.erro.descricao = 'Sem comunicação com o dispositivo remoto'; retornoConexao.erro.isSemConexao = true; } else if (retornoConectar.erro.isFalhaAutenticar) { retornoConexao.erro.descricao = `Falha ao autenticar com o dispositivo remoto: ${retornoConectar.erro.descricao}`; retornoConexao.erro.isAutenticar = true; } else if (retornoConectar.erro.isDispositivoRecusou) { retornoConexao.erro.descricao = 'Dispositivo remoto recusou a conexão'; retornoConexao.erro.isDispositivoRecusou = true; } else { retornoConexao.erro.descricao = `Erro ao tentar se conectar: ${retornoConectar.erro.descricao}`; } } else { retornoConexao.isConectou = true; } return retornoConexao; } /** * Desconectar o Socket EtherNet/IP. É enviado ao dispositivo a solicitação de UnRegister Session para excluir a sessão da memoria e o Socket TCP é destruido junto. ** Mesmo que o comando não seja enviado(por exemplo se esta sem conexão com o dispositivo), o socket é destruido de qualquer forma e é necessario conectar novamente. */ async desconectar() { await this.#ENIPSocket.desconectar(); } /** * Retorna se o Socket de comunicação com o controlador CompactLogix está conectado(Isso não é o mesmo que se está autenticado, apenas informa se o socket TCP está aberto) */ isSocketConectado() { return this.#ENIPSocket.getEstadoConexao().isConectado; } /** * Retorna se a autenticação está estabelecida com o controlador CompactLogix ** Se estiver desconectado, isso será false, então da pra usar esse cara pra saber se tá tudo conectado certo ou não */ isAutenticado() { return this.#ENIPSocket.getEstadoAutenticacao().isAutenticado; } /** * Ativa/desativa os logs no console * @param {Boolean} bool */ toggleLogs(bool = false) { this.#configuracao.logs.habilitarLogsConsole = bool; } /** * @callback CallbackLog * @param {String} mensagem - Mensagem disparada no log */ /** * Adicionar um callback para quando um log for disparado * @param {CallbackLog} cb */ onLog(cb) { this.#estado.emissorEvento.addEvento('log', cb); } /** * Retornar o Socket de comunicação com o dispositivo EtherNet/IP */ getENIPSocket() { return this.#ENIPSocket; } /** * Realizar a leitura de uma unica tag do CompactLogix * @param {String} tag - Tag a ser lida */ async lerTag(tag) { if (tag == undefined) throw new Error('Tag a ser lida não informada'); if (typeof tag != 'string') throw new Error('Tag a ser lida deve ser uma string'); if (tag == '') throw new Error('Tag a ser lida não pode ser vazia'); const retornoTag = { /** * Se deu sucesso em realizar a leitura da tag */ isSucesso: false, tagSolicitada: tag, /** * Detalhes dos tempos de leitura da tag */ msDetalhes: { /** * Data atual em millisegundos de quando a leitura foi iniciada e o pacote ENIP foi enviada * @type {Number} */ dateTimeInicio: new Date().getTime(), /** * Data atual em millisegundos de quando o pacote ENIP foi recebido e parseado * @type {Number} */ dateTimeFim: undefined, /** * Tempo em ms total de leitura da tag */ totalMsLeitura: undefined }, /** * Se sucesso, contém os dados da tag lida */ sucesso: { tag: { /** * Se o valor lido é atomico(numeros DataType de 193 a 202) */ isAtomico: false, /** * Se isAtomico, contém os detalhes do valor lido */ atomico: { /** * O valor númerico */ valor: undefined, /** * O DataType do valor lido */ dataType: undefined }, /** * Se o valor lido é do tipo de uma Struct */ isStruct: false, /** * Se Struct, contém os detalhes da Struct */ struct: { /** * O Data Type da Struct lido * @type {DataTypeTagStruct} */ dataTypeStruct: undefined, /** * O valor da Struct lido. Esse campo é dinamico depenendo do tipo da Struct * @type {DataTypeTagStructASCIIString82} */ valor: undefined } } }, /** * Detalhes especificos do tipo do erro que foi ocorrido se isSucesso não for true */ erro: { descricao: '', /** * Se foi erro ao enviar o pacote ENIP */ isEnviarENIP: false, /** * Se o erro foi causado devido a falha ao enviar o pacote ENIP, contém os detalhes extras desse erro */ enviarENIP: { /** * Se foi erro ao escrever o pacote ENIP no socket */ isWriteSocket: false, /** * Se foi erro ao gerar o buffer do pacote ENIP */ isGerarBuffer: false, /** * Detalhes do erro ao gerar o Buffer do pacote ENIP se isGerarBuffer for true */ gerarBuffer: { /** * Histórico de logs do erro ao gerar o Buffer do pacote ENIP */ traceLog: [] } }, /** * Se foi erro ao receber o pacote ENIP */ isReceberENIP: false, /** * Se o erro foi causado devido a falha ao receber o pacote ENIP, contém os detalhes extras desse erro. Esse erro só verifica pelo campo principal ENIP, os layers encapsulados posteriormente podem ter erros próprios, * porém a garantia aqui é que o pacote ENIP foi recebido com sucesso */ receberENIP: { /** * Se foi erro ao receber o pacote ENIP */ isDemorouResposta: false }, /** * O pacote EtherNet/IP retornado não retornou sucesso */ isENIPStatusErro: false, /** * Se isENIPStatusErro, contém os detalhes do erro */ ENIPStatusErro: { /** * Código de identificação do erro */ codigo: '', /** * Descrição da identificação do erro */ mensagem: '' }, /** * Se o erro ocorrido foi devido a algum dos layers do pacote ENIP recebido estarem incorretos. É somente para erros de parse dos Buffers recebidos e suas camadas e garantir que os bytes recebidos estão em conformidade. Por exemplo * se foi requisitado ler uma tag que não existe, o controlador vai responder com um status de invalido no SingleServicePacket, então não caira em tratativas dos erros, e sim de status. */ isErroLayers: false, /** * Se isErroLayers, contém qual layer ocorreu o erro */ erroLayers: { /** * Se o primeiro layer que é o EtherNet/IP é o que está com erros */ isENIPInvalido: false, /** * Se isENIPInvalido, contém os detalhes do erro */ ENIPInvalido: { /** * Array de mensagens com o historico de cada etapa do processamento do Buffer, conterá onde ocorreu o erro no Buffer */ trace: [] }, /** * Se o layer do SendRRData é valido */ isSendRRDataInvalido: false, /** * Se o erro foi causado por falha ao processar o Buffer SendRRData */ sendRRDataInvalido: { /** * Array de mensagens com o historico de cada etapa do processamento do Buffer, conterá onde ocorreu o erro no Buffer */ trace: [] }, /** * Se o layer do CIP é valido */ isCIPInvalido: false, /** * Se o erro foi causado por falha ao processar o Buffer CIP */ CIPInvalido: { /** * Array de mensagens com o historico de cada etapa do processamento do Buffer, conterá onde ocorreu o erro no Buffer */ trace: [] }, /** * Se o layer Single Service Packet é valido */ isSingleServicePacket: false, /** * Se o erro foi causado por falha ao processar o Buffer Single Service Packet */ singleServicePacket: { /** * Array de mensagens com o historico de cada etapa do processamento do Buffer, conterá onde ocorreu o erro no Buffer */ trace: [] } }, /** * Se o erro foi causado devido a um erro de status, ou seja os layers estão todos em conformidade, porém o dispositivo retornou um status diferente de sucesso para a operação de leitura da tag(Esse erro é somente para o CIP) */ isStatusInvalido: false, /** * Se isStatusInvalido, contém os detalhes do status invalido */ statusInvalido: { /** * A descrição basica do erro original ocorrido */ descricaoStatus: '', /** * Código do erro recebido conforme CIP Response Codes */ codigoDeErro: undefined, /** * Opcionalmente um Additional Status code se disponível. Geralmente se o status for sucesso, esse campo é ignorado * @type {Buffer} */ additionalStatusCode: undefined, /** * A tag não existe */ isTagNaoExiste: false, /** * A tag existe, porém foi solicitado um index que não existe no controlador */ isIndexArrayNaoExiste: false }, /** * Se o erro foi causado devido a um erro de conversão do Buffer recebido no ENIP para o valor real(numero, string, array, etc..) */ isConverterValor: false } } const layerENIP = this.getENIPSocket().getNovoLayerBuilder(); const layerConnectionManager = layerENIP.buildSendRRData().criarServicoCIP().buildCIPConnectionManager(); // O Single Service Packet eu configuro qual vai ser a tag solicitada const layerServicePacket = layerConnectionManager.getCIPMessage().buildSingleServicePacket(); // Validar se o usuario informou uma tag de array dimensinal (verificar se existe o [] e o index dentro) let isArrayIndex = tag.match(/\[\d+\]/g); // Se contiver o index do numero if (isArrayIndex) { // Cortar somente o nome da tag sem os [] let nomeTagCortadad = tag.split('[')[0]; let indexSolicitado = parseInt(isArrayIndex[0].replace('[', '').replace(']', '')); // Para leituras de tags que correspondem a localização de um array, devo incluir o Member Request Path que inclua qual o membro(index do array no CompactLogix) layerServicePacket.setAsGetAttribute({ nome: `${nomeTagCortadad}`, CIPGenericBuffer: Buffer.from([0x01, 0x00]), MemberRequestPath: Buffer.from([0x28, indexSolicitado]) }) } else { // Pra ler uma tag pelo menos no CompactLogix, o CIP Generic data deve ser só o Request Path pra string da tag, e o CIP Class Generic é só um array vazio já que não precisa enviar informações layerServicePacket.setAsGetAttribute({ nome: `${tag}`, CIPGenericBuffer: Buffer.from([0x01, 0x00]) }) } this.log(`Lendo tag ${tag}...`); // Enviar o pacote ENIP let statusEnviaENIP = await this.getENIPSocket().enviarENIP(layerENIP); retornoTag.msDetalhes.dateTimeFim = new Date().getTime(); retornoTag.msDetalhes.totalMsLeitura = retornoTag.msDetalhes.dateTimeFim - retornoTag.msDetalhes.dateTimeInicio; // Analisar se o envio e o recebimento dos pacotes ENIPs obtiveram sucesso if (!statusEnviaENIP.isSucesso) { // Se deu erro ao processar o ENIP, verificar. // Se deu erro ao processar o ENIP, verificar. if (!statusEnviaENIP.enipEnviar.isEnviou) { if (statusEnviaENIP.enipEnviar.erro.isWriteSocket) { // O erro foi causado na hora de usar o Write pra escrever no Socket retornoTag.erro.enviarENIP.isWriteSocket = true; } else if (statusEnviaENIP.enipEnviar.erro.isGerarBuffer) { // O erro foi causado na geração do Buffer do Builder do ENIP. Algum campo invalido provavelmente retornoTag.erro.enviarENIP.isGerarBuffer = true; retornoTag.erro.enviarENIP.gerarBuffer.traceLog = statusEnviaENIP.enipEnviar.erro.erroGerarBuffer.traceLog; } retornoTag.erro.descricao = `O envio do ENIP retornou: ${statusEnviaENIP.enipEnviar.erro.descricao}`; retornoTag.erro.isEnviarENIP = true; return retornoTag; } // Se deu erro ao receber o ENIP, verificar. if (!statusEnviaENIP.enipReceber.isRecebeu) { if (statusEnviaENIP.enipReceber.erro.isDemorouResposta) { // O erro foi causado por demorar a resposta do pacote ENIP retornoTag.erro.receberENIP.isDemorouResposta = true; } retornoTag.erro.descricao = `O recebimento do ENIP retornou: ${statusEnviaENIP.enipReceber.erro.descricao}`; retornoTag.erro.isReceberENIP = true; return retornoTag; } } // Se chegou aqui, eu tenho o pacote ENIP de resposta do controlador. Agora preciso verificar se ele retornou sucesso ou não na operação de leitura const ENIPResposta = statusEnviaENIP.enipReceber.enipParser; // Se o comando ENIP principal deu erro if (!ENIPResposta.isValido().isValido) { retornoTag.erro.descricao = `O pacote de resposta ENIP não é valido, alguma informação no Buffer está incorreta: ${ENIPResposta.isValido().erro.descricao}`; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isENIPInvalido = true; retornoTag.erro.erroLayers.ENIPInvalido.trace = ENIPResposta.isValido().tracer.getHistoricoOrdenado(); return retornoTag; } // Se o ENIP for validado, verificar se ele retornou o estado de sucesso if (!ENIPResposta.isStatusSucesso().isSucesso) { retornoTag.erro.descricao = `O pacote de resposta ENIP retornou um status de erro: ${ENIPResposta.getStatus().codigo} - ${ENIPResposta.getStatus().mensagem}`; retornoTag.erro.ENIPStatusErro.codigo = ENIPResposta.getStatus().codigo; retornoTag.erro.ENIPStatusErro.mensagem = ENIPResposta.getStatus().mensagem; retornoTag.erro.isENIPStatusErro = true; return retornoTag; } // O comando que deve ser retornado é um SendRRData. Na teoria isso nunca deveria cair aqui pq a resposta do ENIP sempre deve corresponder a solicitação original, se enviou um SendRRData, deve receber um SendRRData if (!ENIPResposta.isSendRRData()) { retornoTag.erro.descricao = 'O pacote de resposta não é um SendRRData.'; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isSendRRDataInvalido = true; return retornoTag; } // Obter as informações do SendRRData const ENIPSendRRData = ENIPResposta.getAsSendRRData(); // Se retornou um SendRRData, validar se o parser conseguiu extrair as informações corretamente if (!ENIPSendRRData.isValido().isValido) { // Se não for valido, isso significa que deu algum erro em dar parse no Buffer recebido pro SendRRData retornoTag.erro.descricao = `O pacote SendRRData não é valido, alguma informação no Buffer está incorreta: ${ENIPSendRRData.isValido().erro.descricao}`; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isSendRRDataInvalido = true; retornoTag.erro.erroLayers.sendRRDataInvalido.trace = ENIPSendRRData.isValido().tracer.getHistoricoOrdenado(); return retornoTag; } // Obrigatoriamente deve ser um serviço CIP que contém os dados encapsulados da informação de leitura da tag(já que a comunicação no momento com o Compact tá sendo via CIP) if (!ENIPSendRRData.isServicoCIP()) { retornoTag.erro.descricao = 'O pacote de resposta não contém um serviço CIP'; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isCIPInvalido = true; return retornoTag; } // Obter as informações do Serviço CIP que contém nessa altura do jogo, o CIP Connection Manager com as informações solicitadas const ENIPCIP = ENIPSendRRData.getAsServicoCIP(); if (!ENIPCIP.isValido().isValido) { retornoTag.erro.descricao = `O pacote CIP não é valido, alguma informação no Buffer está incorreta: ${ENIPCIP.isValido().erro.descricao}`; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isCIPInvalido = true; retornoTag.erro.erroLayers.CIPInvalido.trace = ENIPCIP.isValido().tracer.getHistoricoOrdenado(); return retornoTag; } const CIPConnectionManager = ENIPCIP.getAsConnectionManager(); if (!CIPConnectionManager.isValido().isValido) { retornoTag.erro.descricao = `O pacote CIP Connection Manager não é valido, alguma informação no Buffer está incorreta: ${CIPConnectionManager.isValido().erro.descricao}`; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isCIPInvalido = true; retornoTag.erro.erroLayers.CIPInvalido.trace = CIPConnectionManager.isValido().tracer.getHistoricoOrdenado(); return retornoTag; } // Por último, o pacote CIP deve encapsular o Single Service Packet que foi a informação da tag requisitada if (!CIPConnectionManager.isSingleServicePacket()) { retornoTag.erro.descricao = 'O pacote de resposta não contém um Single Service Packet'; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isSingleServicePacket = true; return retornoTag; } const ENIPSingleService = CIPConnectionManager.getAsSingleServicePacket(); // Validar se é deu pra dar parse no Buffer sem erros. if (!ENIPSingleService.isValido().isValido) { retornoTag.erro.descricao = `O pacote Single Service Packet não é valido, alguma informação no Buffer está incorreta: ${ENIPSingleService.isValido().erro.descricao}`; retornoTag.erro.isErroLayers = true; retornoTag.erro.erroLayers.isSingleServicePacket = true; retornoTag.erro.erroLayers.singleServicePacket.trace = ENIPSingleService.isValido().tracer.getHistoricoOrdenado(); return retornoTag; } let detalhesStatus = ENIPSingleService.isStatusSucesso(); // Ok pronto! Se chegou aqui, todas as camadas do pacote ENIP foram processadas com sucesso. Analisar se a ação de leitura foi bem sucedida ou não if (!detalhesStatus.isSucesso) { retornoTag.erro.isStatusInvalido = true; retornoTag.erro.statusInvalido.codigoDeErro = detalhesStatus.erro.codigoStatus retornoTag.erro.statusInvalido.descricaoStatus = `${detalhesStatus.erro.descricaoStatus} - ${detalhesStatus.erro.descricao}`; switch (detalhesStatus.erro.codigoStatus) { // Se o status for Path Segment Error, significa que a tag não existe case CIPGeneralStatusCodes.PathSegmentError.hex: { retornoTag.erro.descricao = 'A tag não existe.'; retornoTag.erro.statusInvalido.isTagNaoExiste = true; break; } case CIPGeneralStatusCodes.PathDestinationUnknown.hex: { retornoTag.erro.descricao = 'A tag existe, porém o index de array solicitado não existe.'; retornoTag.erro.statusInvalido.isIndexArrayNaoExiste = true; break; } // Para qualquer outro erro default: { retornoTag.erro.descricao = `O pacote Single Service Packet retornou um status de erro: ${ENIPSingleService.getStatus().codigoStatus} - ${ENIPSingleService.getStatus().descricaoStatus} `; let bufferAdditionalStatus = ENIPSingleService.getStatus().additionalStatusCode.buffer; // Se foi retornado o Additional Status if (bufferAdditionalStatus != undefined) { retornoTag.erro.statusInvalido.additionalStatusCode = bufferAdditionalStatus; let codigoErroExtra = bufferAdditionalStatus.readUInt16LE(0); switch (codigoErroExtra) { default: retornoEscrita.erro.descricao += `/ Additional Status Code retornado: ${codigoErroExtra}`; break; } } break; } } return retornoTag; } // Converter o Buffer que contém as informações da tag lida let converteBufferPraValor = this.#converteDataTypeToValor(ENIPSingleService.getAsCIPClassCommandSpecificData()); // Se não foi possível converter o valor if (!converteBufferPraValor.isConvertido) { retornoTag.erro.isConverterValor = true; retornoTag.erro.descricao = `Não foi possível converter o valor do Buffer: ${converteBufferPraValor.erro.descricao}`; return retornoTag; } // Finalmente, se tudo deu certo, estou com o valor em mãos if (converteBufferPraValor.conversao.isAtomico) { retornoTag.sucesso.tag.isAtomico = true; retornoTag.sucesso.tag.atomico.valor = converteBufferPraValor.conversao.atomico.valor; retornoTag.sucesso.tag.atomico.dataType = converteBufferPraValor.conversao.atomico.dataType; } else if (converteBufferPraValor.conversao.isStruct) { retornoTag.sucesso.tag.isStruct = true; retornoTag.sucesso.tag.struct.dataTypeStruct = converteBufferPraValor.conversao.struct.dataType; // Analisar o tipo da Struct e devolver corretamente no retorno if (converteBufferPraValor.conversao.struct.dataType.codigoTipoStruct == dataTypes.structs.ASCIISTRING82.codigoTipoStruct) { /** * @type {DataTypeTagStructASCIIString82} */ let structTipoString = { stringConteudo: converteBufferPraValor.conversao.struct.structData.stringConteudo, tamanho: converteBufferPraValor.conversao.struct.structData.tamanho } retornoTag.sucesso.tag.struct.valor = structTipoString } } else { retornoTag.erro.descricao = 'O valor lido não é nem atomico nem uma struct'; retornoTag.erro.isConverterValor = true; return retornoTag; } retornoTag.isSucesso = true; return retornoTag; } /** * Solicita multiplas tags para serem lidas * @param {Array<String>} tags - Array de tags a serem lidas */ async lerMultiplasTags(tags) { if (tags == undefined) throw new Error('Tags a serem lidas não informadas'); if (!Array.isArray(tags)) throw new Error('Tags a serem lidas devem ser um array'); if (tags.length == 0) throw new Error('Array de tags a serem lidas não pode ser vazio'); // Verificar se alguma tag do Array não é uma string if (tags.some(tag => typeof tag != 'string')) throw new Error('Todas as tags a serem lidas devem ser strings'); const retornoLeituraMultipla = { /** * Se pelo menos a requisição de leitura chegou ao dispositivo remoto e retornou uma resposta(mesmo que seja uma resposta de erro) */ isSucesso: false, /** * Se sucesso, contém os detalhes das tags lidas */ sucesso: { /** * @type {TagLidaMultipla[]} */ tags: [] }, /** * Se ocorreu algum erro durante a requisição. */ erro: { descricao: '', /** * Se o erro foi causado pelo recibo do pacote ENIP com erros no Buffer */ isENIPInvalido: false, /** * Se isENIPInvalido, contém os detalhes do erro */ ENIPInvalido: { /** * Array de mensagens com o historico de cada etapa do processamento do Buffer, conterá onde ocorreu o erro no Buffer */ trace: [] }, /** * Se o erro foi causado devido ao status de retorno do pacote ENIP diferente de Sucesso. */ isENIPStatusErro: false, /** * Se isENIPStatusErro, contém os detalhes do erro */ ENIPStatusErro: { /** * Código de identificação do erro */ codigo: '', /** * Descrição da identificação do erro */ descricao: '' } } } const layerENIP = this.getENIPSocket().getNovoLayerBuilder(); // O Connection Manager é o primeiro layer que deve ser enviado const layerConnectionManager = layerENIP.buildSendRRData().criarServicoCIP().buildCIPConnectionManager(); // Adicionar o Multiple Service Packet que é o pacote que contém as informações de leitura de multiplas tags const layerMultipleService = layerConnectionManager.getCIPMessage().buildMultipleServicePacket(); layerMultipleService.setAsMessageRouter(); // Maximo de bytes que podem ser enviados em um pacote ENIP const maximoBytes = 512; // Se o pacote ENIP atual ultrapassou o limite de bytes let isAtingiuLimiteTags = false; // Array de tags que ultrapassaram o limite de bytes e que serão enviados em um novo ENIP const tagsUltrapassaramLimite = []; // Array de todas as tags em sequencia ordenada que foram enviadas no pacote ENIP const tagsEmSequencia = []; // Ordenar as tags pelo menor ao maior tamanho. É importante que esteja ordenado pois a resposta do dispositivo também retorna a mesma sequencia de tags(e como ele não retorna na resposta o nome da tag, o index do serviço é a unica forma de reconhecer) // Se não ordenar, pode acabar pulando uma tag grande na sequencia que não caberia, porém a proxima cabe, o que resulta no meu array ser a sequencia A, B, C(digamos que esse era a tag grande que nao cabe), D(e essa pequena cabe) e na resposta eu receber A, B, D. for (const tag of tags.sort((a, b) => a.length - b.length)) { // Se a tag já foi adicionado, não permito adicionar novamente if (layerMultipleService.getServicesPackets().find(servicePacket => servicePacket.servico.getStringPath() == tag) != undefined) continue; // Adicionar a tag ao array de tags solicitadas(mesmo que não caiba, eu adiciono pra mais pra frente eu ja ter o array completo de todas as tags solicitadas, tenham elas sido enviadas nesse pacote ENIP ou em outro..) tagsEmSequencia.push(tag); // Se já atingiu o limite vou pular, pois como o array foi ordenado crescente, eu sei que as próximas tags também não vão caber nesse ENIP. Assim evito ficar gerando Buffer de forma desnecessária. if (isAtingiuLimiteTags) { tagsUltrapassaramLimite.push(tag); continue; } // Adiciona um Single Service ao Multiple Service let layerSingleService = layerMultipleService.addSingleServicePacket(); // Validar se o usuario informou uma tag de array dimensinal (verificar se existe o [] e o index dentro) let isArrayIndex = tag.match(/\[\d+\]/g); // Se contiver o index do numero if (isArrayIndex) { // Cortar somente o nome da tag sem os [] let nomeTagCortadad = tag.split('[')[0]; let indexSolicitado = parseInt(isArrayIndex[0].replace('[', '').replace(']', '')); // Para leituras de tags que correspondem a localização de um array, devo incluir o Member Request Path que inclua qual o membro(index do array no CompactLogix) layerSingleService.servico.setAsGetAttribute({ nome: `${nomeTagCortadad}`, CIPGenericBuffer: Buffer.from([0x01, 0x00]), MemberRequestPath: Buffer.from([0x28, indexSolicitado]) }) } else { // Pra ler uma tag pelo menos no CompactLogix, o CIP Generic data deve ser só o Request Path pra string da tag, e o CIP Class Generic é só um array vazio já que não precisa enviar informações layerSingleService.servico.setAsGetAttribute({ nome: `${tag}`, CIPGenericBuffer: Buffer.from([0x01, 0x00]) }) } let bufferENIP = layerENIP.criarBuffer(); if (!bufferENIP.isSucesso) { retornoLeituraMultipla.erro.descricao = `Erro ao verificar tamanho do Buffer para o Single Service Pack da tag ${tag}: ${bufferENIP.erro.descricao}`; return retornoLeituraMultipla; } // Veficiar se não ultrapassou o limite de bytes if (bufferENIP.sucesso.buffer.length >= maximoBytes) { // Se ultrapassou o limite de bytes, remover o Single Service Packet que foi adicionado layerMultipleService.deleteSingleServicePacket(layerSingleService.id); // Adicionar a lista de tags que ultrapassaram o limite tagsUltrapassaramLimite.push(tag); // Seta pras próximas tags não serem adicionadas a esse ENIP isAtingiuLimiteTags = true; } } // Ok, adicionar os layers, tentar enviar o ENIP const statusEnviaENIP = await this.getENIPSocket().enviarENIP(layerENIP); // Se ocorreu algum tipo de erro durante o envio ou rcebimento do ENIP if (!statusEnviaENIP.isSucesso) { // Se o erro foi devido ao envio do ENIP if (!statusEnviaENIP.enipEnviar.isEnviou) { retornoLeituraMultipla.erro.descricao = `Erro ao enviar o pacote ENIP: ${statusEnviaENIP.enipEnviar.erro.descricao}`; return retornoLeituraMultipla; } // Se o erro foi devido ao não recebimento da resposta da solicitação ENIP if (!statusEnviaENIP.enipReceber.isRecebeu) { retornoLeituraMultipla.erro.descricao = `Erro ao receber o pacote ENIP: ${statusEnviaENIP.enipReceber.erro.descricao}`; return retornoLeituraMultipla; } } // Se chegou aqui, o pacote ENIP foi enviado e recebido com sucesso. Agora é analisar o conteúdo da resposta nos próximos layers const ENIPResposta = statusEnviaENIP.enipReceber.enipParser; // Se o pacote ENIP não é valido if (!ENIPResposta.isValido().isValido) { retornoLeituraMultipla.erro.descricao = `O pacote de resposta ENIP não é valido, alguma informação no Buffer está incorreta: ${ENIPResposta.isValido().erro.descricao}`; retornoLeituraMultipla.erro.isENIPInvalido = true; retornoLeituraMultipla.erro.ENIPInvalido.trace = ENIPResposta.isValido().tracer.getHistoricoOrdenado(); return retornoLeituraMultipla; } // Ok, se o pacote ENIP for valido, verificar o status retornou Sucesso if (!ENIPResposta.isStatusSucesso().isSucesso) { retornoLeituraMultipla.erro.descricao = `O pacote de resposta ENIP retornou um status de erro: ${ENIPResposta.getStatus().codigo} - ${ENIPResposta.getStatus().mensagem}`; retornoLeituraMultipla.erro.isENIPStatusErro = true; retornoLeituraMultipla.erro.ENIPStatusErro.codigo = ENIPResposta.getStatus().codigo; retornoLeituraMultipla.erro.ENIPStatusErro.descricao = ENIPResposta.getStatus().mensagem; return retornoLeituraMultipla; } // O comando da resposta deve ser um SendRRData if (!ENIPResposta.isSendRRData()) { retornoLeituraMultipla.erro.descricao = 'O pacote de resposta não é um SendRRData'; return retornoLeituraMultipla; } // Obter as informações do SendRRData const ENIPSendRRData = ENIPResposta.getAsSendRRData(); // Validar se o parser conseguiu extrair as informações corretamente if (!ENIPSendRRData.isValido().isValido) { retornoLeituraMultipla.erro.descricao = `O pacote SendRRData não é valido, alguma informação no Buffer está incorreta: ${ENIPSendRRData.isValido().erro.descricao}`; return retornoLeituraMultipla; } // Obrigatoriamente deve ser um serviço CIP que contém os dados encapsulados da informação de leitura da tag(já que a comunicação no momento com o Compact tá sendo via CIP) if (!ENIPSendRRData.isServicoCIP()) { retornoLeituraMultipla.erro.descricao = 'O pacote de resposta não contém um serviço CIP'; return retornoLeituraMultipla; } // Obter as informações do Serviço CIP que contém nessa altura do jogo, o CIP Connection Manager com as informações solicitadas const ENIPCIP = ENIPSendRRData.getAsServicoCIP(); if (!ENIPCIP.isValido().isValido) { retornoLeituraMultipla.erro.descricao = `O pacote CIP não é valido, alguma informação no Buffer está incorreta: ${ENIPCIP.isValido().erro.descricao}`; return retornoLeituraMultipla; } // Validar o código de status do CIP, pois tem alguns status que são erros fatais e não tem como prosseguir if (ENIPCIP.getStatusCIP().codigo != CIPGeneralStatusCodes.Success.hex) { /** * Se o erro retornado é fatal e não da pra continuar a função de leitura das tags */ let isErroFatal = false; switch (ENIPCIP.getStatusCIP().codigo) { // O unico erro que permite continuar a leitura das tags é o Path Segment Error, que é um erro de caminho da tag, e como nesse caso são Multiple Service, uma pode ter dado erro porém outras não case CIPGeneralStatusCodes.PathSegmentError.hex: { break } /** * Alguns dos serviços solicitados no CIP embeded deu erro, porém permito continuar pois pode haver outras que deram certo */ case CIPGeneralStatusCodes.EmbeddedServiceError.hex: { break; } // Pra qualquer outro tipo de erro, eu não permito continuar default: { retornoLeituraMultipla.erro.descricao = `O pacote CIP retornou um status de erro: ${ENIPCIP.getStatusCIP().codigo} - ${ENIPCIP.getStatusCIP().descricao}`; isErroFatal = true; break; } } if (isErroFatal) { return retornoLeituraMultipla; } } // Esperado que o conteudo do CIP seja uma resposta de uma solicitação Connection Manager const CIPConnectionManager = ENIPCIP.getAsConnectionManager(); if (!CIPConnectionManager.isValido().isValido) { retornoLeituraMultipla.erro.descricao = `O pacote CIP Connection Manager não é valido, alguma informação no Buffer está incorreta: ${CIPConnectionManager.isValido().erro.descricao}`; return retornoLeituraMultipla } // Ok, validado o código de status do CIP, agora devo ter um Multiple Service Packet encapsulado com os dados de cada Single Service if (!CIPConnectionManager.isMultipleServicePacket()) { retornoLeituraMultipla.erro.descricao = 'O pacote de resposta não contém um Multiple Service Packet'; return retornoLeituraMultipla; } // Obter o Multiple Service Packet com as informações dos serviços solicitados const ENIPMultipleService = CIPConnectionManager.getAsMultipleServicePacket(); if (!ENIPMultipleService.isValido().isValido) { retornoLeituraMultipla.erro.descricao = `O pacote Multiple Service Packet não é valido, alguma informação no Buffer está incorreta: ${ENIPMultipleService.isValido().erro.descricao}`; return retornoLeituraMultipla; } /** * @typedef TagLidaMultipla * @property {String} tag - Tag lida * @property {Boolean} isSucesso - Se a leitura da tag foi bem sucedida * @property {Object} sucesso - Se isSucesso for true, contém os detalhes da tag lida * @property {Boolean} sucesso.isAtomico - Se o valor lido é atomico(numeros DataType de 193 a 202) * @property {Object} sucesso.atomico - Se isAtomico for true, contém os detalhes do valor lido * @property {Number} sucesso.atomico.valor - O valor númerico * @property {DataTypeTagAtomico} sucesso.atomico.dataType - O DataType do valor lido * @property {Boolean} sucesso.isStruct - Se o valor lido é do tipo de uma Struct * @property {Object} sucesso.struct - Se isStruct for true, contém os detalhes da Struct * @property {DataTypeTagStruct} sucesso.struct.dataTypeStruct - O Data Type da Struct lido * @property {DataTypeTagStructASCIIString82} sucesso.struct.valor - O valor da Struct lido. Esse campo é dinamico depenendo do tipo da Struct * @property {Object} erro - Se isSucesso for false, contém os detalhes do erro ocorrido * @property {String} erro.descricao - Descrição do erro ocorrido * @property {Boolean} erro.isCIPNaoRetornado - A resposta ENIP recebida não continha um CIP com os dados dessa tag solicitada * @property {Boolean} erro.isCIPInvalido - O CIP Packet retornado não é valido * @property {Object} erro.CIPInvalido - Se isCIPInvalido, contém o motivo de ser invalido * @property {Array<String>} erro.CIPInvalido.trace - Trace de cada etapa do processamento do Buffer do CIP, deve ter onde ocorreu o erro de CIP invalido * @property {Boolean} erro.isSingleServicePacketNaoRetornado - A resposta ENIP recebida não continha um Single Service Packet com os dados dessa tag solicitada * @property {Boolean} erro.isSingleServicePacketInvalido - O Single Service Packet retornado tem algo erro no Buffer e não é valido * @property {Object} erro.singleServicePacketInvalido - Se isSingleServicePacketInvalido, contém o motivo de ser invalido * @property {Array<String>} erro.singleServicePacketInvalido.trace - Trace de cada etapa do processamento do Buffer do Single Service Packet, deve ter onde ocorreu o erro de Single Service Packet invalido * @property {Boolean} erro.isConverteValor - O valor recebido não foi possivel converter para o valor real da tag * @property {Boolean} erro.isSingleServicePacketStatusErro - O Single Service Packet retornou um status de erro * @property {Object} erro.singleServicePacketStatusErro - Se isSingleServicePacketStatusErro, contém o motivo do erro de status * @property {Number} erro.singleServicePacketStatusErro.codigo - O código de status retornado * @property {String} erro.singleServicePacketStatusErro.descricao - A descrição do status retornado * @property {Boolean} erro.singleServicePacketStatusErro.isTagNaoExiste - Se o status retornado foi de Path Segment Error, significa que a tag não existe * @property {Boolean} erro.singleServicePacketStatusErro.isIndexArrayNaoExiste - Se o status retornado foi de Path Destination Unknown, significa que a tag existe, porém o index do array solicitado não existe */ /** * @type {TagLidaMultipla[]} */ const tagsLidas = []; // Iterar sobre as tags solicitadas para verificar se todas foram retornadas na resposta ENIP. Essa ordem do array deve ser exatamente como foi enviada no pacote ENIP for (const tag of tagsEmSequencia) { /** * @type {TagLida