smartmanufacturinglab_
← Todos os tutoriais
OEE Agent · EP01

OEE Agent EP01 — Lendo dados OPC-UA do PLC em tempo real

Como conectar ao servidor OPC-UA de um PLC com node-opcua, subscrever tags de estado de máquina e calcular Disponibilidade OEE — do jeito que Siemens e Rockwell já fazem em produção.

OEE OPC-UA Node.js PLC MES
10 de abril de 2025

A cena que todo integrador já viveu

Segunda-feira, 6h da manhã. O telefone toca.

É o supervisor de produção: “A linha 3 parou, o MES está mostrando alarme mas não sei o motivo, e o turno já perdeu 40 minutos.”

Você acessa o SCADA remotamente. Vê o alarme. Mas o código de falha não diz muita coisa — FaultCode: 47. Abre o manual do PLC. Encontra a descrição genérica. Liga para o técnico de manutenção. Ele já está lá, mas também não tem contexto: não sabe qual ordem estava rodando, qual era a meta, quanto isso impactou o OEE do turno.

Essa é a lacuna. O PLC sabe o que aconteceu com a máquina. O MES sabe o que acontecia com a produção. Mas ninguém conectou essas duas fontes em tempo real, com linguagem que o técnico entenda sem precisar consultar três sistemas.

É exatamente isso que vamos construir nessa série.


O que grandes fabricantes já estão fazendo

Antes de colocar a mão no código, vale situar onde esse projeto se encaixa no mercado.

Siemens lançou o Industrial Copilot em 2023 — um assistente baseado em LLM integrado ao TIA Portal e ao SIMATIC. Ele responde perguntas sobre lógica de ladder, sugere diagnósticos de falha e gera código de automação. Apresentado na Hannover Messe como o futuro da engenharia de automação.

Rockwell Automation anunciou parceria com Microsoft para integrar Azure OpenAI ao FactoryTalk. O objetivo declarado é exatamente o que estamos construindo: transformar dados de chão de fábrica em linguagem natural para operadores e técnicos.

Bosch usa LLMs internamente para cruzar históricos de manutenção com dados de OPC-UA e sugerir janelas de manutenção preditiva antes que a falha aconteça.

O padrão que todos usam é o mesmo: IA no loop de informação, nunca no loop de controle. O modelo sugere, o humano decide e age. Vamos seguir exatamente esse princípio aqui.


O que vamos construir nessa série

Um processo Node.js que:

  1. Lê tags do PLC via OPC-UA em tempo real (EP01 — este episódio)
  2. Consulta a API do MES para enriquecer com contexto da ordem de produção (EP02)
  3. Calcula OEE do turno (Disponibilidade, Performance, Qualidade) (EP03)
  4. Envia tudo para o Claude via API e recebe um diagnóstico em linguagem natural (EP04–06)
  5. Manda um Adaptive Card no Microsoft Teams para o técnico de manutenção (EP07–08)

O resultado final é algo assim no celular do técnico:

🔴 LINHA 3 — PARADA · 18 min

Diagnóstico: Falha no sensor de posição (Cód 47).
Essa mesma falha ocorreu 3x essa semana.

Ação sugerida: trocar sensor antes do turno noturno.
OEE do turno caiu de 91% → 72%.

[Confirmar] [Escalar] [Ignorar]

Neste EP01, vamos construir a base: conexão OPC-UA e leitura de tags.


Por que Node.js e não Python?

É a pergunta mais comum. Resposta direta:

  • node-opcua é a biblioteca OPC-UA mais madura do ecossistema JavaScript — mantida ativamente, com suporte a todos os modos de segurança do protocolo
  • Node.js roda bem em edge computing industrial (Raspberry Pi, gateways Moxa, Advantech)
  • O mesmo processo que lê OPC-UA vai chamar a API do Claude e mandar o card no Teams — JavaScript unifica tudo sem mudar de linguagem
  • Python também funciona para OPC-UA, mas o ecossistema de integração com Teams e APIs REST é mais verboso

Se o seu ambiente já tem Python consolidado, a lógica que vamos construir se traduz diretamente.


Pré-requisitos

  • Node.js ≥ 18
  • Acesso a um servidor OPC-UA (PLC real, Prosys Simulation Server, ou o servidor de simulação que vamos criar no final deste tutorial)
  • JavaScript async/await básico

Instalação

mkdir oee-agent && cd oee-agent
npm init -y
npm install node-opcua

Os tags que vamos monitorar

Para calcular Disponibilidade precisamos de três informações do PLC:

TagTipoSignificado
MachineStateInt320 = Parado · 1 = Rodando · 2 = Alarme
FaultCodeInt32Código do alarme ativo (0 = nenhum)
GoodPartCountInt32Peças boas produzidas no turno

