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:
- Lê tags do PLC via OPC-UA em tempo real (EP01 — este episódio)
- Consulta a API do MES para enriquecer com contexto da ordem de produção (EP02)
- Calcula OEE do turno (Disponibilidade, Performance, Qualidade) (EP03)
- Envia tudo para o Claude via API e recebe um diagnóstico em linguagem natural (EP04–06)
- 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:
| Tag | Tipo | Significado |
|---|---|---|
MachineState | Int32 | 0 = Parado · 1 = Rodando · 2 = Alarme |
FaultCode | Int32 | Código do alarme ativo (0 = nenhum) |
GoodPartCount | Int32 | Peç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, useSignAndEncryptcom 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 FAZ | O agente NÃO FAZ |
|---|---|
| Sugerir diagnósticos | Acionar comandos no PLC |
| Alertar o técnico no Teams | Parar ou ligar máquinas |
| Recomendar ações | Tomar decisões autônomas |
| Resumir o turno para o supervisor | Alterar 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: 47e 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”.