const current = Number(msg.payload) || 0; let zeroStreak = flow.get("zeroStreak") || 0; zeroStreak = current === 0 ? zeroStreak + 1 : 0; flow.set("zeroStreak", zeroStreak); const prev = flow.get("lastMachineState") ?? 0; flow.set("lastMachineState", current); global.set("machineOnline", true); let productionRunning = !!global.get("productionStarted"); let stateChanged = false; if (current === 1 && !productionRunning) { productionRunning = true; stateChanged = true; } else if (current === 0 && zeroStreak >= 2 && productionRunning) { productionRunning = false; stateChanged = true; } global.set("productionStarted", productionRunning); const stateMsg = stateChanged ? { _mode: "production-state", machineOnline: true, productionStarted: productionRunning } : null; const activeOrder = global.get("activeWorkOrder"); // FIX: moldActive is object, extract cavities property const cavities = Number(global.get("moldActive")) || 1; if (!activeOrder || !activeOrder.id || cavities <= 0) { return [null, stateMsg, { _triggerKPI: true }, null]; } var trackingEnabled = !!global.get("trackingEnabled"); if (!trackingEnabled) { return [null, stateMsg, { _triggerKPI: true }, null]; } if (prev === 1 || current !== 1) { return [null, stateMsg, { _triggerKPI: true }, null]; } let cycles = Number(global.get("cycleCount") || 0) + 1; global.set("cycleCount", cycles); // Clear startup mode after first real cycle if (global.get("kpiStartupMode")) { global.set("kpiStartupMode", false); node.warn('[MACHINE CYCLE] First cycle - cleared kpiStartupMode'); } const now = Date.now(); const lastCycleTime = global.get("lastCycleTime") || now; const timeSinceLastCycle = now - lastCycleTime; let operatingTime = global.get("operatingTime") || 0; operatingTime += (timeSinceLastCycle / 1000); global.set("operatingTime", operatingTime); node.warn(`[MACHINE CYCLE] operatingTime updated to ${operatingTime}`); global.set("lastCycleTime", now); const scrapTotal = Number(activeOrder.scrap) || 0; const totalProduced = cycles * cavities; // FIX: Guard negative produced const produced = Math.max(0, totalProduced - scrapTotal); const target = Number(activeOrder.target) || 0; const progress = target > 0 ? Math.min(100, Math.round((produced / target) * 100)) : 0; activeOrder.good = produced; activeOrder.progressPercent = progress; activeOrder.lastUpdateIso = new Date().toISOString(); global.set("activeWorkOrder", activeOrder); const promptIssued = global.get("scrapPromptIssuedFor") || null; if (!promptIssued && target > 0 && produced >= target) { global.set("scrapPromptIssuedFor", activeOrder.id); msg._mode = "scrap-prompt"; msg.scrapPrompt = { id: activeOrder.id, sku: activeOrder.sku || "", target, produced }; return [null, msg, null, null]; } const dbMsg = { _mode: "cycle", cycle: { id: activeOrder.id, sku: activeOrder.sku || "", target, good: produced, scrap: Number(activeOrder.scrap) || 0, cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0), progressPercent: progress, lastUpdateIso: activeOrder.lastUpdateIso, machineOnline: true, productionStarted: productionRunning }, // SQL with bound parameters for safety topic: "UPDATE work_orders SET good_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?", payload: [produced, progress, activeOrder.id] }; const kpiTrigger = { _triggerKPI: true }; if (trackingEnabled && dbMsg) { global.set("lastMachineCycleTime", Date.now()); } // 4th output: persist cycle_count and good_parts (throttled to 5 seconds) const lastDbWrite = global.get("lastWorkOrderDbWrite") || 0; const timeSinceLastWrite = now - lastDbWrite; let persistCycleCount = null; // Only send DB update every 5 seconds (5000ms) to reduce DB load if (timeSinceLastWrite >= 5000) { persistCycleCount = { topic: "UPDATE work_orders SET cycle_count = ?, good_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?", payload: [cycles, produced, progress, activeOrder.id] }; global.set("lastWorkOrderDbWrite", now); node.warn(`[DB PERSIST] Writing to work_orders: cycles=${cycles}, good_parts=${produced}, progress=${progress}%`); } return [dbMsg, stateMsg, kpiTrigger, persistCycleCount];