// ============================================================================ // UPDATED "Machine cycles" Function // Location: flows.json, line 1150, node ID: 0d023d87a13bf56f // Outputs: 3 (added third output for database state backup) // ============================================================================ const current = Number(msg.payload) || 0; // ============================================================================ // SECTION 1: TIME TRACKING (NEW - Must run BEFORE any early returns) // ============================================================================ const now = Date.now(); const trackingEnabled = !!global.get("trackingEnabled"); const lastUpdate = global.get("lastUpdateTime") || now; const deltaMs = now - lastUpdate; const deltaSeconds = deltaMs / 1000; // Sanity check: Protect against clock skew (negative delta or >5 min gap) if (deltaSeconds < 0 || deltaSeconds > 300) { node.warn(`[TIME] Abnormal delta: ${deltaSeconds.toFixed(2)}s - clock skew or restart detected, resetting timer`); global.set("lastUpdateTime", now); // Don't accumulate time, but continue processing } else { // Normal delta, accumulate time if tracking is enabled if (trackingEnabled) { // Initialize timing vars if they don't exist (handles restart scenario) if (!global.get("productionStartTime")) { node.warn("[TIME] Production start time missing, initializing now"); global.set("productionStartTime", now); global.set("operatingTime", 0); global.set("downtime", 0); } // Accumulate operating time when machine is running (state 1) if (current === 1) { const opTime = global.get("operatingTime") || 0; global.set("operatingTime", opTime + deltaSeconds); } // Accumulate downtime when machine is stopped (state 0) while tracking else if (current === 0) { const downTime = global.get("downtime") || 0; global.set("downtime", downTime + deltaSeconds); } } global.set("lastUpdateTime", now); } // ============================================================================ // SECTION 2: EXISTING CYCLE COUNTING LOGIC (UNCHANGED) // ============================================================================ 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); // force ONLINE for now 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"); const cavities = Number(global.get("moldActive") || 0); if (!activeOrder || !activeOrder.id || cavities <= 0) { // We still want to pass along any state change even if there's no active WO. return [null, stateMsg, null]; } // Check if tracking is enabled (START button clicked) if (!trackingEnabled) { // Cycles are happening but we're not tracking them yet return [null, stateMsg, null]; } // only count rising edges (0 -> 1) for production totals if (prev === 1 || current !== 1) { return [null, stateMsg, null]; } let cycles = Number(global.get("cycleCount") || 0) + 1; global.set("cycleCount", cycles); // Calculate good parts: total produced minus accumulated scrap const scrapTotal = Number(activeOrder.scrap) || 0; const totalProduced = cycles * cavities; const produced = 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) { node.warn(`[DEBUG] TRIGGERING PROMPT!`); global.set("scrapPromptIssuedFor", activeOrder.id); msg._mode = "scrap-prompt"; msg.scrapPrompt = { id: activeOrder.id, sku: activeOrder.sku || "", target, produced }; return [null, msg, null]; // bypass the DB update on this cycle } // ============================================================================ // SECTION 3: DATABASE UPDATE MESSAGE (EXISTING) // ============================================================================ 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 }, topic: ` UPDATE work_orders SET good_parts = ${produced}, progress_percent = ${progress}, updated_at = NOW() WHERE work_order_id = '${activeOrder.id}'; ` }; // ============================================================================ // SECTION 4: STATE BACKUP TO DATABASE (NEW - Every 10th cycle to reduce load) // ============================================================================ let cyclesSinceBackup = flow.get("cyclesSinceBackup") || 0; cyclesSinceBackup++; flow.set("cyclesSinceBackup", cyclesSinceBackup); let stateBackupMsg = null; if (cyclesSinceBackup >= 10) { // Reset counter flow.set("cyclesSinceBackup", 0); // Backup current state to database const productionStartTime = global.get("productionStartTime") || null; const operatingTime = global.get("operatingTime") || 0; const downtime = global.get("downtime") || 0; const lastUpdateTime = global.get("lastUpdateTime") || null; const scrapPromptIssuedFor = global.get("scrapPromptIssuedFor") || null; stateBackupMsg = { _mode: "state-backup", topic: ` UPDATE session_state SET work_order_id = '${activeOrder.id}', cycle_count = ${cycles}, production_start_time = ${productionStartTime}, operating_time = ${operatingTime.toFixed(2)}, downtime = ${downtime.toFixed(2)}, last_update_time = ${lastUpdateTime}, tracking_enabled = ${trackingEnabled ? 1 : 0}, machine_state = ${current}, scrap_prompt_issued_for = ${scrapPromptIssuedFor ? `'${scrapPromptIssuedFor}'` : 'NULL'} WHERE session_key = 'current_session'; ` }; } // ============================================================================ // OUTPUTS: // Output 1: Database update for work_orders table (existing) // Output 2: State message to UI (existing) // Output 3: State backup to session_state table (NEW) // ============================================================================ return [dbMsg, stateMsg, stateBackupMsg];