Os NodeIds variam por fabricante. Nos exemplos usamos ns=2;s=PLC1.MachineState — ajuste para o seu ambiente.


Passo 1 — Conectar ao servidor OPC-UA

// opc-client.js
import { OPCUAClient, MessageSecurityMode, SecurityPolicy } from 'node-opcua';

const endpointUrl = 'opc.tcp://192.168.1.100:4840';

const client = OPCUAClient.create({
  applicationName: 'OEEAgent',
  connectionStrategy: {
    initialDelay: 1000,
    maxRetry: 5,
  },
  securityMode: MessageSecurityMode.None,
  securityPolicy: SecurityPolicy.None,
  endpointMustExist: false,
});

export async function connect() {
  await client.connect(endpointUrl);
  console.log('[OPC-UA] Conectado a', endpointUrl);
  const session = await client.createSession();
  console.log('[OPC-UA] Sessão criada');
  return session;
}

export async function disconnect(session) {
  await session.close();
  await client.disconnect();
  console.log('[OPC-UA] Desconectado');
}

Nota: SecurityMode.None é só para desenvolvimento em rede isolada. Em produção, use SignAndEncrypt com certificados — exatamente como o Siemens Industrial Copilot exige nas suas integrações OPC-UA.


Passo 2 — Validar os tags com uma leitura pontual

Antes de subscrever, confirme que os NodeIds estão corretos:

// read-once.js
import { connect, disconnect } from './opc-client.js';

const NODE_IDS = [
  'ns=2;s=PLC1.MachineState',
  'ns=2;s=PLC1.FaultCode',
  'ns=2;s=PLC1.GoodPartCount',
];

async function main() {
  const session = await connect();

  const dataValues = await session.read(
    NODE_IDS.map((nodeId) => ({ nodeId, attributeId: 13 }))
  );

  dataValues.forEach((dv, i) => {
    console.log(`${NODE_IDS[i]} = ${dv.value.value} (${dv.statusCode.name})`);
  });

  await disconnect(session);
}

main().catch(console.error);

Se os três tags retornarem status: Good, a fundação está pronta.


Passo 3 — Subscription em tempo real

Leitura pontual não captura o instante exato da parada — e esse timestamp é o dado mais crítico para calcular Disponibilidade com precisão.

A ClientSubscription resolve isso: o PLC empurra o dado para o agente no momento em que o valor muda, sem polling.

// opc-subscription.js
import { connect } from './opc-client.js';
import {
  ClientSubscription,
  ClientMonitoredItem,
  TimestampsToReturn,
  AttributeIds,
} from 'node-opcua';

const TAGS = {
  machineState: 'ns=2;s=PLC1.MachineState',
  faultCode:    'ns=2;s=PLC1.FaultCode',
  partCount:    'ns=2;s=PLC1.GoodPartCount',
};

export const state = {
  machineState: null,
  faultCode: 0,
  partCount: 0,
  lastStateChange: null,
  downtimeAccumMs: 0,
  shiftStartMs: Date.now(),
  _downtimeStart: null,
};

function onStateChange(tag, newValue, sourceTimestamp) {
  const prev = state[tag];
  state[tag] = newValue;
  state.lastStateChange = sourceTimestamp ?? new Date();

  if (tag !== 'machineState') return;

  const label = ['Parado', 'Rodando', 'Alarme'][newValue] ?? `Estado ${newValue}`;
  console.log(`[${state.lastStateChange.toISOString()}] MachineState → ${label}`);

  // Máquina parou
  if (prev === 1 && newValue !== 1) {
    state._downtimeStart = state.lastStateChange;
    console.log('[OEE] Parada iniciada');
  }

  // Máquina voltou
  if (prev !== 1 && newValue === 1 && state._downtimeStart) {
    const durationMs = state.lastStateChange - state._downtimeStart;
    state.downtimeAccumMs += durationMs;
    state._downtimeStart = null;
    console.log(`[OEE] Parada encerrada — ${(durationMs / 60000).toFixed(1)} min`);
  }
}

export async function startSubscription() {
  const session = await connect();

  const subscription = ClientSubscription.create(session, {
    requestedPublishingInterval: 1000,
    requestedMaxKeepAliveCount: 10,
    maxNotificationsPerPublish: 100,
    publishingEnabled: true,
    priority: 10,
  });

  subscription.on('started', () =>
    console.log(`[OPC-UA] Subscription ativa (id: ${subscription.subscriptionId})`)
  );

  for (const [key, nodeId] of Object.entries(TAGS)) {
    const item = ClientMonitoredItem.create(
      subscription,
      { nodeId, attributeId: AttributeIds.Value },
      { samplingInterval: 500, discardOldest: true, queueSize: 10 },
      TimestampsToReturn.Both
    );
    item.on('changed', (dv) => onStateChange(key, dv.value.value, dv.sourceTimestamp));
  }

  return { session, subscription, state };
}

