[
    {
        "id": "a1f0c0de00000001",
        "type": "tab",
        "label": "FaultDemo supervisor",
        "disabled": false,
        "info": "Subscribes to the FaultDemo task on a B&R PLC over OPC UA, logs fault events, and resets a latched fault once the PLC reports the condition has gone."
    },
    {
        "id": "a1f0c0de000000e1",
        "type": "OpcUa-Endpoint",
        "name": "FaultDemo PLC",
        "endpoint": "${OPCUA_ENDPOINT}",
        "secpol": "None",
        "secmode": "None",
        "none": true,
        "login": false,
        "usercert": false,
        "usercertificate": "",
        "userprivatekey": ""
    },
    {
        "id": "a1f0c0de00000c01",
        "type": "comment",
        "z": "a1f0c0de00000001",
        "name": "1. Subscribe to the PLC status",
        "info": "",
        "x": 170,
        "y": 40,
        "wires": []
    },
    {
        "id": "a1f0c0de00000101",
        "type": "inject",
        "z": "a1f0c0de00000001",
        "name": "subscribe at start",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": "2",
        "topic": "",
        "payload": "",
        "payloadType": "str",
        "x": 140,
        "y": 140,
        "wires": [
            [
                "a1f0c0de00000201",
                "a1f0c0de00000202",
                "a1f0c0de00000203",
                "a1f0c0de00000204",
                "a1f0c0de00000205"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000201",
        "type": "OpcUa-Item",
        "z": "a1f0c0de00000001",
        "item": "ns=6;s=::FaultDemo:task.status.fault",
        "datatype": "Boolean",
        "value": "",
        "name": "status.fault",
        "x": 360,
        "y": 60,
        "wires": [
            [
                "a1f0c0de00000301"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000202",
        "type": "OpcUa-Item",
        "z": "a1f0c0de00000001",
        "item": "ns=6;s=::FaultDemo:task.status.conditionActive",
        "datatype": "Boolean",
        "value": "",
        "name": "status.conditionActive",
        "x": 360,
        "y": 100,
        "wires": [
            [
                "a1f0c0de00000301"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000203",
        "type": "OpcUa-Item",
        "z": "a1f0c0de00000001",
        "item": "ns=6;s=::FaultDemo:task.status.faultCode",
        "datatype": "UInt16",
        "value": "",
        "name": "status.faultCode",
        "x": 360,
        "y": 140,
        "wires": [
            [
                "a1f0c0de00000301"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000204",
        "type": "OpcUa-Item",
        "z": "a1f0c0de00000001",
        "item": "ns=6;s=::FaultDemo:task.status.faultCount",
        "datatype": "UInt32",
        "value": "",
        "name": "status.faultCount",
        "x": 360,
        "y": 180,
        "wires": [
            [
                "a1f0c0de00000301"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000205",
        "type": "OpcUa-Item",
        "z": "a1f0c0de00000001",
        "item": "ns=6;s=::FaultDemo:task.status.temperature",
        "datatype": "Float",
        "value": "",
        "name": "status.temperature",
        "x": 360,
        "y": 220,
        "wires": [
            [
                "a1f0c0de00000301"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000301",
        "type": "OpcUa-Client",
        "z": "a1f0c0de00000001",
        "endpoint": "a1f0c0de000000e1",
        "action": "subscribe",
        "deadbandtype": "a",
        "deadbandvalue": 1,
        "time": 250,
        "timeUnit": "ms",
        "certificate": "n",
        "localfile": "",
        "localkeyfile": "",
        "securitymode": "None",
        "securitypolicy": "None",
        "useTransport": false,
        "maxChunkCount": 1,
        "maxMessageSize": 8192,
        "receiveBufferSize": 8192,
        "sendBufferSize": 8192,
        "setstatusandtime": false,
        "keepsessionalive": false,
        "name": "PLC (subscribe, write)",
        "applicationName": "",
        "applicationUri": "",
        "x": 590,
        "y": 140,
        "wires": [
            [
                "a1f0c0de00000401"
            ],
            [
                "a1f0c0de00000701"
            ],
            []
        ]
    },
    {
        "id": "a1f0c0de00000701",
        "type": "rbe",
        "z": "a1f0c0de00000001",
        "name": "status changes",
        "func": "rbe",
        "gap": "",
        "start": "",
        "inout": "out",
        "septopics": false,
        "property": "status",
        "topi": "topic",
        "x": 810,
        "y": 60,
        "wires": [
            [
                "a1f0c0de00000601"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000c02",
        "type": "comment",
        "z": "a1f0c0de00000001",
        "name": "2. Decide: log faults, reset once the condition has gone",
        "info": "",
        "x": 870,
        "y": 260,
        "wires": []
    },
    {
        "id": "a1f0c0de00000102",
        "type": "inject",
        "z": "a1f0c0de00000001",
        "name": "tick 250 ms",
        "props": [
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "0.25",
        "crontab": "",
        "once": true,
        "onceDelay": "3",
        "topic": "tick",
        "x": 590,
        "y": 220,
        "wires": [
            [
                "a1f0c0de00000401"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000401",
        "type": "function",
        "z": "a1f0c0de00000001",
        "name": "supervisor",
        "func": "// Supervises the FaultDemo task on the PLC.\n// In:    data changes from the OPC UA subscription (msg.topic = NodeId), and a 'tick' every 250 ms\n// Out 1: the reset command, back to the OPC UA client as a write\n// Out 2: log lines, to the log file and the debug sidebar\nconst NS = 'ns=6;s=::FaultDemo:task.';\nconst SETTLE_MS = 1000;      // the condition must stay clear this long before we reset\nconst RETRY_MS = 5000;       // resend a reset the PLC did not act on\nconst FAULT_DETAIL_MS = 300; // fault, faultCode and faultCount change in one PLC scan but arrive\n                             // as separate notifications: read the details this long after the edge\nconst FAULT_TEXT = { 1: 'over-temperature', 2: 'forced' };\n\nif (msg.topic !== 'tick' && !String(msg.topic).startsWith(NS + 'status.')) {\n    return null; // the subscriptionId message, and the results of our own writes\n}\n\nconst plc = context.get('plc') || {};\nconst now = Date.now();\nconst lines = [];   // [time, text]\nlet reset = null;\n\nif (msg.topic !== 'tick') {\n    const name = msg.topic.slice(NS.length);   // e.g. 'status.fault'\n    const before = plc[name];\n    plc[name] = msg.payload;\n    context.set('plc', plc);\n\n    if (name === 'status.fault' && msg.payload !== before) {\n        if (msg.payload === true) {\n            context.set('faultSince', now);\n            context.set('faultDetailDue', now + FAULT_DETAIL_MS);\n            // Restart the settle timer: conditionActive may still arrive in a later notification\n            context.set('clearSince', plc['status.conditionActive'] === false ? now : null);\n        } else if (before === true) {\n            const seconds = (now - context.get('faultSince')) / 1000;\n            lines.push([now, `CLEARED  fault cleared after ${seconds.toFixed(1)} s`]);\n            context.set('resetSentAt', null);\n        }\n    }\n\n    if (name === 'status.conditionActive' && msg.payload !== before) {\n        context.set('clearSince', msg.payload === false ? now : null);\n        if (msg.payload === false && plc['status.fault'] === true) {\n            lines.push([now, `OK       condition gone, resetting in ${SETTLE_MS} ms`]);\n        }\n    }\n}\n\n// FAULT: log the details once the rest of the PLC scan has arrived\nconst faultDetailDue = context.get('faultDetailDue');\nif (faultDetailDue && now >= faultDetailDue && plc['status.fault'] === true) {\n    context.set('faultDetailDue', null);\n    const code = plc['status.faultCode'];\n    const temperature = plc['status.temperature'];\n    lines.unshift([context.get('faultSince'),\n        `FAULT    code=${code} (${FAULT_TEXT[code] || 'unknown'}), ` +\n        `temperature ${temperature?.toFixed(1)} C, fault #${plc['status.faultCount']}`]);\n}\n\n// RESET: the fault is latched and the condition has stayed clear for SETTLE_MS\nconst clearSince = context.get('clearSince');\nconst resetSentAt = context.get('resetSentAt');\nif (plc['status.fault'] === true && plc['status.conditionActive'] === false &&\n    clearSince && now - clearSince >= SETTLE_MS &&\n    (!resetSentAt || now - resetSentAt >= RETRY_MS)) {\n    context.set('resetSentAt', now);\n    reset = { action: 'write', topic: NS + 'command.reset', datatype: 'Boolean', payload: true };\n    lines.push([now, 'RESET    writing command.reset']);\n}\n\nreturn [reset, lines.map(([time, text]) => ({ payload: `${new Date(time).toISOString()} ${text}` }))];\n",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "outputLabels": [
            "reset command",
            "log line"
        ],
        "x": 810,
        "y": 160,
        "wires": [
            [
                "a1f0c0de00000301"
            ],
            [
                "a1f0c0de00000501",
                "a1f0c0de00000602"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000501",
        "type": "file",
        "z": "a1f0c0de00000001",
        "name": "fault-events.log",
        "filename": "/logs/fault-events.log",
        "filenameType": "str",
        "appendNewline": true,
        "createDir": true,
        "overwriteFile": "false",
        "encoding": "none",
        "x": 1020,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "a1f0c0de00000602",
        "type": "debug",
        "z": "a1f0c0de00000001",
        "name": "log",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 990,
        "y": 200,
        "wires": []
    },
    {
        "id": "a1f0c0de00000601",
        "type": "debug",
        "z": "a1f0c0de00000001",
        "name": "client status",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "status",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1010,
        "y": 60,
        "wires": []
    },
    {
        "id": "a1f0c0de00000c03",
        "type": "comment",
        "z": "a1f0c0de00000001",
        "name": "3. Manual test: hold a forced fault",
        "info": "Force ON latches a fault with code 2. Node-RED resets it about a second after Force OFF.",
        "x": 170,
        "y": 300,
        "wires": []
    },
    {
        "id": "a1f0c0de00000103",
        "type": "inject",
        "z": "a1f0c0de00000001",
        "name": "force fault ON",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "action",
                "v": "write",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "true",
        "payloadType": "bool",
        "x": 140,
        "y": 360,
        "wires": [
            [
                "a1f0c0de00000206"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000104",
        "type": "inject",
        "z": "a1f0c0de00000001",
        "name": "force fault OFF",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "action",
                "v": "write",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "false",
        "payloadType": "bool",
        "x": 140,
        "y": 400,
        "wires": [
            [
                "a1f0c0de00000206"
            ]
        ]
    },
    {
        "id": "a1f0c0de00000206",
        "type": "OpcUa-Item",
        "z": "a1f0c0de00000001",
        "item": "ns=6;s=::FaultDemo:task.command.forceFault",
        "datatype": "Boolean",
        "value": "",
        "name": "command.forceFault",
        "x": 360,
        "y": 380,
        "wires": [
            [
                "a1f0c0de00000301"
            ]
        ]
    }
]
