[
    {
        "id": "308dc674bb303c04",
        "type": "inject",
        "z": "e3c5d5c9d69b398e",
        "name": "Init (once at startup)",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.5,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 130,
        "y": 80,
        "wires": [
            [
                "8987745bbeac9f5d",
                "522d515b932d1819",
                "77f482322c776fb8"
            ]
        ]
    },
    {
        "id": "8987745bbeac9f5d",
        "type": "file in",
        "z": "e3c5d5c9d69b398e",
        "name": "Read challenge3.csv",
        "filename": "/data/challenge3.csv",
        "filenameType": "str",
        "format": "utf8",
        "chunk": false,
        "sendError": false,
        "encoding": "utf8",
        "allProps": false,
        "x": 360,
        "y": 60,
        "wires": [
            [
                "2ee1f44671673e30"
            ]
        ]
    },
    {
        "id": "2ee1f44671673e30",
        "type": "csv",
        "z": "e3c5d5c9d69b398e",
        "name": "CSV → array of objects",
        "sep": ",",
        "hdrin": true,
        "hdrout": "none",
        "multi": "mult",
        "ret": "\\r\\n",
        "temp": "",
        "skip": "0",
        "strings": true,
        "include_empty_strings": true,
        "include_null_values": false,
        "x": 600,
        "y": 60,
        "wires": [
            [
                "6970768b9888be2a"
            ]
        ]
    },
    {
        "id": "6970768b9888be2a",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Index rows by Packet Number",
        "func": "// msg.payload dovrebbe essere un array di oggetti riga.\n// Per sicurezza gestiamo anche il caso di singola riga.\nconst rows = Array.isArray(msg.payload) ? msg.payload : (msg.payload ? [msg.payload] : []);\nconst byPkt = {};\n\nfor (const r of rows) {\n    if (!r || typeof r !== 'object') continue;\n\n    // Supporta intestazioni leggermente diverse / spazi accidentali.\n    const pktRaw = r['Packet Number'] ?? r['PacketNumber'] ?? r['packet number'];\n    const pkt = parseInt(pktRaw, 10);\n\n    if (!isNaN(pkt)) {\n        byPkt[pkt] = r;\n    }\n}\n\nflow.set('rows_by_pkt', byPkt);\nflow.set('rows_count', Object.keys(byPkt).length);\nnode.warn('challenge3.csv loaded: ' + Object.keys(byPkt).length + ' rows indexed by Packet Number');\n\nreturn null;\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 880,
        "y": 60,
        "wires": [
            []
        ]
    },
    {
        "id": "522d515b932d1819",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Reset counters + write CSV headers",
        "func": "// reset stato persistente\nflow.set('idlog_no', 0);\nflow.set('filt_no', 0);\nflow.set('rcv', 0);\nflow.set('flushed', false);\nflow.set('linkmap', {});\nflow.set('stop', false);\n\n// genera 3 messaggi per scrivere headers (overwrite)\nconst h1 = { filename: '/data/id_log.csv',         payload: 'No.,ID,TIMESTAMP\\n' };\nconst h2 = { filename: '/data/filtered_elems.csv', payload: 'No.,Timestamp,Sequence Number,Attribute,Status,Data Type,Data Value\\n' };\nconst h3 = { filename: '/data/outgoing_cost.csv',  payload: 'No.,Source,Destination,Cost\\n' };\nreturn [[h1, h2, h3]];\n",
        "outputs": 1,
        "noerr": 0,
        "x": 360,
        "y": 140,
        "wires": [
            [
                "be703105c59d4138"
            ]
        ]
    },
    {
        "id": "77f482322c776fb8",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Reset dashboard charts",
        "func": "// Pulisce i dati storici dei grafici della Dashboard ad ogni Init.\nnode.status({ fill: 'blue', shape: 'dot', text: 'charts reset' });\nreturn [\n    { payload: [], topic: 'RMS Voltage' },\n    { payload: [], topic: 'RMS Current' }\n];\n",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 360,
        "y": 190,
        "wires": [
            [
                "e65085035ca74bc0"
            ],
            [
                "18af4942488285b9"
            ]
        ]
    },
    {
        "id": "be703105c59d4138",
        "type": "file",
        "z": "e3c5d5c9d69b398e",
        "name": "Write header (overwrite)",
        "filename": "filename",
        "filenameType": "msg",
        "appendNewline": false,
        "createDir": true,
        "overwriteFile": "true",
        "encoding": "utf8",
        "x": 660,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "9652e9978262bb40",
        "type": "inject",
        "z": "e3c5d5c9d69b398e",
        "name": "Tick 1s",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "1",
        "crontab": "",
        "once": false,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 110,
        "y": 240,
        "wires": [
            [
                "c3fdac30bd0e6d6f"
            ]
        ]
    },
    {
        "id": "c3fdac30bd0e6d6f",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Generate id+ts (stop after 200 rcv)",
        "func": "// stop generator se subscriber ha già ricevuto 200 (sync con stop globale)\nif (flow.get('stop')) { return null; }\n\nconst id = Math.floor(Math.random() * 30001);  // 0..30000 inclusi\nconst ts = Math.floor(Date.now() / 1000);      // UNIX seconds\n\nconst payloadObj = { id: id, timestamp: ts };\n\n// out 1: MQTT publish\nconst mqttMsg = {\n    topic: 'challenge3/id_generator',\n    payload: JSON.stringify(payloadObj),\n    qos: 0,\n    retain: false\n};\n\n// out 2: CSV log\nlet no = (flow.get('idlog_no') || 0) + 1;\nflow.set('idlog_no', no);\nconst csvMsg = {\n    filename: '/data/id_log.csv',\n    payload: `${no},${id},${ts}\\n`\n};\n\nreturn [mqttMsg, csvMsg];\n",
        "outputs": 2,
        "noerr": 0,
        "x": 360,
        "y": 240,
        "wires": [
            [
                "49c3a3f5ab5f4f0e"
            ],
            [
                "7f400d33a6b4a9c0"
            ]
        ]
    },
    {
        "id": "49c3a3f5ab5f4f0e",
        "type": "mqtt out",
        "z": "e3c5d5c9d69b398e",
        "name": "MQTT pub challenge3/id_generator",
        "topic": "",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "mqtt_broker_local",
        "x": 700,
        "y": 220,
        "wires": []
    },
    {
        "id": "7f400d33a6b4a9c0",
        "type": "file",
        "z": "e3c5d5c9d69b398e",
        "name": "Append id_log.csv",
        "filename": "filename",
        "filenameType": "msg",
        "appendNewline": false,
        "createDir": true,
        "overwriteFile": "false",
        "encoding": "utf8",
        "x": 670,
        "y": 280,
        "wires": [
            []
        ]
    },
    {
        "id": "5030a7f40df4b44f",
        "type": "mqtt in",
        "z": "e3c5d5c9d69b398e",
        "name": "MQTT sub challenge3/id_generator",
        "topic": "challenge3/id_generator",
        "qos": "0",
        "datatype": "json",
        "broker": "mqtt_broker_local",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 200,
        "y": 400,
        "wires": [
            [
                "6b6a612e0847d71f"
            ]
        ]
    },
    {
        "id": "6b6a612e0847d71f",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Counter + 200-stop",
        "func": "// incrementa counter (anche per messaggi che verranno ignorati)\nlet n = (flow.get('rcv') || 0) + 1;\nif (n > 200) {\n    // safety: già stoppato\n    return null;\n}\nflow.set('rcv', n);\n\n// payload arriva già come oggetto perché mqtt-in ha datatype=json\nconst sub = msg.payload;\nif (!sub || typeof sub.id === 'undefined') {\n    node.warn('Bad MQTT payload: ' + JSON.stringify(msg.payload));\n    // se mal formato, conta lo stesso ma non procede\n    if (n === 200) {\n        flow.set('stop', true);\n        return [null, { trigger: true, _at: 'badpayload' }];\n    }\n    return null;\n}\n\nmsg.subId = sub.id;\nmsg.subTs = sub.timestamp;\nmsg.rcvN  = n;\n\n// se questo è il 200esimo, prepariamo trigger flush dopo aver processato\nlet flushMsg = null;\nif (n === 200) {\n    flow.set('stop', true);\n    flushMsg = { trigger: true };\n}\n\nreturn [msg, flushMsg];\n",
        "outputs": 2,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 450,
        "y": 400,
        "wires": [
            [
                "551889fbd5392c4d"
            ],
            [
                "4d8f6d3b398671f1"
            ]
        ]
    },
    {
        "id": "551889fbd5392c4d",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Compute N + lookup row",
        "func": "const id = Number(msg.subId);\nif (!Number.isFinite(id)) {\n    node.warn('Invalid id: ' + msg.subId);\n    return null;\n}\n\nconst N0 = id % 5218;\nconst rows = flow.get('rows_by_pkt') || {};\n\n// Alcuni dataset numerano Packet Number da 1 a 5218, altri da 0.\n// Proviamo prima N, poi N+1 come fallback.\nlet N = N0;\nlet row = rows[N];\nif (!row && rows[N0 + 1]) {\n    N = N0 + 1;\n    row = rows[N];\n}\n\nif (!row) {\n    node.warn('No row for N=' + N0 + ' or N+1=' + (N0 + 1) + '. rows_count=' + (flow.get('rows_count') || 0));\n    return null;\n}\n\nmsg.N      = N;\nmsg.row    = row;\nmsg.cmdStr = row['Command String'] || '';\nmsg.pktTyp = row['Packet Type'] || '';\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "x": 710,
        "y": 400,
        "wires": [
            [
                "972d2531c710fe8e"
            ]
        ]
    },
    {
        "id": "972d2531c710fe8e",
        "type": "switch",
        "z": "e3c5d5c9d69b398e",
        "name": "ZBEE_ZCL? / LinkStatus? / else",
        "property": "cmdStr",
        "propertyType": "msg",
        "rules": [
            {
                "t": "cont",
                "v": "Layer ZBEE_ZCL",
                "vt": "str"
            },
            {
                "t": "regex",
                "v": "Link Status \\(0x08\\)",
                "vt": "str",
                "case": false
            },
            {
                "t": "else"
            }
        ],
        "checkall": "false",
        "repair": false,
        "outputs": 3,
        "x": 970,
        "y": 400,
        "wires": [
            [
                "745d50a6355a3596",
                "9f1b0c70499de243"
            ],
            [
                "cbfe21bd7cbfddec"
            ],
            []
        ]
    },
    {
        "id": "745d50a6355a3596",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Build ZCL publish payload",
        "func": "const r = msg.row;\nconst ts = Math.floor(Date.now() / 1000);\nconst devName = r['Device Name ZigBee Source'] || '';\n\nconst payloadObj = {\n    timestamp: ts,\n    id: msg.subId,\n    'wpan.src':  r['Source Address']           || '',\n    'wpan.dst':  r['Destination Address']      || '',\n    'zbee.src':  r['Source Address ZigBee']    || '',\n    'zbee.dst':  r['Destination Address ZigBee'] || '',\n    'topic':     'ZigBee/' + devName,\n    'payload':   r['Command String']           || ''\n};\n\nmsg.payload = JSON.stringify(payloadObj);\nmsg.topic   = devName;  // topic MQTT letterale = Device Name ZigBee Source\nmsg.qos     = 0;\nmsg.retain  = false;\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "x": 1250,
        "y": 360,
        "wires": [
            [
                "ae6205465959b3ce"
            ]
        ]
    },
    {
        "id": "ae6205465959b3ce",
        "type": "delay",
        "z": "e3c5d5c9d69b398e",
        "name": "Rate 10 msg/min (queue)",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "10",
        "nbRateUnits": "1",
        "rateUnits": "minute",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 1520,
        "y": 360,
        "wires": [
            [
                "16fdb7350daec8c8"
            ]
        ]
    },
    {
        "id": "16fdb7350daec8c8",
        "type": "mqtt out",
        "z": "e3c5d5c9d69b398e",
        "name": "MQTT pub <DeviceName>",
        "topic": "",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "mqtt_broker_local",
        "x": 1770,
        "y": 360,
        "wires": []
    },
    {
        "id": "9f1b0c70499de243",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Filter Active/RMS attrs",
        "func": "function parseCmd(s) {\n    if (!s) return null;\n    if (typeof s === 'object') return s;\n\n    // Command String nel dataset è spesso simile a un dict Python con apici singoli.\n    const normalized = String(s)\n        .replace(/\\bNone\\b/g, 'null')\n        .replace(/\\bTrue\\b/g, 'true')\n        .replace(/\\bFalse\\b/g, 'false')\n        .replace(/'/g, '\"');\n\n    return JSON.parse(normalized);\n}\n\nlet cmd;\ntry {\n    cmd = parseCmd(msg.cmdStr || '');\n} catch (e) {\n    node.warn('ZCL parse fail: ' + e.message + ' | ' + (msg.cmdStr || '').slice(0, 160));\n    return null;\n}\n\nif (!cmd || typeof cmd !== 'object') return null;\n\nconst attrs = cmd['Attribute'];\nif (!Array.isArray(attrs)) return null;\n\nconst targets = ['Active Power', 'RMS Current', 'RMS Voltage'];\n\nconst statuses = cmd['Status'] || [];\nconst dtypes   = cmd['Data Type'] || [];\nconst seq      = cmd['Sequence Number'] || '';\nconst ts       = Math.floor(Date.now() / 1000);\n\nconst dtypeKey = {\n    '16-Bit Signed Integer':   'Int16',\n    '16-Bit Unsigned Integer': 'Uint16',\n    '8-Bit Unsigned Integer':  'Uint8',\n    '8-Bit Signed Integer':    'Int8',\n    'Boolean':                 'Boolean',\n    '32-Bit Unsigned Integer': 'Uint32',\n    '32-Bit Signed Integer':   'Int32'\n};\n\nconst stripParen = s => (s || '').toString().replace(/\\s*\\(0x[^)]*\\)/, '').trim();\n\nconst getAt = (x, i) => {\n    if (Array.isArray(x)) return x[i] ?? '';\n    return x ?? '';\n};\n\nconst offsets = {};\nconst out = [];\n\nfor (let i = 0; i < attrs.length; i++) {\n    const attrName   = stripParen(attrs[i]);\n    const dtypeName  = stripParen(getAt(dtypes, i));\n    const statusName = stripParen(getAt(statuses, i));\n\n    const key = dtypeKey[dtypeName];\n\n    let valStr = '';\n    if (key) {\n        const off = offsets[key] || 0;\n        const raw = cmd[key];\n\n        if (Array.isArray(raw)) {\n            valStr = raw[off] ?? '';\n        } else if (raw !== undefined) {\n            valStr = off === 0 ? raw : '';\n        }\n\n        offsets[key] = off + 1;\n    }\n\n    const m = valStr.toString().match(/-?\\d+(?:\\.\\d+)?/);\n    const val = m ? m[0] : '';\n\n    // Scrive solo righe complete e utili.\n    if (targets.includes(attrName) && statusName && dtypeName && val !== '') {\n        out.push({\n            ts: ts,\n            seq: seq,\n            attr: attrName,\n            status: statusName,\n            dtype: dtypeName,\n            val: val\n        });\n    }\n}\n\nif (out.length === 0) return null;\n\nmsg.filtered = out;\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "x": 1250,
        "y": 460,
        "wires": [
            [
                "384f50d643e94f43",
                "81bfbf6fe91c3436"
            ]
        ]
    },
    {
        "id": "384f50d643e94f43",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Build CSV rows + append",
        "func": "let no = flow.get('filt_no') || 0;\nlet lines = '';\n\nconst csvEscape = v => {\n    const s = (v ?? '').toString();\n    if (/[\",\\n\\r]/.test(s)) return '\"' + s.replace(/\"/g, '\"\"') + '\"';\n    return s;\n};\n\nfor (const e of msg.filtered || []) {\n    no += 1;\n    lines += [\n        no,\n        e.ts,\n        e.seq,\n        e.attr,\n        e.status,\n        e.dtype,\n        e.val\n    ].map(csvEscape).join(',') + '\\n';\n}\n\nif (!lines) return null;\n\nflow.set('filt_no', no);\nmsg.payload  = lines;\nmsg.filename = '/data/filtered_elems.csv';\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "x": 1520,
        "y": 440,
        "wires": [
            [
                "f481e9e532238d50"
            ]
        ]
    },
    {
        "id": "f481e9e532238d50",
        "type": "file",
        "z": "e3c5d5c9d69b398e",
        "name": "Append filtered_elems.csv",
        "filename": "filename",
        "filenameType": "msg",
        "appendNewline": false,
        "createDir": true,
        "overwriteFile": "false",
        "encoding": "utf8",
        "x": 1780,
        "y": 440,
        "wires": [
            []
        ]
    },
    {
        "id": "81bfbf6fe91c3436",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Split → Voltage / Current msgs",
        "func": "const vMsgs = [];\nconst iMsgs = [];\nfor (const e of msg.filtered) {\n    const v = parseFloat(e.val);\n    if (isNaN(v)) continue;\n    if (e.attr === 'RMS Voltage') vMsgs.push({ topic: 'RMS Voltage', payload: v });\n    else if (e.attr === 'RMS Current') iMsgs.push({ topic: 'RMS Current', payload: v });\n}\nreturn [vMsgs, iMsgs];\n",
        "outputs": 2,
        "noerr": 0,
        "x": 1520,
        "y": 520,
        "wires": [
            [
                "e65085035ca74bc0"
            ],
            [
                "18af4942488285b9"
            ]
        ]
    },
    {
        "id": "e65085035ca74bc0",
        "type": "ui_chart",
        "z": "e3c5d5c9d69b398e",
        "name": "RMS Voltage",
        "group": "dash_group",
        "order": 1,
        "width": 12,
        "height": 6,
        "label": "RMS Voltage",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": "30",
        "removeOlderPoints": "",
        "removeOlderUnit": "60",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "x": 1790,
        "y": 500,
        "wires": [
            []
        ]
    },
    {
        "id": "18af4942488285b9",
        "type": "ui_chart",
        "z": "e3c5d5c9d69b398e",
        "name": "RMS Current",
        "group": "dash_group",
        "order": 2,
        "width": 12,
        "height": 6,
        "label": "RMS Current",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": "30",
        "removeOlderPoints": "",
        "removeOlderUnit": "60",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#d62728",
            "#ff7f0e",
            "#ff9896",
            "#2ca02c",
            "#98df8a",
            "#1f77b4",
            "#aec7e8",
            "#9467bd",
            "#c5b0d5"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "x": 1790,
        "y": 540,
        "wires": [
            []
        ]
    },
    {
        "id": "cbfe21bd7cbfddec",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Store/update outgoing costs",
        "func": "function parseCmd(s) {\n    if (!s) return null;\n    if (typeof s === 'object') return s;\n\n    const normalized = String(s)\n        .replace(/\\bNone\\b/g, 'null')\n        .replace(/\\bTrue\\b/g, 'true')\n        .replace(/\\bFalse\\b/g, 'false')\n        .replace(/'/g, '\"');\n\n    return JSON.parse(normalized);\n}\n\nlet cmd;\ntry {\n    cmd = parseCmd(msg.cmdStr || '');\n} catch (e) {\n    node.warn('LS parse fail: ' + e.message + ' | ' + (msg.cmdStr || '').slice(0, 160));\n    return null;\n}\n\nif (!cmd || typeof cmd !== 'object') return null;\n\nconst links = cmd['Links'];\nif (!Array.isArray(links)) return null;\n\nconst src = msg.row['Source Address ZigBee'] || msg.row['Source Address'] || '';\nif (!src) return null;\n\nconst map = flow.get('linkmap') || {};\nif (!map[src]) map[src] = {};\n\nfor (const l of links) {\n    if (!l || typeof l !== 'object') continue;\n    const dst  = l['Address'];\n    const cost = l['Outgoing Cost'];\n\n    if (dst !== undefined && cost !== undefined) {\n        map[src][dst] = cost;  // overwrite per update\n    }\n}\n\nflow.set('linkmap', map);\nreturn null;\n",
        "outputs": 1,
        "noerr": 0,
        "x": 1180,
        "y": 620,
        "wires": [
            []
        ]
    },
    {
        "id": "4d8f6d3b398671f1",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Build outgoing_cost.csv body",
        "func": "const map = flow.get('linkmap') || {};\nlet no = 0;\nlet body = '';\n\nconst csvEscape = v => {\n    const s = (v ?? '').toString();\n    if (/[\",\\n\\r]/.test(s)) return '\"' + s.replace(/\"/g, '\"\"') + '\"';\n    return s;\n};\n\nfor (const src of Object.keys(map)) {\n    for (const dst of Object.keys(map[src])) {\n        no += 1;\n        body += [no, src, dst, map[src][dst]].map(csvEscape).join(',') + '\\n';\n    }\n}\n\nif (!body) {\n    node.warn('No Link Status records collected: outgoing_cost.csv remains header-only');\n    return [null, null];\n}\n\nmsg.payload  = body;\nmsg.filename = '/data/outgoing_cost.csv';\nreturn [msg, { trigger: true }];  // out2 → ThingSpeak pipeline\n",
        "outputs": 2,
        "noerr": 0,
        "x": 380,
        "y": 740,
        "wires": [
            [
                "a237daf5de4ead23"
            ],
            [
                "342e5121808c1b36"
            ]
        ]
    },
    {
        "id": "a237daf5de4ead23",
        "type": "file",
        "z": "e3c5d5c9d69b398e",
        "name": "Append outgoing_cost.csv",
        "filename": "filename",
        "filenameType": "msg",
        "appendNewline": false,
        "createDir": true,
        "overwriteFile": "false",
        "encoding": "utf8",
        "x": 660,
        "y": 720,
        "wires": [
            []
        ]
    },
    {
        "id": "342e5121808c1b36",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Pick min src + sort dst (hex)",
        "func": "const map = flow.get('linkmap') || {};\nconst srcs = Object.keys(map);\nif (srcs.length === 0) return null;\n\nconst hexVal = h => parseInt(String(h).replace(/^0x/i, ''), 16);\nconst minSrc = srcs.reduce((a, b) => hexVal(a) < hexVal(b) ? a : b);\nconst dsts = Object.keys(map[minSrc]).sort((a, b) => hexVal(a) - hexVal(b));\n\nconst out = dsts.map(d => ({\n    payload: { src: minSrc, dst: d, cost: map[minSrc][d] }\n}));\nnode.warn('ThingSpeak: src=' + minSrc + ', n_dst=' + dsts.length);\nreturn [out];\n",
        "outputs": 1,
        "noerr": 0,
        "x": 660,
        "y": 800,
        "wires": [
            [
                "7c62754dd607b672"
            ]
        ]
    },
    {
        "id": "7c62754dd607b672",
        "type": "delay",
        "z": "e3c5d5c9d69b398e",
        "name": "Rate 1 msg / 20s",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "20",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 920,
        "y": 800,
        "wires": [
            [
                "7c87ddd473e71909"
            ]
        ]
    },
    {
        "id": "7c87ddd473e71909",
        "type": "function",
        "z": "e3c5d5c9d69b398e",
        "name": "Build ThingSpeak URL",
        "func": "const KEY = 'FILHKRYA89BRRZ8R';  // <-- SOSTITUISCI con la tua write key\nconst p = msg.payload;\nmsg.url = `https://api.thingspeak.com/update?api_key=${KEY}&field1=${encodeURIComponent(p.cost)}`;\nmsg.method = 'GET';\nnode.warn(`TS pub: src=${p.src} dst=${p.dst} cost=${p.cost}`);\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "x": 1170,
        "y": 800,
        "wires": [
            [
                "b699883d2e9c7449"
            ]
        ]
    },
    {
        "id": "b699883d2e9c7449",
        "type": "http request",
        "z": "e3c5d5c9d69b398e",
        "name": "HTTP GET ThingSpeak",
        "method": "use",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "authType": "",
        "x": 1450,
        "y": 800,
        "wires": [
            [
                "a42fad9af672088d"
            ]
        ]
    },
    {
        "id": "a42fad9af672088d",
        "type": "debug",
        "z": "e3c5d5c9d69b398e",
        "name": "TS response",
        "active": true,
        "tosidebar": true,
        "console": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 1700,
        "y": 800,
        "wires": []
    },
    {
        "id": "mqtt_broker_local",
        "type": "mqtt-broker",
        "name": "Mosquitto local 1884",
        "broker": "localhost",
        "port": "1884",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "closeTopic": "",
        "closePayload": "",
        "willTopic": "",
        "willQos": "0",
        "willPayload": ""
    },
    {
        "id": "dash_group",
        "type": "ui_group",
        "name": "RMS",
        "tab": "dash_tab",
        "order": 1,
        "disp": true,
        "width": "12"
    },
    {
        "id": "dash_tab",
        "type": "ui_tab",
        "name": "Challenge3",
        "icon": "dashboard",
        "order": 1
    },
    {
        "id": "e5d02af676a96b22",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-dashboard": "3.6.6"
        }
    }
]