Files
Plastic-Raspi-5/flows.json

387 lines
73 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[
{
"id": "ea088d9e256f2e4a",
"type": "tab",
"label": "Raspi",
"disabled": false,
"info": "",
"env": []
},
{
"id": "ea803f6a2965854b",
"type": "mqtt-broker",
"name": "Broker fuera casa",
"broker": "mqtt.maliountech.com.mx",
"port": "1883",
"clientid": "raspi-maquina1",
"autoConnect": true,
"usetls": false,
"protocolVersion": 4,
"keepalive": "30",
"cleansession": false,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthRetain": "false",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closeRetain": "false",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willRetain": "false",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
},
{
"id": "615493954251cda9",
"type": "ui_tab",
"name": "Home",
"icon": "dashboard",
"disabled": false,
"hidden": true
},
{
"id": "6fe5cca8f548047a",
"type": "ui_group",
"name": "HMI Dashboard",
"tab": "615493954251cda9",
"order": 1,
"disp": false,
"width": "25",
"collapse": false,
"className": ""
},
{
"id": "044327e0d3d61535",
"type": "ui_base",
"theme": {
"name": "theme-light",
"lightTheme": {
"default": "#0094CE",
"baseColor": "#0094CE",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
"edited": true,
"reset": false
},
"darkTheme": {
"default": "#097479",
"baseColor": "#097479",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
"edited": false
},
"customTheme": {
"name": "Untitled Theme 1",
"default": "#4B7930",
"baseColor": "#4B7930",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
},
"themeState": {
"base-color": {
"default": "#0094CE",
"value": "#0094CE",
"edited": false
},
"page-titlebar-backgroundColor": {
"value": "#0094CE",
"edited": false
},
"page-backgroundColor": {
"value": "#fafafa",
"edited": false
},
"page-sidebar-backgroundColor": {
"value": "#ffffff",
"edited": false
},
"group-textColor": {
"value": "#1bbfff",
"edited": false
},
"group-borderColor": {
"value": "#ffffff",
"edited": false
},
"group-backgroundColor": {
"value": "#ffffff",
"edited": false
},
"widget-textColor": {
"value": "#111111",
"edited": false
},
"widget-backgroundColor": {
"value": "#0094ce",
"edited": false
},
"widget-borderColor": {
"value": "#ffffff",
"edited": false
},
"base-font": {
"value": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
}
},
"angularTheme": {
"primary": "indigo",
"accents": "blue",
"warn": "red",
"background": "grey",
"palette": "light"
}
},
"site": {
"name": "Node-RED Dashboard",
"hideToolbar": "false",
"allowSwipe": "false",
"lockMenu": "false",
"allowTempTheme": "true",
"dateFormat": "DD/MM/YYYY",
"sizes": {
"sx": 48,
"sy": 48,
"gx": 6,
"gy": 6,
"cx": 6,
"cy": 6,
"px": 0,
"py": 0
}
}
},
{
"id": "c195f94afd7183e6",
"type": "ui_template",
"z": "ea088d9e256f2e4a",
"group": "6fe5cca8f548047a",
"name": "HMI Dashboard Template",
"order": 0,
"width": "25",
"height": "25",
"format": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n <title>Industrial HMI Dashboard</title>\n <script src=\"https://unpkg.com/mqtt@4.3.7/dist/mqtt.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js\"></script>\n <style>\n /* Reset and base styles */\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n html,\n body {\n height: 100%;\n overflow: hidden;\n }\n\n /* CSS Variables */\n #hmi-dashboard {\n --bg-0: #0e1116;\n --bg-1: #141a1f;\n --panel: #1a2230;\n --panel-hi: #222c3e;\n --text: #eaf1fb;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #2ecc71;\n --bad: #ff4d4f;\n --warning: #f6c049;\n --shadow-1: 0 8px 16px rgba(0, 0, 0, 0.35);\n --shadow-2: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 2px 6px rgba(0, 0, 0, 0.3);\n --radius-lg: 16px;\n --radius-md: 12px;\n --kpi-min-h: 120px;\n --gap: 12px;\n --pad: 12px;\n --ring: 0 0 0 3px rgba(46, 163, 255, 0.45);\n color: var(--text);\n background: var(--bg-0);\n width: 100%;\n height: 100vh;\n -webkit-tap-highlight-color: transparent;\n user-select: none;\n font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, \"Noto Sans\", \"Helvetica Neue\", Arial;\n transform: scale(0.8);\n transform-origin: top left;\n width: 125%;\n height: 125%;\n }\n\n /* Sidebar Styles */\n #sidebar {\n width: 60px;\n height: 100vh;\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-right: 1px solid rgba(255, 255, 255, 0.1);\n box-shadow: var(--shadow-1);\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: var(--gap) 0;\n gap: var(--gap);\n position: fixed;\n left: 0;\n top: 0;\n z-index: 1000;\n }\n\n .sidebar-item {\n width: 48px;\n height: 48px;\n border: none;\n background: rgba(255, 255, 255, 0.05);\n border-radius: var(--radius-md);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 120ms ease;\n position: relative;\n font-size: 24px;\n color: var(--muted);\n border: 1px solid rgba(255, 255, 255, 0.08);\n }\n\n .sidebar-item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--text);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);\n }\n\n .sidebar-item.active {\n background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);\n color: white;\n box-shadow: 0 4px 12px rgba(46, 163, 255, 0.3);\n }\n\n .sidebar-item:active {\n transform: translateY(0) scale(0.95);\n }\n\n .sidebar-item.warning-icon {\n color: var(--bad);\n }\n\n .sidebar-item.warning-icon:hover {\n color: #ff6b6b;\n background: rgba(255, 77, 79, 0.1);\n }\n\n /* Tooltip */\n .sidebar-item::after {\n content: attr(data-tooltip);\n position: absolute;\n left: 60px;\n top: 50%;\n transform: translateY(-50%);\n background: var(--panel-hi);\n color: var(--text);\n padding: 8px 12px;\n border-radius: var(--radius-md);\n font-size: 12px;\n font-weight: 600;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 120ms ease;\n pointer-events: none;\n border: 1px solid rgba(255, 255, 255, 0.1);\n box-shadow: var(--shadow-1);\n z-index: 1001;\n }\n\n .sidebar-item:hover::after {\n opacity: 1;\n visibility: visible;\n transform: translateY(-50%) translateX(4px);\n }\n\n /* Main Layout */\n .main-layout {\n margin-left: 60px;\n width: calc(100% - 60px);\n height: 100vh;\n overflow: hidden;\n }\n\n /* View Container */\n .view {\n display: none;\n animation: fadeIn 0.3s ease;\n }\n\n .view.active {\n display: block;\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* Common Grid for all views */\n .view-grid {\n display: grid;\n grid-template-rows: auto 1fr;\n gap: var(--gap);\n height: 100%;\n padding: var(--pad);\n }\n\n /* Page Header */\n .page-header {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 20px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n .page-title {\n font-size: clamp(24px, 3vw, 32px);\n font-weight: 700;\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .page-title-icon {\n font-size: clamp(28px, 3.5vw, 38px);\n }\n\n /* Buttons */\n .btn {\n padding: 10px 20px;\n border: none;\n border-radius: var(--radius-md);\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: all 120ms ease;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n }\n\n .btn-primary {\n background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);\n color: white;\n box-shadow: 0 4px 12px rgba(46, 163, 255, 0.3);\n }\n\n .btn-primary:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(46, 163, 255, 0.4);\n }\n\n .btn-success {\n background: var(--good);\n color: white;\n }\n\n .btn-danger {\n background: var(--bad);\n color: white;\n }\n\n .btn-secondary {\n background: rgba(255, 255, 255, 0.1);\n color: var(--text);\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n\n /* Cards */\n .card {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 20px;\n }\n\n .card-header {\n font-size: 18px;\n font-weight: 700;\n margin-bottom: 16px;\n padding-bottom: 12px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n /* Forms */\n .form-group {\n margin-bottom: 16px;\n }\n\n .form-label {\n display: block;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 600;\n color: var(--muted);\n }\n\n .form-input,\n .form-select,\n .form-textarea {\n width: 100%;\n padding: 12px;\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: var(--radius-md);\n color: var(--text);\n font-size: 14px;\n font-family: inherit;\n transition: all 120ms ease;\n }\n\n .form-input:focus,\n .form-select:focus,\n .form-textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: var(--ring);\n }\n\n .form-textarea {\n min-height: 100px;\n resize: vertical;\n }\n\n /* Tables */\n .table {\n width: 100%;\n border-collapse: collapse;\n }\n\n .table th {\n background: rgba(255, 255, 255, 0.05);\n padding: 12px;\n text-align: left;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--muted);\n border-bottom: 2px solid rgba(255, 255, 255, 0.1);\n }\n\n .table td {\n padding: 12px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n }\n\n .table tr:hover {\n background: rgba(255, 255, 255, 0.02);\n }\n\n /* Status badges */\n .badge {\n display: inline-block;\n padding: 4px 12px;\n border-radius: 12px;\n font-size: 12px;\n font-weight: 600;\n }\n\n .badge-success {\n background: var(--good);\n color: white;\n }\n\n .badge-danger {\n background: var(--bad);\n color: white;\n }\n\n .badge-warning {\n background: var(--warning);\n color: #000;\n }\n\n .badge-info {\n background: var(--accent);\n color: white;\n }\n\n /* Notification badge on sidebar */\n .notification-count {\n position: absolute;\n top: -4px;\n right: -4px;\n background: var(--bad);\n color: white;\n font-size: 10px;\n font-weight: 700;\n padding: 2px 6px;\n border-radius: 10px;\n min-width: 18px;\n text-align: center;\n }\n\n /* Include dashboard-specific styles from original */\n .top-kpis {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: var(--gap);\n }\n\n .kpi {\n appearance: none;\n border: 0;\n border-radius: var(--radius-lg);\n min-height: var(--kpi-min-h);\n padding: 14px;\n text-align: left;\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n color: var(--text);\n box-shadow: var(--shadow-1), var(--shadow-2);\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n cursor: pointer;\n position: relative;\n overflow: hidden;\n transition: transform 120ms ease;\n }\n\n .kpi:hover {\n transform: translateY(-2px);\n }\n\n .kpi::after {\n content: \"\";\n position: absolute;\n inset: 0;\n background: radial-gradient(1200px 1200px at 0% 0%, rgba(46, 163, 255, 0.07), transparent 50%);\n pointer-events: none;\n }\n\n .kpi-label {\n font-size: clamp(12px, 1.6vw, 18px);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--muted);\n }\n\n .kpi-value {\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n line-height: 1.1;\n font-size: clamp(28px, 4.2vw, 48px);\n }\n\n /* Work Order Section */\n .workorder {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 16px;\n }\n\n .wo-header {\n font-size: clamp(14px, 2vw, 20px);\n font-weight: 700;\n margin-bottom: 12px;\n }\n\n .wo-grid {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--gap);\n }\n\n .wo-item {\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: var(--radius-md);\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .wo-label {\n font-size: clamp(11px, 1.4vw, 14px);\n text-transform: uppercase;\n color: var(--muted);\n letter-spacing: 0.06em;\n }\n\n .wo-value {\n font-size: clamp(16px, 2.2vw, 24px);\n font-variant-numeric: tabular-nums;\n font-weight: 600;\n }\n\n .wo-progress {\n grid-column: 1 / -1;\n }\n\n .progress-track {\n position: relative;\n height: clamp(24px, 4vw, 36px);\n width: 100%;\n background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 999px;\n overflow: hidden;\n }\n\n .progress-fill {\n position: absolute;\n inset: 0 auto 0 0;\n width: 0%;\n background: linear-gradient(90deg, var(--accent) 0%, var(--accent-2) 100%);\n transition: width 250ms ease;\n }\n\n .progress-text {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n font-weight: 700;\n font-size: clamp(13px, 2vw, 18px);\n }\n\n /* Bottom Counters */\n .bottom {\n display: grid;\n grid-template-columns: 1fr 1fr minmax(220px, 0.8fr);\n gap: var(--gap);\n }\n\n .counter {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 14px;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 10px;\n }\n\n .counter-label {\n font-size: clamp(13px, 1.6vw, 16px);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--muted);\n }\n\n .counter-value {\n font-size: clamp(34px, 6vw, 64px);\n font-weight: 800;\n font-variant-numeric: tabular-nums;\n line-height: 1;\n }\n\n .counter.good .counter-value {\n color: var(--good);\n }\n\n .counter.scrap .counter-value {\n color: var(--bad);\n }\n\n .status {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 14px;\n display: grid;\n grid-auto-rows: minmax(44px, auto);\n align-content: center;\n gap: 10px;\n }\n\n .status-item {\n display: grid;\n grid-template-columns: 28px 1fr auto;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n }\n\n .led {\n width: 18px;\n height: 18px;\n border-radius: 999px;\n background: #606b78;\n box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25);\n }\n\n .led.online {\n background: var(--good);\n box-shadow: 0 0 12px rgba(46, 204, 113, 0.7);\n }\n\n .led.running {\n background: var(--accent);\n box-shadow: 0 0 12px rgba(46, 163, 255, 0.7);\n }\n\n .status-label {\n font-weight: 600;\n color: var(--text);\n font-size: clamp(14px, 1.8vw, 18px);\n }\n\n .status-value {\n color: var(--muted);\n font-size: clamp(12px, 1.6vw, 14px);\n }\n\n /* Footer */\n .footer {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n padding: 8px 4px 2px;\n color: var(--muted);\n font-size: clamp(12px, 1.6vw, 14px);\n }\n\n /* Dashboard specific grid */\n .dashboard-grid {\n display: grid;\n grid-template-rows: auto auto 1fr auto;\n gap: var(--gap);\n height: 100vh;\n padding: var(--pad);\n box-sizing: border-box;\n }\n\n /* Chart container */\n .chart-container {\n position: relative;\n height: 300px;\n margin: 16px 0;\n }\n </style>\n</head>\n\n<body>\n <div id=\"hmi-dashboard\">\n <!-- Sidebar -->\n <div id=\"sidebar\">\n <button class=\"sidebar-item active\" data-view=\"dashboard\" data-tooltip=\"Dashboard\">\n &#127968;\n </button>\n <button class=\"sidebar-item\" data-view=\"workorders\" data-tooltip=\"Work Orders\">\n &#128203;\n </button>\n <button class=\"sidebar-item warning-icon\" data-view=\"report\" data-tooltip=\"Report Issue\">\n &#9940;\n </button>\n <button class=\"sidebar-item\" data-view=\"machine\" data-tooltip=\"Machine Status\">\n &#128202;\n </button>\n <button class=\"sidebar-item\" data-view=\"notifications\" data-tooltip=\"Notifications\">\n &#128276;\n <span class=\"notification-count\" id=\"notif-count\">3</span>\n </button>\n <button class=\"sidebar-item\" data-view=\"help\" data-tooltip=\"Help\">\n &#10067;\n </button>\n <button class=\"sidebar-item\" data-view=\"settings\" data-tooltip=\"Settings\">\n &#9881;\n </button>\n </div>\n\n <!-- Main Content -->\n <div class=\"main-layout\">\n\n <!-- VIEW 1: Dashboard (Main HMI) -->\n <div id=\"view-dashboard\" class=\"view active\">\n <div class=\"dashboard-grid\">\n <!-- TOP: KPI Buttons -->\n <div class=\"top-kpis\">\n <button class=\"kpi\">\n <div class=\"kpi-label\">OEE</div>\n <div class=\"kpi-value\" id=\"oee_value\">--%</div>\n </button>\n <button class=\"kpi\">\n <div class=\"kpi-label\">Availability</div>\n <div class=\"kpi-value\" id=\"availability_value\">--%</div>\n </button>\n <button class=\"kpi\">\n <div class=\"kpi-label\">Performance</div>\n <div class=\"kpi-value\" id=\"performance_value\">--%</div>\n </button>\n <button class=\"kpi\">\n <div class=\"kpi-label\">Quality</div>\n <div class=\"kpi-value\" id=\"quality_value\">--%</div>\n </button>\n </div>\n\n <!-- MIDDLE: Current Work Order -->\n <div class=\"workorder\">\n <div class=\"wo-header\">Current Work Order</div>\n <div class=\"wo-grid\">\n <div class=\"wo-item\">\n <div class=\"wo-label\">Work Order</div>\n <div class=\"wo-value\" id=\"workorder_number\">—</div>\n </div>\n <div class=\"wo-item\">\n <div class=\"wo-label\">SKU</div>\n <div class=\"wo-value\" id=\"workorder_sku\">—</div>\n </div>\n <div class=\"wo-item\">\n <div class=\"wo-label\">Cycle Time</div>\n <div class=\"wo-value\" id=\"cycle_time\">—</div>\n </div>\n <div class=\"wo-progress\">\n <div class=\"progress-track\">\n <div class=\"progress-fill\" id=\"progress_bar\"></div>\n <div class=\"progress-text\" id=\"progress_value\">0%</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- BOTTOM: Counters + Status -->\n <div class=\"bottom\">\n <div class=\"counter good\">\n <div class=\"counter-label\">Good Parts</div>\n <div class=\"counter-value\" id=\"good_parts\">0</div>\n </div>\n <div class=\"counter scrap\">\n <div class=\"counter-label\">Scrap Parts</div>\n <div class=\"counter-value\" id=\"scrap_parts\">0</div>\n </div>\n <div class=\"status\">\n <div class=\"status-item\">\n <span class=\"led\" id=\"status_online\"></span>\n <span class=\"status-label\">Online</span>\n <span class=\"status-value\" id=\"status_online_text\">—</span>\n </div>\n <div class=\"status-item\">\n <span class=\"led\" id=\"status_running\"></span>\n <span class=\"status-label\">Running</span>\n <span class=\"status-value\" id=\"status_running_text\">—</span>\n </div>\n </div>\n </div>\n\n <!-- FOOTER -->\n <div class=\"footer\">\n <span id=\"footer_datetime\">—</span>\n </div>\n </div>\n </div>\n\n <!-- VIEW 2: Work Orders -->\n <div id=\"view-workorders\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">📋</span>\n Work Orders\n </div>\n <button class=\"btn btn-primary\" onclick=\"window.hmiApp.createWorkOrder()\">\n New Work Order\n </button>\n </div>\n <div class=\"card\" style=\"overflow-y: auto;\">\n <table class=\"table\">\n <thead>\n <tr>\n <th>ID</th>\n <th>SKU</th>\n <th>Description</th>\n <th>Target Qty</th>\n <th>Completed</th>\n <th>Progress</th>\n <th>Status</th>\n <th>Due Date</th>\n </tr>\n </thead>\n <tbody id=\"workorder-table-body\">\n <tr>\n <td>WO-2024-001</td>\n <td>SKU-ABC123</td>\n <td>Plastic Housing - Black</td>\n <td>2000</td>\n <td id=\"wo-completed\">1200</td>\n <td>\n <div class=\"progress-track\" style=\"height: 20px; width: 100px;\">\n <div class=\"progress-fill\" id=\"wo-progress\" style=\"width: 60%;\"></div>\n </div>\n </td>\n <td><span class=\"badge badge-info\">In Progress</span></td>\n <td>2024-10-15</td>\n </tr>\n <tr>\n <td>WO-2024-002</td>\n <td>SKU-DEF456</td>\n <td>Cover Panel - White</td>\n <td>1500</td>\n <td>0</td>\n <td>\n <div class=\"progress-track\" style=\"height: 20px; width: 100px;\">\n <div class=\"progress-fill\" style=\"width: 0%;\"></div>\n </div>\n </td>\n <td><span class=\"badge badge-warning\">Pending</span></td>\n <td>2024-10-20</td>\n </tr>\n <tr>\n <td>WO-2024-003</td>\n <td>SKU-GHI789</td>\n <td>Mounting Bracket - Grey</td>\n <td>800</td>\n <td>800</td>\n <td>\n <div class=\"progress-track\" style=\"height: 20px; width: 100px;\">\n <div class=\"progress-fill\" style=\"width: 100%;\"></div>\n </div>\n </td>\n <td><span class=\"badge badge-success\">Completed</span></td>\n <td>2024-09-28</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <!-- VIEW 3: Report Issue -->\n <div id=\"view-report\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">⚠️</span>\n Report Issue\n </div>\n </div>\n <div class=\"card\" style=\"overflow-y: auto;\">\n <form id=\"issue-form\" onsubmit=\"window.hmiApp.submitIssue(event)\">\n <div class=\"form-group\">\n <label class=\"form-label\">Issue Category</label>\n <select class=\"form-select\" name=\"category\" required>\n <option value=\"\">Select category...</option>\n <option value=\"mechanical\">Mechanical Failure</option>\n <option value=\"electrical\">Electrical Issue</option>\n <option value=\"quality\">Quality Problem</option>\n <option value=\"software\">Software/HMI Issue</option>\n <option value=\"safety\">Safety Concern</option>\n <option value=\"other\">Other</option>\n </select>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Severity</label>\n <select class=\"form-select\" name=\"severity\" required>\n <option value=\"\">Select severity...</option>\n <option value=\"critical\">🔴 Critical - Machine Stopped</option>\n <option value=\"high\">🟠 High - Production Affected</option>\n <option value=\"medium\">🟡 Medium - Workaround Available</option>\n <option value=\"low\">🟢 Low - Minor Issue</option>\n </select>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Machine/Area</label>\n <input type=\"text\" class=\"form-input\" name=\"machine\" value=\"Maquina 1\" required>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Issue Description</label>\n <textarea class=\"form-textarea\" name=\"description\" placeholder=\"Describe the issue in detail...\" required></textarea>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Reported By</label>\n <input type=\"text\" class=\"form-input\" name=\"reporter\" value=\"Operator\" required>\n </div>\n\n <div style=\"display: flex; gap: 12px;\">\n <button type=\"submit\" class=\"btn btn-danger\">🚨 Submit Issue Report</button>\n <button type=\"reset\" class=\"btn btn-secondary\">Clear Form</button>\n </div>\n </form>\n\n <div class=\"card-header\" style=\"margin-top: 32px;\">Recent Reports</div>\n <div id=\"recent-reports\">\n <p style=\"color: var(--muted); padding: 16px;\">No recent reports</p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 4: Machine Status (Charts) -->\n <div id=\"view-machine\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">📊</span>\n Machine Status & Analytics\n </div>\n </div>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); overflow-y: auto;\">\n <div class=\"card\">\n <div class=\"card-header\">OEE Trend (Last Hour)</div>\n <div class=\"chart-container\">\n <canvas id=\"oee-chart\"></canvas>\n </div>\n </div>\n <div class=\"card\">\n <div class=\"card-header\">Production Rate</div>\n <div class=\"chart-container\">\n <canvas id=\"production-chart\"></canvas>\n </div>\n </div>\n <div class=\"card\">\n <div class=\"card-header\">Quality Metrics</div>\n <div class=\"chart-container\">\n <canvas id=\"quality-chart\"></canvas>\n </div>\n </div>\n <div class=\"card\">\n <div class=\"card-header\">Machine State Timeline</div>\n <div class=\"chart-container\">\n <canvas id=\"state-chart\"></canvas>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 5: Notifications -->\n <div id=\"view-notifications\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">🔔</span>\n Notifications\n </div>\n <button class=\"btn btn-secondary\" onclick=\"window.hmiApp.clearNotifications()\">\n Clear All\n </button>\n </div>\n <div class=\"card\" style=\"overflow-y: auto;\">\n <div id=\"notifications-list\">\n <!-- Sample notifications -->\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"led online\"></span>\n <div>\n <div class=\"status-label\">Machine Started</div>\n <div class=\"status-value\">2 minutes ago</div>\n </div>\n <span class=\"badge badge-success\">Info</span>\n </div>\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"led running\"></span>\n <div>\n <div class=\"status-label\">Work Order Progress: 75%</div>\n <div class=\"status-value\">5 minutes ago</div>\n </div>\n <span class=\"badge badge-info\">Update</span>\n </div>\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"led\" style=\"background: var(--warning);\"></span>\n <div>\n <div class=\"status-label\">Quality Alert: Scrap rate increasing</div>\n <div class=\"status-value\">15 minutes ago</div>\n </div>\n <span class=\"badge badge-warning\">Warning</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 6: Help -->\n <div id=\"view-help\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">❓</span>\n Help & Documentation\n </div>\n </div>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); overflow-y: auto;\">\n <div class=\"card\">\n <div class=\"card-header\">Quick Start Guide</div>\n <p style=\"color: var(--muted); line-height: 1.6;\">\n <strong>Dashboard View:</strong> Main HMI showing real-time machine data, OEE metrics,\n and production counters.<br><br>\n <strong>Navigation:</strong> Use the sidebar on the left to switch between different\n views.<br><br>\n <strong>Real-time Updates:</strong> All data updates automatically via MQTT connection.\n </p>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Troubleshooting</div>\n <p style=\"color: var(--muted); line-height: 1.6;\">\n <strong>No Data Showing:</strong><br>\n • Check MQTT broker connection<br>\n • Verify Node-RED is running<br>\n • Check browser console (F12)<br><br>\n <strong>Slow Updates:</strong><br>\n • Check network connection<br>\n • Restart local MQTT broker\n </p>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Keyboard Shortcuts</div>\n <table class=\"table\">\n <tr>\n <td><strong>F11</strong></td>\n <td>Toggle Fullscreen</td>\n </tr>\n <tr>\n <td><strong>F12</strong></td>\n <td>Developer Console</td>\n </tr>\n <tr>\n <td><strong>Ctrl+R</strong></td>\n <td>Refresh Dashboard</td>\n </tr>\n </table>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">System Information</div>\n <p style=\"color: var(--muted); line-height: 1.6;\">\n <strong>Version:</strong> 1.0.0<br>\n <strong>MQTT Broker:</strong> localhost:9001<br>\n <strong>Update Rate:</strong> 1 second<br>\n <strong>Browser:</strong> <span id=\"browser-info\"></span>\n </p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 7: Settings -->\n <div id=\"view-settings\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">⚙️</span>\n Settings\n </div>\n </div>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); overflow-y: auto;\">\n <div class=\"card\">\n <div class=\"card-header\">Display Settings</div>\n <div class=\"form-group\">\n <label class=\"form-label\">Screen Brightness</label>\n <input type=\"range\" class=\"form-input\" min=\"50\" max=\"100\" value=\"100\"\n onchange=\"window.hmiApp.setBrightness(this.value)\">\n </div>\n <div class=\"form-group\">\n <label class=\"form-label\">Theme</label>\n <select class=\"form-select\">\n <option value=\"dark\" selected>Dark (Current)</option>\n <option value=\"light\">Light</option>\n <option value=\"auto\">Auto (System)</option>\n </select>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Connection Settings</div>\n <div class=\"form-group\">\n <label class=\"form-label\">MQTT Broker</label>\n <input type=\"text\" class=\"form-input\" value=\"ws://localhost:9001\" readonly>\n </div>\n <div class=\"form-group\">\n <label class=\"form-label\">Connection Status</label>\n <div class=\"status-item\">\n <span class=\"led online\" id=\"settings-mqtt-status\"></span>\n <span class=\"status-label\" id=\"settings-mqtt-text\">Connected</span>\n </div>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Data Settings</div>\n <div class=\"form-group\">\n <label class=\"form-label\">Update Interval</label>\n <select class=\"form-select\">\n <option value=\"1000\" selected>1 second (Current)</option>\n <option value=\"2000\">2 seconds</option>\n <option value=\"5000\">5 seconds</option>\n </select>\n </div>\n <button class=\"btn btn-secondary\" onclick=\"window.hmiApp.exportData()\">\n 📥 Export Historical Data\n </button>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">System Actions</div>\n <div style=\"display: flex; flex-direction: column; gap: 12px;\">\n <button class=\"btn btn-secondary\" onclick=\"location.reload()\">\n 🔄 Reload Dashboard\n </button>\n <button class=\"btn btn-secondary\" onclick=\"window.hmiApp.clearCache()\">\n 🗑️ Clear Cache\n </button>\n <button class=\"btn btn-danger\" onclick=\"window.hmiApp.resetSettings()\">\n ⚠️ Reset to Defaults\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n </div>\n </div>\n\n <script>\n // Application State - prevent conflicts with Node-RED UI\n if (typeof window.hmiApp === 'undefined') {\n window.hmiApp = {\n mqttClient: null,\n currentView: 'dashboard',\n charts: {},\n data: {\n oee: [],\n production: [],\n quality: [],\n notifications: []\n },\n\n // Initialize application\n init() {\n this.setupNavigation();\n this.connectMQTT();\n this.startClock();\n this.initCharts();\n this.loadBrowserInfo();\n },\n\n // Setup sidebar navigation\n setupNavigation() {\n const sidebarItems = document.querySelectorAll('.sidebar-item');\n \n sidebarItems.forEach(item => {\n item.addEventListener('click', () => {\n const viewName = item.getAttribute('data-view');\n if (viewName) {\n this.switchView(viewName);\n }\n });\n });\n },\n\n // Switch between views\n switchView(viewName) {\n // Hide all views\n document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));\n \n // Show selected view\n document.getElementById(`view-${viewName}`).classList.add('active');\n \n // Update sidebar\n document.querySelectorAll('.sidebar-item').forEach(item => {\n item.classList.remove('active');\n if (item.getAttribute('data-view') === viewName) {\n item.classList.add('active');\n }\n });\n \n this.currentView = viewName;\n \n // Update charts if switching to machine view\n if (viewName === 'machine') {\n setTimeout(() => this.updateCharts(), 100);\n }\n },\n\n // Connect to MQTT (for backward compatibility)\n connectMQTT() {\n // MQTT connection removed - now using global values directly\n console.log('Dashboard initialized - using global values from Node-RED');\n \n // Start polling for global value updates\n this.startGlobalValuePolling();\n },\n\n // Poll global values from Node-RED\n startGlobalValuePolling() {\n // Poll every 500ms for updates\n setInterval(() => {\n this.updateFromGlobalValues();\n }, 500);\n \n // Initial update\n this.updateFromGlobalValues();\n },\n\n // Update dashboard from global values\n updateFromGlobalValues() {\n // This will be called by Node-RED when global values change\n // The actual data will be passed via msg.payload from the template node\n if (window.globalData) {\n const data = window.globalData;\n \n if (data.machine_status) {\n this.updateStatus(data.machine_status);\n }\n if (data.oeeData) {\n this.updateOEE(data.oeeData);\n }\n if (data.productionData) {\n this.updateProduction(data.productionData);\n }\n if (data.workOrderData) {\n this.updateWorkOrder(data.workOrderData);\n }\n }\n },\n\n // Update functions\n updateStatus(data) {\n if (data.isOnline !== undefined) {\n const led = document.getElementById('status_online');\n led.className = 'led' + (data.isOnline ? ' online' : '');\n document.getElementById('status_online_text').textContent = \n data.isOnline ? 'Connected' : 'Disconnected';\n }\n \n if (data.isRunning !== undefined) {\n const led = document.getElementById('status_running');\n led.className = 'led' + (data.isRunning ? ' running' : '');\n document.getElementById('status_running_text').textContent = \n data.isRunning ? 'Active' : 'Stopped';\n }\n },\n\n updateOEE(data) {\n if (data.oee !== undefined) {\n document.getElementById('oee_value').textContent = Math.round(data.oee) + '%';\n this.data.oee.push({time: Date.now(), value: data.oee});\n if (this.data.oee.length > 60) this.data.oee.shift();\n }\n if (data.availability !== undefined) {\n document.getElementById('availability_value').textContent = Math.round(data.availability) + '%';\n }\n if (data.performance !== undefined) {\n document.getElementById('performance_value').textContent = Math.round(data.performance) + '%';\n }\n if (data.quality !== undefined) {\n document.getElementById('quality_value').textContent = Math.round(data.quality) + '%';\n this.data.quality.push({time: Date.now(), value: data.quality});\n if (this.data.quality.length > 60) this.data.quality.shift();\n }\n },\n\n updateProduction(data) {\n if (data.goodParts !== undefined) {\n document.getElementById('good_parts').textContent = data.goodParts.toLocaleString();\n document.getElementById('wo-completed').textContent = data.goodParts;\n this.data.production.push({time: Date.now(), value: data.goodParts});\n if (this.data.production.length > 60) this.data.production.shift();\n }\n if (data.scrapParts !== undefined) {\n document.getElementById('scrap_parts').textContent = data.scrapParts;\n }\n if (data.progress !== undefined) {\n const bar = document.getElementById('progress_bar');\n const text = document.getElementById('progress_value');\n const woBar = document.getElementById('wo-progress');\n const progress = Math.round(data.progress);\n bar.style.width = progress + '%';\n text.textContent = progress + '%';\n if (woBar) woBar.style.width = progress + '%';\n }\n if (data.cycleTime !== undefined) {\n document.getElementById('cycle_time').textContent = data.cycleTime;\n }\n },\n\n updateWorkOrder(data) {\n if (data.workOrder !== undefined) {\n document.getElementById('workorder_number').textContent = data.workOrder;\n }\n if (data.sku !== undefined) {\n document.getElementById('workorder_sku').textContent = data.sku;\n }\n },\n\n // Clock\n startClock() {\n setInterval(() => {\n const now = new Date();\n const datetime = now.toISOString().replace('T', ' ').substring(0, 19);\n document.getElementById('footer_datetime').textContent = datetime;\n }, 1000);\n },\n\n // Initialize Charts\n initCharts() {\n const commonOptions = {\n responsive: true,\n maintainAspectRatio: false,\n plugins: {\n legend: { labels: { color: '#eaf1fb' } }\n },\n scales: {\n y: { ticks: { color: '#96a5b7' }, grid: { color: 'rgba(255,255,255,0.1)' } },\n x: { ticks: { color: '#96a5b7' }, grid: { color: 'rgba(255,255,255,0.1)' } }\n }\n };\n\n // OEE Chart\n const oeeCtx = document.getElementById('oee-chart');\n if (oeeCtx) {\n this.charts.oee = new Chart(oeeCtx, {\n type: 'line',\n data: {\n labels: Array(20).fill(''),\n datasets: [{\n label: 'OEE %',\n data: Array(20).fill(0),\n borderColor: '#2ea3ff',\n backgroundColor: 'rgba(46, 163, 255, 0.1)',\n tension: 0.4\n }]\n },\n options: commonOptions\n });\n }\n\n // Production Chart\n const prodCtx = document.getElementById('production-chart');\n if (prodCtx) {\n this.charts.production = new Chart(prodCtx, {\n type: 'bar',\n data: {\n labels: Array(20).fill(''),\n datasets: [{\n label: 'Parts Produced',\n data: Array(20).fill(0),\n backgroundColor: '#2ecc71'\n }]\n },\n options: commonOptions\n });\n }\n\n // Quality Chart\n const qualityCtx = document.getElementById('quality-chart');\n if (qualityCtx) {\n this.charts.quality = new Chart(qualityCtx, {\n type: 'doughnut',\n data: {\n labels: ['Good Parts', 'Scrap Parts'],\n datasets: [{\n data: [95, 5],\n backgroundColor: ['#2ecc71', '#ff4d4f']\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: {\n legend: { labels: { color: '#eaf1fb' } }\n }\n }\n });\n }\n\n // State Chart\n const stateCtx = document.getElementById('state-chart');\n if (stateCtx) {\n this.charts.state = new Chart(stateCtx, {\n type: 'line',\n data: {\n labels: Array(20).fill(''),\n datasets: [{\n label: 'Running State',\n data: Array(20).fill(0),\n borderColor: '#2ea3ff',\n stepped: true\n }]\n },\n options: commonOptions\n });\n }\n },\n\n // Update charts with latest data\n updateCharts() {\n if (this.charts.oee && this.data.oee.length > 0) {\n this.charts.oee.data.datasets[0].data = this.data.oee.map(d => d.value);\n this.charts.oee.update();\n }\n \n if (this.charts.production && this.data.production.length > 0) {\n this.charts.production.data.datasets[0].data = this.data.production.map(d => d.value);\n this.charts.production.update();\n }\n },\n\n // UI Actions\n submitIssue(event) {\n event.preventDefault();\n const formData = new FormData(event.target);\n const issue = {\n category: formData.get('category'),\n severity: formData.get('severity'),\n machine: formData.get('machine'),\n description: formData.get('description'),\n reporter: formData.get('reporter'),\n timestamp: new Date().toISOString()\n };\n \n console.log('Issue reported:', issue);\n \n // Store in localStorage\n const issues = JSON.parse(localStorage.getItem('issues') || '[]');\n issues.unshift(issue);\n localStorage.setItem('issues', JSON.stringify(issues));\n \n alert('✅ Issue reported successfully!');\n event.target.reset();\n this.loadRecentReports();\n },\n\n loadRecentReports() {\n const issues = JSON.parse(localStorage.getItem('issues') || '[]');\n const container = document.getElementById('recent-reports');\n \n if (issues.length === 0) {\n container.innerHTML = '<p style=\"color: var(--muted); padding: 16px;\">No recent reports</p>';\n return;\n }\n \n container.innerHTML = issues.slice(0, 5).map(issue => `\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"badge badge-${issue.severity === 'critical' ? 'danger' : 'warning'}\">${issue.severity}</span>\n <div style=\"flex: 1;\">\n <div class=\"status-label\">${issue.category}: ${issue.description.substring(0, 60)}...</div>\n <div class=\"status-value\">${new Date(issue.timestamp).toLocaleString()}</div>\n </div>\n </div>\n `).join('');\n },\n\n createWorkOrder() {\n alert('Create Work Order functionality coming soon!');\n },\n\n clearNotifications() {\n document.getElementById('notifications-list').innerHTML = \n '<p style=\"color: var(--muted); padding: 16px;\">No notifications</p>';\n document.getElementById('notif-count').textContent = '0';\n },\n\n setBrightness(value) {\n document.body.style.opacity = value / 100;\n },\n\n exportData() {\n const data = {\n oee: this.data.oee,\n production: this.data.production,\n quality: this.data.quality,\n timestamp: new Date().toISOString()\n };\n \n const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `hmi-data-${Date.now()}.json`;\n a.click();\n },\n\n clearCache() {\n localStorage.clear();\n alert('Cache cleared!');\n },\n\n resetSettings() {\n if (confirm('Reset all settings to default?')) {\n localStorage.clear();\n location.reload();\n }\n },\n\n loadBrowserInfo() {\n document.getElementById('browser-info').textContent = navigator.userAgent.split(' ').pop();\n }\n };\n\n // Start application when DOM is ready\n document.addEventListener('DOMContentLoaded', () => {\n window.hmiApp.init();\n });\n \n // Execute incoming script from Node-RED\n if (typeof msg !== 'undefined' && msg.payload) {\n try {\n eval(msg.payload);\n } catch (error) {\n console.error('Error executing update script:', error);\n }\n }\n } // End of hmiApp conditional block\n </script>\n</body>\n\n</html>",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 790,
"y": 320,
"wires": [
[]
]
},
{
"id": "d9407fca93d82949",
"type": "function",
"z": "ea088d9e256f2e4a",
"name": "Machine status function",
"func": "// Get previous state\nconst previousStatus = global.get(\"machine_status\") || {\n state: 'Running',\n isOnline: true,\n isRunning: true\n};\n\nlet state = previousStatus.state;\n\n// Random transitions\nif (Math.random() < 0.5) {\n state = state === 'Running' ? 'Stopped' : 'Running';\n}\n\n// Create status data\nconst statusData = {\n state: state,\n isOnline: state !== 'Error',\n isRunning: state === 'Running',\n timestamp: new Date().toISOString()\n};\n\n// Store in global\nglobal.set(\"machine_status\", statusData);\n\n// Send to UI\nmsg.payload = statusData;\nmsg.topic = \"pimsa/maquina1/status\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 120,
"wires": [
[
"aaf07ca380495b06",
"c195f94afd7183e6"
]
]
},
{
"id": "547062128fa6337a",
"type": "inject",
"z": "ea088d9e256f2e4a",
"name": "Machine Data Timer",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "1",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 140,
"y": 300,
"wires": [
[
"d9407fca93d82949",
"207fdc449918affd",
"031c7ccb3125177b",
"2a35e2907dc04de0",
"global_data_reader"
]
]
},
{
"id": "4f66d69c5bce7ba0",
"type": "mqtt out",
"z": "ea088d9e256f2e4a",
"name": "MQTT Out - Work Order",
"topic": "pimsa/erp/workorder/current",
"qos": "1",
"retain": "true",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "ea803f6a2965854b",
"x": 810,
"y": 540,
"wires": []
},
{
"id": "aaf07ca380495b06",
"type": "mqtt out",
"z": "ea088d9e256f2e4a",
"name": "MQTT Out - Machine Status",
"topic": "pimsa/maquina1/status",
"qos": "1",
"retain": "true",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "ea803f6a2965854b",
"x": 820,
"y": 100,
"wires": []
},
{
"id": "fee4e024db51e661",
"type": "mqtt out",
"z": "ea088d9e256f2e4a",
"name": "MQTT Out - Machine OEE",
"topic": "pimsa/maquina1/oee",
"qos": "0",
"retain": "false",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "ea803f6a2965854b",
"x": 800,
"y": 260,
"wires": []
},
{
"id": "12c37a80895dcc3e",
"type": "mqtt out",
"z": "ea088d9e256f2e4a",
"name": "MQTT Out - Production",
"topic": "pimsa/maquina1/production",
"qos": "1",
"retain": "false",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "ea803f6a2965854b",
"x": 810,
"y": 400,
"wires": []
},
{
"id": "207fdc449918affd",
"type": "function",
"z": "ea088d9e256f2e4a",
"name": "OEE Function",
"func": "// OEE Function\nconst oeeData = {\n oee: 85 + Math.random() * 15,\n availability: 90 + Math.random() * 10,\n performance: 80 + Math.random() * 20,\n quality: 95 + Math.random() * 5,\n timestamp: new Date().toISOString()\n};\n\n// Store in global context (NEW!)\nglobal.set(\"oeeData\", oeeData);\n\nmsg.payload = oeeData;\nmsg.topic = \"pimsa/maquina1/oee\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 440,
"y": 260,
"wires": [
[
"fee4e024db51e661",
"c195f94afd7183e6"
]
]
},
{
"id": "031c7ccb3125177b",
"type": "function",
"z": "ea088d9e256f2e4a",
"name": "Production Function",
"func": "// Production Function with Auto-Reset for Demo\nconst targetQty = 2000;\n\n// Initialize or get current values\nlet goodParts = context.get('goodParts') || 100; // Start at 100 instead of 1200\nlet scrapParts = context.get('scrapParts') || 5;\n\n// Increment production\ngoodParts += Math.floor(Math.random() * 3) + 1;\nscrapParts += Math.floor(Math.random() * 2);\n\n// Calculate progress\nconst progress = Math.min(100, (goodParts / targetQty) * 100);\n\n// Auto-reset when reaching 100% (for continuous demo)\nif (goodParts >= targetQty) {\n goodParts = 100; // Reset to beginning\n scrapParts = 5;\n node.warn(\"Production cycle completed! Resetting for next work order...\");\n}\n\n// Save updated values\ncontext.set('goodParts', goodParts);\ncontext.set('scrapParts', scrapParts);\n\n// Create production data object\nconst productionData = {\n goodParts: goodParts,\n scrapParts: scrapParts,\n totalParts: goodParts + scrapParts,\n progress: progress,\n cycleTime: (45 + Math.random() * 10).toFixed(1) + \"s\",\n timestamp: new Date().toISOString()\n};\n\n// Store in global context (NEW!)\nglobal.set(\"productionData\", productionData);\n\nmsg.payload = productionData;\nmsg.topic = \"pimsa/maquina1/production\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 460,
"y": 400,
"wires": [
[
"12c37a80895dcc3e",
"c195f94afd7183e6"
]
]
},
{
"id": "2a35e2907dc04de0",
"type": "function",
"z": "ea088d9e256f2e4a",
"name": "Work Order Function",
"func": "// Work Order Function\nconst workOrder = {\n id: 'WO-2024-001',\n sku: 'SKU-ABC123',\n description: 'Plastic Housing - Black',\n targetQty: 2000,\n dueDate: '2024-10-15'\n};\n\n// Create work order data object\nconst workOrderData = {\n workOrder: workOrder.id,\n sku: workOrder.sku,\n description: workOrder.description,\n targetQty: workOrder.targetQty,\n completedQty: context.get('goodParts') || 1200,\n dueDate: workOrder.dueDate,\n timestamp: new Date().toISOString()\n};\n\n// Store in global context (NEW!)\nglobal.set(\"workOrderData\", workOrderData);\n\nmsg.payload = workOrderData;\nmsg.topic = \"pimsa/erp/workorder/current\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 460,
"y": 540,
"wires": [
[
"4f66d69c5bce7ba0",
"c195f94afd7183e6"
]
]
},
{
"id": "global_data_reader",
"type": "function",
"z": "ea088d9e256f2e4a",
"name": "Global Data Reader",
"func": "// Read all global values and send to template\nconst globalData = {\n machine_status: global.get(\"machine_status\") || {},\n oeeData: global.get(\"oeeData\") || {},\n productionData: global.get(\"productionData\") || {},\n workOrderData: global.get(\"workOrderData\") || {}\n};\n\n// Create a script that will update the dashboard\nconst updateScript = `\n window.globalData = ${JSON.stringify(globalData)};\n if (window.app && window.app.updateFromGlobalValues) {\n window.app.updateFromGlobalValues();\n }\n`;\n\n// Send the update script to the template\nmsg.payload = updateScript;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 500,
"y": 320,
"wires": [
[
"c195f94afd7183e6"
]
]
}
]