UNPKG

wow-another-ethernet-ip

Version:

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

1,058 lines (923 loc) 45.1 kB
# Sumário - [Projeto](#projeto) - [Builders e Parsers](#builders-e-parsers) - [Serviços Suportados](#serviços-suportados) - [Sumário](#sumário) - [Como usar](#como-usar) - [EtherNet/IP](#ethernetip) - [Estabelecendo uma conexão com um dispositivo EtherNet/IP](#estabelecendo-uma-conexão-com-um-dispositivo-ethernetip) - [Enviando um pacote ENIP de ListIdentity](#enviando-um-pacote-enip-de-listidentity) - [Implementação do CompactLogix usando o EtherNet/IP](#implementação-do-compactlogix-usando-o-ethernetip) - [Estabelecendo conexão com um CompactLogix](#estabelecendo-conexão-com-um-compactlogix) - [Leitura de Tags](#leitura-de-tags) - [Escrita de Tags](#escrita-de-tags) - [Listar Tags](#listar-tags) - [CompactLogixV2](#compactlogixv2) - [Conectar](#conectar-1) - [Ler Tags](#ler-tags) - [Escrever Tags](#escrever-tags) - [Observar Tags](#observar-tags) - [Parar Observação de Tags](#parar-observação-de-tags) - [MicroLogix 1400](#micrologix-1400) - [Conectar](#conectar) - [Leitura](#leitura) - [Escrita](#escrita) - [Observação](#observação) - [Recursos Utilizados](#recursos-utilizados) # Projeto Esse projeto permite estabelecer conexões via protocolo EtherNet/IP(EtherNet Industrial Protocol) para troca de informações com dispositivos que suportem o protocolo CIP. De inicio, ele fornece uma classe EtherNet/IP que permite abrir uma conexão TCP com o dispositivo remoto, e através dela é possível customizar a informação que vão ser enviadas ao dispositivo. Outras bibliotecas de EtherNet/IP até fazem a mesma função porém de forma basica e superficial, eu queria algo que tivesse mais controle sobre as informações retornadas e deixasse facíl de interagir com os dispositivos, então decidi criar um do zero. Não tenho certeza ainda pois só cheguei a implementar um tipo de comunicação até o momento com um controlador CompactLogix da RockWell, mas pelas minhas pesquisas, de forma geral todos os dispositivos que suportam o protocolo CIP possuem até um certo nivel de padrão onde todos devem seguir, e depois disso vem os detalhes especificos que mudam dependendo da fabricante do CLP. Por exemplo, a forma de escrever um valor em um controlador X é diferente no controlador Y. Utilizando a classe EtherNet/IP contida no projeto, é possível customizar livremente todas as informações de um comando CIP para que ele fique compatível com o dispositivo que você deseja se comunicar. Na pasta Controladores tem uma classe pronta que usa o EtherNet/IP para se comunicar com um CompactLogix. Assim como eu fiz dessa forma, é possível criar outras classes para outros tipos de dispositivos usando como base o EtherNet/IP, você só precisa saber como montar o payload corretamente. # Builders e Parsers A classe EtherNet/IP utiliza os Builders/Parsers para auxiliar no gerenciamento dos pacotes ENIPs. - Os Builders facilitam a montagem de todos os tipos de comandos disponiveis, sem necessidade de ficar informando onde cada byte deve ficar no Buffer(alguns Builders permitem configurar exatamente os bytes caso necessário). - Os Parsers auxiliam na leitura dos Buffers recebidos que sejam um pacote EtherNet/IP. A única tarefa que você precisa fazer para ambos é compor a sequência de comandos. # Serviços Suportados - ✅ CIP Connection Manager (Unconnected Messages): O Connection Manager é usado pra enviar mensagens do tipo "UCMM". É uma conexão TCP que fica aberta e funciona no estilo requisição-resposta, onde o dispositivo remoto só devolve alguma informação se você solicitar ela. - ❌ CIP Connected Messages: Connected Messages utiliza uma conexão UDP para a troca de informações. Ao contrário do UCMM que utiliza o formato requisição-resposta, nesse modo o dispositivo remoto pode enviar informações sem o servidor ter solicitado, no caso isso acontece se você configurar que quer receber tal informação, por exemplo acompanhar em tempo real quando um endereço X mudar de valor. - ✅ CIP PCCC: O PCCC é usado para transportar os comandos do protocolo DF1 da Rockwell Automation para dispositivos que suportem o CIP PCCC. No mínimo oferece opções de ler e escrever em endereços, e dependendo do dispositivo pode suportar mais comandos diferentes. # Como usar ## EtherNet/IP A classe do EtherNet/IP utiliza o Builder(Monta o pacote de Buffer que vai ser enviado ao dispositivo) e o Parser(Recebe o Buffer e organiza a informação sem muita dor de cabeça). ### Estabelecendo uma conexão com um dispositivo EtherNet/IP ```javascript import { EtherNetIPSocket } from "wow-another-ethernet-ip" async function conectaEtherNetIP() { const connEthernetIP = new EtherNetIPSocket({ conexao: { ip: '192.168.3.120', porta: 44818, }, isHabilitaLogs: true }) let isConectou = await connEthernetIP.conectar(); if (!isConectou.isConectou) { console.log(`Erro ao conectar com o dispositivo: ${isConectou.erro.descricao}`); return; } console.log(`Conectado com sucesso ao dispositivo. Session Handler é ${connEthernetIP.getSessionHandlerID()}`); // A partir daqui, é possível enviar os pacotes ENIPS para o dispositivo. } ``` Com a autenticação realizada, os próximos pacotes ENIPs que forem enviados pela classe já serão automaticamente configurados com o Session Handler ### Enviando um pacote ENIP de ListIdentity ```javascript // O Layer Builder é o inicio de tudo onde vc começa a preparar o que vc vai solicitar ao dispositivo const layerEtherNetIP = connEthernetIP.getNovoLayerBuilder(); // Inicialmente, vamos solicitar um comando pra pegar as informações basicas do dispositivo como nome, IP, versão, etc.. // (isso não é um comando relativo ao protocolo CIP e sim algo do protocolo EtherNet/IP que todo dispositivo tem implementado) // O comando que vamos solicitar é o "List Identity" do EtherNet/IP, que retorna informações basicas que descrevem o dispositivo. // A função build irá configurar o layer automaticamente com as informações necessarias. layerEtherNetIP.buildListIdentity(); // Agora vamos enviar o pacote construido para o dispositivo let aguardaResposta = await connEthernetIP.enviarENIP(layerEtherNetIP); // A operação é considerada somente sucesso se a requisição conseguiu ser enviada // ao dispositivo E o dispositivo respondeu com sucesso de volta. if (!aguardaResposta.isSucesso) { // Aqui podemos tratar o erro e saber o que aconteceu if (!aguardaResposta.enipEnviar.isEnviou) { // Se isEnviou for false, isso indica que nem sequer foi enviada ao dispositivo. // Ou não há conexão com o dispositivo, ou o pacote ENIP tem algum erro. // Se isGerarBuffer for true, então o erro ocorreu ao gerar o buffer do pacote ENIP. // A causa disso pode ter sido em escrever em um offset invalido, algum valor invalido informado em // algum campo que não coube no Buffet, etc.. if (aguardaResposta.enipEnviar.erro.isGerarBuffer) { // Se esse for o erro, você pode verificar o traceLog para saber exatamente onde ocorreu o erro. // O trace log é um array de mensagens que descreve em ordem exatamente a sequencia de // eventos da geração do Buffer, com ele é possível saber exatamente onde ocorreu o erro. console.log(`Não foi possível enviar o pacote ENIP, ocorreu um erro ao gerar o buffer: ${aguardaResposta.enipEnviar.erro.descricao}. Trace log da geração: ${aguardaResposta.enipEnviar.erro.erroGerarBuffer.traceLog.join(' -> ')}`); } else if (aguardaResposta.enipEnviar.erro.isWriteSocket) { // Se isWriteSocket for true, é sem dúvidas algum erro de conexão console.log(`Não foi possível enviar o pacote ENIP, ocorreu um erro ao escrever no Socket(ta sem conexão provavélmente): ${aguardaResposta.enipEnviar.erro.descricao}`); } else { // Pra qualquer outro erro generico. console.log(`Ocorreu um erro ao enviar o pacote ENIP: ${aguardaResposta.enipEnviar.erro.descricao}`); } // Paramos a execução aqui return; } // Se não caiu no if acima, então o pacote foi enviado, mas a resposta não foi processada com sucesso if (!aguardaResposta.enipReceber.isRecebeu) { // Por padrão, se o dispositivo não responder a solicitação em 9000ms, a solicitação é // considerada como "demorou" e o pacote é descartado. if (aguardaResposta.enipReceber.erro.isDemorouResposta) { console.log(`O dispositivo demorou para responder a solicitação, o pacote será ignorado.`); } else { // Para erros genéricos console.log(`Ocorreu um erro ao receber a resposta do dispositivo: ${aguardaResposta.enipReceber.erro.descricao}`); } return; } } // Se chegou até aqui, então a solicitação foi enviada com sucesso e o dispositivo respondeu com sucesso. // Vamos extrair as informações do pacote recebido utilizando o Parser. O Parser é responsável por // interpretar o pacote recebido e extrair as informações que queremos. let parserENIPResposta = aguardaResposta.enipReceber.enipParser; // Verificar se a resposta recebida é valida(as informações do Buffer recebidas estão corretas). Se não for, então // o pacote recebido está corrompido e não deve ser considerado. if (!parserENIPResposta.isValido().isValido) { // Se não for valido, o Buffer recebido está corrompido e não deve ser considerado. isValido retorna a descrição // em qual parte do Buffer deu erro e também tem o Tracer que é um array de mensagens que // descreve exatamente a sequencia de eventos que ocorreu até chegar no erro. console.log(`O pacote recebido não é valido, não é possivel extrair informações. Motivo: ${parserENIPResposta.isValido().erro.descricao}. Trace log: ${parserENIPResposta.isValido().tracer.getHistoricoOrdenado().join(' -> ')}`); return; } // Lembrando que isValido só verifica se o Buffer recebido está correto. O pacote pode ter // sido recebido corretamente, mas a resposta do dispositivo pode ter sido um erro devido a // algum comando ilegal. Pra isso existe o comando de getStatus também. // Ok, se o Buffer é valido, agora precisamos verificar se o status da solicitação é valido, // já que pode ter ocorrido algum erro interno no dispositivo ou alguma informação que não foi possivel ser processada. if (!parserENIPResposta.isStatusSucesso().isSucesso) { console.log(`O pacote recebido não é um status de sucesso, não é possivel extrair informações. Status recebido: ${parserENIPResposta.getStatus().codigo}: ${parserENIPResposta.getStatus().mensagem}`); return; } // Se o Buffer é valido e com status sucesso, podemos extrair as informações // do pacote recebido para o comando que desejamos. // Como enviamos um comando ListIdentity, é esperado que a resposta seja também um ListIdentity. // Na teoria se você enviou um comando X, ele nunca deve retornar outra coisa além de X, mas sei lá coisas estranhas podem acontecer. if (!parserENIPResposta.isListIdentity()) { console.log(`O pacote recebido não é um ListIdentity, não é possivel extrair informações.`); return; } // Como é um ListIdentity, chamamos o parser do ListIdentity que irá pegar // o Buffer recebido e extrair as informações do ListIdentity. const parserListIdentity = parserENIPResposta.getAsListIdentity(); // Assim como fizemos assim, precisamos validar se o Buffer recebido do // ListIdentity é valido e não contém informações corrompidas. if (!parserListIdentity.isValido().isValido) { console.log(`O pacote recebido do ListIdentity não é valido, não é possivel extrair informações. Motivo: ${parserListIdentity.isValido().erro.descricao}. Trace log: ${parserListIdentity.isValido().tracer.getHistoricoOrdenado().join(' -> ')}`); return; } // Finalmente, se tudo estiver com sucesso, temos acesso as informações do dispositivo solicitadas. console.log(parserListIdentity.getIdentidadeCIP()); // A saida atual constaria algo como: // { // "tipo": 12, // "versao_protocolo_encapsulamento": 1, // "endereco_socket": { // "endereco": "192.168.3.120", // "familia": 2, // "porta": 44818, // "zeros": "00000000" // }, // "fabricante_id": 1, // "tipo_dispositivo": 12, // "codigo_produto": 120, // "versao_revisao": "17.4", // "status_dispositivo": 96, // "numero_serial": 1079850447, // "tamanho_nome_dispositivo": 23, // "nome_dispositivo": "1769-L35E Ethernet Port", // "estado_dispositivo": 3 // } // Cada "Parser" precisa ser verificado se é valido e status sucesso. É meio que se fosse um "lego", você recebe todo o Buffer inicial, e vai montando ele aos poucos, e a cada montagem, você precisa verificar se o que você recebeu está no formato que deveria. // A mesma lógica aqui irá se aplicar a praticamente todos os Parsers. ``` Todos os comandos do EtherNet/IP seguem a mesma lógica do código acima. ## Implementação do CompactLogix usando o EtherNet/IP A classe CompactLogix implementa o uso da classe EtherNet/IP pra tratar os pacotes. Ela já vem configurada com a maioria dos metódos úteis de comunicação, leituras unicas e multiplas, escritas unicas e multiplas e obtenção de tags existentes no controlador. ### Estabelecendo conexão com um CompactLogix ```javascript // Informa os parametros de conexão com o dispositivo const compactLogix = new CompactLogixRockwell({ ip: '192.168.3.120', porta: 44818, habilitaLogs: true, autoReconectar: true }) // A função conectar trata realiza todo o processo de conexão inicial, abertura do Socket, Register Session, etc.. const aguardaConectar = await compactLogix.conectar(); if (!aguardaConectar.isConectou) { console.log(`Erro ao tentar se conectar ao CompactLogix: ${aguardaConectar.erro.descricao}`); return; } // Após conectado, é possível solicitar leituras de tags, escritas e outras funções disponiveis. ``` É interessante colocar como auto reconectar, pois em caso de perca de conexão ele irá automaticamente tentar se conectar quando possível e já vai iniciar todo aquele trabalho de registrar a sessão. ### Leitura de Tags No momento, é compatível a leitura de tipos atomicos(numeros no geral, como Small Int, Double int, Int, Double Int, Boolean, Real), Structs tem suporte apenas StringASCII82(no momento somente esse pois é o que eu uso e preciso.) e também arrays, acessando via nomeTag[index] Segue alguns exemplos abaixos para leituras ```javascript // Ler uma unica tag numerica const retornoLeituraUnica = await compactLogix.lerTag('TESTE2'); console.log(retornoLeituraUnica); // O retorno da Promise das leituras e escritas é bem verbose na parte de erros, não deixarei visivel porém é bem // extenso e permite saber exatamente quais tipos de erros que ocorreram durante a leitura(sem conexão, erro de buffer, status de erro, tag invalida, etc...) // Toda leitura de uma tag retorna o seu valor atual e informações do seu data type no controlador // { // "isSucesso": true, // "tagSolicitada": "TESTE2", // "msDetalhes": { // "dateTimeInicio": 1732335653335, // "dateTimeFim": 1732335653404, // "totalMsLeitura": 69 // }, // "sucesso": { // "tag": { // "isAtomico": true, // "atomico": { // "valor": 123, // "dataType": { // "codigo": 196, // "descricao": "Double Int", // "tamanho": 4, // "isSigned": true // } // }, // "isStruct": false, // "struct": {} // } // } // } // ------------------------------- // A resposta contém os booleans isAtomico e isStruct pra determinar onde vc deve buscar o valor correspondente na chave do objeto. // Ler uma unica tag do tipo String const retornoLerStringUnica = await compactLogix.lerTag('STRING_NOME_OPERADOR') console.log(retornoLerStringUnica); // { // "isSucesso": true, // "tagSolicitada": "STRING_NOME_OPERADOR", // "msDetalhes": { // "dateTimeInicio": 1732336018190, // "dateTimeFim": 1732336018299, // "totalMsLeitura": 109 // }, // "sucesso": { // "tag": { // "isAtomico": false, // "atomico": {}, // "isStruct": true, // "struct": { // "dataTypeStruct": { // "descricao": "String ASCII de 82 bytes", // "codigoTipoStruct": 4046 // }, // "valor": { // "stringConteudo": "NOME OPERADOOO", // "tamanho": 14 // } // } // } // } // } // ------------------------------- // Ler uma unica tag de um array const retornoLeIndiceArray = await compactLogix.lerTag('MINHA_TAG[3]'); console.log(retornoLeIndiceArray); // { // "isSucesso": true, // "tagSolicitada": "MINHA_TAG[3]", // "msDetalhes": { // "dateTimeInicio": 1732336512924, // "dateTimeFim": 1732336513001, // "totalMsLeitura": 77 // }, // "sucesso": { // "tag": { // "isAtomico": true, // "atomico": { // "valor": 5, // "dataType": { // "codigo": 202, // "descricao": "Real", // "tamanho": 4, // "isSigned": true // } // }, // "isStruct": false, // "struct": {} // } // } // } // ------------------------------- // Ler multipltas tag em uma requisição const retornoLerVarias = await compactLogix.lerMultiplasTags(['TESTE2', 'TESTE']) console.log(retornoLerVarias); // A operação de leitura múltipla é considerada sucesso se o pacote ENIP foi enviado e recebido com sucesso. // Se houve isSucesso, as tags solicitadas estarão no array sucesso com seus status individuais de cada um. // { // "isSucesso": true, // "sucesso": { // "tags": [ // { // "tag": "TESTE", // "isSucesso": true, // "sucesso": { // "isAtomico": true, // "atomico": { // "dataType": { // "codigo": 202, // "descricao": "Real", // "tamanho": 4, // "isSigned": true // }, // "valor": 5 // }, // "isStruct": false, // "struct": {} // }, // }, // { // "tag": "TESTE2", // "isSucesso": true, // "sucesso": { // "isAtomico": true, // "atomico": { // "dataType": { // "codigo": 196, // "descricao": "Double Int", // "tamanho": 4, // "isSigned": true // }, // "valor": 123 // }, // "isStruct": false, // "struct": {} // } // } // ] // } // } ``` ### Escrita de Tags Para realizar a escrita é praticamente a mesma coisa. Você pode tanto informar o Data Type no parâmetro da função ou utilizar a opção extra que automaticamente realiza a descoberta do tipo(solicitando ao controlador). As escritas retornam sucesso se a operação foi feita com sucesso, e também é retornado as informações do valor escrito junto com seu data type. ```javascript // Como já sei o tipo da TESTE2, eu informo no segundo parametro da função. A classe CompactLogix tem os Data Types pra vc saber qual é qual. const escreveTag = await compactLogix.escreveTag('TESTE2', { isAtomico: true, atomico: { codigoAtomico: compactLogix.getDataTypes().atomicos.DINT.codigo, valor: 5 } }); console.log(escreveTag); // { // "isSucesso": true, // "msDetalhes": { // "dateTimeInicio": 1732336979251, // "dateTimeFim": 1732336979321, // "totalMsEscrita": 70 // }, // "sucesso": { // "tag": { // "isAtomico": true, // "atomico": { // "valor": 5, // "dataType": { // "codigo": 196, // "descricao": "Double Int", // "tamanho": 4, // "isSigned": true // } // }, // "isStruct": false, // "struct": {} // } // } // } // ------------------------------- // Escreve uma tag que resolve automaticamente o tipo, informando apenas o valor final que quero const escreveTagResolveSozin = await compactLogix.escreveTag('TESTE2', { isResolverAutomaticamente: true, resolverAutomaticamente: { valor: '2' } }); console.log(escreveTagResolveSozin); // { // "isSucesso": true, // "msDetalhes": { // "dateTimeInicio": 1732337160516, // "dateTimeFim": 1732337160589, // "totalMsEscrita": 73 // }, // "sucesso": { // "tag": { // "isAtomico": true, // "atomico": { // "valor": "2", // "dataType": { // "codigo": 196, // "descricao": "Double Int", // "tamanho": 4, // "isSigned": true // } // }, // "isStruct": false, // "struct": {} // } // } // } // ------------------------------- const escreveTagResolveSozin2 = await compactLogix.escreveTag('STRING_NOME_OPERADOR', { isResolverAutomaticamente: true, resolverAutomaticamente: { valor: 'ola eu sou uma string' } }); console.log(escreveTagResolveSozin2); // { // "isSucesso": true, // "msDetalhes": { // "dateTimeInicio": 1732337320394, // "dateTimeFim": 1732337320436, // "totalMsEscrita": 42 // }, // "sucesso": { // "tag": { // "isAtomico": false, // "atomico": {}, // "isStruct": true, // "struct": { // "dataTypeStruct": { // "descricao": "String ASCII de 82 bytes", // "codigoTipoStruct": 4046 // }, // "valor": { // "string": "ola eu sou uma string" // } // } // } // } // } // ------------------------------- // Para escrever múltiplas tags, informe um array de objetos seguindo o padrão esperado. const escreveMultiplasTags = await compactLogix.escreveMultiplasTags([ {tag: 'TESTE', dataType: {isAtomico: true, atomico: {codigoAtomico: compactLogix.getDataTypes().atomicos.DINT.codigo, valor: 5}}}, {tag: 'TESTE2', dataType: {isResolverAutomaticamente: true, resolverAutomaticamente: {valor: 5}}}, {tag: 'STRING_NOME_OPERADOR', dataType: {isResolverAutomaticamente: true, resolverAutomaticamente: {valor: 'eu sooo a strinng'}}} ]) console.log(escreveMultiplasTags); // Para as escritas de multiplas tags, a chave isSucesso é considerada sucesso se o pacote ENIP foi transmitido e recebido de volta. // E então, cada tag é retornado com seu status individual da operação da escrita, com detalhes de erro ou sucesso. // Nessa solicitação especifiquei errado de proposito o Data Type da tag TESTE pra mostrar de forma superficial como o erro é exibido. // { // "isSucesso": true, // "sucesso": { // "tags": [ // { // "tag": "TESTE", // "dataTypeDados": { // "isValido": true, // "isAtomico": true, // "isStruct": false, // "atomico": { // "codigoAtomico": 196, // "valor": 5 // }, // "struct": {} // }, // "isSucesso": false, // "erro": { // "descricao": "O pacote Single Service Packet retornou um status de erro: 255 - Specific Object Class Error/ Data Type informado para a tag é incorreto", // "isSingleServicePacketStatusErro": true, // "SingleServicePacketStatusErro": { // "codigoStatus": 255, // "descricaoStatus": "Specific Object Class Error", // "isTagNaoExiste": false, // "isDataTypeIncorreto": true, // "additionalStatusBuffer": { // "type": "Buffer", // "data": [ // 7, // 33 // ] // } // } // } // }, // { // "tag": "TESTE2", // "dataTypeDados": { // "isValido": true, // "isAtomico": true, // "isStruct": false, // "atomico": { // "codigoAtomico": 196, // "valor": 5 // }, // "struct": {} // }, // "isSucesso": true // }, // { // "tag": "STRING_NOME_OPERADOR", // "dataTypeDados": { // "isValido": true, // "isAtomico": false, // "isStruct": true, // "atomico": {}, // "struct": { // "classeStruct": { // "string": "eu sooo a strinng" // }, // "codigoStruct": 4046 // } // }, // "isSucesso": true, // } // ] // } // } // ------------------------------- // Tentar escrever uma tag que não existe informando pra resolver automaticamente const escreveTagNaoExiste = await compactLogix.escreveTag('DASDSADA', { isResolverAutomaticamente: true, resolverAutomaticamente: { valor: '2' } }) console.log(escreveTagNaoExiste); // { // "isSucesso": false, // "msDetalhes": { // "dateTimeInicio": 1732338339214 // }, // "sucesso": { // "tag": { // "isAtomico": false, // "atomico": {}, // "isStruct": false, // "struct": {} // } // }, // "erro": { // "descricao": "Erro ao tentar resolver automaticamente o Data Type da tag DASDSADA: A tag não existe.", // "isObterDataTypeAutomatico": true, // } // } // Nesse caso, como o erro é inicialmente em tentar obter o Data Type, ele informa isObterDataTypeAutomatico // ------------------------------- // Tentar escrever uma tag que não existe, informando o tipo dessa vez const escreveTagNaoExisteComTipo = await compactLogix.escreveTag('DASDSADA', { isAtomico: true, atomico: { codigoAtomico: compactLogix.getDataTypes().atomicos.DINT.codigo, valor: 5 } }) console.log(escreveTagNaoExisteComTipo); // { // "isSucesso": false, // "msDetalhes": { // "dateTimeInicio": 1732338492426, // "dateTimeFim": 1732338492480, // "totalMsEscrita": 54 // }, // "sucesso": { // "tag": { // "isAtomico": true, // "atomico": { // "valor": 5, // "dataType": { // "codigo": 196, // "descricao": "Double Int", // "tamanho": 4, // "isSigned": true // } // }, // "isStruct": false, // "struct": {} // } // }, // "erro": { // "descricao": "A tag não existe no controlador", // "isStatusInvalido": true, // "statusInvalido": { // "descricaoStatus": "Path segment error", // "codigoDeErro": 4, // "isTagNaoExiste": true, // "isDataTypeIncorreto": false // }, // } // } // Nesse caso eu informei o Data Type, porém a tag não existe no controlador. ``` ### Listar Tags É possível solicitar a lista de todas as tags presentes no CompactLogix com seus Data Types inclusos ```javascript // Solicitar a lista de todas as tags. Dependendo do numero de tags pode demorar alguns segundos. const solicitaListaTags = await compactLogix.obterListaDeTags(); console.log(solicitaListaTags); // O retorno contém o data type, instancia dela no controlador e se ela é suportada atualmente para interagir utilizando essa classe // { // "isSucesso": true, // "sucesso": { // "tags": [ // { // "tag": "TAG1", // "instancia": 7, // "dataType": { // "classeDataType": { // "codigo": 195, // "descricao": "Int", // "tamanho": 2, // "isSigned": true // }, // "isAtomico": true, // "isStruct": false, // "isDataTypeSuportado": true, // "dataTypeNaoSuportado": {} // } // }, // { // "tag": "TAG2", // "instancia": 119, // "dataType": { // "classeDataType": { // "codigo": 195, // "descricao": "Int", // "tamanho": 2, // "isSigned": true // }, // "isAtomico": true, // "isStruct": false, // "isDataTypeSuportado": true, // "dataTypeNaoSuportado": {} // } // }, // { // "tag": "TAG3", // "instancia": 143, // "dataType": { // "classeDataType": { // "codigo": 195, // "descricao": "Int", // "tamanho": 2, // "isSigned": true // }, // "isAtomico": true, // "isStruct": false, // "isDataTypeSuportado": true, // "dataTypeNaoSuportado": {} // } // }, // { // "tag": "G6_DADOS", // "instancia": 185, // "dataType": { // "classeDataType": {}, // "isAtomico": false, // "isStruct": false, // "isDataTypeSuportado": false, // "dataTypeNaoSuportado": { // "codigo": 4095, // "descricao": "Data Type não é atomico e nem struct." // } // } // }, // { // "tag": "TAG5", // "instancia": 189, // "dataType": { // "classeDataType": { // "codigo": 195, // "descricao": "Int", // "tamanho": 2, // "isSigned": true // }, // "isAtomico": true, // "isStruct": false, // "isDataTypeSuportado": true, // "dataTypeNaoSuportado": {} // } // } // ] // }, // } ``` ## MicroLogix 1400 A classe Micrologix 1400, permite comunicação com modelos A/B e oferece suporte para as seguintes operações: - ** Leitura de Data Files - ** Escrita de Data Files - ** Observações de Data Files No momento, foi testado apenas para valores de Data Files (N) e (ST). ### Conectar ```javascript const micoLogix = new MicroLogix1400({ ip: '192.168.3.190', porta: 44818, autoReconectar: false, habilitaLogs: true }) // Tentar se conectar let statusConecta = await micoLogix.conectar(); if (!statusConecta.isConectou) { console.log(`Não foi possível se conectar: ${statusConecta.erro.descricao}`); return; } console.log(`MicroLogix conectado com sucesso!`); // Conectado ``` ### Leitura As leituras são feitas através de Data Files, onde você pode especificar o tipo de dado que deseja ler. Devido as limitações do protocolo CIP via PCCC para MicroLogix, não é possível ler ou escrever múltiplos Data Files em uma única requisição, então cada leitura e escrita é feita de forma individual. ```javascript // Ler o Data File tipo inteiro, identificado pelo número 7, e o elemento 33 dentro desse Data File const lerN7_33 = await micoLogix.lerDataFile('N7:33'); if (lerN7_33.isSucesso) { console.log(`Meu número N7:33 é ${lerN7_33.sucesso.valor}`); } else { console.error(`Erro ao ler N7:33: ${lerN7_33.erro.descricao}`); } // Ler o Data File tipo string, identificado pelo número 10, e o elemento 5 dentro desse Data File const lerST_5 = await micoLogix.lerDataFile('ST10:5'); if (lerST_5.isSucesso) { console.log(`Minha string ST10:5 é ${lerST_5.sucesso.valor}`); } else { console.error(`Erro ao ler ST10:5: ${lerST_5.erro.descricao}`); } ``` Sempre é bom ficar atento ao objeto de isSucesso, se ela for false, a leitura falhou e você deve verificar o objeto de erro para mais informações. Possíveis erros podem ser: O Data File não existe, o tipo de dado não é suportado, o dispositivo não está conectado, etc. ### Escrita Os métodos de escrita são quase idênticos aos de leitura, mas você deve informar o valor que deseja escrever no Data File ```javascript // Escrever o número 1234 no Data File tipo inteiro, identificado pelo número 7, e o elemento 55 dentro desse Data File const escreveN7_55 = await micoLogix.writeDataFile('N7:55', 1234); if (escreveN7_55.isSucesso) { console.log(`Escrevi com sucesso o número 1234 em N7:55`); } else { console.error(`Erro ao escrever N7:55: ${escreveN7_55.erro.descricao}`); } // Escrever a string "Ola, mundo!"(yeah sem acento) no Data File tipo string, identificado pelo número 10, e o elemento 5 dentro desse Data File const escreveST = await micoLogix.writeDataFile('ST10:5', 'Ola, mundo!'); if (escreveST.isSucesso) { console.log(`Escrevi com sucesso a string "Ola, mundo!" em ST10:5`); } else { console.error(`Erro ao escrever ST10:5: ${escreveST.erro.descricao}`); } ``` ### Observação Para observar um Data File, você deve informar o nome do Data File e o callback que será chamado quando o valor do Data File mudar. ```javascript const statusObservaString = await microLogix.observarDataFile('ST10:5', { // Toda vez que o valor desse Data File for alterado, esse callback é chamado onDataFileAlterado: (valorAntigo, valorNovo) => { console.log(`Valor antigo: ${valorAntigo}, novo valor: ${valorNovo}`); }, // Toda vez que um erro de leitura do Data File ocorrer, esse callback é chamado onErroLeitura: (erro) => { console.error(`Erro ao ler Data File: ${erro}`); } }); // Só não será possível observar se ocorrer algum erro de leitura no Data File solicitado if (statusObservaString.isSucesso) { console.log(`Observação de Data File ST10:5 iniciada com sucesso! ID do callback: ${statusObservaString.sucesso.idCallback}`); setTimeout(() => { // Para parar a observação, basta chamar o método pararObservacaoDataFile passando o ID do callback retornado microLogix.pararObservadorUnicoDataFile('ST10:5', statusObservaString.sucesso.idUnicoCallback); // Ou, se você quiser, parar todas as observações de um endereço Data File específico microLogix.pararObservarDataFile('ST10:5'); }, 10000); } else { console.log(`Erro ao iniciar observação de Data File: ${statusObservaString.erro.descricao}`); } ``` ## CompactLogixV2 A classe CompactLogixV2 utiliza a classe CompactLogix como base para facilitar ainda mais a comunicação com o CompactLogix. CompactLogix oferece as comunicações básicas: leitura e escrita de forma livre sem restrições, utilize ela caso queira customizar sua interação com o dispositivo. CompactLogixV2 usa a CompactLogix para oferecer uma implementação robusta de leitura, escrita e observação de tags. As operações de leituras e escritas são colocadas em filas separadas para executar a cada X millisegundos, assim chamadas subsequentes em um for ou algo do tipo não irão spamar o dispositivo com requisições. ### Conectar ```javascript // Conectar com um CompactLogix const controlador = new CompactLogixV2({ conexao: { ip: '192.168.3.120', porta: 44818 }, isAutoReconectar: true, isMostrarConsoleLogs: true }) const statusConectou = await controlador.conectar(); if (!statusConectou.isConectado) { console.log(`Erro ao conectar-se: ${statusConectou.erro.descricao}`); return; } console.log(`Conectado`) // Conectado ``` ### Ler Tags Realiza leituras de tags(Disponível somente leitura de múltiplas tags). ```javascript // Ler tags const leTag = await controlador.lerTags(['TESTE2', 'TESTE']); console.log(leTag); // { // "isSucesso": true, // "sucesso": { // "tags": [ // { // "tag": "TESTE2", // "valor": { // "isAtomico": true, // "atomico": { // "numero": 456 // } // }, // "erro": { // "descricao": "" // }, // "isLeituraRecebida": true, // "isTagValida": true, // "dataType": { // "isAtomico": true, // "atomico": { // "codigoAtomico": 196, // "descricao": "Double Int", // "isSigned": true, // "tamanho": 4 // } // } // }, // { // "tag": "TESTE", // "valor": { // "isAtomico": true, // "atomico": { // "numero": 6.866362475191604e-44 // } // }, // "erro": { // "descricao": "" // }, // "isLeituraRecebida": true, // "isTagValida": true, // "dataType": { // "isAtomico": true, // "atomico": { // "codigoAtomico": 202, // "descricao": "Real", // "isSigned": true, // "tamanho": 4 // } // } // } // ] // }, // } ``` ### Escrever Tags Realiza escritas de tags(Disponível somente escrita de múltiplas tags) ```javascript // Escrever tags const escreveTags = await controlador.escreverTags([ { tag: 'TESTE2', valor: 5 } ]) console.log(escreveTags); // { // "isSucesso": true, // "sucesso": { // "tags": [ // { // "tag": "TESTE2", // "valor": 5, // "isEscritoSucesso": true, // "erro": { // "descricao": "" // }, // "isRecebidoConfirmacao": true // } // ] // }, // "erro": { // "descricao": "" // } // } ``` ### Observar Tags Realiza o cadastro de um observador em uma tag. Quando o valor da tag mudar, o callback passado será executado com os valores antigos e novos. A classe lida com o cadastro de callbacks de forma eficiente pra cada tag, então não se preocupe em spamar observações na mesma tag. ```javascript // Observar tags const observaTags = await controlador.observarTag('TESTE2', { onAlterado: (antigo, novo) => { console.log(`Valor antigo: ${JSON.stringify(antigo)}, novo valor: ${JSON.stringify(novo)}`); // Valor antigo: {"isAtomico":true,"atomico":{"numero":123}}, novo valor: {"isAtomico":true,"atomico":{"numero":456}} // Valor antigo: {"isAtomico":true,"atomico":{"numero":456}}, novo valor: {"isAtomico":true,"atomico":{"numero":123}} } }) if (observaTags.isSucesso) { // A observação com sucesso retorna um ID de callback para cancelar a observação posteriormente. console.log(`TAG observada com sucesso via ID ${observaTags.sucesso.idCallback}`); setTimeout(() => { console.log('Parando observação da tag TESTE2'); controlador.pararCallbackObservacaoTag('TESTE2', observaTags.sucesso.idCallback); }, 5000); } ``` ### Parar Observação de Tags ```javascript // Isso irá remover o observador ID 2 da tag TESTE2, enquanto outros observadores da tag continuam sendo chamados. controlador.pararCallbackObservacaoTag('TESTE2', 2); // Isso irá remover TODOS os observadores da tag. controlador.pararObservacaoTag('TESTE2'); ``` ## Recursos Utilizados Utilizei outras bibliotecas como st-ethernet-ip e ethernet-ip pra ter uma ideia de como iniciar, utilizei também os manuais da AODV de implementação do EtherNet/IP e CIP(esta nos documentos aqui do projeto), e também a ferramente WireShark que me auxiliou bastante pra entender e fazer os Parsers e Builders dos bytes que são enviados e recebidos.