[ { "id": "f54138caa1c8a1ae", "type": "subflow", "name": "Grove Vision AI V2 - Expert", "info": "", "category": "※camera-grove-vision-ai-v2", "in": [ { "x": 40, "y": 360, "wires": [ { "id": "7c1022aaa1def947" } ] } ], "out": [ { "x": 1680, "y": 740, "wires": [ { "id": "e27b61c642230636", "port": 0 } ] }, { "x": 1680, "y": 840, "wires": [ { "id": "7de5e61c8b98f05a", "port": 0 } ] }, { "x": 1810, "y": 1000, "wires": [ { "id": "e5805b647a121dcc", "port": 0 } ] }, { "x": 1930, "y": 1240, "wires": [ { "id": "50f7be4c6b69ddcc", "port": 0 }, { "id": "2119f860c6c1eb12", "port": 0 } ] } ], "env": [ { "name": "----- Device Settings -----", "type": "str", "value": "", "ui": { "type": "none" } }, { "name": "mqtt-broker", "type": "mqtt-broker", "value": "ff55020100010001", "ui": { "icon": "font-awesome/fa-server", "label": { "zh-CN": "*Server", "en-US": "*Server" }, "type": "conf-types" } }, { "name": "!note_camera", "type": "str", "value": "", "ui": { "label": { "zh-CN": "", "en-US": "....Note: You can also leave it blank and configure it dynamically through input" }, "type": "none" } }, { "name": "----- Output 4: Trigger -----", "type": "str", "value": "", "ui": { "type": "none" } }, { "name": "triggerMode", "type": "str", "value": "yes", "ui": { "label": { "zh-CN": "When ...", "en-US": "When ..." }, "type": "select", "opts": { "opts": [ { "l": { "zh-CN": "Detected", "en-US": "Detected" }, "v": "yes" }, { "l": { "zh-CN": "Not detected", "en-US": "Not detected" }, "v": "no" } ] } } } ], "meta": { "module": "Grove Vision AI V2 - Get Camera", "version": "0.0.1", "author": "seeed", "license": "Apache-2.0" }, "color": "#B8EA4F", "outputLabels": [ "Original Image", "Inference Result", "Iriginal Data (only can connect to 'tracking analyze' node)", "Detection Trigger" ], "status": { "x": 1660, "y": 1060, "wires": [ { "id": "638e3329feab7f3b", "port": 0 } ] } }, { "id": "34ff1840310f347b", "type": "change", "z": "f54138caa1c8a1ae", "g": "69cc677d5eeb6285", "name": "subscribe Action Builder", "rules": [ { "t": "set", "p": "action", "pt": "msg", "to": "subscribe", "tot": "str" }, { "t": "set", "p": "topic", "pt": "msg", "to": "mqtt_tx", "tot": "flow", "dc": true }, { "t": "set", "p": "qos", "pt": "msg", "to": "0", "tot": "num" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 990, "y": 320, "wires": [ [ "81acb1decc5c4b42" ] ] }, { "id": "83c2e443f9356992", "type": "mqtt in", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "when receive data", "topic": "", "qos": "2", "datatype": "auto-detect", "broker": "34867ff4d0f1a72d", "nl": false, "rap": true, "rh": 0, "inputs": 1, "x": 590, "y": 800, "wires": [ [ "7f40b6ba3ad0fd72" ] ] }, { "id": "e5805b647a121dcc", "type": "switch", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "Check msg is valid", "property": "payload", "propertyType": "msg", "rules": [ { "t": "nempty" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 830, "y": 800, "wires": [ [ "43ac01de39db9fa4", "7de5e61c8b98f05a" ] ] }, { "id": "32e42483ac76b0e4", "type": "change", "z": "f54138caa1c8a1ae", "g": "305158fcab074826", "name": "send a stop command to camera", "rules": [ { "t": "set", "p": "topic", "pt": "msg", "to": "mqtt_rx", "tot": "flow", "dc": true }, { "t": "set", "p": "payload", "pt": "msg", "to": "[65,84,43,66,82,69,65,75,13,10]", "tot": "bin" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 520, "y": 560, "wires": [ [ "bf72268f8a9d27a0" ] ] }, { "id": "7b60f4ba019f1749", "type": "inject", "z": "f54138caa1c8a1ae", "d": true, "g": "409871c0949177f8", "name": "auto resend command", "props": [ { "p": "payload" } ], "repeat": "3", "crontab": "", "once": true, "onceDelay": "0", "topic": "", "payload": "", "payloadType": "date", "x": 450, "y": 120, "wires": [ [ "e48cf7986bc612ae" ] ] }, { "id": "23a2040bdd246943", "type": "change", "z": "f54138caa1c8a1ae", "g": "409871c0949177f8", "name": "send a start command to camera", "rules": [ { "t": "set", "p": "topic", "pt": "msg", "to": "mqtt_rx", "tot": "flow" }, { "t": "set", "p": "payload", "pt": "msg", "to": "[65,84,43,73,78,86,79,75,69,61,45,49,44,48,44,48,13,10]", "tot": "bin" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 1280, "y": 120, "wires": [ [ "80ea757c32b9913b" ] ] }, { "id": "e48cf7986bc612ae", "type": "switch", "z": "f54138caa1c8a1ae", "g": "409871c0949177f8", "name": "Confirm that the camera is currently expected to be connected", "property": "mqtt_rx", "propertyType": "flow", "rules": [ { "t": "nempty" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 850, "y": 120, "wires": [ [ "23a2040bdd246943" ] ] }, { "id": "6748f017d2dabecb", "type": "comment", "z": "f54138caa1c8a1ae", "g": "409871c0949177f8", "name": "reconnect logic", "info": "", "x": 380, "y": 60, "wires": [] }, { "id": "43ac01de39db9fa4", "type": "change", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "get image", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "payload.data.image", "tot": "msg" }, { "t": "set", "p": "topic", "pt": "msg", "to": "raw", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 1080, "y": 740, "wires": [ [ "e27b61c642230636" ] ] }, { "id": "7de5e61c8b98f05a", "type": "change", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "get inference result", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "payload.data.boxes", "tot": "msg", "dc": true }, { "t": "set", "p": "topic", "pt": "msg", "to": "raw", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 1110, "y": 800, "wires": [ [ "4834b4fbeb890aaf" ] ], "info": "[[118,150,237,175,100,0],[114,119,76,84,100,2]]" }, { "id": "e27b61c642230636", "type": "switch", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "Check image is valid", "property": "payload.length", "propertyType": "msg", "rules": [ { "t": "gte", "v": "100", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 1280, "y": 740, "wires": [ [] ] }, { "id": "208a200fb554527a", "type": "change", "z": "f54138caa1c8a1ae", "g": "69cc677d5eeb6285", "name": "override mqtt_tx and mqtt_rx", "rules": [ { "t": "set", "p": "mqtt_tx", "pt": "flow", "to": "\"sscma/v0/\" & payload & \"/tx\"", "tot": "jsonata" }, { "t": "set", "p": "mqtt_rx", "pt": "flow", "to": "\"sscma/v0/\" & payload & \"/rx\"", "tot": "jsonata" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 700, "y": 360, "wires": [ [ "34ff1840310f347b", "2191648825898ba5" ] ] }, { "id": "2191648825898ba5", "type": "change", "z": "f54138caa1c8a1ae", "g": "69cc677d5eeb6285", "name": "send a start command to camera", "rules": [ { "t": "set", "p": "topic", "pt": "msg", "to": "mqtt_rx", "tot": "flow", "dc": true }, { "t": "set", "p": "payload", "pt": "msg", "to": "[65,84,43,73,78,86,79,75,69,61,45,49,44,48,44,48,13,10]", "tot": "bin" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 1020, "y": 400, "wires": [ [ "a44331a86a7dc340" ] ] }, { "id": "c87d0fd5fafe4f59", "type": "change", "z": "f54138caa1c8a1ae", "g": "305158fcab074826", "name": "Unsubscribe Action Builder", "rules": [ { "t": "set", "p": "topic", "pt": "msg", "to": "mqtt_tx", "tot": "flow" }, { "t": "set", "p": "action", "pt": "msg", "to": "unsubscribe", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 660, "y": 640, "wires": [ [ "32ad13252fa2a166" ] ] }, { "id": "195442578d4a91b3", "type": "comment", "z": "f54138caa1c8a1ae", "g": "305158fcab074826", "name": "Handle stop", "info": "", "x": 530, "y": 520, "wires": [] }, { "id": "d2e1fd0a7353c49b", "type": "comment", "z": "f54138caa1c8a1ae", "g": "a971c4f96839364e", "name": "Handle start", "info": "", "x": 370, "y": 220, "wires": [] }, { "id": "300b5a664aa446f0", "type": "comment", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "Received data", "info": "", "x": 370, "y": 740, "wires": [] }, { "id": "81acb1decc5c4b42", "type": "link out", "z": "f54138caa1c8a1ae", "g": "69cc677d5eeb6285", "name": "sub/unsub", "mode": "link", "links": [ "827d086314b3653a" ], "x": 1250, "y": 320, "wires": [], "l": true }, { "id": "827d086314b3653a", "type": "link in", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "sub/unsub", "links": [ "81acb1decc5c4b42", "32ad13252fa2a166" ], "x": 400, "y": 800, "wires": [ [ "83c2e443f9356992" ] ], "l": true }, { "id": "32ad13252fa2a166", "type": "link out", "z": "f54138caa1c8a1ae", "g": "305158fcab074826", "name": "sub/unsub", "mode": "link", "links": [ "827d086314b3653a" ], "x": 870, "y": 640, "wires": [], "l": true }, { "id": "6ad80085190e5ec4", "type": "mqtt out", "z": "f54138caa1c8a1ae", "g": "8c88939dfc248608", "name": "mqtt out", "topic": "", "qos": "0", "retain": "false", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "34867ff4d0f1a72d", "x": 1200, "y": 580, "wires": [] }, { "id": "bf72268f8a9d27a0", "type": "link out", "z": "f54138caa1c8a1ae", "g": "305158fcab074826", "name": "pub", "mode": "link", "links": [ "a3ff982e6c0d205d" ], "x": 850, "y": 560, "wires": [], "l": true }, { "id": "a3ff982e6c0d205d", "type": "link in", "z": "f54138caa1c8a1ae", "g": "8c88939dfc248608", "name": "pub", "links": [ "80ea757c32b9913b", "a44331a86a7dc340", "bf72268f8a9d27a0" ], "x": 1070, "y": 580, "wires": [ [ "6ad80085190e5ec4" ] ], "l": true }, { "id": "80ea757c32b9913b", "type": "link out", "z": "f54138caa1c8a1ae", "g": "409871c0949177f8", "name": "pub", "mode": "link", "links": [ "a3ff982e6c0d205d" ], "x": 1510, "y": 120, "wires": [], "l": true }, { "id": "a44331a86a7dc340", "type": "link out", "z": "f54138caa1c8a1ae", "g": "69cc677d5eeb6285", "name": "pub", "mode": "link", "links": [ "a3ff982e6c0d205d" ], "x": 1250, "y": 400, "wires": [], "l": true }, { "id": "006879ed95d2920d", "type": "comment", "z": "f54138caa1c8a1ae", "g": "8c88939dfc248608", "name": "Basic mqtt publish", "info": "", "x": 1090, "y": 520, "wires": [] }, { "id": "638e3329feab7f3b", "type": "status", "z": "f54138caa1c8a1ae", "name": "", "scope": null, "x": 1450, "y": 1060, "wires": [ [] ] }, { "id": "fa36ac27d76f6c9f", "type": "comment", "z": "f54138caa1c8a1ae", "name": "image buffer", "info": "", "x": 1670, "y": 700, "wires": [] }, { "id": "24209c5fc89d3273", "type": "comment", "z": "f54138caa1c8a1ae", "name": "inference result", "info": "", "x": 1680, "y": 800, "wires": [] }, { "id": "7c1022aaa1def947", "type": "switch", "z": "f54138caa1c8a1ae", "name": "start or stop", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "start", "vt": "str" }, { "t": "eq", "v": "stop", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 150, "y": 360, "wires": [ [ "cfdacde8f6653612", "8732ffe74f7d75d7" ], [ "32e42483ac76b0e4", "964fdf92823e0d1c" ] ] }, { "id": "a6e005eefcf3eefb", "type": "comment", "z": "f54138caa1c8a1ae", "name": "original data", "info": "", "x": 1670, "y": 960, "wires": [] }, { "id": "4834b4fbeb890aaf", "type": "function", "z": "f54138caa1c8a1ae", "name": "Check if the target is detected", "func": "if (Array.isArray(msg.payload)) {\n var length = msg.payload.length;\n if (length > 1) {\n msg.payload = 1;\n } else if (length === 0) {\n msg.payload = 0;\n } else {\n msg.payload = 1;\n }\n} else {\n msg.payload = 0;\n}\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1390, "y": 1240, "wires": [ [ "5b36c809f82364a8" ] ] }, { "id": "5b36c809f82364a8", "type": "switch", "z": "f54138caa1c8a1ae", "name": "yes or no", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "1", "vt": "str" }, { "t": "eq", "v": "0", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 1620, "y": 1240, "wires": [ [ "50f7be4c6b69ddcc" ], [ "2119f860c6c1eb12" ] ] }, { "id": "50f7be4c6b69ddcc", "type": "switch", "z": "f54138caa1c8a1ae", "name": "", "property": "triggerMode", "propertyType": "env", "rules": [ { "t": "eq", "v": "yes", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 1750, "y": 1200, "wires": [ [] ] }, { "id": "2119f860c6c1eb12", "type": "switch", "z": "f54138caa1c8a1ae", "name": "", "property": "triggerMode", "propertyType": "env", "rules": [ { "t": "eq", "v": "no", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 1750, "y": 1280, "wires": [ [] ] }, { "id": "cfdacde8f6653612", "type": "debug", "z": "f54138caa1c8a1ae", "name": "debug 1", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "statusVal": "", "statusType": "auto", "x": 200, "y": 260, "wires": [] }, { "id": "7f40b6ba3ad0fd72", "type": "change", "z": "f54138caa1c8a1ae", "g": "cc6c35456a15da28", "name": "Add camera_id", "rules": [ { "t": "set", "p": "camera_id", "pt": "msg", "to": "camera_id", "tot": "flow", "dc": true } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 700, "y": 760, "wires": [ [ "e5805b647a121dcc" ] ] }, { "id": "8732ffe74f7d75d7", "type": "change", "z": "f54138caa1c8a1ae", "g": "a971c4f96839364e", "name": "set flow.camera_id", "rules": [ { "t": "set", "p": "camera_id", "pt": "flow", "to": "topic", "tot": "msg", "dc": true }, { "t": "set", "p": "payload", "pt": "msg", "to": "topic", "tot": "msg", "dc": true } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 410, "y": 300, "wires": [ [ "208a200fb554527a" ] ] }, { "id": "964fdf92823e0d1c", "type": "delay", "z": "f54138caa1c8a1ae", "g": "305158fcab074826", "name": "delay 0.2s", "pauseType": "delay", "timeout": "0.2", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": false, "allowrate": false, "outputs": 1, "x": 450, "y": 640, "wires": [ [ "c87d0fd5fafe4f59" ] ] }, { "id": "8c88939dfc248608", "type": "group", "z": "f54138caa1c8a1ae", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "6ad80085190e5ec4", "a3ff982e6c0d205d", "006879ed95d2920d" ], "x": 974, "y": 479, "w": 312, "h": 142 }, { "id": "cc6c35456a15da28", "type": "group", "z": "f54138caa1c8a1ae", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "83c2e443f9356992", "e5805b647a121dcc", "43ac01de39db9fa4", "7de5e61c8b98f05a", "e27b61c642230636", "300b5a664aa446f0", "827d086314b3653a", "7f40b6ba3ad0fd72" ], "x": 274, "y": 699, "w": 1132, "h": 142 }, { "id": "a971c4f96839364e", "type": "group", "z": "f54138caa1c8a1ae", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "d2e1fd0a7353c49b", "8732ffe74f7d75d7", "69cc677d5eeb6285" ], "x": 274, "y": 179, "w": 1098, "h": 288 }, { "id": "305158fcab074826", "type": "group", "z": "f54138caa1c8a1ae", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "32e42483ac76b0e4", "c87d0fd5fafe4f59", "195442578d4a91b3", "32ad13252fa2a166", "bf72268f8a9d27a0", "964fdf92823e0d1c" ], "x": 354, "y": 479, "w": 612, "h": 202 }, { "id": "409871c0949177f8", "type": "group", "z": "f54138caa1c8a1ae", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "7b60f4ba019f1749", "23a2040bdd246943", "e48cf7986bc612ae", "6748f017d2dabecb", "80ea757c32b9913b" ], "x": 274, "y": 19, "w": 1312, "h": 142 }, { "id": "69cc677d5eeb6285", "type": "group", "z": "f54138caa1c8a1ae", "g": "a971c4f96839364e", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "34ff1840310f347b", "208a200fb554527a", "2191648825898ba5", "81acb1decc5c4b42", "a44331a86a7dc340" ], "x": 554, "y": 279, "w": 792, "h": 162 }, { "id": "34867ff4d0f1a72d", "type": "mqtt-broker", "name": "", "broker": "mqtt://localhost", "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": "face_access_main_flow", "type": "tab", "label": "Face Recognition Access Control", "disabled": false, "info": "Main flow for the face recognition access control system based on Grove Vision AI V2 and Hailo-8 - Simplified configuration version" }, { "id": "global_config_trigger", "type": "inject", "z": "face_access_main_flow", "name": "Global Config (Load on Start)", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 150, "y": 20, "wires": [ [ "global_config_node" ] ] }, { "id": "global_config_node", "type": "function", "z": "face_access_main_flow", "name": "Global Config", "func": "// --- Global Config ---\n// Set all external service addresses and parameters here\n// This node will run automatically once on startup\n\n// Hailo AI Chip (Face Vector API) Configuration\nflow.set('hailo_host', '192.168.10.179');\nflow.set('hailo_port', '8000');\n\n// Grove Vision AI Camera Configuration\nflow.set('image_width', 640);\nflow.set('image_height', 480);\n\n// Grove Vision AI Resolution Configuration (AT+RSL=,)\n// For supported resolutions, please refer to the device documentation, e.g., '240,240' or '480,480'\n// Reference: https://github.com/Seeed-Studio/SSCMA-Micro/blob/main/docs/protocol/at-protocol-en_US.md\nflow.set('resolution', '640,480');\n\nnode.log(\"Global configuration loaded.\");\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 430, "y": 20, "wires": [ [] ] }, { "id": "api_url_configurator", "type": "function", "z": "face_access_main_flow", "name": "API URL Configuration for Detect & Embed", "func": "// Read environment variables or use default values\nconst faceEmbedHost = flow.get('hailo_host') || '192.168.10.179';\nconst faceEmbedPort = flow.get('hailo_port') || '8000';\n\n// Set the API URL to the new all-in-one endpoint\nmsg.url = `http://${faceEmbedHost}:${faceEmbedPort}/detect_and_embed`;\nmsg.timeout = 5000; // Set a 5-second timeout\n\n// Prepare the request body, containing only the image\nmsg.payload = {\n image_base64: msg.payload.img_b64\n};\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 450, "y": 280, "wires": [ [ "face_embed_request" ] ] }, { "id": "face_embed_request", "type": "http request", "z": "face_access_main_flow", "name": "FaceEmbed API (remote)", "method": "POST", "ret": "obj", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [ { "keyType": "other", "keyValue": "Content-Type", "valueType": "other", "valueValue": "application/json" }, { "keyType": "other", "keyValue": "User-Agent", "valueType": "other", "valueValue": "NodeRED-FaceAccess/1.0" } ], "x": 750, "y": 280, "wires": [ [ "vision_frame_processor" ] ] }, { "id": "vector_search_request", "type": "function", "z": "face_access_main_flow", "name": "Prepare Vector Search", "func": "// Get face vector\nconst embedding = msg.payload.vector;\nconst confidence = msg.payload.confidence;\nconst processingTime = msg.payload.processing_time_ms;\n\n// Check vector quality\nif (confidence < 0.5) {\n node.warn(`Low face quality: ${confidence}`);\n return null;\n}\n\n// Device to Collection mapping - add new devices here\nconst deviceCollectionMap = {\n 'grove_vision_ai_v2_eb9e4d18': 'office_entrance',\n 'grove_vision_ai_v2_002': 'warehouse_door',\n 'grove_vision_ai_v2_003': 'lab_access',\n 'default': 'default_faces'\n};\n\n// Get device configuration\nconst deviceId = msg.deviceId;\nconst collectionName = deviceCollectionMap[deviceId] || deviceCollectionMap['default'];\nconst threshold = 0.32; // Similarity threshold, adjustable here\n\n// Prepare FaceEmbed API search request\nmsg.collectionName = collectionName;\nmsg.threshold = threshold;\nmsg.embedding = embedding;\nmsg.confidence = confidence;\nmsg.processingTime = processingTime;\n\n// Get Hailo API configuration from flow context\nconst hailoHost = flow.get('hailo_host') || '192.168.10.179';\nconst hailoPort = flow.get('hailo_port') || '8000';\nmsg.url = `http://${hailoHost}:${hailoPort}/vectors/search`;\n\nmsg.payload = {\n collection: collectionName,\n vector: embedding,\n threshold: threshold,\n top_k: 1\n};\n\n// Set HTTP headers\nmsg.headers = { 'Content-Type': 'application/json' };\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1180, "y": 280, "wires": [ [ "qdrant_search" ] ] }, { "id": "qdrant_search", "type": "http request", "z": "face_access_main_flow", "name": "Qdrant Search", "method": "POST", "ret": "obj", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 1370, "y": 280, "wires": [ [ "access_decision" ] ] }, { "id": "access_decision", "type": "function", "z": "face_access_main_flow", "name": "Access Decision", "func": "const searchResponse = msg.payload;\nconst deviceId = msg.deviceId;\nconst threshold = msg.threshold;\nconst timestamp = new Date().toISOString();\n\nlet decision = false;\nlet name = null;\nlet similarity = 0.0;\nlet matchedId = null;\n\n// Check search results from our new API\nif (searchResponse && searchResponse.status === 'found' && searchResponse.results.length > 0) {\n const bestMatch = searchResponse.results[0];\n similarity = bestMatch.similarity;\n \n // The API already filters by threshold, but we can double-check if needed.\n // The check is implicit now: if we have a result, it's a match.\n decision = true;\n name = bestMatch.user_id; // The user_id is the name\n matchedId = bestMatch.id;\n \n}\n\n// Construct result message\nconst result = {\n ts: timestamp,\n device_id: deviceId,\n decision: decision,\n name: name,\n similarity: parseFloat(similarity.toFixed(4)),\n confidence: msg.confidence,\n processing_time_ms: msg.processingTime,\n matched_id: matchedId\n};\n\n// Prepare MQTT publish\nmsg.topic = `access/result/${deviceId}`;\nmsg.payload = result;\n\n// Save to context for logging\ncontext.set('lastAccess', result);\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1570, "y": 280, "wires": [ [ "access_result_publisher", "access_logger", "0221797157934c01" ] ] }, { "id": "access_result_publisher", "type": "mqtt out", "z": "face_access_main_flow", "name": "Publish Access Result", "topic": "", "qos": "1", "retain": "false", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "mqtt_broker", "x": 1770, "y": 260, "wires": [] }, { "id": "access_logger", "type": "function", "z": "face_access_main_flow", "name": "Access Log", "func": "const result = msg.payload;\n\n// Record access log\nnode.log(`Access Decision - Device: ${result.device_id}, Decision: ${result.decision}, Name: ${result.name || 'Unknown'}, Distance: ${result.distance}`);\n\n// More detailed logging logic can be added here\n// e.g., write to file, database, etc.\n\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1770, "y": 320, "wires": [ [] ] }, { "id": "manual_enroll_trigger", "type": "inject", "z": "face_access_main_flow", "name": "Face Enroll", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "access/enroll/grove_vision_ai_v2_eb9e4d18", "payload": "{\"name\":\"Jane Smith\",\"action\":\"start\",\"collection\":\"office_entrance\"}", "payloadType": "json", "x": 100, "y": 440, "wires": [ [ "enroll_processor" ] ] }, { "id": "enroll_processor", "type": "function", "z": "face_access_main_flow", "name": "Process Enroll Request", "func": "const payload = msg.payload;\nconst topic = msg.topic;\nconst deviceId = topic.split('/')[2];\n\n\n// Validate enroll request\nif (!payload.name || !payload.action || !payload.collection) {\n node.error('Invalid enroll request: missing name, action, or collection');\n return null;\n}\n\nif (payload.action === 'start') {\n // Start enrollment process\n msg.deviceId = deviceId;\n msg.enrollName = payload.name;\n msg.payload = {\n action: 'start_collection',\n name: payload.name,\n device_id: deviceId\n };\n \n // Set enrollment status\n flow.set(`enroll_${deviceId}`, {\n name: payload.name,\n collection: payload.collection, // Store target collection\n status: 'collecting',\n vectors: [], // Initialize vector array\n startTime: Date.now()\n });\n \n return msg;\n}\n\nreturn null;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 320, "y": 440, "wires": [ [ "enroll_frame_collector" ] ] }, { "id": "enroll_frame_collector", "type": "function", "z": "face_access_main_flow", "name": "Collect Enroll Frames", "func": "// This node listens for subsequent vision frames to collect face data\n// In practice, it needs to coordinate with vision_frame_processor\n\nconst deviceId = msg.deviceId;\nconst enrollState = context.get(`enroll_${deviceId}`);\n\nif (!enrollState || enrollState.status !== 'collecting') {\n return null;\n}\n\n// Send command to start collection\nmsg.topic = `access/enroll_status/${deviceId}`;\nmsg.payload = {\n status: 'collecting',\n message: `Starting to collect face data for ${enrollState.name}, please face the camera`,\n frames_needed: 10\n};\n\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 510, "y": 440, "wires": [ [ "enroll_status_publisher" ] ] }, { "id": "enroll_status_publisher", "type": "mqtt out", "z": "face_access_main_flow", "name": "Publish Enroll", "topic": "", "qos": "1", "retain": "false", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "mqtt_broker", "x": 740, "y": 440, "wires": [] }, { "id": "95fddc256a11e927", "type": "change", "z": "face_access_main_flow", "name": "send start", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "start", "tot": "str", "dc": true } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 240, "y": 80, "wires": [ [ "786320639347a67f" ] ] }, { "id": "950c446d7e951013", "type": "inject", "z": "face_access_main_flow", "name": "Start", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "grove_vision_ai_v2_eb9e4d18", "payload": "", "payloadType": "date", "x": 90, "y": 80, "wires": [ [ "95fddc256a11e927" ] ] }, { "id": "786320639347a67f", "type": "subflow:f54138caa1c8a1ae", "z": "face_access_main_flow", "name": "Grove Vision AI V2", "env": [ { "name": "mqtt-broker", "value": "34867ff4d0f1a72d", "type": "conf-type" }, { "name": "triggerMode", "value": "yes", "type": "str" } ], "x": 480, "y": 120, "wires": [ [ "grove_data_combiner", "ef88f812ba82787c" ], [ "grove_data_combiner" ], [], [ "detection_trigger_handler" ] ] }, { "id": "grove_data_combiner", "type": "function", "z": "face_access_main_flow", "name": "Grove Data Combiner", "func": "// Combine Grove Vision AI V2's output 1 (image) and output 2 (detection result)\n// Assemble directly into standard vision frame format\n\nconst camera_id = msg.camera_id || 'grove_vision_ai_v2_eb9e4d18';\n\n// Determine data source: output 1 is image data (buffer), output 2 is detection result (array)\nif (Buffer.isBuffer(msg.payload) || typeof msg.payload === 'string') {\n // Output 1: Raw image data\n context.set(`image_${camera_id}`, {\n img_b64: msg.payload,\n timestamp: Date.now()\n });\n \n // Image data is not output directly, waits for detection result\n return null;\n \n} else if (Array.isArray(msg.payload)) {\n // Output 2: Detection result - combine image and bounding boxes\n const bboxes = msg.payload;\n \n // Get corresponding image data\n const imageContext = context.get(`image_${camera_id}`);\n if (!imageContext) {\n node.warn('No image data found for camera: ' + camera_id);\n return null;\n }\n \n // Check if image data is expired (older than 3 seconds)\n if (Date.now() - imageContext.timestamp > 3000) {\n node.warn('Image data too old for camera: ' + camera_id);\n context.set(`image_${camera_id}`, null);\n return null;\n }\n \n // Convert Grove Vision AI V2's bbox format to standard format\n // Grove format: [x_center, y_center, width, height, confidence, class]\n const convertedBboxes = [];\n if (Array.isArray(bboxes)) {\n for (const bbox of bboxes) {\n if (bbox.length < 5) continue;\n \n const x_center = bbox[0];\n const y_center = bbox[1];\n const width = bbox[2];\n const height = bbox[3];\n const confidence = bbox[4];\n \n // Convert from center coordinates to top-left coordinates\n const x_topleft = x_center - width / 2;\n const y_topleft = y_center - height / 2;\n \n // --- Bounding box correction logic (fixed floating point error issue) ---\n const maxImageWidth = flow.get('image_width') || 480;\n const maxImageHeight = flow.get('image_height') || 480;\n\n // 1. Calculate the original four floating-point boundaries\n const left = x_topleft;\n const top = y_topleft;\n const right = x_topleft + width;\n const bottom = y_topleft + height;\n\n // 2. Clamp the boundaries to the image dimensions, then round to integers for safety\n const final_left = Math.round(Math.max(0, left));\n const final_top = Math.round(Math.max(0, top));\n const final_right = Math.round(Math.min(maxImageWidth, right));\n const final_bottom = Math.round(Math.min(maxImageHeight, bottom));\n\n // 3. Calculate the final integer x, y, w, h from the corrected integer boundaries\n const final_x = final_left;\n const final_y = final_top;\n const final_w = final_right - final_left;\n const final_h = final_bottom - final_top;\n\n // 4. Re-check for valid size (skip if width or height is <= 0 after clipping)\n if (final_w <= 0 || final_h <= 0) {\n node.warn(`Bbox has zero size after clipping, skipping. Original: [${bbox.join(', ')}]`);\n continue;\n }\n \n const score = confidence / 100.0; // Convert to 0-1 range\n \n convertedBboxes.push({\n x: final_x,\n y: final_y,\n w: final_w,\n h: final_h,\n score: score\n });\n }\n }\n \n const standardPayload = {\n ts: new Date().toISOString(),\n img_b64: imageContext.img_b64,\n bboxes: convertedBboxes\n };\n \n msg.deviceId = camera_id;\n msg.topic = `vision/frames/${camera_id}`;\n msg.payload = standardPayload;\n \n context.set(`image_${camera_id}`, null);\n \n return msg;\n \n} else {\n return null;\n}", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 690, "y": 120, "wires": [ [ "vision_frame_rate_limiter" ] ] }, { "id": "vision_frame_rate_limiter", "type": "delay", "z": "face_access_main_flow", "name": "Speed Limit (1fps)", "pauseType": "rate", "timeout": "5", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": true, "allowrate": false, "outputs": 1, "x": 970, "y": 120, "wires": [ [ "detection_filter" ] ] }, { "id": "detection_filter", "type": "function", "z": "face_access_main_flow", "name": "Has Detected?", "func": "// Check if `msg.payload.bboxes` array exists and is not empty\nif (msg.payload && Array.isArray(msg.payload.bboxes) && msg.payload.bboxes.length > 0) {\n // If there is a detection result, pass the message to the next node\n return msg; \n} else {\n // If no target is detected, abort the flow and do not return any message\n node.warn(\"No target detected, API call aborted.\");\n return null; \n}", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1190, "y": 120, "wires": [ [ "api_url_configurator" ] ] }, { "id": "detection_trigger_handler", "type": "function", "z": "face_access_main_flow", "name": "Detection Trigger Handler", "func": "// Handle detection trigger signal from Grove Vision AI V2\n// Can be used for statistics, logging, etc.\n\nconst camera_id = msg.camera_id || 'unknown';\nconst detected = msg.payload;\n\nif (detected) {\n node.log(`Face detected by camera: ${camera_id}`);\n \n // Update detection statistics\n const stats = context.get('detection_stats') || {};\n const today = new Date().toDateString();\n \n if (!stats[today]) {\n stats[today] = {};\n }\n if (!stats[today][camera_id]) {\n stats[today][camera_id] = 0;\n }\n \n stats[today][camera_id]++;\n context.set('detection_stats', stats);\n}\n\nreturn null; // Do not propagate the message", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 720, "y": 180, "wires": [ [] ] }, { "id": "vision_frame_processor", "type": "function", "z": "face_access_main_flow", "name": "Process Vision Frame", "func": "const results = msg.payload;\n\n// Check if the API returned a valid array of faces\nif (!Array.isArray(results) || results.length === 0) {\n // If no face is detected, terminate the flow\n return null; \n}\n\nlet largestFace = null;\nlet maxArea = 0;\n\nfor (const face of results) {\n const bbox = face.bbox;\n \n // Fix: Check if bbox is an object containing x,y,w,h\n if (bbox && typeof bbox.x === 'number' && typeof bbox.y === 'number' && typeof bbox.w === 'number' && typeof bbox.h === 'number') {\n const area = bbox.w * bbox.h;\n if (area > maxArea) {\n maxArea = area;\n largestFace = face;\n }\n } else {\n node.warn(`Received incorrect bbox format: ${JSON.stringify(bbox)}`);\n }\n}\n\nif (!largestFace) {\n node.warn(\"No valid bounding box in detected face data.\");\n return null;\n}\n\n// Fix: Correctly extract nested data from API response\nconst newMsg = {\n ...msg,\n payload: {\n embedding: largestFace.embedding.vector,\n bbox: largestFace.bbox,\n landmarks: largestFace.landmarks,\n // Prepare data for the next node\n vector: largestFace.embedding.vector,\n confidence: largestFace.embedding.confidence,\n processing_time_ms: largestFace.embedding.processing_time_ms,\n detection_confidence: largestFace.detection_confidence\n },\n originalImage: msg.originalImage || msg.payload.image_base64\n};\n\n// Determine if it's a recognition or enrollment flow based on context\nconst deviceId = msg.deviceId;\nconst enrollState = flow.get('enroll_' + deviceId);\n\nif (enrollState && enrollState.status === 'collecting') {\n return [null, newMsg];\n} else {\n return [newMsg, null];\n}\n", "outputs": 2, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 980, "y": 280, "wires": [ [ "vector_search_request" ], [ "collect_and_save_enroll_vector" ] ] }, { "id": "collect_and_save_enroll_vector", "type": "function", "z": "face_access_main_flow", "name": "Collect/Average/Store Vector", "func": "const deviceId = msg.deviceId;\nconst enrollState = flow.get('enroll_' + deviceId);\n\nif (!enrollState || enrollState.status !== 'collecting') {\n node.warn('Not in collecting state, ignoring vector.');\n return null;\n}\n\n// 1. Collect vectors\nconst vector = msg.payload.vector;\nif (vector) {\n enrollState.vectors.push(vector);\n}\nflow.set('enroll_' + deviceId, enrollState);\n\nconst collectedCount = enrollState.vectors.length;\nconst neededCount = 10;\n\n// 2. Send progress update\nconst statusUpdate = {\n topic: `access/enroll_status/${deviceId}`,\n payload: {\n status: 'collecting',\n message: `Collecting face data... (${collectedCount}/${neededCount})`,\n collected: collectedCount,\n needed: neededCount\n }\n};\n\n// If not fully collected, just send progress and stop\nif (collectedCount < neededCount) {\n return [statusUpdate, null];\n}\n\n// --- If collection is complete, start processing --- \nnode.log(`Collected ${neededCount} vectors for ${enrollState.name}. Averaging and preparing...`);\nenrollState.status = 'saving';\nflow.set('enroll_' + deviceId, enrollState);\n\n// 3. Calculate average vector\nconst vectors = enrollState.vectors;\nconst vectorDim = vectors[0].length;\nconst avgVector = new Array(vectorDim).fill(0);\n\nfor (const v of vectors) {\n for (let i = 0; i < vectorDim; i++) {\n avgVector[i] += v[i];\n }\n}\nfor (let i = 0; i < vectorDim; i++) {\n avgVector[i] /= vectors.length;\n}\n\n// 4. Prepare request for our new /vectors/add endpoint\nconst hailoHost = flow.get('hailo_host') || '192.168.10.179';\nconst hailoPort = flow.get('hailo_port') || '8000';\nconst collectionName = enrollState.collection;\n\nif (!collectionName) {\n node.error(`Enrollment for ${enrollState.name} failed: Collection name not found in context.`);\n const errorMsg = {\n topic: `access/enroll_status/${deviceId}`,\n payload: { status: 'error', message: 'Enrollment failed: Target database (collection) not specified.' }\n };\n flow.set('enroll_' + deviceId, null); // Clean up state\n return [errorMsg, null]; // Send error and stop\n}\n\n// Prepare the message for the HTTP request node\nconst addVectorMsg = { ...msg }; // Inherit properties\naddVectorMsg.url = `http://${hailoHost}:${hailoPort}/vectors/add`;\naddVectorMsg.method = 'POST';\naddVectorMsg.headers = { 'Content-Type': 'application/json' };\naddVectorMsg.payload = {\n collection: collectionName,\n user_id: enrollState.name,\n vector: avgVector\n};\n\nconst finalStatusUpdate = {\n topic: `access/enroll_status/${deviceId}`,\n payload: {\n status: 'saving',\n message: 'Data collection complete, saving to database...'\n }\n};\n\n// Output 1: MQTT status update\n// Output 2: HTTP request to add vector\nreturn [finalStatusUpdate, addVectorMsg];", "outputs": 2, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 990, "y": 340, "wires": [ [ "enroll_status_publisher" ], [ "qdrant_upsert" ] ] }, { "id": "qdrant_upsert", "type": "http request", "z": "face_access_main_flow", "name": "Add Vector to DB", "method": "use", "ret": "obj", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 1610, "y": 400, "wires": [ [ "enroll_final_status" ] ] }, { "id": "enroll_final_status", "type": "function", "z": "face_access_main_flow", "name": "Enrollment Final Status", "func": "const deviceId = msg.deviceId;\nconst enrollState = flow.get('enroll_' + deviceId);\nlet finalMessage;\n\n// Check the response from our /vectors/add endpoint\nif (msg.statusCode === 200 && msg.payload.status === 'success') {\n finalMessage = {\n status: 'success',\n message: `User ${enrollState.name} enrolled successfully!`\n };\n node.log(`Enrollment success for ${enrollState.name}.`);\n} else {\n const errorDetail = msg.payload.detail || JSON.stringify(msg.payload);\n finalMessage = {\n status: 'error',\n message: `User ${enrollState.name} enrollment failed: ${errorDetail}`\n };\n node.error(`Enrollment failed: ${errorDetail}`);\n}\n\n// Clean up context\nflow.set('enroll_' + deviceId, null);\n\nmsg.topic = `access/enroll_status/${deviceId}`;\nmsg.payload = finalMessage;\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1800, "y": 400, "wires": [ [ "enroll_status_publisher" ] ] }, { "id": "0221797157934c01", "type": "debug", "z": "face_access_main_flow", "name": "debug 2", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1470, "y": 200, "wires": [] }, { "id": "set_resolution_trigger", "type": "inject", "z": "face_access_main_flow", "name": "Set Camera Resolution", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "grove_vision_ai_v2_eb9e4d18", "payload": "", "payloadType": "date", "x": 150, "y": 220, "wires": [ [ "build_resolution_command" ] ] }, { "id": "build_resolution_command", "type": "function", "z": "face_access_main_flow", "name": "Build AT Command", "func": "// Get the resolution string from global config, e.g., '240,240' or '480,480'\nconst resolution = flow.get('resolution') || '240,240';\n// Get device ID from trigger message\nconst deviceId = msg.topic || 'grove_vision_ai_v2_eb9e4d18';\n\n// --- Resolution to OPT_ID Mapping ---\n// !!Important!!: This mapping is inferred from common practice, you need to confirm the correct OPT_ID from the full device documentation\n// AT+SENSOR=\\r\\n\n// Reference: https://github.com/Seeed-Studio/SSCMA-Micro/blob/main/docs/protocol/at-protocol-en_US.md\nconst resolutionMap = {\n '240,240': 0, // Assuming OPT_ID 0 is 240x240\n '480,480': 1, // Assuming OPT_ID 1 is 480x480 (needs verification)\n '640,480': 2\n};\n\nconst opt_id = resolutionMap[resolution];\nif (typeof opt_id === 'undefined') {\n node.error(`Unsupported resolution config: ${resolution}`);\n return null;\n}\n\n// Construct MQTT topic for sending AT command\nconst commandTopic = `sscma/v0/${deviceId}/rx`;\n\n// Construct AT command to set sensor (sensor_id=1, enable=1)\nconst atCommand = `AT+SENSOR=1,1,${opt_id}\\r\\n`;\n\nnode.log(`Preparing to send command to ${commandTopic}: ${atCommand}`);\n\nmsg.topic = commandTopic;\n// Convert the string command to the Buffer format required by the device\nmsg.payload = Buffer.from(atCommand);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 370, "y": 220, "wires": [ [ "resolution_mqtt_out" ] ] }, { "id": "resolution_mqtt_out", "type": "mqtt out", "z": "face_access_main_flow", "name": "Send Command", "topic": "", "qos": "1", "retain": "false", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "mqtt_broker", "x": 580, "y": 220, "wires": [] }, { "id": "delete_person_trigger", "type": "inject", "z": "face_access_main_flow", "name": "Delete Face", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"collection\":\"office_entrance\",\"name\":\"Jane Smith\"}", "payloadType": "json", "x": 100, "y": 500, "wires": [ [ "build_qdrant_delete_request" ] ] }, { "id": "build_qdrant_delete_request", "type": "function", "z": "face_access_main_flow", "name": "Build Delete Request", "func": "// Get collection and name from input\nconst collection = msg.payload.collection;\nconst nameToDelete = msg.payload.name;\n\nif (!collection || !nameToDelete) {\n node.error(\"Delete failed: 'collection' and 'name' must be provided in the input\", msg);\n return null;\n}\n\n// Get Hailo API configuration from flow context\nconst hailoHost = flow.get('hailo_host') || '192.168.10.179';\nconst hailoPort = flow.get('hailo_port') || '8000';\n\n// Set API endpoint for our new delete endpoint\nmsg.url = `http://${hailoHost}:${hailoPort}/vectors/delete`;\n\n// This is a POST request\nmsg.method = 'POST';\n\n// Build the payload\nmsg.payload = {\n collection: collection,\n user_id: nameToDelete\n};\n\n// Set HTTP headers\nmsg.headers = { 'Content-Type': 'application/json' };\n\nnode.log(`Preparing to delete data for user '${nameToDelete}' from collection '${collection}'`);\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 500, "wires": [ [ "send_qdrant_delete" ] ] }, { "id": "send_qdrant_delete", "type": "http request", "z": "face_access_main_flow", "name": "Send Delete Request", "method": "use", "ret": "obj", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 580, "y": 500, "wires": [ [ "delete_result_debug" ] ] }, { "id": "delete_result_debug", "type": "debug", "z": "face_access_main_flow", "name": "Delete Result", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "true", "targetType": "full", "statusVal": "payload.status", "statusType": "msg", "x": 760, "y": 500, "wires": [] }, { "id": "ef88f812ba82787c", "type": "function", "z": "face_access_main_flow", "name": "Image Resolution Check", "func": "function getJpegSize(buffer) {\n let i = 2; // Skip the JPEG header (0xFFD8)\n while (i < buffer.length) {\n if (buffer[i] !== 0xFF) {\n break;\n }\n\n const marker = buffer[i + 1];\n const length = buffer.readUInt16BE(i + 2);\n\n // Start Of Frame markers (not all SOF markers, but common ones)\n if (marker === 0xC0 || marker === 0xC2) {\n const height = buffer.readUInt16BE(i + 5);\n const width = buffer.readUInt16BE(i + 7);\n return { width, height };\n }\n\n i += 2 + length;\n }\n return null;\n}\n\n// Step 1: Parse base64 to Buffer\nconst base64 = msg.payload.replace(/^data:image\\/\\w+;base64,/, '');\nconst buffer = Buffer.from(base64, 'base64');\n\n// Step 2: Get dimensions\nconst size = getJpegSize(buffer);\nif (size) {\n msg.payload = size;\n} else {\n msg.payload = { error: \"Could not parse JPEG dimensions\" };\n}\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 730, "y": 40, "wires": [ [ "5ab02b1b4c2ba758" ] ] }, { "id": "5ab02b1b4c2ba758", "type": "debug", "z": "face_access_main_flow", "name": "debug 3", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 960, "y": 40, "wires": [] }, { "id": "mqtt_broker", "type": "mqtt-broker", "name": "Face Access MQTT Broker", "broker": "localhost", "port": "1883", "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": "4", "keepalive": "60", "cleansession": true, "autoUnsubscribe": true, "birthTopic": "system/nodered", "birthQos": "0", "birthRetain": "true", "birthPayload": "online", "birthMsg": {}, "closeTopic": "system/nodered", "closeQos": "0", "closeRetain": "true", "closePayload": "offline", "closeMsg": {}, "willTopic": "", "willQos": "0", "willRetain": "false", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" } ]