wow-another-ethernet-ip
v1.1.2
Published
Uma biblioteca para comunicação com dispositivos com o protocolo CIP via Ethernet/IP
Maintainers
Readme
Sumário
- Projeto
- Builders e Parsers
- Serviços Suportados
- Sumário
- Como usar
- 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
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
// 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
// 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
// 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.
// 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
// 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
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!`);
// ConectadoLeitura
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.
// 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
// 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.
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
// 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`)
// ConectadoLer Tags
Realiza leituras de tags(Disponível somente leitura de múltiplas tags).
// 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)
// 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.
// 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
// 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.
