wow-another-ethernet-ip
Version:
Uma biblioteca para comunicação com dispositivos com o protocolo CIP via Ethernet/IP
732 lines (611 loc) • 30 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";
/**
* Clase para interactuar con un controlador MicroLogix 1400 de Rockwell
*/
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(),
}
/**
* 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;
}
/**
* Logar uma mensagem
* @param {String} msg
*/
log(msg) {
this.#estado.emissorEvento.disparaEvento('log', msg);
if (!this.#configuracao.logs.habilitarLogsConsole) return;
let dataAgora = new Date();
let dataFormatada = `${dataAgora.getDate().toString().padStart(2, '0')}/${(dataAgora.getMonth() + 1).toString().padStart(2, '0')}/${dataAgora.getFullYear()} ${dataAgora.getHours().toString().padStart(2, '0')}:${dataAgora.getMinutes().toString().padStart(2, '0')}:${dataAgora.getSeconds().toString().padStart(2, '0')}`;
let conteudoMsg = ''
if (typeof msg == 'object') {
conteudoMsg = JSON.stringify(msg);
} else {
conteudoMsg = msg;
}
console.log(`[${dataFormatada}] [MicroLogix 1400] - ${conteudoMsg}`);
}
}
/**
* Possíveis estados que um controlador deve estar
*/
const ModoControlador = {
RemoteDownloadInProgress: {
id: 0,
descricao: "Remote download in progress."
},
RemoteProgram: {
id: 1,
descricao: "Remote Program mode."
},
RemoteSuspend: {
id: 3,
descricao: "Remote suspend mode. Operation halted by execution of the SUS instruction."
},
RemoteRun: {
id: 6,
descricao: "Remote Run mode."
},
RemoteTestContinuous: {
id: 7,
descricao: "Remote Test continuous mode."
},
RemoteTestSingleScan: {
id: 8,
descricao: "Remote Test single scan mode."
},
DownloadInProgress: {
id: 16,
descricao: "Download in progress."
},
Program: {
id: 17,
descricao: "Program mode."
},
Suspend: {
id: 27,
descricao: "Suspend mode. Operation halted by execution of the SUS instruction."
},
Run: {
id: 30,
descricao: "Run mode."
}
}