[
    {
        "id": "mobilesam_tab",
        "type": "tab",
        "label": "MobileSAM 分割一切",
        "disabled": false,
        "info": "# MobileSAM 分割一切\n\n## 功能介绍\n\nMobileSAM分割算法，支持USB摄像头拍照、USB/MIPI实时视频流和本地图片回灌三种模式。MobileSAM依赖检测框输入进行分割，无需指定目标的类别信息，仅需提供框。\n\n## 使用模式\n\n### 📷 拍照模式\n1. 点击\"📷 USB摄像头拍照\"按钮\n2. 系统会自动对照片进行MobileSAM分割\n3. 分割结果会直接显示在Node-RED编辑器中\n\n### 🎥 USB视频流模式（实时）\n1. **启动视频流**：点击\"🎥 USB摄像头视频流\"按钮\n2. **查看可视化**：点击\"打开可视化页面\"在浏览器中查看实时分割结果\n3. **停止服务**：完成后点击\"停止分割服务\"\n\n### 📹 MIPI视频流模式（实时）\n1. **启动视频流**：点击\"📹 MIPI摄像头视频流\"按钮\n2. **查看可视化**：点击\"打开可视化页面\"在浏览器中查看实时分割结果\n3. **停止服务**：完成后点击\"停止分割服务\"\n\n### 🖼️ 回灌模式\n1. 点击\"启动本地图片分割\"按钮\n2. 系统会对指定图片进行分割\n3. 分割结果会直接显示在Node-RED编辑器中\n\n## 支持平台\n\n- RDK X5\n- RDK X5 Module\n\n## 算法信息\n\n| 模型 | 平台 | 输入尺寸 | 推理帧率(fps) |\n|------|------|---------|-------------|\n| mobilesam | X5 | 1×3×384×384 | 6.6 |\n\n## 核心通信配置\n\n### WebSocket视频流\n- **视频流地址**: `http://{host}:8000`\n- **图像Topic**: `/image` (JPEG编码的图像流)\n- **性能**: 实时检测，延迟低\n\n## 注意事项\n\n- 分割结果会自动保存到 `/tmp/mobilesam/` 目录\n- MobileSAM依赖检测框输入，本示例使用固定框（图片中央）进行分割\n- 视频流模式需要先启动视频流，再打开可视化页面\n- 分割过程需要几秒钟时间，请耐心等待\n- 需要从tros安装路径复制配置文件：`cp -r /opt/tros/humble/lib/mono_mobilesam/config/ .`",
        "env": []
    },
    {
        "id": "comment_photo_section",
        "type": "comment",
        "z": "mobilesam_tab",
        "name": "📷 拍照分割流程",
        "info": "点击拍照按钮，系统会自动进行分割并显示结果",
        "x": 150,
        "y": 40,
        "wires": []
    },
    {
        "id": "inject_take_photo",
        "type": "inject",
        "z": "mobilesam_tab",
        "name": "📷 USB摄像头拍照",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 140,
        "y": 100,
        "wires": [
            [
                "rdk_camera_take_photo"
            ]
        ]
    },
    {
        "id": "rdk_camera_take_photo",
        "type": "rdk-camera takephoto",
        "z": "mobilesam_tab",
        "cameratype": "1",
        "filemode": "2",
        "filename": "photo.jpg",
        "filedefpath": "0",
        "filepath": "/tmp/mobilesam",
        "fileformat": "jpeg",
        "resolution": "2",
        "rotation": "0",
        "fliph": "0",
        "flipv": "0",
        "brightness": "50",
        "contrast": "0",
        "sharpness": "0",
        "quality": "80",
        "imageeffect": "none",
        "exposuremode": "auto",
        "iso": "0",
        "agcwait": "1.0",
        "led": "0",
        "awb": "auto",
        "name": "拍照",
        "x": 350,
        "y": 100,
        "wires": [
            [
                "debug_camera_output",
                "function_prepare_segmentation"
            ]
        ]
    },
    {
        "id": "debug_camera_output",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "调试：拍照节点输出",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "x": 550,
        "y": 60,
        "wires": []
    },
    {
        "id": "debug_prepare_seg",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "调试：准备分割",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "x": 750,
        "y": 60,
        "wires": []
    },
    {
        "id": "function_prepare_segmentation",
        "type": "function",
        "z": "mobilesam_tab",
        "name": "准备分割",
        "func": "// 确保目录存在并获取照片路径\n// fs 和 path 已在 libs 中声明，直接使用\nconst saveDir = '/tmp/mobilesam';\n\n// 确保目录存在\nif (!fs.existsSync(saveDir)) {\n    fs.mkdirSync(saveDir, { recursive: true });\n}\n\n// rdk-camera takephoto节点会在msg.payload中返回实际保存的文件路径\n// 例如: \"/tmp/mobilesam/image-20251121-154610.jpg\"\nlet photoPath = null;\nlet photoFileName = null;\n\n// 优先使用msg.payload中的文件路径（字符串）\nif (msg.payload && typeof msg.payload === 'string' && msg.payload.trim()) {\n    photoPath = msg.payload.trim();\n    \n    // 检查文件是否存在\n    if (fs.existsSync(photoPath)) {\n        photoFileName = path.basename(photoPath);\n        node.status({ fill: 'green', shape: 'dot', text: '照片: ' + photoFileName });\n        \n        // 文件存在，直接使用\n        msg.photoPath = photoPath;\n        msg.photoFileName = photoFileName;\n        msg.photoDir = saveDir;\n        \n        return msg;\n    } else {\n        // 文件不存在，等待一下（最多2.5秒）\n        const self = node;\n        let retryCount = 0;\n        const maxRetries = 5;\n        \n        const checkFile = function() {\n            if (fs.existsSync(photoPath)) {\n                const newMsg = {\n                    photoPath: photoPath,\n                    photoFileName: path.basename(photoPath),\n                    photoDir: saveDir\n                };\n                self.status({ fill: 'green', shape: 'dot', text: '照片已找到' });\n                self.send(newMsg);\n            } else if (retryCount < maxRetries) {\n                retryCount++;\n                setTimeout(checkFile, 500);\n            } else {\n                self.error('照片文件不存在: ' + photoPath);\n                self.status({ fill: 'red', shape: 'dot', text: '照片不存在' });\n            }\n        };\n        \n        setTimeout(checkFile, 500);\n        return null;\n    }\n} else if (msg.payload && Buffer.isBuffer(msg.payload)) {\n    // 如果payload是Buffer，保存到指定目录\n    photoFileName = 'photo.jpg';\n    photoPath = path.join(saveDir, photoFileName);\n    try {\n        fs.writeFileSync(photoPath, msg.payload);\n        node.status({ fill: 'green', shape: 'dot', text: '照片已保存' });\n        \n        msg.photoPath = photoPath;\n        msg.photoFileName = photoFileName;\n        msg.photoDir = saveDir;\n        \n        return msg;\n    } catch (err) {\n        node.error('保存照片失败: ' + err.message);\n        return null;\n    }\n} else {\n    // 如果都没有，尝试使用默认路径\n    photoFileName = 'photo.jpg';\n    photoPath = path.join(saveDir, photoFileName);\n    if (!fs.existsSync(photoPath)) {\n        node.error('无法找到照片文件，请检查拍照节点配置');\n        node.status({ fill: 'red', shape: 'dot', text: '照片不存在' });\n        return null;\n    }\n    node.status({ fill: 'green', shape: 'dot', text: '使用默认路径' });\n    \n    msg.photoPath = photoPath;\n    msg.photoFileName = photoFileName;\n    msg.photoDir = saveDir;\n    \n    return msg;\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [
            {
                "var": "fs",
                "module": "fs"
            },
            {
                "var": "path",
                "module": "path"
            }
        ],
        "x": 550,
        "y": 100,
        "wires": [
            [
                "debug_prepare_seg",
                "function_build_seg_command"
            ]
        ]
    },
    {
        "id": "function_build_seg_command",
        "type": "function",
        "z": "mobilesam_tab",
        "name": "构建分割命令",
        "func": "// 构建分割命令\n// MobileSAM使用 mono_mobilesam sam.launch.py\n// 回灌模式使用环境变量 CAM_TYPE=fb\n// 官方示例：export CAM_TYPE=fb && ros2 launch mono_mobilesam sam.launch.py\n// 注意：需要先复制配置文件\n// 修复：添加超时机制，防止分割服务一直运行导致卡住\nconst photoDir = path.normalize(msg.photoDir || '/tmp/mobilesam');\nconst photoFileName = msg.photoFileName || 'photo.jpg';\nconst photoPath = path.normalize(msg.photoPath || path.join(photoDir, photoFileName));\n\n// 规范化目标路径（用于比较）\nconst targetPath = path.normalize(path.join(photoDir, photoFileName));\n\n// 记录分割开始时间，用于后续只显示本次分割的结果\nif (typeof global.segmentationStartTime === 'undefined') {\n    global.segmentationStartTime = Date.now();\n} else {\n    global.segmentationStartTime = Date.now(); // 每次分割都更新开始时间\n}\n\n// 确保图片文件在工作目录下（如果不在，需要复制或移动）\n// 构建完整的launch命令\n// 工作目录设置为 photoDir，图片文件应该在 photoDir 下\n// publish_image_source 参数使用相对路径（相对于工作目录）\n// 如果文件不在工作目录，先复制到工作目录\n// 添加超时机制（60秒），确保分割服务不会一直运行\n// 参考 EdgeSAM 的实现方式，使用 timeout 命令\n// 修复：使用规范化后的路径进行比较，并正确转义路径中的特殊字符\nconst needCopy = photoPath !== targetPath;\nconst copyCmd = needCopy ? 'echo \"复制图片文件到工作目录...\" && cp \"' + photoPath.replace(/\"/g, '\\\"') + '\" \"' + targetPath.replace(/\"/g, '\\\"') + '\" 2>&1 && echo \"图片已复制\" || echo \"复制失败或文件已存在\"; ' : '';\n\nmsg.payload = 'cd \"' + photoDir.replace(/\"/g, '\\\"') + '\" && source /opt/tros/humble/setup.bash && cp -r /opt/tros/humble/lib/mono_mobilesam/config/ . 2>/dev/null || true && echo \"=== 开始MobileSAM分割 ===\" && echo \"工作目录: $(pwd)\" && echo \"原始图片路径: ' + photoPath + '\" && echo \"目标文件名: ' + photoFileName + '\" && ' + copyCmd + 'echo \"检查图片是否存在...\" && ls -lh \"' + photoFileName.replace(/\"/g, '\\\"') + '\" 2>&1 && echo \"启动分割（60秒超时）...\" && export CAM_TYPE=fb && timeout 60 ros2 launch mono_mobilesam sam.launch.py publish_image_source:=' + photoFileName + ' publish_image_format:=jpg 2>&1 || echo \"分割完成或超时（60秒）\"';\n\nnode.status({ fill: 'blue', shape: 'dot', text: '命令已构建（含超时）' });\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [
            {
                "var": "path",
                "module": "path"
            }
        ],
        "x": 750,
        "y": 100,
        "wires": [
            [
                "debug_build_command",
                "exec_start_segmentation"
            ]
        ]
    },
    {
        "id": "debug_build_command",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "调试：构建的命令",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 950,
        "y": 60,
        "wires": []
    },
    {
        "id": "exec_start_segmentation",
        "type": "exec",
        "z": "mobilesam_tab",
        "name": "启动分割",
        "command": "",
        "addpay": true,
        "append": "",
        "useSpawn": true,
        "timer": "0",
        "oldrc": false,
        "x": 950,
        "y": 100,
        "wires": [
            [
                "debug_seg_output"
            ],
            [
                "debug_seg_error"
            ],
            []
        ]
    },
    {
        "id": "debug_seg_output",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "分割输出",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 1180,
        "y": 80,
        "wires": []
    },
    {
        "id": "debug_seg_error",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "分割错误",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 1180,
        "y": 120,
        "wires": []
    },
    {
        "id": "comment_video_section",
        "type": "comment",
        "z": "mobilesam_tab",
        "name": "🎥 USB视频流分割流程",
        "info": "启动USB摄像头实时视频流，进行连续分割",
        "x": 150,
        "y": 360,
        "wires": []
    },
    {
        "id": "inject_usb_video",
        "type": "inject",
        "z": "mobilesam_tab",
        "name": "🎥 USB摄像头视频流",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "usb_video",
        "payload": "start",
        "payloadType": "str",
        "x": 140,
        "y": 420,
        "wires": [
            [
                "exec_usb_video"
            ]
        ]
    },
    {
        "id": "exec_usb_video",
        "type": "exec",
        "z": "mobilesam_tab",
        "name": "USB视频流启动器",
        "command": "source /opt/tros/humble/setup.bash && cp -r /opt/tros/humble/lib/mono_mobilesam/config/ . 2>/dev/null || true && export CAM_TYPE=usb && ros2 launch mono_mobilesam sam.launch.py",
        "addpay": false,
        "append": "",
        "useSpawn": true,
        "timer": "120",
        "oldrc": false,
        "x": 380,
        "y": 420,
        "wires": [
            [
                "debug_usb_video_output"
            ],
            [
                "debug_usb_video_error"
            ],
            []
        ]
    },
    {
        "id": "debug_usb_video_output",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "USB视频流输出",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 650,
        "y": 400,
        "wires": []
    },
    {
        "id": "debug_usb_video_error",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "USB视频流错误",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 650,
        "y": 440,
        "wires": []
    },
    {
        "id": "comment_mipi_video_section",
        "type": "comment",
        "z": "mobilesam_tab",
        "name": "📹 MIPI视频流分割流程",
        "info": "启动MIPI摄像头实时视频流，进行连续分割",
        "x": 150,
        "y": 500,
        "wires": []
    },
    {
        "id": "inject_mipi_video",
        "type": "inject",
        "z": "mobilesam_tab",
        "name": "📹 MIPI摄像头视频流",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "mipi_video",
        "payload": "start",
        "payloadType": "str",
        "x": 140,
        "y": 560,
        "wires": [
            [
                "exec_mipi_video"
            ]
        ]
    },
    {
        "id": "exec_mipi_video",
        "type": "exec",
        "z": "mobilesam_tab",
        "name": "MIPI视频流启动器",
        "command": "source /opt/tros/humble/setup.bash && cp -r /opt/tros/humble/lib/mono_mobilesam/config/ . 2>/dev/null || true && export CAM_TYPE=mipi && ros2 launch mono_mobilesam sam.launch.py",
        "addpay": false,
        "append": "",
        "useSpawn": true,
        "timer": "120",
        "oldrc": false,
        "x": 380,
        "y": 560,
        "wires": [
            [
                "debug_mipi_video_output"
            ],
            [
                "debug_mipi_video_error"
            ],
            []
        ]
    },
    {
        "id": "debug_mipi_video_output",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "MIPI视频流输出",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 650,
        "y": 540,
        "wires": []
    },
    {
        "id": "debug_mipi_video_error",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "MIPI视频流错误",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 650,
        "y": 580,
        "wires": []
    },
    {
        "id": "comment_browser_section",
        "type": "comment",
        "z": "mobilesam_tab",
        "name": "2️⃣ 查看检测结果",
        "info": "在浏览器中查看实时检测画面",
        "x": 200,
        "y": 640,
        "wires": []
    },
    {
        "id": "inject_browser",
        "type": "inject",
        "z": "mobilesam_tab",
        "name": "打开可视化页面",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "http://{host}:8000",
        "payloadType": "str",
        "x": 140,
        "y": 680,
        "wires": [
            [
                "openurl_browser"
            ]
        ]
    },
    {
        "id": "openurl_browser",
        "type": "rdk-tools openurl",
        "z": "mobilesam_tab",
        "name": "",
        "x": 380,
        "y": 680,
        "wires": []
    },
    {
        "id": "comment_control_section",
        "type": "comment",
        "z": "mobilesam_tab",
        "name": "🎮 控制功能",
        "info": "停止分割服务",
        "x": 150,
        "y": 720,
        "wires": []
    },
    {
        "id": "inject_stop",
        "type": "inject",
        "z": "mobilesam_tab",
        "name": "停止分割服务",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "topic": "",
        "payload": "stop",
        "payloadType": "str",
        "x": 140,
        "y": 780,
        "wires": [
            [
                "exec_stop_all"
            ]
        ]
    },
    {
        "id": "exec_stop_all",
        "type": "exec",
        "z": "mobilesam_tab",
        "name": "停止服务",
        "command": "sudo pkill -9 -f hobot_usb_cam; sudo pkill -9 -f mono_mobilesam; sudo pkill -9 -f hobot_codec_republish; sudo pkill -9 -f websocket; sudo pkill -9 -f python3; sudo pkill -9 -f 'ros2 launch'; sudo pkill -9 -f 'ros2 run'; sleep 2; sudo rm -rf /dev/shm/*; ros2 daemon stop 2>/dev/null; sleep 1; echo '✓ 服务已彻底停止，资源已释放'",
        "addpay": false,
        "append": "",
        "useSpawn": false,
        "timer": "10",
        "oldrc": false,
        "x": 350,
        "y": 780,
        "wires": [
            [
                "debug_stop_info"
            ],
            [],
            []
        ]
    },
    {
        "id": "debug_stop_info",
        "type": "debug",
        "z": "mobilesam_tab",
        "name": "停止状态",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 550,
        "y": 780,
        "wires": []
    },
    {
        "id": "global_config_mobilesam",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-node-rdk-camera": "0.0.17",
            "node-red-contrib-image-tools": "2.1.1"
        }
    }
]

