[ { "id": "e4331dc79c943b4a", "type": "tab", "label": "Flow 1", "disabled": false, "info": "", "env": [] }, { "id": "308e29d8bbf046b2", "type": "ui-in", "z": "e4331dc79c943b4a", "client": "fa403183de7ff104", "name": "", "payloadOutput": "string", "x": 110, "y": 300, "wires": [ [ "998285703c4a6fc5" ] ] }, { "id": "998285703c4a6fc5", "type": "function", "z": "e4331dc79c943b4a", "name": "De-duplicate", "func": "// Node-RED Function node code\n// Dedupes APRS packets from node-red-contrib-ax25 ui-in node\n// Ignores digipeater path (via) and timestamp — same source+destination+payload = duplicate\n\nconst WINDOW_MS = 5000; // ignore duplicates seen within 5 seconds\n\nif (!msg.source) {\n // Not a recognizable APRS packet — pass through unchanged\n return msg;\n}\n\n// Identity of the packet: who sent it, to what destination, with what content\n// (deliberately excludes \"via\"/digipeater path and timestamp, since those\n// differ between repeated copies of the same packet)\nconst key = msg.source + '|' + msg.destination + '|' + msg.payload;\n\nconst now = Date.now();\nlet cache = context.get('dedupeCache') || {};\n\n// Purge stale entries\nfor (const k in cache) {\n if (now - cache[k] > WINDOW_MS) {\n delete cache[k];\n }\n}\n\nif (cache[key] !== undefined && (now - cache[key]) < WINDOW_MS) {\n // Duplicate within window — drop it\n return null;\n}\n\ncache[key] = now;\ncontext.set('dedupeCache', cache);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 290, "y": 300, "wires": [ [ "73a2ff6ca5655cdd" ] ] }, { "id": "419465eb08c13f67", "type": "function", "z": "e4331dc79c943b4a", "name": "Calc", "func": "// Node-RED Function node code\n// Evaluates msg.payload as an arithmetic expression and replaces it with the result\n\nconst expr = msg.payload;\n\nif (typeof expr !== 'string') {\n node.warn('payload is not a string, cannot evaluate: ' + JSON.stringify(expr));\n return null;\n}\n\n// Only allow digits, whitespace, and basic arithmetic operators/parentheses\n// This prevents arbitrary code execution via Function constructor / eval\nif (!/^[0-9+\\-*/().\\s]+$/.test(expr)) {\n node.warn('payload contains disallowed characters: ' + expr);\n return null;\n}\n\nlet result;\ntry {\n // Safer than eval() since scope is restricted to just this expression,\n // and input is already validated against the whitelist regex above\n result = Function('\"use strict\"; return (' + expr + ')')();\n} catch (err) {\n node.warn('failed to evaluate expression \"' + expr + '\": ' + err.message);\n return null;\n}\n\nif (typeof result !== 'number' || !isFinite(result)) {\n node.warn('expression did not evaluate to a finite number: ' + expr);\n return null;\n}\n\nreturn {\n source: msg.destination,\n destination: msg.source,\n payload: String(result),\n via: 'wide1-1,wide2-1',\n};\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 270, "y": 420, "wires": [ [ "bef74fb3cb84794a" ] ] }, { "id": "73a2ff6ca5655cdd", "type": "function", "z": "e4331dc79c943b4a", "name": "Filter", "func": "\nif (msg.destination.toLowerCase() !== 'calc') {\n return null;\n}\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 270, "y": 360, "wires": [ [ "419465eb08c13f67" ] ] }, { "id": "bef74fb3cb84794a", "type": "ui-out", "z": "e4331dc79c943b4a", "client": "fa403183de7ff104", "name": "", "source": "", "destination": "", "via": "", "payload": "", "x": 430, "y": 420, "wires": [ [] ] }, { "id": "fa403183de7ff104", "type": "agwpe-client", "name": "", "host": "127.0.0.1", "port": 8000, "callsigns": "", "username": "", "password": "", "monitor": false, "raw": true, "reconnect": true, "reconnectDelay": 5000 }, { "id": "6eaa8ab7bc3bdcb6", "type": "global-config", "env": [], "modules": { "node-red-contrib-ax25": "1.1.0" } } ]