Skip to content

Displaying Factory Data with Digital Twin

Available inCloudEnterprise

The digital twin feature in Tier0 is for you to view live UNS data on 3D models in lifelike scenes. In this section, we bring a workshop to life. It contains the following machines:

  • 2 lathes
  • 1 robot arm working between these lathes
  • 1 inbound buffer station and 1 outbound buffer
  • 1 quality checker
  • 1 Unified Control System
  • 2 raw material shelves and 2 product shelves
  • 1 AGV

Collect all data to UNS, including data that controls machine movements and indicates machine status and the workshop environment.

  1. Build data models in UNS.

    Show UNS model JSON
    Terminal window
    {
    "namespace": [
    {
    "name": "Metric",
    "alias": "metric_151a7edd68bf4269a229",
    "type": "PATH",
    "children": [
    {
    "name": "RobotArm",
    "alias": "Robot_0_9d5dc450dc1f4c0c9a86",
    "type": "TOPIC",
    "topicType": "METRIC",
    "extendProperties": {
    "J0_Cur": "0"
    },
    "fields": [
    {
    "name": "J1_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J1_Init",
    "type": "FLOAT"
    },
    {
    "name": "J2_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J2_Init",
    "type": "FLOAT"
    },
    {
    "name": "J3_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J3_Init",
    "type": "FLOAT"
    },
    {
    "name": "J4_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J4_Init",
    "type": "FLOAT"
    },
    {
    "name": "J5_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J5_Init",
    "type": "FLOAT"
    },
    {
    "name": "J6_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J6_Init",
    "type": "FLOAT"
    },
    {
    "name": "Model_Name",
    "type": "STRING"
    },
    {
    "name": "Max_Payload_kg",
    "type": "FLOAT"
    },
    {
    "name": "Reach_Radius_mm",
    "type": "FLOAT"
    },
    {
    "name": "Gripper_Force_N",
    "type": "FLOAT"
    }
    ],
    "enableHistory": "TRUE",
    "mockData": "FALSE"
    },
    {
    "name": "ProLathe-01",
    "alias": "ProLathe_ef325e0a9db047d6abac",
    "type": "TOPIC",
    "topicType": "METRIC",
    "fields": [
    {
    "name": "J_Changer_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Door_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Finger_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Spindle_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_TailStock_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_X_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Y_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Z_Cur",
    "type": "FLOAT"
    },
    {
    "name": "Model_Name",
    "type": "STRING"
    },
    {
    "name": "Max_Placement_Rate_cph",
    "type": "INTEGER"
    },
    {
    "name": "Placement_Speed_cph",
    "type": "INTEGER"
    },
    {
    "name": "Feeder_Usage_pct",
    "type": "INTEGER"
    },
    {
    "name": "Machine_State_code",
    "type": "INTEGER"
    }
    ],
    "enableHistory": "FALSE",
    "mockData": "FALSE"
    },
    {
    "name": "ProLathe-02",
    "alias": "ProLathe2_ef325e0a9db047d6abac",
    "type": "TOPIC",
    "topicType": "METRIC",
    "fields": [
    {
    "name": "J_Changer_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Door_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Finger_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Spindle_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_TailStock_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_X_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Y_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J_Z_Cur",
    "type": "FLOAT"
    },
    {
    "name": "Model_Name",
    "type": "STRING"
    },
    {
    "name": "Max_Placement_Rate_cph",
    "type": "INTEGER"
    },
    {
    "name": "Placement_Speed_cph",
    "type": "INTEGER"
    },
    {
    "name": "Feeder_Usage_pct",
    "type": "INTEGER"
    },
    {
    "name": "Machine_State_code",
    "type": "INTEGER"
    }
    ],
    "enableHistory": "FALSE",
    "mockData": "FALSE"
    },
    {
    "name": "AGV",
    "alias": "AGV_7eddee0054a0",
    "type": "TOPIC",
    "topicType": "METRIC",
    "fields": [
    {
    "name": "Position_X_Cur",
    "type": "FLOAT"
    },
    {
    "name": "Position_X_Init",
    "type": "FLOAT"
    },
    {
    "name": "Position_Y_Cur",
    "type": "FLOAT"
    },
    {
    "name": "Position_Y_Init",
    "type": "FLOAT"
    },
    {
    "name": "Rotation_Y_Cur",
    "type": "FLOAT"
    },
    {
    "name": "Rotation_Y_Init",
    "type": "FLOAT"
    },
    {
    "name": "Tray_Height_Cur",
    "type": "FLOAT"
    },
    {
    "name": "Tray_Height_Init",
    "type": "FLOAT"
    },
    {
    "name": "State_Cur",
    "type": "STRING"
    },
    {
    "name": "State_Init",
    "type": "STRING"
    },
    {
    "name": "Position_Z_Cur",
    "type": "FLOAT"
    },
    {
    "name": "Position_Z_Init",
    "type": "FLOAT"
    },
    {
    "name": "Model_Name",
    "type": "STRING"
    },
    {
    "name": "Max_Load_kg",
    "type": "FLOAT"
    },
    {
    "name": "Task_Progress_pct",
    "type": "INTEGER"
    },
    {
    "name": "Remaining_Distance_m",
    "type": "FLOAT"
    },
    {
    "name": "AGV_State_code",
    "type": "INTEGER"
    }
    ],
    "enableHistory": "FALSE",
    "mockData": "FALSE"
    },
    {
    "name": "RobotArm-1",
    "alias": "RobotArm_1_b9f06e21346d",
    "type": "TOPIC",
    "topicType": "METRIC",
    "fields": [
    {
    "name": "J1_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J1_Init",
    "type": "FLOAT"
    },
    {
    "name": "J2_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J2_Init",
    "type": "FLOAT"
    },
    {
    "name": "J3_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J3_Init",
    "type": "FLOAT"
    },
    {
    "name": "J4_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J4_Init",
    "type": "FLOAT"
    },
    {
    "name": "J5_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J5_Init",
    "type": "FLOAT"
    },
    {
    "name": "J6_Cur",
    "type": "FLOAT"
    },
    {
    "name": "J6_Init",
    "type": "FLOAT"
    }
    ],
    "enableHistory": "TRUE",
    "mockData": "FALSE"
    }
    ]
    },
    {
    "name": "State",
    "alias": "state_3912d0df3fee",
    "type": "PATH",
    "children": [
    {
    "name": "RawMaterialStorage",
    "alias": "RawMaterialS_c3307f680405",
    "type": "TOPIC",
    "topicType": "STATE",
    "fields": [
    {
    "name": "StockAmount",
    "type": "INTEGER"
    }
    ],
    "enableHistory": "FALSE",
    "mockData": "FALSE"
    },
    {
    "name": "ProductStorage",
    "alias": "ProductStora_3c145f140bd9",
    "type": "TOPIC",
    "topicType": "STATE",
    "fields": [
    {
    "name": "StockAmount",
    "type": "INTEGER"
    }
    ],
    "enableHistory": "FALSE",
    "mockData": "FALSE"
    },
    {
    "name": "InboundBuffer",
    "alias": "LoaderBuffer_9feece9b3231",
    "type": "TOPIC",
    "topicType": "STATE",
    "fields": [
    {
    "name": "Amount",
    "type": "INTEGER"
    }
    ],
    "enableHistory": "FALSE",
    "mockData": "FALSE"
    },
    {
    "name": "OutboundBuffer",
    "alias": "OutboundBuff_75433b98fde6",
    "type": "TOPIC",
    "topicType": "STATE",
    "fields": [
    {
    "name": "Amount",
    "type": "INTEGER"
    }
    ],
    "enableHistory": "FALSE",
    "mockData": "FALSE"
    }
    ]
    }
    ]
    }
  2. Use a Source Flow to connect data sources to UNS models.

    • Robot Arm
    Show Robot Arm flow JSON
    Terminal window
    [
    {
    "id": "03933a7eb1a7b992",
    "type": "function",
    "z": "8daeb61ebd9e5f52",
    "name": "Cache Data",
    "func": "const CONFIG = {\n fields: ['Joint_1', 'Joint_2', 'Joint_3', 'Joint_4', 'Joint_5', 'Joint_6'],\n payloadFields: {\n value: 'CurrentValue',\n initialValue: 'InitialValue',\n name: 'Name',\n timeStamp: 'timeStamp',\n time_stamp: 'time_stamp',\n quality: 'quality'\n }\n};\n\nlet cache = flow.get('dataCache') || { instant: {}, metadata: {} };\n\nconst topicParts = msg.topic.split('/');\nconst fieldName = topicParts[topicParts.length - 1];\n\nlet payload = msg.payload;\nif (typeof payload === 'string') {\n try {\n payload = JSON.parse(payload);\n } catch (e) {\n return null;\n }\n}\n\nlet value = payload[CONFIG.payloadFields.value];\nif (value === undefined) return null;\n\nconst metadata = {\n timeStamp: payload[CONFIG.payloadFields.timeStamp],\n time_stamp: payload[CONFIG.payloadFields.time_stamp],\n quality: payload[CONFIG.payloadFields.quality],\n name: payload[CONFIG.payloadFields.name],\n initialValue: payload[CONFIG.payloadFields.initialValue]\n};\n\nif (CONFIG.fields.includes(fieldName)) {\n cache.instant[fieldName] = value;\n cache.metadata[fieldName] = metadata;\n} else {\n return null;\n}\n\nflow.set('dataCache', cache);\nreturn null;",
    "outputs": 0,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 880,
    "y": 500,
    "wires": []
    },
    {
    "id": "0c4dbdcbe5e3db92",
    "type": "mqtt in",
    "z": "8daeb61ebd9e5f52",
    "name": "Subscribe Joint_1~6",
    "topic": "FactorySim_Supos/Flow_Group__1/MechEquips/Transporter_Kawasaki_BX100N/Motor_Joints/+",
    "qos": "1",
    "datatype": "auto",
    "broker": "c11decb07a68083d",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 610,
    "y": 480,
    "wires": [
    [
    "03933a7eb1a7b992"
    ]
    ]
    },
    {
    "id": "ba42bca5aa9dc561",
    "type": "inject",
    "z": "8daeb61ebd9e5f52",
    "name": "100ms Timer",
    "props": [
    {
    "p": "payload"
    },
    {
    "vt": "str",
    "p": "topic"
    }
    ],
    "repeat": "0.1",
    "crontab": "",
    "once": true,
    "onceDelay": "0.1",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 520,
    "y": 600,
    "wires": [
    [
    "0fc94449c377ad9a"
    ]
    ]
    },
    {
    "id": "0fc94449c377ad9a",
    "type": "function",
    "z": "8daeb61ebd9e5f52",
    "name": "Aggregate & Publish (Flat)",
    "func": "const CONFIG = {\n fieldMapping: {\n 'Joint_1': 'J1',\n 'Joint_2': 'J2',\n 'Joint_3': 'J3',\n 'Joint_4': 'J4',\n 'Joint_5': 'J5',\n 'Joint_6': 'J6'\n },\n missingStrategy: 'lastValue',\n outputTopic: 'robot/state',\n windowMs: 100\n};\n\nconst FIELDS = Object.keys(CONFIG.fieldMapping);\n\nlet cache = flow.get('dataCache') || { instant: {}, metadata: {} };\nlet lastValues = flow.get('lastValues') || {};\nlet lastMetadata = flow.get('lastMetadata') || {};\n\nlet output = {\n timestamp: new Date().toISOString()\n};\n\nfor (const field of FIELDS) {\n const shortName = CONFIG.fieldMapping[field];\n \n if (cache.instant[field] !== undefined) {\n output[shortName + '_Cur'] = cache.instant[field];\n output[shortName + '_Init'] = cache.metadata[field] ? cache.metadata[field].initialValue : 0;\n output[shortName + '_Qly'] = cache.metadata[field] ? cache.metadata[field].quality : 0;\n \n lastValues[field] = cache.instant[field];\n lastMetadata[field] = cache.metadata[field];\n } else {\n let fallbackValue = CONFIG.missingStrategy === 'lastValue' \n ? (lastValues[field] !== undefined ? lastValues[field] : 0) \n : 0;\n \n output[shortName + '_Cur'] = fallbackValue;\n output[shortName + '_Init'] = lastMetadata[field] ? lastMetadata[field].initialValue : 0;\n output[shortName + '_Qly'] = -1;\n }\n}\n\nmsg.payload = output;\nmsg.topic = CONFIG.outputTopic;\n\nflow.set('dataCache', { instant: {}, metadata: {} });\nflow.set('lastValues', lastValues);\nflow.set('lastMetadata', lastMetadata);\n\nreturn msg;",
    "outputs": 1,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 760,
    "y": 600,
    "wires": [
    [
    "c9eee2264fe78846",
    "a685279629e73d9a",
    "4ce075fda1d3d993",
    "8e8779918eab6f89"
    ]
    ]
    },
    {
    "id": "c9eee2264fe78846",
    "type": "mqtt out",
    "z": "8daeb61ebd9e5f52",
    "name": "Publish robot/state",
    "topic": "Metric/RobotArm",
    "qos": "1",
    "retain": "false",
    "respTopic": "",
    "contentType": "",
    "userProps": "",
    "correl": "",
    "expiry": "",
    "broker": "9836929a70c74929",
    "x": 1070,
    "y": 540,
    "wires": []
    },
    {
    "id": "c11decb07a68083d",
    "type": "mqtt-broker",
    "name": "supos-demo.supos.app",
    "broker": "supos-demo.supos.app",
    "port": 1883,
    "clientid": "",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": 4,
    "keepalive": 60,
    "cleansession": true,
    "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": "9836929a70c74929",
    "type": "mqtt-broker",
    "z": "8daeb61ebd9e5f52",
    "name": "emqx",
    "broker": "mqtt.pre.tier0.dev",
    "port": "1883",
    "clientid": "336462685553088&336463047745872",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": "4",
    "keepalive": "60",
    "cleansession": true,
    "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": ""
    }
    ]
    • AGV
    Show AGV flow JSON
    Terminal window
    [
    {
    "id": "fa46cf31be8d751e",
    "type": "tab",
    "label": "Metric/AGV",
    "disabled": false,
    "info": ""
    },
    {
    "id": "1f0e9d8c7b6a5a41",
    "type": "mqtt in",
    "z": "fa46cf31be8d751e",
    "name": "Subscribe AGV telemetry",
    "topic": "FactorySim_Supos/Flow_Group__1/MechEquips/AGV_zOnron/Motor_Joints/+",
    "qos": "1",
    "datatype": "auto",
    "broker": "c11decb07a68083d",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 190,
    "y": 380,
    "wires": [
    [
    "2a1b3c4d5e6f7081"
    ]
    ]
    },
    {
    "id": "2a1b3c4d5e6f7081",
    "type": "function",
    "z": "fa46cf31be8d751e",
    "name": "Cache AGV telemetry",
    "func": "const CONFIG = {\n fields: [\n 'Position_X',\n 'Position_Y',\n 'Position_Z',\n 'Rotation_Y',\n 'Tray_Height',\n 'State'\n ],\n payloadFields: {\n value: 'CurrentValue',\n initialValue: 'InitialValue',\n name: 'Name',\n timeStamp: 'timeStamp',\n timeStampAlt: 'time_stamp'\n }\n};\n\nlet cache = flow.get('agvDataCache') || { instant: {}, metadata: {} };\n\nconst topicParts = (msg.topic || '').split('/');\nconst fieldName = topicParts[topicParts.length - 1];\nif (!CONFIG.fields.includes(fieldName)) {\n return null;\n}\n\nlet payload = msg.payload;\nif (typeof payload === 'string') {\n try {\n payload = JSON.parse(payload);\n } catch (error) {\n return null;\n }\n}\n\nif (typeof payload !== 'object' || payload === null) {\n return null;\n}\n\nconst currentValue = payload[CONFIG.payloadFields.value];\nif (currentValue === undefined) {\n return null;\n}\n\ncache.instant[fieldName] = currentValue;\ncache.metadata[fieldName] = {\n name: payload[CONFIG.payloadFields.name] || fieldName,\n initialValue: payload[CONFIG.payloadFields.initialValue],\n timeStamp: payload[CONFIG.payloadFields.timeStampAlt] ?? payload[CONFIG.payloadFields.timeStamp] ?? 0\n};\n\nflow.set('agvDataCache', cache);\nreturn null;",
    "outputs": 0,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 470,
    "y": 380,
    "wires": []
    },
    {
    "id": "3b2c1d0e9f8a7b61",
    "type": "inject",
    "z": "fa46cf31be8d751e",
    "name": "100ms Timer",
    "props": [
    {
    "p": "payload"
    },
    {
    "p": "topic",
    "vt": "str"
    }
    ],
    "repeat": "0.1",
    "crontab": "",
    "once": true,
    "onceDelay": "0.1",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 150,
    "y": 500,
    "wires": [
    [
    "4c3d2e1f0a9b8c71"
    ]
    ]
    },
    {
    "id": "4c3d2e1f0a9b8c71",
    "type": "function",
    "z": "fa46cf31be8d751e",
    "name": "Aggregate AGV UNS payload",
    "func": "const CONFIG = {\n fields: [\n 'Position_X',\n 'Position_Y',\n 'Position_Z',\n 'Rotation_Y',\n 'Tray_Height',\n 'State'\n ],\n outputTopic: 'Metric/AGV',\n defaultNumeric: 0,\n defaultString: ''\n};\n\nlet cache = flow.get('agvDataCache') || { instant: {}, metadata: {} };\nlet lastValues = flow.get('agvLastValues') || {};\nlet lastMetadata = flow.get('agvLastMetadata') || {};\nlet lastTimestamp = flow.get('agvLastTimestamp') || 0;\n\nlet output = {};\nlet candidateTimestamps = [];\n\nfor (const field of CONFIG.fields) {\n const hasCurrentFrameValue = Object.prototype.hasOwnProperty.call(cache.instant, field);\n const metadata = hasCurrentFrameValue ? cache.metadata[field] : lastMetadata[field];\n const currentValue = hasCurrentFrameValue ? cache.instant[field] : lastValues[field];\n const isStringField = field === 'State';\n\n output[field + '_Cur'] = currentValue !== undefined\n ? currentValue\n : (isStringField ? CONFIG.defaultString : CONFIG.defaultNumeric);\n output[field + '_Init'] = metadata && metadata.initialValue !== undefined\n ? metadata.initialValue\n : (isStringField ? CONFIG.defaultString : CONFIG.defaultNumeric);\n\n if (hasCurrentFrameValue) {\n lastValues[field] = cache.instant[field];\n lastMetadata[field] = cache.metadata[field];\n\n const rawTime = cache.metadata[field] ? Number(cache.metadata[field].timeStamp) : NaN;\n if (!Number.isNaN(rawTime)) {\n candidateTimestamps.push(rawTime);\n }\n }\n}\n\nif (candidateTimestamps.length > 0) {\n lastTimestamp = Math.max(...candidateTimestamps);\n}\n\noutput.timestamp = lastTimestamp;\n\nmsg.topic = CONFIG.outputTopic;\nmsg.payload = output;\n\nflow.set('agvDataCache', { instant: {}, metadata: {} });\nflow.set('agvLastValues', lastValues);\nflow.set('agvLastMetadata', lastMetadata);\nflow.set('agvLastTimestamp', lastTimestamp);\n\nreturn msg;",
    "outputs": 1,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 460,
    "y": 500,
    "wires": [
    [
    "6d5e4f3a2b1c0d91",
    "7c6b5a4d3e2f1098"
    ]
    ]
    },
    {
    "id": "6d5e4f3a2b1c0d91",
    "type": "mqtt out",
    "z": "fa46cf31be8d751e",
    "name": "Publish Metric/AGV",
    "topic": "Metric/AGV",
    "qos": "1",
    "retain": "false",
    "respTopic": "",
    "contentType": "",
    "userProps": "",
    "correl": "",
    "expiry": "",
    "broker": "f2d431c2d2264341",
    "x": 710,
    "y": 400,
    "wires": []
    },
    {
    "id": "7c6b5a4d3e2f1098",
    "type": "debug",
    "z": "fa46cf31be8d751e",
    "name": "Debug AGV payload",
    "active": true,
    "tosidebar": true,
    "console": false,
    "complete": "payload",
    "targetType": "msg",
    "statusVal": "",
    "statusType": "auto",
    "x": 740,
    "y": 580,
    "wires": []
    },
    {
    "id": "c11decb07a68083d",
    "type": "mqtt-broker",
    "name": "supos-demo.supos.app",
    "broker": "supos-demo.supos.app",
    "port": 1883,
    "clientid": "",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": 4,
    "keepalive": 60,
    "cleansession": true,
    "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": "f2d431c2d2264341",
    "type": "mqtt-broker",
    "z": "fa46cf31be8d751e",
    "name": "emqx",
    "broker": "mqtt.pre.tier0.dev",
    "port": "1883",
    "clientid": "336462685553088&341668146888752",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": "4",
    "keepalive": "60",
    "cleansession": true,
    "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": ""
    }
    ]
    • Lathe 1
    Show Lathe 1 flow JSON
    Terminal window
    [
    {
    "id": "88fed08889390f28",
    "type": "tab",
    "label": "Metric/ProLathe-01",
    "disabled": false,
    "info": ""
    },
    {
    "id": "f2bae08de3b55bb0",
    "type": "mqtt in",
    "z": "88fed08889390f28",
    "name": "Subscribe ProLathe joints",
    "topic": "FactorySim_Supos/Flow_Group__1/MechEquips/porcess_prolathe_gs_260ms_1/Motor_Joints/+",
    "qos": "1",
    "datatype": "auto",
    "broker": "6f8ade8021b85933",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 590,
    "y": 340,
    "wires": [
    [
    "ac84eab3cf5bb664",
    "8a8a5572465dac5b"
    ]
    ]
    },
    {
    "id": "ac84eab3cf5bb664",
    "type": "function",
    "z": "88fed08889390f28",
    "name": "Cache Data",
    "func": "const CONFIG = {\n fields: [\"JointChanger\", \"JointDoors\", \"JointFinger\", \"JointSpindle\", \"JointTailStock\", \"Joint_X\", \"Joint_Y\", \"Joint_Z\"],\n payloadFields: {\n value: 'CurrentValue',\n initialValue: 'InitialValue',\n name: 'Name',\n timeStamp: 'timeStamp',\n time_stamp: 'time_stamp',\n quality: 'quality'\n }\n};\n\nlet cache = flow.get('dataCache') || { instant: {}, metadata: {} };\n\nconst topicParts = msg.topic.split('/');\nconst fieldName = topicParts[topicParts.length - 1];\n\nlet payload = msg.payload;\nif (typeof payload === 'string') {\n try {\n payload = JSON.parse(payload);\n } catch (e) {\n return null;\n }\n}\n\nlet value = payload[CONFIG.payloadFields.value];\nif (value === undefined) return null;\n\nconst metadata = {\n timeStamp: payload[CONFIG.payloadFields.timeStamp],\n time_stamp: payload[CONFIG.payloadFields.time_stamp],\n quality: payload[CONFIG.payloadFields.quality],\n name: payload[CONFIG.payloadFields.name],\n initialValue: payload[CONFIG.payloadFields.initialValue]\n};\n\nif (CONFIG.fields.includes(fieldName)) {\n cache.instant[fieldName] = value;\n cache.metadata[fieldName] = metadata;\n} else {\n return null;\n}\n\nflow.set('dataCache', cache);\nreturn null;",
    "outputs": 0,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 860,
    "y": 360,
    "wires": []
    },
    {
    "id": "ca4b6b2146df5093",
    "type": "inject",
    "z": "88fed08889390f28",
    "name": "100ms Timer",
    "props": [
    {
    "p": "payload"
    },
    {
    "vt": "str",
    "p": "topic"
    }
    ],
    "repeat": "0.1",
    "crontab": "",
    "once": true,
    "onceDelay": "0.1",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 500,
    "y": 460,
    "wires": [
    [
    "e47cbdc7b7dae23f"
    ]
    ]
    },
    {
    "id": "e47cbdc7b7dae23f",
    "type": "function",
    "z": "88fed08889390f28",
    "name": "Aggregate & Publish (Flat)",
    "func": "const CONFIG = {\n fieldMapping: {\n \"JointChanger\": \"J_Changer\",\n \"JointDoors\": \"J_Door\",\n \"JointFinger\": \"J_Finger\",\n \"JointSpindle\": \"J_Spindle\",\n \"JointTailStock\": \"J_TailStock\",\n \"Joint_X\": \"J_X\",\n \"Joint_Y\": \"J_Y\",\n \"Joint_Z\": \"J_Z\"\n},\n missingStrategy: 'lastValue',\n outputTopic: \"Metric/porcess_prolathe_gs_260ms_1\",\n windowMs: 100\n};\n\nconst FIELDS = Object.keys(CONFIG.fieldMapping);\n\nlet cache = flow.get('dataCache') || { instant: {}, metadata: {} };\nlet lastValues = flow.get('lastValues') || {};\nlet lastMetadata = flow.get('lastMetadata') || {};\nlet output = {};\n\noutput.timestamp = new Date().toISOString();\n\nfor (const field of FIELDS) {\n const shortName = CONFIG.fieldMapping[field];\n\n if (cache.instant[field] !== undefined) {\n output[shortName + '_Cur'] = Number(cache.instant[field]);\n output[shortName + '_Init'] = cache.metadata[field] && cache.metadata[field].initialValue !== undefined\n ? Number(cache.metadata[field].initialValue)\n : 0;\n lastValues[field] = cache.instant[field];\n lastMetadata[field] = cache.metadata[field] || {};\n } else {\n let fallbackValue = CONFIG.missingStrategy === 'lastValue'\n ? (lastValues[field] !== undefined ? lastValues[field] : 0)\n : 0;\n output[shortName + '_Cur'] = Number(fallbackValue);\n output[shortName + '_Init'] = lastMetadata[field] && lastMetadata[field].initialValue !== undefined\n ? Number(lastMetadata[field].initialValue)\n : 0;\n }\n}\n\nmsg.payload = output;\nmsg.topic = CONFIG.outputTopic;\n\nflow.set('dataCache', { instant: {}, metadata: {} });\nflow.set('lastValues', lastValues);\nflow.set('lastMetadata', lastMetadata);\n\nreturn msg;",
    "outputs": 1,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 740,
    "y": 460,
    "wires": [
    [
    "ad22bd2f8b228324"
    ]
    ]
    },
    {
    "id": "ad22bd2f8b228324",
    "type": "mqtt out",
    "z": "88fed08889390f28",
    "name": "Metric/Prolathe",
    "topic": "Metric/ProLathe-01",
    "qos": "1",
    "retain": "false",
    "respTopic": "",
    "contentType": "",
    "userProps": "",
    "correl": "",
    "expiry": "",
    "broker": "c14ef68b0e3b48ac",
    "x": 1060,
    "y": 360,
    "wires": []
    },
    {
    "id": "8a8a5572465dac5b",
    "type": "debug",
    "z": "88fed08889390f28",
    "name": "Debug Output",
    "active": true,
    "tosidebar": true,
    "console": false,
    "complete": "payload",
    "statusVal": "",
    "statusType": "auto",
    "x": 980,
    "y": 540,
    "wires": []
    },
    {
    "id": "6f8ade8021b85933",
    "type": "mqtt-broker",
    "name": "supos-demo.supos.app",
    "broker": "supos-demo.supos.app",
    "port": 1883,
    "clientid": "",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": 4,
    "keepalive": 60,
    "cleansession": true,
    "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": "c14ef68b0e3b48ac",
    "type": "mqtt-broker",
    "z": "88fed08889390f28",
    "name": "emqx",
    "broker": "mqtt.pre.tier0.dev",
    "port": "1883",
    "clientid": "336462685553088&337035452420768",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": "4",
    "keepalive": "60",
    "cleansession": true,
    "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": ""
    }
    ]
    • Lathe 2
    Show Lathe 2 flow JSON
    Terminal window
    [
    {
    "id": "c974bfc5fbc79b0e",
    "type": "tab",
    "label": "Metric/ProLathe-02",
    "disabled": false,
    "info": ""
    },
    {
    "id": "2ff367f08cbe470f",
    "type": "mqtt in",
    "z": "c974bfc5fbc79b0e",
    "name": "Subscribe ProLathe joints",
    "topic": "FactorySim_Supos/Flow_Group__1/MechEquips/porcess_prolathe_gs_260ms_2/Motor_Joints/+",
    "qos": "1",
    "datatype": "auto",
    "broker": "6f8ade8021b85933",
    "nl": false,
    "rap": true,
    "rh": 0,
    "inputs": 0,
    "x": 590,
    "y": 340,
    "wires": [
    [
    "7c54f7852fb5430e"
    ]
    ]
    },
    {
    "id": "7c54f7852fb5430e",
    "type": "function",
    "z": "c974bfc5fbc79b0e",
    "name": "Cache Data",
    "func": "const CONFIG = {\n fields: [\"JointChanger\", \"JointDoors\", \"JointFinger\", \"JointSpindle\", \"JointTailStock\", \"Joint_X\", \"Joint_Y\", \"Joint_Z\"],\n payloadFields: {\n value: 'CurrentValue',\n initialValue: 'InitialValue',\n name: 'Name',\n timeStamp: 'timeStamp',\n time_stamp: 'time_stamp',\n quality: 'quality'\n }\n};\n\nlet cache = flow.get('dataCache') || { instant: {}, metadata: {} };\n\nconst topicParts = msg.topic.split('/');\nconst fieldName = topicParts[topicParts.length - 1];\n\nlet payload = msg.payload;\nif (typeof payload === 'string') {\n try {\n payload = JSON.parse(payload);\n } catch (e) {\n return null;\n }\n}\n\nlet value = payload[CONFIG.payloadFields.value];\nif (value === undefined) return null;\n\nconst metadata = {\n timeStamp: payload[CONFIG.payloadFields.timeStamp],\n time_stamp: payload[CONFIG.payloadFields.time_stamp],\n quality: payload[CONFIG.payloadFields.quality],\n name: payload[CONFIG.payloadFields.name],\n initialValue: payload[CONFIG.payloadFields.initialValue]\n};\n\nif (CONFIG.fields.includes(fieldName)) {\n cache.instant[fieldName] = value;\n cache.metadata[fieldName] = metadata;\n} else {\n return null;\n}\n\nflow.set('dataCache', cache);\nreturn null;",
    "outputs": 0,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 860,
    "y": 360,
    "wires": []
    },
    {
    "id": "a69b0f994c614213",
    "type": "inject",
    "z": "c974bfc5fbc79b0e",
    "name": "100ms Timer",
    "props": [
    {
    "p": "payload"
    },
    {
    "vt": "str",
    "p": "topic"
    }
    ],
    "repeat": "0.1",
    "crontab": "",
    "once": true,
    "onceDelay": "0.1",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 500,
    "y": 460,
    "wires": [
    [
    "a182dc40725e4801"
    ]
    ]
    },
    {
    "id": "a182dc40725e4801",
    "type": "function",
    "z": "c974bfc5fbc79b0e",
    "name": "Aggregate & Publish (Flat)",
    "func": "const CONFIG = {\n fieldMapping: {\n \"JointChanger\": \"J_Changer\",\n \"JointDoors\": \"J_Door\",\n \"JointFinger\": \"J_Finger\",\n \"JointSpindle\": \"J_Spindle\",\n \"JointTailStock\": \"J_TailStock\",\n \"Joint_X\": \"J_X\",\n \"Joint_Y\": \"J_Y\",\n \"Joint_Z\": \"J_Z\"\n},\n missingStrategy: 'lastValue',\n outputTopic: \"Metric/porcess_prolathe_gs_260ms_2\",\n windowMs: 100\n};\n\nconst FIELDS = Object.keys(CONFIG.fieldMapping);\n\nlet cache = flow.get('dataCache') || { instant: {}, metadata: {} };\nlet lastValues = flow.get('lastValues') || {};\nlet lastMetadata = flow.get('lastMetadata') || {};\nlet output = {};\n\noutput.timestamp = new Date().toISOString();\n\nfor (const field of FIELDS) {\n const shortName = CONFIG.fieldMapping[field];\n\n if (cache.instant[field] !== undefined) {\n output[shortName + '_Cur'] = Number(cache.instant[field]);\n output[shortName + '_Init'] = cache.metadata[field] && cache.metadata[field].initialValue !== undefined\n ? Number(cache.metadata[field].initialValue)\n : 0;\n lastValues[field] = cache.instant[field];\n lastMetadata[field] = cache.metadata[field] || {};\n } else {\n let fallbackValue = CONFIG.missingStrategy === 'lastValue'\n ? (lastValues[field] !== undefined ? lastValues[field] : 0)\n : 0;\n output[shortName + '_Cur'] = Number(fallbackValue);\n output[shortName + '_Init'] = lastMetadata[field] && lastMetadata[field].initialValue !== undefined\n ? Number(lastMetadata[field].initialValue)\n : 0;\n }\n}\n\nmsg.payload = output;\n\nmsg.payload.Model_Name = \"Turbo\";\nmsg.payload.Max_Carry_Weight_kg = 15.0;\n\nmsg.topic = CONFIG.outputTopic;\n\nflow.set('dataCache', { instant: {}, metadata: {} });\nflow.set('lastValues', lastValues);\nflow.set('lastMetadata', lastMetadata);\n\nreturn msg;",
    "outputs": 1,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 740,
    "y": 460,
    "wires": [
    [
    "b4e1817aa3744ab9",
    "84c88149d8d24eae"
    ]
    ]
    },
    {
    "id": "b4e1817aa3744ab9",
    "type": "mqtt out",
    "z": "c974bfc5fbc79b0e",
    "name": "Metric/Prolathe2",
    "topic": "Metric/ProLathe-02",
    "qos": "1",
    "retain": "false",
    "respTopic": "",
    "contentType": "",
    "userProps": "",
    "correl": "",
    "expiry": "",
    "broker": "6befe0f78bf64759",
    "x": 1060,
    "y": 360,
    "wires": []
    },
    {
    "id": "84c88149d8d24eae",
    "type": "debug",
    "z": "c974bfc5fbc79b0e",
    "name": "Debug Output",
    "active": true,
    "tosidebar": true,
    "console": false,
    "complete": "payload",
    "statusVal": "",
    "statusType": "auto",
    "x": 980,
    "y": 540,
    "wires": []
    },
    {
    "id": "6f8ade8021b85933",
    "type": "mqtt-broker",
    "name": "supos-demo.supos.app",
    "broker": "supos-demo.supos.app",
    "port": 1883,
    "clientid": "",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": 4,
    "keepalive": 60,
    "cleansession": true,
    "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": "6befe0f78bf64759",
    "type": "mqtt-broker",
    "z": "c974bfc5fbc79b0e",
    "name": "emqx",
    "broker": "mqtt.pre.tier0.dev",
    "port": "1883",
    "clientid": "336462685553088&338098843755632",
    "autoConnect": true,
    "usetls": false,
    "protocolVersion": "4",
    "keepalive": "60",
    "cleansession": true,
    "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": ""
    }
    ]
  1. In Model, import models exist in the workshop, map UNS data to and create instances for them.
  2. Go to Scene and add instances in the workshop to a scene from Instance Library.
  3. Move models to their positions in the workshop and set environment lighting and other details.