Passo 4 — Calcular Disponibilidade

// oee-calculator.js

/**
 * Disponibilidade = Tempo Produtivo / Tempo Planejado
 * @param {object} state      Estado atual do agente
 * @param {number} shiftMins  Duração planejada do turno (padrão: 480min = 8h)
 */
export function calcAvailability(state, shiftMins = 480) {
  const nowMs = Date.now();
  const currentDowntimeMs = state._downtimeStart ? (nowMs - state._downtimeStart) : 0;
  const totalDowntimeMs = state.downtimeAccumMs + currentDowntimeMs;

  const elapsedMs = Math.min(nowMs - state.shiftStartMs, shiftMins * 60 * 1000);
  const uptimeMs  = Math.max(elapsedMs - totalDowntimeMs, 0);

  return {
    availability: parseFloat(((uptimeMs / elapsedMs) * 100).toFixed(2)),
    plannedMins:  parseFloat((elapsedMs / 60000).toFixed(1)),
    downtimeMins: parseFloat((totalDowntimeMs / 60000).toFixed(1)),
    uptimeMins:   parseFloat((uptimeMs / 60000).toFixed(1)),
  };
}

Passo 5 — Juntando tudo

// index.js
import { startSubscription } from './opc-subscription.js';
import { calcAvailability } from './oee-calculator.js';

async function main() {
  console.log('[OEE Agent] EP01 iniciado\n');

  const { state } = await startSubscription();

  setInterval(() => {
    const oee = calcAvailability(state, 480);
    console.log('\n--- OEE Report ---');
    console.log(`Disponibilidade : ${oee.availability}%`);
    console.log(`Planejado       : ${oee.plannedMins} min`);
    console.log(`Downtime        : ${oee.downtimeMins} min`);
    console.log(`Uptime          : ${oee.uptimeMins} min`);
    console.log('------------------\n');
  }, 60_000);
}

main().catch(console.error);

Testando sem PLC físico

Não tem acesso a um PLC agora? Sobe um servidor de simulação local:

// sim-server.js
import { OPCUAServer, Variant, DataType } from 'node-opcua';

const server = new OPCUAServer({ port: 4840 });
await server.initialize();

const ns = server.engine.addressSpace.getOwnNamespace();
const device = ns.addObject({
  organizedBy: server.engine.addressSpace.rootFolder.objects,
  browseName: 'PLC1',
});

let machineState = 1;

ns.addVariable({
  componentOf: device,
  browseName: 'MachineState',
  nodeId: 'ns=2;s=PLC1.MachineState',
  dataType: DataType.Int32,
  value: { get: () => new Variant({ dataType: DataType.Int32, value: machineState }) },
});

await server.start();
console.log('Servidor OPC-UA rodando em opc.tcp://localhost:4840');

// Simula parada a cada 30s por 5s
setInterval(() => {
  machineState = 2;
  setTimeout(() => { machineState = 1; }, 5000);
}, 30_000);

Usando IA na indústria com segurança

Nos próximos episódios vamos conectar esses dados ao Claude para gerar diagnósticos em linguagem natural. Antes de chegar lá, é importante entender o modelo de segurança que vamos seguir — o mesmo adotado por Siemens, Rockwell e Bosch nas suas implementações.

O princípio central: IA no loop de informação, nunca no loop de controle.

Isso significa que o agente vai:

O agente FAZO agente NÃO FAZ
Sugerir diagnósticosAcionar comandos no PLC
Alertar o técnico no TeamsParar ou ligar máquinas
Recomendar açõesTomar decisões autônomas
Resumir o turno para o supervisorAlterar parâmetros de processo

Outras precauções que vamos implementar na série:

  • Fallback sem IA — se a API do Claude estiver indisponível, o agente continua funcionando e enviando o alarme sem o diagnóstico
  • Dados anonimizados — o LLM recebe FaultCode: 47 e métricas, não nome de cliente, produto ou dados estratégicos da empresa
  • Latência — respostas do Claude levam 1–3 segundos. Adequado para alertas, inadequado para controle em tempo real
  • Humano no loop — o card no Teams sempre exige uma ação do técnico antes de qualquer coisa acontecer

Para ambientes com restrição de dados: nos próximos episódios também vamos abordar como rodar um modelo local (Llama/Mistral via Ollama) para cenários onde os dados não podem sair da rede da fábrica — que é a abordagem que Bosch usa em algumas plantas.


O que vem no EP02

Agora que o agente está lendo o PLC, no próximo episódio vamos conectar à MES REST API para enriquecer esses dados com o contexto da produção: qual produto está rodando, qual é a meta de peças/hora, qual operador está no turno.

Com isso, o agente vai conseguir dizer não só “a máquina parou”, mas “a Ordem 4471 está em risco de atraso de 2 horas”.