wow-another-ethernet-ip
Version:
Uma biblioteca para comunicação com dispositivos com o protocolo CIP via Ethernet/IP
1,168 lines (970 loc) • 63.5 kB
JavaScript
import { ServicosPCCC } from "../../../EtherNetIP/Builder/Layers/EtherNetIP/CommandSpecificDatas/SendRRData/CIP/Servicos/CIPPCCC/CIPPCCC.js";
import { EtherNetIPSocket } from "../../../EtherNetIP/EtherNetIP.js";
import { EmissorEvento } from "../../../Utils/EmissorEvento.js";
/**
* @typedef ObservacaoDeDataFile
* @property {String} enderecoDataFile - Endereço completo do Data File, exemplo S:1, ST10:1, N7:10
* @property {*} valor - O valor atual nesse Data File
* @property {CallbackDeDataFile[]} callbacks - Callbacks que estão escutando por alterações nesse Data File
*/
/**
* @typedef CallbackDeDataFile
* @property {Number} id - Identificador único do callback para identificação e poder cancelar
* @property {Object} callbacks - Funções callbacks que serão executadas
* @property {FuncaoCallbackDataFileAlterado} callbacks.onDataFileAlterado - Evento a ser executado quando houver alteração no Data File
* @property {*} callbacks.onErroLeitura - Se ocorreu um erro na leitura do Data File para determinar se houve alterações
*/
/**
* Classe para interagir com un controlador MicroLogix 1400 de Rockwell. As seguintes operações são suportadas:
** Leitura de Data Files
** Escrita de Data Files
** Observações de Data Files
** Leitura de informações do Data File de Status(isso pode ser acessado diretamente via leituras de Data File também)
*
* Para leituras/escritas, no momento foi testado apenas para valores para Data Files do tipo Integer(N) e String(ST).
*/
export class MicroLogix1400 {
/**
* @type {EtherNetIPSocket} - Socket de comunicação com o dispositivo remoto no protocolo EtherNet/IP
*/
#ENIPSocket;
#configuracao = {
ip: '',
porta: 0,
logs: {
habilitarLogsConsole: false
}
}
#estado = {
/**
* Emissor de eventos
*/
emissorEvento: new EmissorEvento(),
/**
* Estado de endereços lógicos sendo observados
*/
dataFilesObservados: {
/**
* Data File sendo observados atualmente
* @type {ObservacaoDeDataFile[]}
*/
observados: [],
/**
* Index de ID unico que incrementa a cada novo Data File observado
*/
indexIdIncrementador: 0,
/**
* O ID do setInterval que será criado para realizar as leituras e disparar os callbacks
*/
setIntervalTriggerLeituras: -1
}
}
/**
* Parametros de conexão com o controlador MicroLogix 1400
* @param {Object} parametros
* @param {String} parametros.ip - Endereço IP do controlador
* @param {Number} parametros.porta - Porta
* @param {Boolean} parametros.habilitaLogs - Se os logs do gerenciador de EtherNetIP devem ser mostrados 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 ao MicroLogix 1400
*/
async conectar() {
/**
* @typedef RetornoConectar
* @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 {RetornoConectar}
*/
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}`;
}
}
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 a autenticação está estabelecida com o controlador MicroLogix 1400
** 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;
}
/**
* Retornar o Socket de comunicação com o dispositivo EtherNet/IP
*/
getENIPSocket() {
return this.#ENIPSocket;
}
/**
* Adicionar um callback para quando um log for disparado
* @param {CallbackLog} cb
*/
onLog(cb) {
this.#estado.emissorEvento.addEvento('log', cb);
}
/**
* Enviar uma solicitação de leitura do Status File(Identificação S) e retorna as informações de SO, processador e compilação
*
** Se o valor de alguma informação for undefined, significa que não foi possível obter ela.
*/
async readVersao() {
const retornoVersion = {
/**
* Se foi possível obter as informações do arquivo de Status
*/
isSucesso: false,
sucesso: {
/**
* Informações do Sistema Operacional
*/
os: {
/**
* Identificação do catalogo do SO
*
* **Documentação do DF1: This register identifies the Catalog Number for the Operating System in the controller.**
* @type {Number}
*/
catalogo: undefined,
/**
* Identificação do número da série do SO
*
* **Documentação do DF1: This register identifies the Series letter for the Operating System in the controller**
* @type {String}
*/
serie: undefined,
/**
* Identificação do FRN do SO
*
* **Documentação do DF1: This register identifies the FRN of the Operating System in the controller..**
* @type {Number}
*/
revisao: undefined
},
/**
* Informações do Processador
*/
processador: {
/**
* Identificação do catalogo do processador
*
* **Documentação do DF1: This register identifies the Catalog Number for the processor.**
* @type {String}
*/
catalogo: undefined,
/**
* Identificação do número da série do processador
*
* **Documentação do DF1: This register identifies the Series of the processor..**
* @type {String}
*/
serie: undefined,
/**
* Identificação da revisão do processador
*
* **Documentação do DF1: This register identifies the revision (Boot FRN) of the processor.**
* @type {Number}
*/
revisao: undefined
},
/**
* Informações do programa que compilou o programa e enviou pro controlador
*/
compilacao: {
/**
* Identificação do Build Number do programa que compilou pro o programa no controlador
*
* **Documentação do DF1: This register identifies the Build Number of the compiler that created the program in the controller.**
* @type {Number}
*/
build: undefined,
/**
* Identificação do Release Number do programa que compilou pro o programa no controlador
*
* **Documentação do DF1: This register identifies the Release of the compiler that created the program in the controller.**
*/
release: undefined
}
},
erro: {
descricao: ''
}
}
const layerBuilder = this.#ENIPSocket.getNovoLayerBuilder();
// O CIP PCCC é onde vou setar os dados da solicitação de leitura
const CIPPCCC = layerBuilder.buildSendRRData().criarServicoCIP().buildCIPPCCC();
CIPPCCC.setServicePCCC(ServicosPCCC.ExecutePCCC.hex)
// Comando PCCC
const CommandData = CIPPCCC.getCommandData();
// Como é uma leitura, uso o comando e função Protected Typed 3 Address Read
let leituraTyped3AddressBuilder = CommandData.setAsCommandProtectedTyped3Address('Read');
// O File Number do Status FIle é o 2 no MicroLogix 1400
leituraTyped3AddressBuilder.setFileNumber(Buffer.from([0x02]));
// O tipo de arquivo é Status
leituraTyped3AddressBuilder.setFileType('Status');
// Segundo o manual do MicroLogix1400, as informações começam do Index 57;
// S:57 OS Catalog Number
// S:58 OS Series
// S:59 OS FRN
// S:60 Processor Catalog Number
// S:61 Processor Series
// S:62 Processor Revision
// S:63 User Program Functionality Type
// S:64L Compiler Revision
// S:64H Compiler Revision
leituraTyped3AddressBuilder.setElementNumber(Buffer.from([57]));
// Vou ler até o index 63, como cada elemento tem 2 bytes, vou ler 2 x (ate onde eu quero) bytes pra frente, que ficaria 2 x 8 = 14 bytes
leituraTyped3AddressBuilder.setByteSize(16);
leituraTyped3AddressBuilder.setSubElementNumber(Buffer.from([0x00]));
const respostaENIP = await this.#ENIPSocket.enviarENIP(layerBuilder);
// Se a resposta não foi bem sucedida, analisar o erro
if (!respostaENIP.isSucesso) {
// Não deu pra enviar, devolver o erro ocorrido
if (!respostaENIP.enipEnviar.isEnviou) {
retornoVersion.erro.descricao = `${respostaENIP.enipEnviar.erro.descricao}`;
} else if (respostaENIP.enipReceber.isRecebeu) {
retornoVersion.erro.descricao = `${respostaENIP.enipReceber.erro.descricao}`;
}
return retornoVersion;
}
const parserENIP = respostaENIP.enipReceber.enipParser;
if (!parserENIP.isSendRRData()) {
retornoVersion.erro.descricao = 'A resposta ENIP recebida não é um comando SendRRData.';
return retornoVersion;
}
const parserSendRRData = parserENIP.getAsSendRRData();
if (!parserSendRRData.isValido().isValido) {
retornoVersion.erro.descricao = `O Buffer SendRRData não é valido. Motivo: ${parserSendRRData.isValido().descricao}`;
return retornoVersion;
}
const parserCIP = parserSendRRData.getAsServicoCIP();
if (!parserCIP.isValido().isValido) {
retornoVersion.erro.descricao = `O Buffer CIP não é valido. Motivo: ${parserCIP.isValido().descricao}`;
return retornoVersion;
}
if (!parserCIP.isStatusSucesso().isSucesso) {
retornoVersion.erro.descricao = `O status retornado do CIP não foi sucesso. Foi retornado o status ${parserCIP.isStatusSucesso().erro.codigo} - ${parserCIP.isStatusSucesso().erro.descricao}`;
return retornoVersion;
}
const parserPCCC = parserCIP.getAsPCCC();
if (!parserPCCC.isValido().isValido) {
retornoVersion.erro.descricao = `O Buffer PCCC não é valido. Motivo: ${parserPCCC.isValido().descricao}`;
return retornoVersion;
}
const parserPCCCResponseData = parserPCCC.getPCCCResponseData();
if (!parserPCCCResponseData.isPCCCSucesso().isSucesso) {
retornoVersion.erro.descricao = `O dispositivo não aceitou executar a solicitação PCCC. Código ${parserPCCCResponseData.isPCCCSucesso().erro.codigo} - ${parserPCCCResponseData.isPCCCSucesso().erro.descricao}`;
return retornoVersion;
}
// Ok, se retornou sucesso eu deveria ter por fim o Function Specific Data que seria os bytes da leitura do Status File onde tem as informações que vou extrair, como OS, firmware, processador
const bufferResposta = parserPCCCResponseData.getFunctionSpecificResponseData();
if (bufferResposta.length == 0) {
retornoVersion.erro.descricao = `O buffer de resposta não contém nenhum byte, não tem como extrair nada das informações do controlador.`;
return retornoVersion;
}
// S:57 OS Catalog Number
// S:58 OS Series
// S:59 OS FRN
// S:60 Processor Catalog Number
// S:61 Processor Series
// S:62 Processor Revision
// S:63 User Program Functionality Type
// S:64L Compiler Revision
// S:64H Compiler Revision
const isPossuiBytes = (offset, quantidade) => {
return bufferResposta.subarray(offset).length >= quantidade;
}
// Os 2 bytes do Buffer é o OS Catalog Number
if (isPossuiBytes(0, 2)) {
retornoVersion.sucesso.os.catalogo = bufferResposta.readUInt16LE(0);
}
// Os próximos 2 bytes são o OS Series
if (isPossuiBytes(2, 2)) {
retornoVersion.sucesso.os.serie = String.fromCharCode(65 + (bufferResposta.readUInt16LE(2) - 1));
}
// Os próximos 2 bytes são o OS FRN
if (isPossuiBytes(4, 2)) {
retornoVersion.sucesso.os.revisao = bufferResposta.readUInt16LE(4);
}
// Os próximos 2 bytes são o Processor Catalog Number
if (isPossuiBytes(6, 2)) {
retornoVersion.sucesso.processador.catalogo = bufferResposta.subarray(6, 8).toString('ASCII')
}
// Os próximos 2 bytes são o Processor Series
if (isPossuiBytes(8, 2)) {
retornoVersion.sucesso.processador.serie = String.fromCharCode(65 + (bufferResposta.readUInt16LE(8) - 1));
}
// Os próximos 2 bytes são o Processor Revision
if (isPossuiBytes(10, 2)) {
retornoVersion.sucesso.processador.revisao = bufferResposta.readUInt16LE(10);
}
// Ignoro o S:63 User Program Functionality Type
if (isPossuiBytes(14, 2)) {
const valorLido = bufferResposta.readUInt16LE(14);
// Os 16 bits retornados contém o Build Number e o Release Number.
// O Low Byte identifica o Build Number, utilizo somente os 8 bits
retornoVersion.sucesso.compilacao.build = valorLido & 0xFF;
// O High Byte identifica o Release Number, utilizo somente os 8 bits
retornoVersion.sucesso.compilacao.release = (valorLido >> 8) & 0xFF;
}
retornoVersion.isSucesso = true;
return retornoVersion;
}
/**
* Envia uma solicitação de leitura do Status File e retorna o modo de operação do controlador
*/
async readControllerMode() {
const retornoControllerMode = {
/**
* Se foi possível realizar a leitura do Status File e encontrar o modo de operação atual
*/
isSucesso: false,
/**
* Se sucesso, contém os detalhes adicionais.
*/
sucesso: {
/**
* Código do tipo do Modo do Controlador
*/
codigo: undefined,
/**
* Descrição
*/
descricao: ''
},
/**
* Se houve algum erro ao tentar realizar a leitura
*/
erro: {
descricao: ''
}
}
const layerBuilder = this.#ENIPSocket.getNovoLayerBuilder();
// O CIP PCCC é onde vou setar os dados da solicitação de leitura
const CIPPCCC = layerBuilder.buildSendRRData().criarServicoCIP().buildCIPPCCC();
CIPPCCC.setServicePCCC(ServicosPCCC.ExecutePCCC.hex)
// Comando PCCC
const CommandData = CIPPCCC.getCommandData();
// Como é uma leitura, uso o comando e função Protected Typed 3 Address Read
let leituraTyped3AddressBuilder = CommandData.setAsCommandProtectedTyped3Address('Read');
// Leitura de 2 bytes
leituraTyped3AddressBuilder.setByteSize(2);
// O File Number do Status FIle é o 2 no MicroLogix 1400
leituraTyped3AddressBuilder.setFileNumber(Buffer.from([0x02]));
// O tipo de arquivo é Status
leituraTyped3AddressBuilder.setFileType('Status');
// O modo do controlador é salvo no index 1 do Status File S:1 (max 65 do MicroLogix 1400)
leituraTyped3AddressBuilder.setElementNumber(Buffer.from([1]));
leituraTyped3AddressBuilder.setSubElementNumber(Buffer.from([0x00]));
const respostaENIP = await this.#ENIPSocket.enviarENIP(layerBuilder);
// Se a resposta não foi bem sucedida, analisar o erro
if (!respostaENIP.isSucesso) {
// Não deu pra enviar, devolver o erro ocorrido
if (!respostaENIP.enipEnviar.isEnviou) {
retornoControllerMode.erro.descricao = `${respostaENIP.enipEnviar.erro.descricao}`;
} else if (respostaENIP.enipReceber.isRecebeu) {
retornoControllerMode.erro.descricao = `${respostaENIP.enipReceber.erro.descricao}`;
}
return retornoControllerMode;
}
const parserENIP = respostaENIP.enipReceber.enipParser;
if (!parserENIP.isSendRRData()) {
retornoControllerMode.erro.descricao = 'A resposta ENIP recebida não é um comando SendRRData.';
return retornoControllerMode;
}
const parserSendRRData = parserENIP.getAsSendRRData();
if (!parserSendRRData.isValido().isValido) {
retornoControllerMode.erro.descricao = `O Buffer SendRRData não é valido. Motivo: ${parserSendRRData.isValido().descricao}`;
return retornoControllerMode;
}
const parserCIP = parserSendRRData.getAsServicoCIP();
if (!parserCIP.isValido().isValido) {
retornoControllerMode.erro.descricao = `O Buffer CIP não é valido. Motivo: ${parserCIP.isValido().descricao}`;
return retornoControllerMode;
}
// Se o layer do CIP for valido, validar se o status retornado é sucesso
if (!parserCIP.isStatusSucesso().isSucesso) {
retornoControllerMode.erro.descricao = `O status retornado do CIP não foi sucesso. Foi retornado o status ${parserCIP.isStatusSucesso().erro.codigo} - ${parserCIP.isStatusSucesso().erro.descricao}`;
return retornoControllerMode;
}
// Se o CIP for válido, então eu vou sem duvidas ter as informações dos próximo layer que deveria ser o PCCC Object
const parserPCCC = parserCIP.getAsPCCC();
if (!parserPCCC.isValido().isValido) {
retornoControllerMode.erro.descricao = `O Buffer PCCC não é valido. Motivo: ${parserPCCC.isValido().descricao}`;
return retornoControllerMode;
}
// Beleza, com os layers confirmados, eu sei que recebi uma resposta PCCC válida.
// O status da solicitação PCCC é diferente do status do CIP acima. A solicitação CIP só diz se o PCCC foi enviado e recebido, porém se a ação solicitada de certo é contida no PCCC Response Data, então preciso checar se foi sucesso
const parserPCCCResponseData = parserPCCC.getPCCCResponseData();
if (!parserPCCCResponseData.isPCCCSucesso().isSucesso) {
retornoControllerMode.erro.descricao = `O dispositivo não aceitou executar a solicitação PCCC. Código ${parserPCCCResponseData.isPCCCSucesso().erro.codigo} - ${parserPCCCResponseData.isPCCCSucesso().erro.descricao}`;
return retornoControllerMode;
}
// Ok, se retornou sucesso eu deveria ter por fim o Function Specific Data que seria os 2 bytes da leitura do Status File onde tem o modo do controlador
const bufferResposta = parserPCCCResponseData.getFunctionSpecificResponseData();
if (bufferResposta.length != 2) {
retornoControllerMode.erro.descricao = `O buffer de resposta não contém 2 bytes. Foram retornados ${bufferResposta.length} bytes, o status do modo do controlador obrigatoriamente tem que ser 2 bytes.`;
return retornoControllerMode;
}
// O estado do controlador é retornado pelos 16 bits. Porém somente os 5 primeiros bytes da direita pra esquerda são utilizados pra determinados o estado
const valorLido = bufferResposta.readUInt16LE();
// Valor dos primeiros 5 bits da direita pra esquerda
const valorBitsIsolado = valorLido & 0b11111;
const modoControlador = Object.values(ModoControlador).find((modo) => {
return modo.id == valorBitsIsolado;
});
retornoControllerMode.isSucesso = true;
retornoControllerMode.sucesso.codigo = valorBitsIsolado;
if (modoControlador == undefined) {
retornoControllerMode.sucesso.descricao = `Modo do controlador desconhecido`;
} else {
retornoControllerMode.sucesso.descricao = modoControlador.descricao;
}
return retornoControllerMode;
}
/**
* Envia uma solicitação ListIdentity e retorna a identidade CIP do controlador
*/
async readIdentidadeCIP() {
let retornoIdenti = {
/**
* Se foi possível receber as informações do CIP desse controlador
*/
isSucesso: false,
sucesso: {
/**
* Nome do dispositivo
* @type {String}
*/
nomeDispositivo: undefined,
/**
* Numero serial
* @type {Number}
*/
serial: undefined,
/**
* ID do fabricante, ex: Rockwell
* @type {Number}
*/
fabricanteId: undefined,
/**
* Tipo de dispositivo, ex: 1766-L32BXB B/11.00 (depende do fabricante ID)
* @type {Number}
*/
tipoDispositivoId: undefined,
/**
* Código do produto, ex: Programmable Logic Controller (dependo do tipo ID que depende do fabricante ID)
*/
codigoProduto: undefined,
/**
* A versão do dispositivo. Ex: 3.5
* @type {String}
*/
versaoRevisao: undefined,
/**
* A versão do protocolo de encapsulamento CIP(sempre deve ser 1)
*/
versaoProtocoloEncapsulamentoCIP: undefined,
/**
* O status do dispositivo contém as informações do status atual dele, se ta em modo de erro, modo de erro recuperavél, etc...(tem que isolar os bits pois cada x parte significa algo.)
* @type {Number}
*/
statusDispositivo: undefined,
/**
* Endereço Ethernet do dispositivo
*/
endereco: {
/**
* IP em string xxx.xxx.xxx.xxx
* @type {String}
*/
ip: undefined,
/**
* Porta utilizada
* @type {Number}
*/
porta: undefined
}
},
erro: {
descricao: ''
}
}
let novoLayerBuilder = this.#ENIPSocket.getNovoLayerBuilder();
novoLayerBuilder.buildListIdentity();
const respostaENIP = await this.#ENIPSocket.enviarENIP(novoLayerBuilder);
if (!respostaENIP.isSucesso) {
if (!respostaENIP.enipEnviar.isEnviou) {
retornoIdenti.erro.descricao = `${respostaENIP.enipEnviar.erro.descricao}`;
} else if (!respostaENIP.enipReceber.isRecebeu) {
retornoIdenti.erro.descricao = `${respostaENIP.enipReceber.erro.descricao}`;
}
return retornoIdenti;
}
const parserENIP = respostaENIP.enipReceber.enipParser;
if (!parserENIP.isListIdentity()) {
retornoIdenti.erro.descricao = 'O buffer ENIP recebido não é um comando ListIdentity';
return retornoIdenti;
}
const parserListIdentity = parserENIP.getAsListIdentity();
if (!parserListIdentity.isValido().isValido) {
retornoIdenti.erro.descricao = `O buffer ListIdentity não é valido. Motivo: ${parserListIdentity.isValido().descricao}`;
return retornoIdenti;
}
retornoIdenti.isSucesso = true;
let identidadeCIP = parserListIdentity.getIdentidadeCIP();
retornoIdenti.sucesso.codigoProduto = identidadeCIP.codigo_produto;
retornoIdenti.sucesso.endereco = {
ip: identidadeCIP.endereco_socket.endereco,
porta: identidadeCIP.endereco_socket.porta
}
retornoIdenti.sucesso.fabricanteId = identidadeCIP.fabricante_id;
retornoIdenti.sucesso.nomeDispositivo = identidadeCIP.nome_dispositivo;
retornoIdenti.sucesso.serial = identidadeCIP.numero_serial;
retornoIdenti.sucesso.statusDispositivo = identidadeCIP.status_dispositivo;
retornoIdenti.sucesso.tipoDispositivoId = identidadeCIP.tipo_dispositivo;
retornoIdenti.sucesso.versaoProtocoloEncapsulamentoCIP = identidadeCIP.versao_protocolo_encapsulamento;
retornoIdenti.sucesso.versaoRevisao = identidadeCIP.versao_revisao;
return retornoIdenti;
}
/**
* Ler um arquivo do controlador MicroLogix 1400
* @param {String} identificacaoFile - Identificação do arquivo a ser lido e sua posição, ex: "S:2" para ler o Status File no index 2, N7:1 em inteiro, ST:3
*/
async lerDataFile(identificacaoFile) {
const retornoRead = {
/**
* Se a operação de leitura foi bem sucedida
*/
isSucesso: false,
/**
* Se sucesso, contém as informações retornadas pela leitura do Data File
*/
sucesso: {
/**
* O valor lido é dinâmico.
*/
valor: -1
},
/**
* Se erro, não contém as informações retornadas pela leitura do Data File
*/
erro: {
descricao: ''
}
}
const detalhesDataFileSolicitado = validarDatafileDeString(identificacaoFile);
if (!detalhesDataFileSolicitado.isValido) {
retornoRead.erro.descricao = detalhesDataFileSolicitado.descricao;
return retornoRead;
}
let novoLayerBuilder = this.#ENIPSocket.getNovoLayerBuilder();
// O CIP PCCC eu seto como um serviço CIP, específico para o PCCC
const CIPPCCC = novoLayerBuilder.buildSendRRData().criarServicoCIP().buildCIPPCCC();
// Seta a função para executar no pacote CIP PCCC(executar)
CIPPCCC.setServicePCCC(ServicosPCCC.ExecutePCCC.hex);
// O comando PCCC onde vou configurar todas as configurações da leitura
const CommandData = CIPPCCC.getCommandData();
// Como é uma leitura, uso o comando e função Protected Typed 3 Address Read
const leituraTyped3AddressBuilder = CommandData.setAsCommandProtectedTyped3Address('Read');
/**
* Detalhes da leitura que será realizada
*/
const detalhesLeitura = {
/**
* Tipo do Data File
* @type {'Integer' | 'String'}
*/
tipoDataFile: '',
/**
* Número do Data File
* @type {Number}
*/
numeroDataFile: 0,
/**
* Index do Data File
* @type {Number}
*/
indexDataFile: 0
}
// Validar o tipo do Data File(Se é inteiro, float, double, etc...)
const tipoDataFile = detalhesDataFileSolicitado.valido.tipoDataFile.identificacao.toUpperCase();
switch (tipoDataFile.toUpperCase()) {
case 'N': {
detalhesLeitura.tipoDataFile = 'Integer'
break;
}
case 'ST': {
detalhesLeitura.tipoDataFile = 'String';
break;
}
default: {
retornoRead.erro.descricao = `Tipo de arquivo "${tipoDataFile}" não suportado.`;
return retornoRead;
}
}
// Validar o número do Data File solicitado
// A identificação do Data File é feita com o número do arquivo, ex: N7:1, onde o 7 é o número do arquivo, podendo ser N50:1, N99:1, etc..
const numeroDataFile = detalhesDataFileSolicitado.valido.numeroDataFile;
if (isNaN(numeroDataFile)) {
retornoRead.erro.descricao = `Número do arquivo "${numeroDataFile}" não suportado. Somente números de 0 a 9 são suportados.`;
return retornoRead;
}
detalhesLeitura.numeroDataFile = parseInt(numeroDataFile);
// O index solicitado é o segundo valor do split, ex: N7:5, onde 5 vai ser o index
const indexSolicitado = detalhesDataFileSolicitado.valido.indexDataFile;
if (isNaN(indexSolicitado)) {
retornoRead.erro.descricao = `Index "${indexSolicitado}" não é um número válido.`;
return retornoRead;
}
detalhesLeitura.indexDataFile = indexSolicitado;
// Com as informações coletadas da leitura única, configurar o comando de leitura
// O File Number do Data File é o número do arquivo, ex: N7:1, onde o número do arquivo é 7
leituraTyped3AddressBuilder.setFileNumber(Buffer.from([detalhesLeitura.numeroDataFile]));
// A partir de qual indice da memória do Data File será iniciada a leitura
leituraTyped3AddressBuilder.setElementNumber(Buffer.from([detalhesLeitura.indexDataFile]));
// Como a quantidade de bytes a serem lidos depende do tipo do Data File, vou setar o tamanho de leitura baseado no tipo do Data File
// O número de bytes deve corresponder ao tamanho da "variavel". Informar um byte maior que o tipo do Data File irá fazer com que ele leia bytes de index vizinhos que não foram solicitados.
switch (detalhesLeitura.tipoDataFile) {
case 'Integer': {
// Leitura de 2 bytes a partir do index inicial
leituraTyped3AddressBuilder.setByteSize(2);
// O tipo de arquivo é o tipo do Data File, ex: Integer
leituraTyped3AddressBuilder.setFileType('Integer');
break;
}
case 'String': {
// Leitura dos próximos 84 bytes que é o tamanho da String no MicroLogix 1400
leituraTyped3AddressBuilder.setByteSize(85);
// Data File é o tipo String
leituraTyped3AddressBuilder.setFileType('String');
break;
}
default: {
// Data File não suportado
retornoRead.erro.descricao = `Erro ao definir Byte Size: Tipo de arquivo "${detalhesLeitura.tipoDataFile}" não suportado. Somente N (Inteiro) é suportado.`;
return retornoRead;
}
}
// Algo com arrays, nem vou mexer no momento
leituraTyped3AddressBuilder.setSubElementNumber(Buffer.from([0x00]));
const respostaENIP = await this.#ENIPSocket.enviarENIP(novoLayerBuilder);
// Se a resposta não foi bem sucedida, analisar o erro
if (!respostaENIP.isSucesso) {
// Não deu pra enviar, devolver o erro ocorrido
if (!respostaENIP.enipEnviar.isEnviou) {
retornoRead.erro.descricao = `${respostaENIP.enipEnviar.erro.descricao}`;
} else if (respostaENIP.enipReceber.isRecebeu) {
retornoRead.erro.descricao = `${respostaENIP.enipReceber.erro.descricao}`;
}
return retornoRead;
}
// Ok, se foi recebida uma resposta ENIP, validar o retorno
const parserENIP = respostaENIP.enipReceber.enipParser;
if (!parserENIP.isSendRRData()) {
retornoRead.erro.descricao = 'A resposta ENIP recebida não é um comando SendRRData.';
return retornoRead;
}
const parserSendRRData = parserENIP.getAsSendRRData();
if (!parserSendRRData.isValido().isValido) {
retornoRead.erro.descricao = `O Buffer SendRRData não é valido. Motivo: ${parserSendRRData.isValido().descricao}`;
return retornoRead;
}
const parserCIP = parserSendRRData.getAsServicoCIP();
if (!parserCIP.isValido().isValido) {
retornoRead.erro.descricao = `O Buffer CIP não é valido. Motivo: ${parserCIP.isValido().descricao}`;
return retornoRead;
}
// Se o layer do CIP for valido, validar se o status retornado é sucesso
if (!parserCIP.isStatusSucesso().isSucesso) {
retornoRead.erro.descricao = `O status retornado do CIP não foi sucesso. Foi retornado o status ${parserCIP.isStatusSucesso().erro.codigo} - ${parserCIP.isStatusSucesso().erro.descricao}`;
return retornoRead;
}
// Se o CIP for válido, então eu vou sem duvidas ter as informações dos próximo layer que deveria ser o PCCC Object
const parserPCCC = parserCIP.getAsPCCC();
if (!parserPCCC.isValido().isValido) {
retornoRead.erro.descricao = `O Buffer PCCC não é valido. Motivo: ${parserPCCC.isValido().descricao}`;
return retornoRead;
}
const parserPCCCResponseData = parserPCCC.getPCCCResponseData();
if (!parserPCCCResponseData.isPCCCSucesso().isSucesso) {
retornoRead.erro.descricao = `O dispositivo não aceitou executar a solicitação PCCC. Código ${parserPCCCResponseData.isPCCCSucesso().erro.codigo} - ${parserPCCCResponseData.isPCCCSucesso().erro.descricao}`;
return retornoRead;
}
// Ok, se retornou sucesso eu deveria ter por fim o Function Specific Data que seria os bytes da leitura do Data File onde tem as informações que vou extrair
const bufferResposta = parserPCCCResponseData.getFunctionSpecificResponseData();
if (bufferResposta.length == 0) {
retornoRead.erro.descricao = `O buffer de resposta não contém nenhum byte, não tem como extrair nada das informações do controlador.`;
return retornoRead;
}
// Agora, tenho certeza que vou ter os bytes da leitura do Data File.
switch (detalhesLeitura.tipoDataFile) {
case 'Integer': {
try {
// Para valores inteiros, eu leio 2 bytes do Buffer retornado no Command Data
const valorLido = bufferResposta.readUInt16LE();
retornoRead.isSucesso = true;
retornoRead.sucesso.valor = valorLido;
} catch (ex) {
retornoRead.erro.descricao = `Erro ao converter o valor do buffer de resposta: ${ex.message}`;
return retornoRead;
}
break;
}
case 'String': {
try {
// Para valores String, eu decodo o Buffer inteiro recebido que contém o tamanho da String nos primeiros 2 bytes, e o restante é a String em si
const stringDecodada = decodarStringDeBuffer(bufferResposta);
retornoRead.isSucesso = true;
retornoRead.sucesso.valor = stringDecodada;
} catch (ex) {
retornoRead.erro.descricao = `Erro ao decodificar a string: ${ex.message}`;
return retornoRead;
}
break;
}
default: {
retornoRead.erro.descricao = `Tipo de arquivo "${detalhesLeitura.tipoDataFile}" não suportado, não é possível converter o valor lido.`;
return retornoRead;
}
}
return retornoRead;
}
/**
* Escrever em um arquivo do controlador MicroLogix 1400
* @param {String} identificacaoFile - Identificação do arquivo a ser escrito e sua posição, ex: "S:2" para escrever no Status File no index 2, N7:1 em inteiro, ST:3
* @param {Number} valor - O valor a ser escrito no arquivo, se for String
*/
async writeDataFile(identificacaoFile, valor) {
const retornoWrite = {
/**
* Se a operação de escrita foi bem sucedida
*/
isSucesso: false,
/**
* Se sucesso, contém as informações retornadas pela escrita do Data File
*/
sucesso: {
valor: -1
},
/**
* Se erro, não contém as informações retornadas pela escrita do Data File
*/
erro: {
descricao: ''
}
}
const detalhesDataFileSolicitado = validarDatafileDeString(identificacaoFile);
if (!detalhesDataFileSolicitado.isValido) {
retornoWrite.erro.descricao = detalhesDataFileSolicitado.erro.descricao;
return retornoWrite;
}
let novoLayerBuilder = this.#ENIPSocket.getNovoLayerBuilder();
// O CIP PCCC eu seto como um serviço CIP, específico para o PCCC
const CIPPCCC = novoLayerBuilder.buildSendRRData().criarServicoCIP().buildCIPPCCC();
// Seta a função para executar no pacote CIP PCCC(executar)
CIPPCCC.setServicePCCC(ServicosPCCC.ExecutePCCC.hex);
// O comando PCCC onde vou configurar todas as configurações da leitura
const CommandData = CIPPCCC.getCommandData();
// Como é uma escrita, uso o comando e função Protected Typed 3 Address Write
const escritaTyped3AddressBuilder = CommandData.setAsCommandProtectedTyped3Address('Write')
const tipoDoDataFile = detalhesDataFileSolicitado.valido.tipoDataFile.identificacao.toUpperCase();
switch (tipoDoDataFile) {
case 'N': {
escritaTyped3AddressBuilder.setFileType('Integer');
escritaTyped3AddressBuilder.setByteSize(2);
// Alocar 2 bytes pro int
const bufferValor = Buffer.alloc(2);
// Escrever o valor desejado
bufferValor.writeUInt16LE(valor, 0);
escritaTyped3AddressBuilder.setData(bufferValor);
break;
}
case 'ST': {
escritaTyped3AddressBuilder.setFileType('String');
escritaTyped3AddressBuilder.setByteSize(85);
const bufferTeste = Buffer.alloc(85);
// 2 Bytes pro tamanho da String
bufferTeste.writeUInt16LE(valor.length, 0);
const bufferDaString = codificarStringParaBuffer(valor, 82);
bufferTeste.set(bufferDaString, 2);
escritaTyped3AddressBuilder.setData(bufferTeste);
break;
}
default: {
retornoWrite.erro.descricao = `Tipo de arquivo "${detalhesDataFileSolicitado.valido.tipoDataFile.identificacao}" não suportado.`;
break;
}
}
escritaTyped3AddressBuilder.setElementNumber(Buffer.from([detalhesDataFileSolicitado.valido.indexDataFile]));
escritaTyped3AddressBuilder.setSubElementNumber(Buffer.from([0x00]));
escritaTyped3AddressBuilder.setFileNumber(Buffer.from([detalhesDataFileSolicitado.valido.numeroDataFile]));
const aguardaPacoteENIP = await this.#ENIPSocket.enviarENIP(novoLayerBuilder);
if (!aguardaPacoteENIP.isSucesso) {
if (!aguardaPacoteENIP.enipEnviar.isEnviou) {
retornoWrite.erro.descricao = `${aguardaPacoteENIP.enipEnviar.erro.descricao}`;
} else if (!aguardaPacoteENIP.enipReceber.isRecebeu) {
retornoWrite.erro.descricao = `${aguardaPacoteENIP.enipReceber.erro.descricao}`;
}
return retornoWrite;
}
const parserENIP = aguardaPacoteENIP.enipReceber.enipParser;
if (!parserENIP.isSendRRData()) {
retornoWrite.erro.descricao = 'A resposta ENIP recebida não é um comando SendRRData.';
return retornoWrite;
}
const parserSendRRData = parserENIP.getAsSendRRData();
if (!parserSendRRData.isValido().isValido) {
retornoWrite.erro.descricao = `O Buffer SendRRData não é valido. Motivo: ${parserSendRRData.isValido().descricao}`;
return retornoWrite;
}
const parserCIP = parserSendRRData.getAsServicoCIP();
if (!parserCIP.isValido().isValido) {
retornoWrite.erro.descricao = `O Buffer CIP não é valido. Motivo: ${parserCIP.isValido().descricao}`;
return retornoWrite;
}
if (!parserCIP.isStatusSucesso().isSucesso) {
retornoWrite.erro.descricao = `O status retornado do CIP não foi sucesso. Foi retornado o status ${parserCIP.isStatusSucesso().erro.codigo} - ${parserCIP.isStatusSucesso().erro.descricao}`;
return retornoWrite;
}
const parserPCCC = parserCIP.getAsPCCC();
if (!parserPCCC.isValido().isValido) {
retornoWrite.erro.descricao = `O Buffer PCCC não é valido. Motivo: ${parserPCCC.isValido().descricao}`;
return retornoWrite;
}
const parserPCCCResponseData = parserPCCC.getPCCCResponseData();
if (!parserPCCCResponseData.isPCCCSucesso().isSucesso) {
retornoWrite.erro.descricao = `O dispositivo não aceitou executar a solicitação PCCC. Código ${parserPCCCResponseData.isPCCCSucesso().erro.codigo} - ${parserPCCCResponseData.isPCCCSucesso().erro.descricao}`;
return retornoWrite;
}
retornoWrite.isSucesso = true;
retornoWrite.sucesso.valor = valor;
return retornoWrite;
}
/**
* @callback FuncaoCallbackDataFileAlterado
* @param {*} valorAntigo - O valor antigo antes da alteração
* @param {*} valorNovo - O novo valor após a alteração
*/
/**
* @callback FuncaoCallbackErroLeituraDataFile
* @param {String} descricao - Descrição do erro ocorrido ao tentar ler o Data File
*/
/**
* Observar por alterações em um Data File
* @param {String} identificacaoFile - Identificação do arquivo a ser observado, ex: "S:2" para observar o Status File no index 2, N7:1 em inteiro, ST:3
* @param {Object} callbacks - Configurar as ações a serem executadas para o Data File
* @param {FuncaoCallbackDataFileAlterado} callbacks.onDataFileAlterado - Ação a ser executada quando o Data File for alterado
* @param {FuncaoCallbackErroLeituraDataFile} callbacks.onErroLeitura - Ação a ser executada quando ocorrer um erro ao tentar ler o Data File
*/
async observarDataFile(identificacaoFile, callbacks) {
const retorno = {
/**
* Se foi possível iniciar a observação do Data File
*/
isSucesso: false,
/**
* Se foi possível iniciar a observação do Data File, contém as informações do observador criado
*/
sucesso: {
/**
* ID único do observador criado
*/
idUnicoCallback: -1,
/**
* Uma função utilitária que pode ser chamada para excluir o observador rapido.
*/
excluir: () => { }
},
/**
* Se ocorreu algum erro ao tentar iniciar a observação do Data File
*/
erro: {
descricao: ''
}
}
const detalhesDataFileSolicitado = validarDatafileDeString(identificacaoFile);
if (!detalhesDataFileSolicitado.isValido) {
retorno.erro.descricao = detalhesDataFileSolicitado.erro.descricao;
return retorno;
}
const solicitaLeitura = await this.lerDataFile(detalhesDataFileSolicitado.valido.enderecoLogicoCompleto);
if (!solicitaLeitura.isSucesso) {
retorno.erro.descricao = `Não foi possível ler o Data File solicitado: ${solicitaLeitura.erro.descricao}`;
return retorno;
}
// Pegar o endereço lógico completo do Data File solicitado
let novoObservadorEndereco = this.#estado.dataFilesObservados.observados.find(ob => ob.enderecoDataFile == detalhesDataFileSolicitado.valido.enderecoLogicoCompleto);
if (novoObservadorEndereco == undefined) {
// Se ele ainda não existir, criar e adicionar a lista de observadores
novoObservadorEndereco = {
callbacks: [],
enderecoDataFile: detalhesDataFileSolicitado.valido.enderecoLogicoCompleto,
valor: solicitaLeitura.sucesso.valor
}
this.#estado.dataFilesObservados.observados.push(novoObservadorEndereco);
}
// Agora, adicionar o novo callback
/**
* @type {CallbackDeDataFile}
*/
const novoCallback = {
id: this.#estado.dataFilesObservados.indexIdIncrementador++,
callbacks: {
onErroLeitura: typeof callbacks.onErroLeitura == 'function' ?