使用 Node-RED 开发 reCamera
Node-RED 简介
Node-RED 的目标是让任何人都能构建收集、转换和可视化数据的应用程序;通过构建流程来自动化他们的世界。其低代码特性使其对任何背景的用户都易于使用,无论是家庭自动化、工业控制系统还是其他应用场景。通过将 Node-RED 集成到 reCamera 中,它提供了一种对初学者友好的开发方法,用户可以直接拖放操作设备。
您可以在Node-RED 概念页面了解更多,或者从视频教程开始学习。
在 reCamera 上,我们已经为 Node-RED 安装了以下调色板:
- SSCMA 调色板(适用于所有操作系统版本)
- Dashboard 调色板(适用于所有操作系统版本)
- reCamera 硬件(适用于 0.1.6 及以上操作系统版本)
结合 Node-RED 默认提供的其他调色板,例如 function、debug、trigger、mqtt 等,您现在可以使用它们构建流程,以实现不同的计算机视觉应用。
将流程导入到 reCamera
有两种方法可以将流程导入到 reCamera:
-
从本地文件或 JSON 导入流程。
- 步骤1:点击右上角的
菜单图标
,然后选择“导入”。
- 步骤2:点击“导入”选项卡。
-
步骤3:粘贴流程的 JSON 代码或上传流程的 JSON 文件。您可以在社区或 GitHub 上找到可用的流程,并将它们集成到 reCamera 中。
-
步骤4:点击“导入”按钮。
- 步骤1:点击右上角的
-
从 SenseCraft reCamera 公共应用程序导入流程。
- 步骤1:在公共应用程序中找到感兴趣的流程,然后点击
克隆
。
-
步骤2:选择通过 USB 或网络连接到 reCamera 的方法。如果使用网络连接,请在文本框中输入 reCamera 的正确 IP 地址,然后点击连接。
-
步骤3:公共应用程序将自动导入到 reCamera。您还可以将自己的流程贡献给社区,以激励其他用户并丰富平台内容。
- 步骤1:在公共应用程序中找到感兴趣的流程,然后点击
将流程部署到 reCamera
一旦您在工作区中添加、删除或更改了节点和连线,请确保点击右上角的 部署
按钮,将最新的流程部署到 reCamera。
SSCMA 调色板

node-red-contrib-sscma
是一个 Node-RED 节点组件,旨在通过基于流程的编程快速部署 AI 模型。sscma
是 Seeed SenseCraft Model Assistant 的缩写。它允许 AI 模型输出与其他设备无缝集成,从而实现智能自动化和智能工作流。
安装
此调色板在安装 Node-RED 时默认安装。如果您想手动安装,可以按照以下步骤操作:
- 通过访问
ip_address/#/workspace
进入 Node-RED 工作区。 - 点击右上角的
菜单图标
并选择“管理调色板”。 - 点击“安装”选项卡。
- 在搜索栏中输入 "node-red-contrib-sscma",然后点击“安装”按钮。
- 等待安装完成。请注意,由于设备限制,下载时间可能会根据网络速度从 30 秒到 5 分钟不等。
Camera 节点

此节点用于启用摄像头,可用于捕获摄像头模块的流。
配置
首次拖出节点时,您将看到以下界面:

音频选择表示您是否希望视频流输出音频,音频的音量是可调的。节点上的红色三角形表示该节点需要一个客户端与之连接。您可以点击 添加图标
来添加一个 SSCMA 客户端。

然后,您可以通过点击右上角的“添加”按钮添加 sscma 配置
节点,默认参数如下。此配置节点仅需为其他节点(如模型节点等)配置一次。选择客户端后,红色三角形将消失。
输入和输出
您还可以通过解析 msg.enabled = true
或 msg.enabled = false
向节点输入参数来控制摄像头是否开启。例如,可以使用时间触发节点在特定时间启用摄像头,以实现节能摄像头。(仅适用于 OS 版本 0.1.5 及以上)
Camera 节点可以连接到 stream
节点以实现 RTSP,或连接到 preview
节点或 model
节点以进行计算机视觉处理。
Model 节点
此模型节点使 reCamera 能够加载不同的视觉 AI 模型(如 Yolo),并调整模型的参数。

配置
请同样为客户端选择 sscma
。选择后,红色三角形将消失。

模型选择
在 reCamera 上部署不同模型有三种方式:
- 选择
设备上的
模型。reCamera 中默认包含几个 Yolo 模型,详情请见 设备上的模型。 - 从
SenseCraft Zoo
中选择模型。这里有多个公共模型可供选择,例如手势和水果识别。用户还可以上传自己的模型,并将其公开以贡献社区。 上传您自己的模型
到 reCamera。按照 [将模型转换为 reCamera](https://wiki.seeedstudio.com/cn/convert xxx) 的说明,用户可以将自己的 AI 模型转换为 INT8 cvimodel 格式以适配 reCamera。然后将模型上传到 reCamera 进行部署。模型上传后,请在Labels
字段中列出模型的类别。

模型参数
Confidence
滑块用于设置 AI 模型的置信度。置信度是模型对特定预测分配的概率或确定性,范围从 0 到 1。较高的置信度表示模型会过滤掉置信度较低的预测。
IoU
滑块用于设置 AI 模型的 IoU。IoU 是一种度量,用于衡量目标检测任务中预测边界框与真实边界框之间的重叠程度。它是两个框的交集面积与并集面积的比值。IoU 值范围从 0 到 1,其中 0 表示没有重叠,1 表示完全匹配。较高的 IoU 阈值(例如 0.5 或 0.7)表示对正确检测的要求更严格。
输出
base64 image output
复选框用于设置是否希望将 base64 图像代码与其他参数一起输出。
Trace
复选框用于启用跟踪模式。当启用跟踪模式时,检测到的对象将被分配 ID。
Counting
复选框用于启用计数模式。当启用计数模式时,节点会将计数信息输出到控制台。
Splitter
字段用于设置计数线。在框中绘制任意线条以统计穿过该线的对象数量。
将模型节点连接到调试节点以查看输出。以下是 Yolo 11n 的输出对象示例:
{
boxes: [
0: box_center_x,
1: box_center_y,
2: box_width,
3: box_height,
4: detected object score,
5: detected object class ID,
],
count: //推理次数 ,
image: //base64 图像代码,
labels: [
0: class name // 例如 person
],
perf: [
0: 0 fps, //预处理 fps
1: 40 ms, //推理时间
2: 20 ms, //后处理时间
],
resolution: [ //图像的像素大小
0: 640,
1: 640,
]
}
模型节点可以连接到 preview
节点以在 Node-RED 工作区中预览效果。您还可以将输出解析到其他节点以进行进一步处理,例如 function 节点
、mqtt 节点
、debug 节点
或 Dashboard UI Palette
中的其他节点。
预览节点
此节点用于启用摄像头模块的预览功能。可以用来预览摄像头模块的视频流。您可以使用绿色开关来启用或禁用预览功能。请注意,由于设备 CPU 的限制,不要同时拖出过多的预览节点和调试节点,因为在控制台打印调试信息时,CPU 负载会更重。

流媒体节点
此节点用于启用摄像头模块的流媒体功能。可以用来将摄像头模块的视频流传输到服务器。

配置
请为客户端选择 sscma
。选择后,红色三角形将消失。
输入和输出
输入:将 camera
节点连接到 stream
节点以启用流媒体功能。
输出:
您可以使用其他应用程序(如 VLC)查看来自 reCamera 的 RTSP 流。例如,在上图的示例中,您可以在 VLC 中使用 rtsp://admin:[email protected]:554/live
来查看 H.264 流媒体视频。
- 视频参数:默认 1920 * 1800 * 15fps。
- 延迟:这取决于您使用的终端应用程序。例如,VLC 的延迟为 500 毫秒。
考虑到设备推流的稳定性,我们推荐的最高配置为 1080p@15fps 视频流。这也是默认配置。 如果您想设置不同的分辨率,可以通过以下步骤更改预设选项:
- 进入 recamera 后端终端
- 输入命令
cd /home/recamera/.node-red/node_modules/node-red-contrib-sscma/nodes
- 输入命令
sudo sed -i 's/option: n.option || 0,/option: n.option !== undefined ? parseInt(n.option) : 1,/' camera.js
设置为 720p 视频。
option: n.option !== undefined ? parseInt(n.option) : 1
选项设置的数值配置关系如下:
if (option.find("1080p") != std::string::npos) {
option_ = 0;
} else if (option.find("720p") != std::string::npos) {
option_ = 1;
} else if (option.find("480p") != std::string::npos) {
option_ = 2;
}
有关详细信息,请参考以下链接。sscma-node 节点可以根据您的需求自定义视频分辨率和帧率。
需要注意的是,修改源代码需要您具备扎实的 C++ 基础,并熟悉交叉编译的技术栈。只需修改 "default" 的配置即可。

保存节点
此节点用于启用摄像头模块的保存功能。可以用来保存摄像头模块的视频流。

配置
请为客户端选择 sscma
。选择后,红色三角形将消失。
输入
输入:将 camera
节点连接到 save
节点以启用保存功能。
保存参数
存储:
- 本地 -> 路径:
/userdata/VIDEO
- 外部 -> 存储在 SD 卡中。

启动复选框:勾选后,保存将立即开始。保存参数将基于以下的 切片
和 时长
。
切片:每个文件的视频时长。(在版本 0.1.6 或更高版本中,您可以在下拉菜单中更改单位)
时长:您想保存的视频总时长。(在版本 0.1.6 或更高版本中,您可以在下拉菜单中更改单位)
例如,如果切片设置为 5 分钟,时长设置为 1 小时,则视频将保存为 12 个文件,每个文件 5 分钟。
使用 SSCMA 节点的示例流程

此流程使用 Yolo 11n 检测模型在工作区中预览检测到的对象,并通过 RTSP 输出原始视频流。
[{"id":"d72dbb768278d92b","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"291219139b4904ee","type":"sscma","host":"localhost","mqttport":"1883","apiport":"80","clientid":"recamera","username":"","password":""},{"id":"7ee52cad4723fbee","type":"camera","z":"d72dbb768278d92b","option":0,"client":"291219139b4904ee","audio":true,"volume":80,"x":120,"y":220,"wires":[["09b5621ae3fa9d71","0fcaef819aa764e6"]]},{"id":"09b5621ae3fa9d71","type":"model","z":"d72dbb768278d92b","name":"","uri":"/usr/share/supervisor/models/yolo11n_detection_cv181x_int8.cvimodel","model":"YOLO11n Detection","tscore":0.45,"tiou":0.25,"debug":false,"trace":false,"counting":false,"classes":"person,bicycle,car,motorcycle,airplane,bus,train,truck,boat,traffic light,fire hydrant,stop sign,parking meter,bench,bird,cat,dog,horse,sheep,cow,elephant,bear,zebra,giraffe,backpack,umbrella,handbag,tie,suitcase,frisbee,skis,snowboard,sports ball,kite,baseball bat,baseball glove,skateboard,surfboard,tennis racket,bottle,wine glass,cup,fork,knife,spoon,bowl,banana,apple,sandwich,orange,broccoli,carrot,hot dog,pizza,donut,cake,chair,couch,potted plant,bed,dining table,toilet,tv,laptop,mouse,remote,keyboard,cell phone,microwave,oven,toaster,sink,refrigerator,book,clock,vase,scissors,teddy bear,hair drier,toothbrush","splitter":"0,0,0,0","client":"291219139b4904ee","x":270,"y":220,"wires":[["9a4aacf197bedbaa"]]},{"id":"9a4aacf197bedbaa","type":"preview","z":"d72dbb768278d92b","name":"","active":true,"pass":false,"outputs":0,"x":440,"y":220,"wires":[]},{"id":"0fcaef819aa764e6","type":"stream","z":"d72dbb768278d92b","name":"stream","protocol":0,"port":554,"session":"live","username":"admin","password":"admin","client":"291219139b4904ee","x":270,"y":300,"wires":[]}]
仪表板 UI 调色板
Dashboard 2.0 调色板 是由 Flowfuse 基于 Dashboard 1.0 调色板开发的(感谢他们的出色工作)。这是一个易于使用的 Node-RED 节点集合,允许您创建数据驱动的仪表板和数据可视化。通过此调色板,您可以创建直接运行在 reCamera 上的交互式仪表板,包含按钮、图表、文本或滑块等组件,并预览效果。
安装
此调色板默认安装在设备中。如果您想手动安装,可以按照以下步骤操作:
- 通过访问
ip_address/#/workspace
进入 Node-RED 工作区。 - 点击右上角的
菜单图标
,选择“管理调色板”。 - 点击“安装”标签。
- 在搜索栏中输入 "node-red-contrib-sscma",然后点击“安装”按钮。
- 等待安装完成。请注意,由于设备限制,下载时间可能会根据网络速度和包大小在 30 秒到 5 分钟之间。
仪表板节点

流行的节点如 button
、slider
、switch
、text
和 template
在为 reCamera 构建仪表板时非常方便。您可以在其官方网站上查看每个节点的详细文档,或者观看他们的初学者教程,以更好地了解此调色板中的节点和小部件。
使用仪表板节点的示例流程
在操作系统版本 0.1.4 及以上的设备中,默认安装了一个仪表板流程,作为用户入门的开箱示例。任何低于 0.1.4 的操作系统版本都不会包含默认的仪表板流程。
此流程的功能是预览模型输出,提供不同的演示,例如计数人、狗、猫或瓶子。它还提供了如何将基本网页嵌入到仪表板的示例,例如网络、终端、SSH 页面以及设备信息(如 CPU、内存、磁盘使用情况等)。

在此仪表板中,使用了以下节点:
slider
节点:用于控制模型的置信度和 IoU。dropdown
节点:用于选择演示。text
节点:用于显示模型名称和一些文本信息。template
节点:用于渲染 base64 图像代码并在图像上绘制边界框。function
节点:用于将模型节点的输出解析到模板节点,并为其他节点添加一些逻辑。

以下是此流程的 JSON:
[{"id":"35ee92b6dbd194c1","type":"tab","label":"Dashboard","disabled":false,"info":"","env":[]},{"id":"39f2b91c983d671f","type":"subflow","name":"Device Info Pages","info":"","category":"sscma","in":[],"out":[],"env":[],"meta":{},"color":"#DDAA99"},{"id":"13a0b285aa95568e","type":"subflow","name":"Default Pages","info":"","category":"sscma","in":[],"out":[],"env":[],"meta":{},"color":"#DDAA99"},{"id":"dec794eaeb95589c","type":"sscma","host":"localhost","mqttport":"1883","apiport":"80","clientid":"recamera","username":"","password":""},{"id":"9ab1ee429e233a80","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true},{"id":"866ca6b212de07b4","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"234998f63c55af55","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"2788be32a24982e1","type":"ui-page","name":"Network","ui":"9ab1ee429e233a80","path":"/network","icon":"wifi","layout":"grid","theme":"234998f63c55af55","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":2,"className":"","visible":true,"disabled":false},{"id":"15bec593c23e2df1","type":"ui-group","name":"Wi-Fi","page":"2788be32a24982e1","width":"12","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"034b986fab50b7bb","type":"ui-page","name":"Device Info","ui":"9ab1ee429e233a80","path":"/Deviceinfo","icon":"cog","layout":"grid","theme":"234998f63c55af55","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":3,"className":"","visible":"true","disabled":"false"},{"id":"cb81f9d78a6a3513","type":"ui-group","name":"Memory","page":"034b986fab50b7bb","width":"6","height":"1","order":4,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"35ddf11ddd1ade60","type":"ui-group","name":"Load","page":"034b986fab50b7bb","width":"6","height":"1","order":3,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"8ee7b1867c318ca3","type":"ui-group","name":"Storage","page":"034b986fab50b7bb","width":"6","height":"1","order":2,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"4b590614656223c2","type":"ui-page","name":"Security","ui":"9ab1ee429e233a80","path":"/security","icon":"security","layout":"grid","theme":"234998f63c55af55","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":4,"className":"","visible":"true","disabled":"false"},{"id":"d3e7dcd4b2447549","type":"ui-page","name":"Terminal","ui":"9ab1ee429e233a80","path":"/terminal","icon":"console","layout":"grid","theme":"234998f63c55af55","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":5,"className":"","visible":true,"disabled":false},{"id":"7f84e6e11f01d5aa","type":"ui-group","name":"Security","page":"4b590614656223c2","width":"12","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"62e3f90362f475e5","type":"ui-group","name":"Terminal","page":"d3e7dcd4b2447549","width":"12","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"eb4ea4ad231b87b6","type":"ui-page","name":"Preview","ui":"9ab1ee429e233a80","path":"/preview","icon":"home","layout":"grid","theme":"234998f63c55af55","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"853d93c4c0f19c38","type":"ui-group","name":"Power","page":"034b986fab50b7bb","width":"6","height":"1","order":5,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"a2f6b486b575c329","type":"ui-group","name":"Sys Info","page":"034b986fab50b7bb","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"53a493606ee6d430","type":"ui-group","name":"Preview","page":"eb4ea4ad231b87b6","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"d9c66abde84c734d","type":"ui-group","name":"Model Selection","page":"eb4ea4ad231b87b6","width":"6","height":"1","order":2,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"0403368ef716b66e","type":"ui-spacer","group":"d9c66abde84c734d","name":"spacer","tooltip":"","order":5,"width":"2","height":"1","className":""},{"id":"f55b8c3e9a243e2d","type":"ui-page","name":"DisplayNone","ui":"9ab1ee429e233a80","path":"/page6","icon":"home","layout":"grid","theme":"866ca6b212de07b4","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":6,"className":"","visible":"false","disabled":"false"},{"id":"56e94a4a52495b4e","type":"ui-group","name":"Hidden","page":"f55b8c3e9a243e2d","width":6,"height":1,"order":1,"showTitle":true,"className":"","visible":false,"disabled":"false","groupType":"default"},{"id":"9ca150fa0779ddf5","type":"inject","z":"39f2b91c983d671f","name":"update","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"30","crontab":"","once":true,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":240,"y":320,"wires":[["a7f51d25943cec64","b27627174b2cb1ac","91b465681153a8a9","8614287768526732","eec7b34928fa4d5e","32570c230c544e73"]]},{"id":"92d7c90757d47543","type":"function","z":"39f2b91c983d671f","name":"","func":"msg.payload = msg.payload.memusage;\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":626,"y":655,"wires":[["d01cf18989f42d3c"]]},{"id":"3483b24989d032a3","type":"function","z":"39f2b91c983d671f","name":"","func":"function formatBytes(bytes,decimals) {\n if(bytes === 0) return '0 Byte';\n var k = 1000; // or 1024 for binary\n var dm = decimals + 1 || 3;\n var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n var i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n}\n\nmsg.payload = formatBytes(msg.payload.totalmem);\nreturn msg;","outputs":1,"noerr":0,"x":626,"y":695,"wires":[["f72a3db98afc3b6c"]]},{"id":"507876942fdfea09","type":"function","z":"39f2b91c983d671f","name":"","func":"function formatBytes(bytes,decimals) {\n if(bytes === 0) return '0 Byte';\n var k = 1000; // or 1024 for binary\n var dm = decimals + 1 || 3;\n var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n var i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n}\n\nmsg.payload = formatBytes(msg.payload.freemem);\nreturn msg;","outputs":1,"noerr":0,"x":626,"y":735,"wires":[["7345921066c58fa5"]]},{"id":"47c24b2506364a5a","type":"function","z":"39f2b91c983d671f","name":"","func":"function timeConversion(millisec) {\n\n var seconds = (millisec / 1000).toFixed(1);\n\n var minutes = (millisec / (1000 * 60)).toFixed(1);\n\n var hours = (millisec / (1000 * 60 * 60)).toFixed(1);\n\n var days = (millisec / (1000 * 60 * 60 * 24)).toFixed(1);\n\n if (seconds < 60) {\n return seconds + \" Sec\";\n } else if (minutes < 60) {\n return minutes + \" Min\";\n } else if (hours < 24) {\n return hours + \" Hrs\";\n } else {\n return days + \" Days\"\n }\n}\n\nmsg.payload = timeConversion(msg.payload.uptime * 1000);\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":607,"y":155,"wires":[["1ad12d7370576a43"]]},{"id":"d204a0af6bfd434e","type":"function","z":"39f2b91c983d671f","name":"","func":"msg.payload = msg.payload.hostname;\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":606,"y":192,"wires":[["36bb8d5e6bdd8744"]]},{"id":"5477c749de145490","type":"function","z":"39f2b91c983d671f","name":"","func":"msg.payload = msg.payload.platform;\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":608,"y":230,"wires":[["86b895252ee31204"]]},{"id":"daa940d746ec2bef","type":"function","z":"39f2b91c983d671f","name":"","func":"msg.payload = msg.payload.arch;\nreturn msg;","outputs":1,"noerr":0,"x":609,"y":269,"wires":[["7a714543abc3b9be"]]},{"id":"a6149aba5c0badd3","type":"function","z":"39f2b91c983d671f","name":"","func":"msg.payload = msg.payload.memusage;\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":626,"y":615,"wires":[["91f21350c7e1df8b"]]},{"id":"3174c5a734aa1d2f","type":"comment","z":"39f2b91c983d671f","name":"Memory Usage","info":"","x":826,"y":575,"wires":[]},{"id":"7ec11bb31e97fa06","type":"comment","z":"39f2b91c983d671f","name":"System Information","info":"","x":836,"y":95,"wires":[]},{"id":"91f21350c7e1df8b","type":"ui-chart","z":"39f2b91c983d671f","group":"cb81f9d78a6a3513","name":"Memory - 24 Hours","label":"24 Hours","order":4,"chartType":"line","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"HH:mm:ss","xmin":"","xmax":"","yAxisLabel":"%","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"","ymax":"","bins":10,"action":"append","stackSeries":false,"pointShape":"circle","pointRadius":4,"showLegend":true,"removeOlder":1,"removeOlderUnit":"86400","removeOlderPoints":"","colors":["#0095ff","#ff0000","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#666666"],"textColorDefault":true,"gridColor":["#e5e5e5"],"gridColorDefault":true,"width":6,"height":8,"className":"","interpolation":"linear","x":836,"y":615,"wires":[[]]},{"id":"d01cf18989f42d3c","type":"ui-gauge","z":"39f2b91c983d671f","name":"Memory Usage","group":"cb81f9d78a6a3513","order":1,"width":3,"height":3,"gtype":"gauge-half","gstyle":"rounded","title":"1 Minute","units":"Usage","icon":"memory","prefix":"","suffix":"%","segments":[{"from":"0","color":"#5cd65c"},{"from":"40","color":"#ffc800"},{"from":"70","color":"#ea5353"}],"min":0,"max":"100","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":826,"y":655,"wires":[]},{"id":"f72a3db98afc3b6c","type":"ui-text","z":"39f2b91c983d671f","group":"cb81f9d78a6a3513","order":3,"width":0,"height":0,"name":"Total Memory","label":"Total Memory","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":826,"y":695,"wires":[]},{"id":"7345921066c58fa5","type":"ui-text","z":"39f2b91c983d671f","group":"cb81f9d78a6a3513","order":2,"width":0,"height":0,"name":"Free Memory","label":"Free Memory","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":816,"y":735,"wires":[]},{"id":"1ad12d7370576a43","type":"ui-text","z":"39f2b91c983d671f","group":"a2f6b486b575c329","order":1,"width":0,"height":0,"name":"Uptime","label":"Uptime","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":806,"y":155,"wires":[]},{"id":"36bb8d5e6bdd8744","type":"ui-text","z":"39f2b91c983d671f","group":"a2f6b486b575c329","order":5,"width":0,"height":0,"name":"Hostname","label":"Hostname","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":816,"y":195,"wires":[]},{"id":"86b895252ee31204","type":"ui-text","z":"39f2b91c983d671f","group":"a2f6b486b575c329","order":4,"width":0,"height":0,"name":"Platform","label":"Platform","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":801,"y":242,"wires":[]},{"id":"7a714543abc3b9be","type":"ui-text","z":"39f2b91c983d671f","group":"a2f6b486b575c329","order":2,"width":0,"height":0,"name":"Arch","label":"Arch","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":791,"y":282,"wires":[]},{"id":"eec7b34928fa4d5e","type":"exec","z":"39f2b91c983d671f","command":"top -d 0.5 -b -n2 | grep \"Cpu(s)\"|tail -n 1 | awk '{print ($2 + $4) / 100}'","addpay":false,"append":"","useSpawn":"","timer":"","winHide":false,"name":"CPU Load","x":476,"y":435,"wires":[["76a3d21caa20cc2a","e75401352787eeb2"],[],[]]},{"id":"04cc577099c60653","type":"comment","z":"39f2b91c983d671f","name":"CPU Load","info":"","x":806,"y":395,"wires":[]},{"id":"76a3d21caa20cc2a","type":"ui-gauge","z":"39f2b91c983d671f","name":"CPU","group":"35ddf11ddd1ade60","order":1,"width":3,"height":3,"gtype":"gauge-half","gstyle":"rounded","title":"CPU","units":"Usage","icon":"cpu-64-bit","prefix":"","suffix":"%","segments":[{"from":"0","color":"#5cd65c"},{"from":"40","color":"#ffc800"},{"from":"70","color":"#ea5353"}],"min":0,"max":"100","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":796,"y":435,"wires":[]},{"id":"e75401352787eeb2","type":"ui-chart","z":"39f2b91c983d671f","group":"35ddf11ddd1ade60","name":"CPU Load%","label":"CPU Load%","order":2,"chartType":"line","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"HH:mm:ss","xmin":"","xmax":"","yAxisLabel":"%","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"","ymax":"","bins":10,"action":"append","stackSeries":false,"pointShape":"circle","pointRadius":4,"showLegend":true,"removeOlder":"5","removeOlderUnit":"60","removeOlderPoints":"","colors":["#0095ff","#ff0000","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#666666"],"textColorDefault":true,"gridColor":["#e5e5e5"],"gridColorDefault":true,"width":6,"height":8,"className":"","interpolation":"linear","x":816,"y":475,"wires":[[]]},{"id":"32570c230c544e73","type":"exec","z":"39f2b91c983d671f","command":"df -h","addpay":false,"append":"","useSpawn":"","timer":"","winHide":false,"name":"Disk Usage","x":436,"y":855,"wires":[["b1b81c47791b54f8"],[],[]]},{"id":"b1b81c47791b54f8","type":"function","z":"39f2b91c983d671f","name":"function 3","func":"// Input payload as a string\nlet data = msg.payload;\n\n// Split the input into lines\nlet lines = data.split('\\n');\n\n// Initialize variables\nlet totalSize = 0; // Total space size in GB\nlet totalUsed = 0.256; // Used space in GB\nlet totalAvailable = 0; // Available space in GB\n\n// Updated regex to match both MB and GB, and all filesystem types\nlet regex = /(\\S+)\\s+([\\d.]+)([MKG]?)\\s+([\\d.]+)([MKG]?)\\s+([\\d.]+)([MKG]?)\\s+(\\d+)%/;\n\n// Function to convert MB to GB\nfunction mbToGb(value, unit) {\n switch (unit) {\n case 'G':\n return value;\n case 'M':\n return value / 1024;\n case 'K':\n return value / 1024 / 1024;\n default:\n return 0;\n }\n}\n\n// Iterate through each line and sum the values\nfor (let line of lines) {\n let match = line.match(regex);\n\n if (match && (match[1] === \"/dev/root\" || match[1] === \"/dev/mmcblk0p6\")) {\n // Extract values and units\n let size = parseFloat(match[2]);\n let sizeUnit = match[3];\n let used = parseFloat(match[4]);\n let usedUnit = match[5];\n let available = parseFloat(match[6]);\n let availUnit = match[7];\n \n // Convert all values to GB\n totalSize += mbToGb(size, sizeUnit);\n totalUsed += mbToGb(used, usedUnit);\n totalAvailable += mbToGb(available, availUnit);\n }\n}\n// Format the results to two decimal places\n// totalSize = totalSize.toFixed(2); \ntotalUsed = totalUsed.toFixed(2); \ntotalAvailable = totalAvailable.toFixed(2); \ntotalSize = (Number(totalUsed) + Number(totalAvailable)).toFixed(2); \n\n// Calculate used and free percentages\nlet usedPercentage = ((totalUsed / totalSize) * 100).toFixed(2);\nlet freePercentage = ((totalAvailable / totalSize) * 100).toFixed(2);\n\n// Create different messages for each output\nlet output1 = { payload: totalSize }; // Total size in GB\nlet output2 = { payload: totalUsed }; // Used space in GB\nlet output3 = { payload: totalAvailable }; // Available space in GB\nlet output4 = { payload: usedPercentage }; // Used percentage\nlet output5 = { payload: freePercentage }; // Free percentage\n\n// Return all five outputs as an array\nreturn [output1, output2, output3, output4, output5];\n","outputs":5,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":626,"y":875,"wires":[["1db5bd9f4dc1f6d1"],["3e2268e93ed0cf68"],["dc2a74aff5e0d651"],["0dfdd42cbadc1a1c"],["da67bb1be4281916"]]},{"id":"023f7f292ccd0164","type":"comment","z":"39f2b91c983d671f","name":"Disk Usage","info":"","x":816,"y":815,"wires":[]},{"id":"1db5bd9f4dc1f6d1","type":"ui-text","z":"39f2b91c983d671f","group":"8ee7b1867c318ca3","order":1,"width":0,"height":0,"name":"Total Storage","label":"Total Storage (GB)","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":816,"y":855,"wires":[]},{"id":"3e2268e93ed0cf68","type":"ui-text","z":"39f2b91c983d671f","group":"8ee7b1867c318ca3","order":3,"width":0,"height":0,"name":"Used Storage","label":"Used Storage (GB)","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":826,"y":895,"wires":[]},{"id":"dc2a74aff5e0d651","type":"ui-text","z":"39f2b91c983d671f","group":"8ee7b1867c318ca3","order":2,"width":0,"height":0,"name":"Free Storage","label":"Free Storage (GB)","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":816,"y":935,"wires":[]},{"id":"0dfdd42cbadc1a1c","type":"ui-gauge","z":"39f2b91c983d671f","name":"Used Storage","group":"8ee7b1867c318ca3","order":5,"width":3,"height":3,"gtype":"gauge-tank","gstyle":"needle","title":"Used Storage","units":"units","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#a8f5ff"},{"from":"15","color":"#55dbec"},{"from":"35","color":"#53b4fd"},{"from":"50","color":"#2397d1"}],"min":0,"max":"100","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":826,"y":975,"wires":[]},{"id":"da67bb1be4281916","type":"ui-gauge","z":"39f2b91c983d671f","name":"Free Storage","group":"8ee7b1867c318ca3","order":4,"width":3,"height":3,"gtype":"gauge-tank","gstyle":"needle","title":"Free Storage","units":"units","icon":"","prefix":"","suffix":"","segments":[{"from":"0","color":"#a8f5ff"},{"from":"15","color":"#55dbec"},{"from":"35","color":"#53b4fd"},{"from":"50","color":"#2397d1"}],"min":0,"max":"100","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":816,"y":1015,"wires":[]},{"id":"a7f51d25943cec64","type":"OS","z":"39f2b91c983d671f","name":"","x":436,"y":195,"wires":[["d204a0af6bfd434e","5477c749de145490","daa940d746ec2bef"]]},{"id":"b27627174b2cb1ac","type":"Uptime","z":"39f2b91c983d671f","name":"","x":446,"y":155,"wires":[["47c24b2506364a5a"]]},{"id":"91b465681153a8a9","type":"CPUs","z":"39f2b91c983d671f","name":"","x":435,"y":245,"wires":[[]]},{"id":"8614287768526732","type":"Memory","z":"39f2b91c983d671f","name":"","x":446,"y":615,"wires":[["92d7c90757d47543","3483b24989d032a3","507876942fdfea09","a6149aba5c0badd3"]]},{"id":"fe3d159d265b0acc","type":"ui-template","z":"13a0b285aa95568e","group":"7f84e6e11f01d5aa","page":"","ui":"","name":"Security","order":1,"width":"0","height":"0","head":"","format":"<template>\n <div>\n <div id=\"iframe_block\">\n <!-- <div v-if=\"isScaning && !iframeUrl\" class=\"skeleton_box\"></div> -->\n <iframe id=\"iframe_recamera\" :src=\"iframeUrl\"></iframe>\n <!-- <div v-else>\n No website found, please check your network connection and\n <button @click=\"function(){location.reload()}\">Refresh</button>\n </div> -->\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data() {\n console.log(12312);\n function getDeviceType() {\n const userAgent = navigator.userAgent.toLowerCase();\n return;\n /mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(\n userAgent\n )\n ? \"PC\"\n : \"mobile\";\n }\n return {\n ipByDevice: \"\",\n enabledIpList: [],\n checkAllIps: true,\n isScaning: false,\n scanningTimeout: 3000, // ms\n deviceType: getDeviceType(),\n iframeUrl: `http://${window.location.hostname}/#/security?disablelayout=1`\n };\n },\n computed: {\n // iframeUrl: function () {\n // if (this.isScaning) {\n // return;\n // }\n // const ipByDevice = this.ipByDevice;\n // const ipList = this.enabledIpList;\n\n // // 无任何可用\n // if (!(ipList.length > 0)) {\n // return ipByDevice ? this.getUrl(ipByDevice) : null;\n // }\n\n // if (ipByDevice && ipList.includes(ipByDevice)) {\n // return this.getUrl(ipByDevice);\n // }\n\n // return this.getUrl(ipList[0]);\n // }\n },\n watch: {\n msg: function (msg, prevMsg) {\n try {\n //debounce 防抖\n if (\n prevMsg &&\n prevMsg.interfaces &&\n JSON.stringify(prevMsg.interfaces) ===\n JSON.stringify(msg.interfaces)\n ) {\n console.log('🙈🙈 Same msg: Skip....')\n return;\n }\n } catch (e) {\n console.log(e);\n }\n this.scanning(msg);\n }\n },\n methods: {\n getUrl(ipAddress) {\n return `http://${window.location.hostname}/#/security?disablelayout=1`;\n },\n scanning(msg) {\n if (!(msg && msg.interfaces)) {\n return;\n }\n this.ipByDevice = this.getIpByDevice(msg.interfaces);\n this.scaningAddreses(msg.interfaces);\n },\n getIpByDevice: function (addresses) {\n const ipAddress =\n (this.deviceType === \"PC\"\n ? addresses[\"usb\"] || addresses[\"wlan\"]\n : addresses[\"wlan\"] || addresses[\"usb\"]) ||\n addresses[\"eth\"] ||\n addresses[\"en\"];\n if (!ipAddress) {\n return null;\n }\n return ipAddress;\n },\n\n scaningAddreses: function (addresses) {\n if (!addresses || this.isScaning) {\n return;\n }\n console.log(\"scanning addresses\");\n const self = this;\n let results = [];\n var keys = Object.keys(addresses);\n const len = keys.length;\n self.isScaning = true;\n\n let fn = (i) => {\n if (\n i >= len ||\n (!self.checkAllIps && self.enabledIpList.length > 0)\n ) {\n self.isScaning = false;\n self.enabledIpList = results;\n console.log(\n `%cScaning Finished ✅\\n✨Enabled Addresses: ${self.enabledIpList.join(\n \",\"\n )}`,\n \"color:#87ba32\"\n );\n\n fn = () => {};\n return;\n }\n let src = self.getUrl(addresses[keys[i]]);\n const xhr = new XMLHttpRequest();\n xhr.timeout = self.scanningTimeout;\n\n const errorFn = () => {\n fn(++i);\n };\n\n xhr.onload = function () {\n if (xhr.status >= 200 && xhr.status < 300) {\n results.push(addresses[keys[i]]);\n console.log(\n `%c✨(${i + 1}/${len})ping test Success: ${src}`,\n \"color: #87ba32;\"\n );\n fn(++i);\n return;\n }\n errorFn();\n };\n\n xhr.onerror = function () {\n errorFn();\n console.log(\n `%c🚥(${i + 1}/${len}) ping test error: ${src}`,\n \"color:red\"\n );\n };\n // 定义超时回调\n xhr.ontimeout = function () {\n console.log(\n `%c🚥(${i + 1}/${len}) ping test timeout: ${src}`,\n \"color:red;\"\n );\n errorFn();\n };\n\n console.log(\n `%c🚥(${i + 1}/${len}) start ping test: ${src}`,\n \"color: #d8eeff;\"\n );\n xhr.open(\"GET\", src, true);\n // 发送请求\n xhr.send();\n };\n fn(0);\n },\n\n // 获取所有可用IP\n getIpAddresses: function (interfaces) {\n const reg = /^(wlan|usb|eth|en)/;\n const addresses = {};\n for (let iface in interfaces) {\n for (let i = 0; i < interfaces[iface].length; i++) {\n let address = interfaces[iface][i];\n /* Ipv4 & 排除内部接口 & 匹配当前优先级的网口名称 */\n var matches = iface.match(reg);\n if (\n matches &&\n matches[1] &&\n address.family === \"IPv4\" &&\n !address.internal\n ) {\n addresses[matches[1]] = address.address;\n }\n }\n }\n return addresses;\n }\n },\n mounted() {\n this.scanning(this.msg);\n }\n };\n</script>\n<style>\n body,\n html {\n overflow: hidden;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n }\n\n #iframe_block {\n overflow: auto;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n min-height: 500px;\n z-index: 10000;\n background-color: #eee;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n #iframe_block iframe {\n width: 100%;\n height: 100%;\n border: 0;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n }\n\n .skeleton_box {\n width: 50%;\n height: 50%;\n background: #e0e0e0;\n border-radius: 20px;\n position: relative;\n overflow: hidden;\n }\n\n .skeleton_box::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: -100%;\n width: 100%;\n height: 100%;\n background: linear-gradient(90deg,\n rgba(255, 255, 255, 0) 0%,\n rgba(255, 255, 255, 0.5) 50%,\n rgba(255, 255, 255, 0) 100%);\n animation: shimmer 1.5s infinite;\n }\n\n @keyframes shimmer {\n 0% {\n left: -100%;\n }\n\n 100% {\n left: 100%;\n }\n }\n\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":600,"y":80,"wires":[[]]},{"id":"e46e9e40df1fba95","type":"ui-template","z":"13a0b285aa95568e","group":"15bec593c23e2df1","page":"","ui":"","name":"Network","order":1,"width":"12","height":"12","head":"","format":"<template>\n <div>\n <div id=\"iframe_block\">\n <!-- <div v-if=\"isScaning && !iframeUrl\" class=\"skeleton_box\"></div> -->\n <iframe id=\"iframe_recamera\" :src=\"iframeUrl\"></iframe>\n <!-- <div v-else>\n No website found, please check your network connection and\n <button @click=\"function(){location.reload()}\">Refresh</button>\n </div> -->\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data() {\n console.log(12312);\n function getDeviceType() {\n const userAgent = navigator.userAgent.toLowerCase();\n return;\n /mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(\n userAgent\n )\n ? \"PC\"\n : \"mobile\";\n }\n return {\n ipByDevice: \"\",\n enabledIpList: [],\n checkAllIps: true,\n isScaning: false,\n scanningTimeout: 3000, // ms\n deviceType: getDeviceType(),\n iframeUrl: `http://${window.location.hostname}/#/network?disablelayout=1`\n };\n },\n computed: {\n // iframeUrl: function () {\n // if (this.isScaning) {\n // return;\n // }\n // const ipByDevice = this.ipByDevice;\n // const ipList = this.enabledIpList;\n\n // // 无任何可用\n // if (!(ipList.length > 0)) {\n // return ipByDevice ? this.getUrl(ipByDevice) : null;\n // }\n\n // if (ipByDevice && ipList.includes(ipByDevice)) {\n // return this.getUrl(ipByDevice);\n // }\n\n // return this.getUrl(ipList[0]);\n // }\n },\n watch: {\n msg: function (msg, prevMsg) {\n try {\n //debounce 防抖\n if (\n prevMsg &&\n prevMsg.interfaces &&\n JSON.stringify(prevMsg.interfaces) ===\n JSON.stringify(msg.interfaces)\n ) {\n console.log('🙈🙈 Same msg: Skip....')\n return;\n }\n } catch (e) {\n console.log(e);\n }\n this.scanning(msg);\n }\n },\n methods: {\n getUrl(ipAddress) {\n return `http://${ipAddress}/#/network?disablelayout=1`;\n },\n scanning(msg) {\n if (!(msg && msg.interfaces)) {\n return;\n }\n this.ipByDevice = this.getIpByDevice(msg.interfaces);\n this.scaningAddreses(msg.interfaces);\n console.log(msg.interfaces, '---msg.interfaces---')\n },\n getIpByDevice: function (addresses) {\n const ipAddress =\n (this.deviceType === \"PC\"\n ? addresses[\"usb\"] || addresses[\"wlan\"]\n : addresses[\"wlan\"] || addresses[\"usb\"]) ||\n addresses[\"eth\"] ||\n addresses[\"en\"];\n if (!ipAddress) {\n return null;\n }\n return ipAddress;\n },\n\n scaningAddreses: function (addresses) {\n if (!addresses || this.isScaning) {\n return;\n }\n console.log(\"scanning addresses\");\n const self = this;\n let results = [];\n var keys = Object.keys(addresses);\n const len = keys.length;\n self.isScaning = true;\n\n let fn = (i) => {\n if (\n i >= len ||\n (!self.checkAllIps && self.enabledIpList.length > 0)\n ) {\n self.isScaning = false;\n self.enabledIpList = results;\n console.log(\n `%cScaning Finished ✅\\n✨Enabled Addresses: ${self.enabledIpList.join(\n \",\"\n )}`,\n \"color:#87ba32\"\n );\n\n fn = () => {};\n return;\n }\n let src = self.getUrl(addresses[keys[i]]);\n const xhr = new XMLHttpRequest();\n xhr.timeout = self.scanningTimeout;\n\n const errorFn = () => {\n fn(++i);\n };\n\n xhr.onload = function () {\n if (xhr.status >= 200 && xhr.status < 300) {\n results.push(addresses[keys[i]]);\n console.log(\n `%c✨(${i + 1}/${len})ping test Success: ${src}`,\n \"color: #87ba32;\"\n );\n fn(++i);\n return;\n }\n errorFn();\n };\n\n xhr.onerror = function () {\n errorFn();\n console.log(\n `%c🚥(${i + 1}/${len}) ping test error: ${src}`,\n \"color:red\"\n );\n };\n // 定义超时回调\n xhr.ontimeout = function () {\n console.log(\n `%c🚥(${i + 1}/${len}) ping test timeout: ${src}`,\n \"color:red;\"\n );\n errorFn();\n };\n\n console.log(\n `%c🚥(${i + 1}/${len}) start ping test: ${src}`,\n \"color: #d8eeff;\"\n );\n xhr.open(\"GET\", src, true);\n // 发送请求\n xhr.send();\n };\n fn(0);\n },\n\n // 获取所有可用IP\n getIpAddresses: function (interfaces) {\n const reg = /^(wlan|usb|eth|en)/;\n const addresses = {};\n for (let iface in interfaces) {\n for (let i = 0; i < interfaces[iface].length; i++) {\n let address = interfaces[iface][i];\n /* Ipv4 & 排除内部接口 & 匹配当前优先级的网口名称 */\n var matches = iface.match(reg);\n if (\n matches &&\n matches[1] &&\n address.family === \"IPv4\" &&\n !address.internal\n ) {\n addresses[matches[1]] = address.address;\n }\n }\n }\n return addresses;\n }\n },\n mounted() {\n this.scanning(this.msg);\n }\n };\n</script>\n<style>\n body,\n html {\n overflow: hidden;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n }\n\n #iframe_block {\n overflow: auto;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n min-height: 500px;\n z-index: 10000;\n background-color: #eee;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n #iframe_block iframe {\n width: 100%;\n height: 100%;\n border: 0;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n }\n\n .skeleton_box {\n width: 50%;\n height: 50%;\n background: #e0e0e0;\n border-radius: 20px;\n position: relative;\n overflow: hidden;\n }\n\n .skeleton_box::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: -100%;\n width: 100%;\n height: 100%;\n background: linear-gradient(90deg,\n rgba(255, 255, 255, 0) 0%,\n rgba(255, 255, 255, 0.5) 50%,\n rgba(255, 255, 255, 0) 100%);\n animation: shimmer 1.5s infinite;\n }\n\n @keyframes shimmer {\n 0% {\n left: -100%;\n }\n\n 100% {\n left: 100%;\n }\n }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":600,"y":160,"wires":[[]]},{"id":"8689baa62fe722f9","type":"ui-template","z":"13a0b285aa95568e","group":"62e3f90362f475e5","page":"","ui":"","name":"Terminal","order":1,"width":"12","height":"12","head":"","format":"<template>\n <div>\n <div id=\"iframe_block\">\n <!-- <div v-if=\"isScaning && !iframeUrl\" class=\"skeleton_box\"></div> -->\n <iframe id=\"iframe_recamera\" :src=\"iframeUrl\"></iframe>\n <!-- <div v-else>\n No website found, please check your network connection and\n <button @click=\"function(){location.reload()}\">Refresh</button>\n </div> -->\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data() {\n console.log(12312);\n function getDeviceType() {\n const userAgent = navigator.userAgent.toLowerCase();\n return;\n /mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(\n userAgent\n )\n ? \"PC\"\n : \"mobile\";\n }\n return {\n ipByDevice: \"\",\n enabledIpList: [],\n checkAllIps: true,\n isScaning: false,\n scanningTimeout: 3000, // ms\n deviceType: getDeviceType(),\n iframeUrl: `http://${window.location.hostname}/#/terminal?disablelayout=1`\n };\n },\n computed: {\n // iframeUrl: function () {\n // if (this.isScaning) {\n // return;\n // }\n // const ipByDevice = this.ipByDevice;\n // const ipList = this.enabledIpList;\n\n // // 无任何可用\n // if (!(ipList.length > 0)) {\n // return ipByDevice ? this.getUrl(ipByDevice) : null;\n // }\n\n // if (ipByDevice && ipList.includes(ipByDevice)) {\n // return this.getUrl(ipByDevice);\n // }\n\n // return this.getUrl(ipList[0]);\n // }\n },\n watch: {\n msg: function (msg, prevMsg) {\n try {\n //debounce 防抖\n if (\n prevMsg &&\n prevMsg.interfaces &&\n JSON.stringify(prevMsg.interfaces) ===\n JSON.stringify(msg.interfaces)\n ) {\n console.log('🙈🙈 Same msg: Skip....')\n return;\n }\n } catch (e) {\n console.log(e);\n }\n this.scanning(msg);\n }\n },\n methods: {\n getUrl(ipAddress) {\n return `http://${ipAddress}/#/terminal?disablelayout=1`;\n },\n scanning(msg) {\n if (!(msg && msg.interfaces)) {\n return;\n }\n this.ipByDevice = this.getIpByDevice(msg.interfaces);\n this.scaningAddreses(msg.interfaces);\n },\n getIpByDevice: function (addresses) {\n const ipAddress =\n (this.deviceType === \"PC\"\n ? addresses[\"usb\"] || addresses[\"wlan\"]\n : addresses[\"wlan\"] || addresses[\"usb\"]) ||\n addresses[\"eth\"] ||\n addresses[\"en\"];\n if (!ipAddress) {\n return null;\n }\n return ipAddress;\n },\n\n scaningAddreses: function (addresses) {\n if (!addresses || this.isScaning) {\n return;\n }\n console.log(\"scanning addresses\");\n const self = this;\n let results = [];\n var keys = Object.keys(addresses);\n const len = keys.length;\n self.isScaning = true;\n\n let fn = (i) => {\n if (\n i >= len ||\n (!self.checkAllIps && self.enabledIpList.length > 0)\n ) {\n self.isScaning = false;\n self.enabledIpList = results;\n console.log(\n `%cScaning Finished ✅\\n✨Enabled Addresses: ${self.enabledIpList.join(\n \",\"\n )}`,\n \"color:#87ba32\"\n );\n\n fn = () => {};\n return;\n }\n let src = self.getUrl(addresses[keys[i]]);\n const xhr = new XMLHttpRequest();\n xhr.timeout = self.scanningTimeout;\n\n const errorFn = () => {\n fn(++i);\n };\n\n xhr.onload = function () {\n if (xhr.status >= 200 && xhr.status < 300) {\n results.push(addresses[keys[i]]);\n console.log(\n `%c✨(${i + 1}/${len})ping test Success: ${src}`,\n \"color: #87ba32;\"\n );\n fn(++i);\n return;\n }\n errorFn();\n };\n\n xhr.onerror = function () {\n errorFn();\n console.log(\n `%c🚥(${i + 1}/${len}) ping test error: ${src}`,\n \"color:red\"\n );\n };\n // 定义超时回调\n xhr.ontimeout = function () {\n console.log(\n `%c🚥(${i + 1}/${len}) ping test timeout: ${src}`,\n \"color:red;\"\n );\n errorFn();\n };\n\n console.log(\n `%c🚥(${i + 1}/${len}) start ping test: ${src}`,\n \"color: #d8eeff;\"\n );\n xhr.open(\"GET\", src, true);\n // 发送请求\n xhr.send();\n };\n fn(0);\n },\n\n // 获取所有可用IP\n getIpAddresses: function (interfaces) {\n const reg = /^(wlan|usb|eth|en)/;\n const addresses = {};\n for (let iface in interfaces) {\n for (let i = 0; i < interfaces[iface].length; i++) {\n let address = interfaces[iface][i];\n /* Ipv4 & 排除内部接口 & 匹配当前优先级的网口名称 */\n var matches = iface.match(reg);\n if (\n matches &&\n matches[1] &&\n address.family === \"IPv4\" &&\n !address.internal\n ) {\n addresses[matches[1]] = address.address;\n }\n }\n }\n return addresses;\n }\n },\n mounted() {\n this.scanning(this.msg);\n }\n };\n</script>\n<style>\n body,\n html {\n overflow: hidden;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n }\n\n #iframe_block {\n overflow: auto;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n min-height: 500px;\n z-index: 10000;\n background-color: #eee;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n #iframe_block iframe {\n width: 100%;\n height: 100%;\n border: 0;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n }\n\n .skeleton_box {\n width: 50%;\n height: 50%;\n background: #e0e0e0;\n border-radius: 20px;\n position: relative;\n overflow: hidden;\n }\n\n .skeleton_box::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: -100%;\n width: 100%;\n height: 100%;\n background: linear-gradient(90deg,\n rgba(255, 255, 255, 0) 0%,\n rgba(255, 255, 255, 0.5) 50%,\n rgba(255, 255, 255, 0) 100%);\n animation: shimmer 1.5s infinite;\n }\n\n @keyframes shimmer {\n 0% {\n left: -100%;\n }\n\n 100% {\n left: 100%;\n }\n }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":600,"y":260,"wires":[[]]},{"id":"eaf134f38ac167a9","type":"function","z":"13a0b285aa95568e","name":"Get IP Address","func":"\n\n\nconst interfaces = os.networkInterfaces()\nmsg.interfaces = context.get('getIpAddresses')(interfaces)\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"\n\nfunction getIpAddresses(interfaces) {\n const reg = /^(wlan|usb|eth|en)/;\n const addresses = {};\n for (let iface in interfaces) {\n for (let i = 0; i < interfaces[iface].length; i++) {\n let address = interfaces[iface][i];\n /* Ipv4 & 排除内部接口 & 匹配当前优先级的网口名称 */\n var matches = iface.match(reg);\n if (\n matches &&\n matches[1] &&\n address.family === \"IPv4\" &&\n !address.internal\n ) {\n addresses[matches[1]] = address.address;\n }\n }\n }\n return addresses;\n}\ncontext.set(\"getIpAddresses\", getIpAddresses); ","finalize":"","libs":[{"var":"os","module":"os"}],"x":300,"y":240,"wires":[["fe3d159d265b0acc","e46e9e40df1fba95","8689baa62fe722f9","e2ce9654d1d5f1d9","135ebaeb1bc34ca3"]]},{"id":"806d5f750dfbbbba","type":"inject","z":"13a0b285aa95568e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":90,"y":240,"wires":[["eaf134f38ac167a9"]]},{"id":"91d77f3c451880cf","type":"comment","z":"13a0b285aa95568e","name":"Basic Web Functions","info":"Here are the basic web functions for reCamera.\nPlease notice that if you change this part, the basic functions for the reCamera could be damaged or missing.","x":640,"y":40,"wires":[]},{"id":"e2ce9654d1d5f1d9","type":"ui-template","z":"13a0b285aa95568e","group":"a2f6b486b575c329","page":"","ui":"","name":"System Update","order":3,"width":"6","height":"6","head":"","format":"<template>\n <div>\n <div id=\"iframe_block\">\n <!-- <div v-if=\"isScaning && !iframeUrl\" class=\"skeleton_box\"></div> -->\n <iframe id=\"iframe_recamera\" :src=\"iframeUrl\"></iframe>\n <!-- <div v-else>\n No website found, please check your network connection and\n <button @click=\"function(){location.reload()}\">Refresh</button>\n </div> -->\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data() {\n console.log(12312);\n function getDeviceType() {\n const userAgent = navigator.userAgent.toLowerCase();\n return;\n /mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(\n userAgent\n )\n ? \"PC\"\n : \"mobile\";\n }\n return {\n ipByDevice: \"\",\n enabledIpList: [],\n checkAllIps: true,\n isScaning: false,\n scanningTimeout: 3000, // ms\n deviceType: getDeviceType(),\n iframeUrl: `http://${window.location.hostname}/#/system?disablelayout=1`\n };\n },\n computed: {\n // iframeUrl: function () {\n // if (this.isScaning) {\n // return;\n // }\n // const ipByDevice = this.ipByDevice;\n // const ipList = this.enabledIpList;\n\n // // 无任何可用\n // if (!(ipList.length > 0)) {\n // return ipByDevice ? this.getUrl(ipByDevice) : null;\n // }\n\n // if (ipByDevice && ipList.includes(ipByDevice)) {\n // return this.getUrl(ipByDevice);\n // }\n // return this.getUrl(ipList[0]);\n // }\n },\n watch: {\n msg: function (msg, prevMsg) {\n try {\n //debounce 防抖\n if (\n prevMsg &&\n prevMsg.interfaces &&\n JSON.stringify(prevMsg.interfaces) ===\n JSON.stringify(msg.interfaces)\n ) {\n console.log('🙈🙈 Same msg: Skip....')\n return;\n }\n } catch (e) {\n console.log(e);\n }\n this.scanning(msg);\n }\n },\n methods: {\n getUrl(ipAddress) {\n return `http://${ipAddress}/#/system?disablelayout=1`;\n },\n scanning(msg) {\n if (!(msg && msg.interfaces)) {\n return;\n }\n this.ipByDevice = this.getIpByDevice(msg.interfaces);\n this.scaningAddreses(msg.interfaces);\n },\n getIpByDevice: function (addresses) {\n const ipAddress =\n (this.deviceType === \"PC\"\n ? addresses[\"usb\"] || addresses[\"wlan\"]\n : addresses[\"wlan\"] || addresses[\"usb\"]) ||\n addresses[\"eth\"] ||\n addresses[\"en\"];\n if (!ipAddress) {\n return null;\n }\n return ipAddress;\n },\n\n scaningAddreses: function (addresses) {\n if (!addresses || this.isScaning) {\n return;\n }\n console.log(\"scanning addresses\");\n const self = this;\n let results = [];\n var keys = Object.keys(addresses);\n const len = keys.length;\n self.isScaning = true;\n\n let fn = (i) => {\n if (\n i >= len ||\n (!self.checkAllIps && self.enabledIpList.length > 0)\n ) {\n self.isScaning = false;\n self.enabledIpList = results;\n console.log(\n `%cScaning Finished ✅\\n✨Enabled Addresses: ${self.enabledIpList.join(\n \",\"\n )}`,\n \"color:#87ba32\"\n );\n\n fn = () => {};\n return;\n }\n let src = self.getUrl(addresses[keys[i]]);\n const xhr = new XMLHttpRequest();\n xhr.timeout = self.scanningTimeout;\n\n const errorFn = () => {\n fn(++i);\n };\n\n xhr.onload = function () {\n if (xhr.status >= 200 && xhr.status < 300) {\n results.push(addresses[keys[i]]);\n console.log(\n `%c✨(${i + 1}/${len})ping test Success: ${src}`,\n \"color: #87ba32;\"\n );\n fn(++i);\n return;\n }\n errorFn();\n };\n\n xhr.onerror = function () {\n errorFn();\n console.log(\n `%c🚥(${i + 1}/${len}) ping test error: ${src}`,\n \"color:red\"\n );\n };\n // 定义超时回调\n xhr.ontimeout = function () {\n console.log(\n `%c🚥(${i + 1}/${len}) ping test timeout: ${src}`,\n \"color:red;\"\n );\n errorFn();\n };\n\n console.log(\n `%c🚥(${i + 1}/${len}) start ping test: ${src}`,\n \"color: #d8eeff;\"\n );\n xhr.open(\"GET\", src, true);\n // 发送请求\n xhr.send();\n };\n fn(0);\n },\n\n // 获取所有可用IP\n getIpAddresses: function (interfaces) {\n const reg = /^(wlan|usb|eth|en)/;\n const addresses = {};\n for (let iface in interfaces) {\n for (let i = 0; i < interfaces[iface].length; i++) {\n let address = interfaces[iface][i];\n /* Ipv4 & 排除内部接口 & 匹配当前优先级的网口名称 */\n var matches = iface.match(reg);\n if (\n matches &&\n matches[1] &&\n address.family === \"IPv4\" &&\n !address.internal\n ) {\n addresses[matches[1]] = address.address;\n }\n }\n }\n return addresses;\n }\n },\n mounted() {\n this.scanning(this.msg);\n }\n };\n</script>\n<style>\n body,\n html {\n overflow: auto;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n }\n\n #iframe_block {\n overflow: auto;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n min-height: 500px;\n z-index: 10000;\n background-color: #eee;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n #iframe_block iframe {\n width: 100%;\n height: 100%;\n border: 0;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n }\n\n .skeleton_box {\n width: 50%;\n height: 50%;\n background: #e0e0e0;\n border-radius: 20px;\n position: relative;\n overflow: hidden;\n }\n\n .skeleton_box::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: -100%;\n width: 100%;\n height: 100%;\n background: linear-gradient(90deg,\n rgba(255, 255, 255, 0) 0%,\n rgba(255, 255, 255, 0.5) 50%,\n rgba(255, 255, 255, 0) 100%);\n animation: shimmer 1.5s infinite;\n }\n\n @keyframes shimmer {\n 0% {\n left: -100%;\n }\n\n 100% {\n left: 100%;\n }\n }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":620,"y":480,"wires":[[]]},{"id":"135ebaeb1bc34ca3","type":"ui-template","z":"13a0b285aa95568e","group":"853d93c4c0f19c38","page":"","ui":"","name":"Power","order":1,"width":"6","height":"6","head":"","format":"<template>\n <div>\n <div id=\"iframe_block\">\n <!-- <div v-if=\"isScaning && !iframeUrl\" class=\"skeleton_box\"></div> -->\n <iframe id=\"iframe_recamera\" :src=\"iframeUrl\"></iframe>\n <!-- <div v-else>\n No website found, please check your network connection and\n <button @click=\"function(){location.reload()}\">Refresh</button>\n </div> -->\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data() {\n console.log(12312);\n function getDeviceType() {\n const userAgent = navigator.userAgent.toLowerCase();\n return;\n /mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(\n userAgent\n )\n ? \"PC\"\n : \"mobile\";\n }\n return {\n ipByDevice: \"\",\n enabledIpList: [],\n checkAllIps: true,\n isScaning: false,\n scanningTimeout: 3000, // ms\n deviceType: getDeviceType(),\n iframeUrl: `http://${window.location.hostname}/#/power?disablelayout=1`\n };\n },\n computed: {\n // iframeUrl: function () {\n // if (this.isScaning) {\n // return;\n // }\n // const ipByDevice = this.ipByDevice;\n // const ipList = this.enabledIpList;\n\n // // 无任何可用\n // if (!(ipList.length > 0)) {\n // return ipByDevice ? this.getUrl(ipByDevice) : null;\n // }\n\n // if (ipByDevice && ipList.includes(ipByDevice)) {\n // return this.getUrl(ipByDevice);\n // }\n\n // return this.getUrl(ipList[0]);\n // }\n },\n watch: {\n msg: function (msg, prevMsg) {\n try {\n //debounce 防抖\n if (\n prevMsg &&\n prevMsg.interfaces &&\n JSON.stringify(prevMsg.interfaces) ===\n JSON.stringify(msg.interfaces)\n ) {\n console.log('🙈🙈 Same msg: Skip....')\n return;\n }\n } catch (e) {\n console.log(e);\n }\n this.scanning(msg);\n }\n },\n methods: {\n getUrl(ipAddress) {\n return `http://${ipAddress}/#/power?disablelayout=1`;\n },\n scanning(msg) {\n if (!(msg && msg.interfaces)) {\n return;\n }\n this.ipByDevice = this.getIpByDevice(msg.interfaces);\n this.scaningAddreses(msg.interfaces);\n },\n getIpByDevice: function (addresses) {\n const ipAddress =\n (this.deviceType === \"PC\"\n ? addresses[\"usb\"] || addresses[\"wlan\"]\n : addresses[\"wlan\"] || addresses[\"usb\"]) ||\n addresses[\"eth\"] ||\n addresses[\"en\"];\n if (!ipAddress) {\n return null;\n }\n return ipAddress;\n },\n\n scaningAddreses: function (addresses) {\n if (!addresses || this.isScaning) {\n return;\n }\n console.log(\"scanning addresses\");\n const self = this;\n let results = [];\n var keys = Object.keys(addresses);\n const len = keys.length;\n self.isScaning = true;\n\n let fn = (i) => {\n if (\n i >= len ||\n (!self.checkAllIps && self.enabledIpList.length > 0)\n ) {\n self.isScaning = false;\n self.enabledIpList = results;\n console.log(\n `%cScaning Finished ✅\\n✨Enabled Addresses: ${self.enabledIpList.join(\n \",\"\n )}`,\n \"color:#87ba32\"\n );\n\n fn = () => {};\n return;\n }\n let src = self.getUrl(addresses[keys[i]]);\n const xhr = new XMLHttpRequest();\n xhr.timeout = self.scanningTimeout;\n\n const errorFn = () => {\n fn(++i);\n };\n\n xhr.onload = function () {\n if (xhr.status >= 200 && xhr.status < 300) {\n results.push(addresses[keys[i]]);\n console.log(\n `%c✨(${i + 1}/${len})ping test Success: ${src}`,\n \"color: #87ba32;\"\n );\n fn(++i);\n return;\n }\n errorFn();\n };\n\n xhr.onerror = function () {\n errorFn();\n console.log(\n `%c🚥(${i + 1}/${len}) ping test error: ${src}`,\n \"color:red\"\n );\n };\n // 定义超时回调\n xhr.ontimeout = function () {\n console.log(\n `%c🚥(${i + 1}/${len}) ping test timeout: ${src}`,\n \"color:red;\"\n );\n errorFn();\n };\n\n console.log(\n `%c🚥(${i + 1}/${len}) start ping test: ${src}`,\n \"color: #d8eeff;\"\n );\n xhr.open(\"GET\", src, true);\n // 发送请求\n xhr.send();\n };\n fn(0);\n },\n\n // 获取所有可用IP\n getIpAddresses: function (interfaces) {\n const reg = /^(wlan|usb|eth|en)/;\n const addresses = {};\n for (let iface in interfaces) {\n for (let i = 0; i < interfaces[iface].length; i++) {\n let address = interfaces[iface][i];\n /* Ipv4 & 排除内部接口 & 匹配当前优先级的网口名称 */\n var matches = iface.match(reg);\n if (\n matches &&\n matches[1] &&\n address.family === \"IPv4\" &&\n !address.internal\n ) {\n addresses[matches[1]] = address.address;\n }\n }\n }\n return addresses;\n }\n },\n mounted() {\n this.scanning(this.msg);\n }\n };\n</script>\n<style>\n body,\n html {\n overflow: auto;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n }\n\n #iframe_block {\n overflow: auto;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n min-height: 500px;\n z-index: 10000;\n background-color: #eee;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n #iframe_block iframe {\n width: 100%;\n height: 100%;\n border: 0;\n margin: 0 0 0 0;\n padding: 0 0 0 0;\n box-sizing: border-box;\n }\n\n .skeleton_box {\n width: 50%;\n height: 50%;\n background: #e0e0e0;\n border-radius: 20px;\n position: relative;\n overflow: hidden;\n }\n\n .skeleton_box::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: -100%;\n width: 100%;\n height: 100%;\n background: linear-gradient(90deg,\n rgba(255, 255, 255, 0) 0%,\n rgba(255, 255, 255, 0.5) 50%,\n rgba(255, 255, 255, 0) 100%);\n animation: shimmer 1.5s infinite;\n }\n\n @keyframes shimmer {\n 0% {\n left: -100%;\n }\n\n 100% {\n left: 100%;\n }\n }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":590,"y":540,"wires":[[]]},{"id":"ea1477f4b57e7973","type":"ui-dropdown","z":"35ee92b6dbd194c1","group":"d9c66abde84c734d","name":"Demo Options","label":"Select Option:","tooltip":"","order":4,"width":0,"height":0,"passthru":false,"multiple":false,"chips":false,"clearable":false,"options":[{"label":"Counting Person","value":"0","type":"str"},{"label":"Counting Cat","value":"1","type":"str"},{"label":"Counting Dog","value":"2","type":"str"},{"label":"Counting Bottle","value":"3","type":"str"}],"payload":"","topic":"topic","topicType":"flow","className":"","typeIsComboBox":true,"msgTrigger":"onChange","x":781.4286003112793,"y":300.0000190734863,"wires":[["06067327cd71d385"]]},{"id":"fdf52bd44d5c7a22","type":"ui-text","z":"35ee92b6dbd194c1","group":"d9c66abde84c734d","order":3,"width":0,"height":0,"name":"Available Demo Label","label":"Available Demo","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":801.4286003112793,"y":240.00001907348633,"wires":[]},{"id":"28a18fc91657aecc","type":"ui-slider","z":"35ee92b6dbd194c1","group":"d9c66abde84c734d","name":"Confidence","label":"Confidence","tooltip":"","order":8,"width":0,"height":0,"passthru":false,"outs":"all","topic":"topic","topicType":"msg","thumbLabel":"true","showTicks":"always","min":0,"max":"100","step":1,"className":"","iconPrepend":"","iconAppend":"","color":"","colorTrack":"","colorThumb":"","x":131.4286003112793,"y":280.0000190734863,"wires":[["6d2751f9f4ec8ad5"]]},{"id":"e1aac7e16efa1d77","type":"ui-slider","z":"35ee92b6dbd194c1","group":"d9c66abde84c734d","name":"IoU","label":"IoU","tooltip":"","order":7,"width":0,"height":0,"passthru":false,"outs":"all","topic":"topic","topicType":"msg","thumbLabel":"true","showTicks":"always","min":0,"max":"100","step":1,"className":"","iconPrepend":"","iconAppend":"","color":"","colorTrack":"","colorThumb":"","showTextField":false,"x":110,"y":200,"wires":[["7342757d0d19d85b"]]},{"id":"8e7dd2ac770921ac","type":"model","z":"35ee92b6dbd194c1","name":"model","uri":"/usr/share/supervisor/models/yolo11n_detection_cv181x_int8.cvimodel","model":"YOLO11n Detection","tscore":"0.5","tiou":"0.4","debug":true,"trace":false,"counting":false,"classes":"person,bicycle,car,motorcycle,airplane,bus,train,truck,boat,traffic light,fire hydrant,stop sign,parking meter,bench,bird,cat,dog,horse,sheep,cow,elephant,bear,zebra,giraffe,backpack,umbrella,handbag,tie,suitcase,frisbee,skis,snowboard,sports ball,kite,baseball bat,baseball glove,skateboard,surfboard,tennis racket,bottle,wine glass,cup,fork,knife,spoon,bowl,banana,apple,sandwich,orange,broccoli,carrot,hot dog,pizza,donut,cake,chair,couch,potted plant,bed,dining table,toilet,tv,laptop,mouse,remote,keyboard,cell phone,microwave,oven,toaster,sink,refrigerator,book,clock,vase,scissors,teddy bear,hair drier,toothbrush","splitter":"0,0,0,0","client":"dec794eaeb95589c","x":570,"y":500,"wires":[["2407afd685361f36","9421501be712ec87"]]},{"id":"062708c8f0051020","type":"camera","z":"35ee92b6dbd194c1","option":0,"client":"dec794eaeb95589c","x":390,"y":500,"wires":[["8e7dd2ac770921ac"]]},{"id":"2407afd685361f36","type":"ui-template","z":"35ee92b6dbd194c1","group":"53a493606ee6d430","page":"","ui":"","name":"Preview Page","order":1,"width":0,"height":0,"head":"","format":"<template>\n <div :id=\"containerId\" style=\"width: 100%; height: 100%\">\n <svg :id=\"svgId\" viewBox=\"0 50 640 640\"></svg>\n </div>\n</template>\n\n<script>\n export default {\n computed: {\n containerId() {\n return `container`;\n },\n svgId() {\n return `svg`;\n },\n },\n methods: {\n createSVGElement(type, attributes = {}) {\n const element = document.createElementNS(\"http://www.w3.org/2000/svg\", type);\n Object.keys(attributes).forEach((attr) => element.setAttribute(attr, attributes[attr]));\n return element;\n },\n getColor(index, opacity = 1) {\n const COLORS = [\n \"#FF0000\",\n \"#FF4500\",\n \"#FF6347\",\n \"#FF8C00\",\n \"#FFA500\",\n \"#FFD700\",\n \"#32CD32\",\n \"#006400\",\n \"#4169E1\",\n \"#0000FF\",\n \"#1E90FF\",\n \"#00FFFF\",\n \"#00CED1\",\n \"#20B2AA\",\n \"#FF1493\",\n \"#FF69B4\",\n \"#800080\",\n \"#8A2BE2\",\n \"#9400D3\",\n \"#9932CC\",\n ];\n const color = COLORS[index % COLORS.length];\n if (opacity < 1 && opacity >= 0) {\n const r = parseInt(color.slice(1, 3), 16);\n const g = parseInt(color.slice(3, 5), 16);\n const b = parseInt(color.slice(5, 7), 16);\n return `rgba(${r}, ${g}, ${b}, ${opacity})`;\n }\n return color;\n },\n renderImage(container, group, data) {\n if (data.image) {\n let img = document.getElementById(`image-output-img`);\n if (!img) {\n img = this.createSVGElement(\"image\", {\n id: `image-output-img`,\n x: \"0\",\n y: \"50\",\n });\n img.addEventListener(\"click\", () => this.removeGroup(group), { once: false });\n container.prepend(img);\n }\n img.setAttribute(\"href\", `data:image/jpeg;base64,${data.image}`);\n } else if (data?.resolution) {\n const rect = this.createSVGElement(\"rect\", {\n x: \"0\",\n y: \"0\",\n width: data.resolution[0],\n height: data.resolution[1],\n fill: \"black\",\n });\n const text = this.createSVGElement(\"text\", {\n x: 10,\n y: 20,\n \"font-size\": \"16\",\n fill: \"yellow\",\n stroke: \"yellow\",\n \"font-family\": \"Arial\",\n });\n text.textContent = \"Warning: Please enable the model node's debug mode to display the actual image.\";\n group.appendChild(rect);\n group.appendChild(text);\n }\n },\n renderLines(group, data) {\n if (data?.lines) {\n data.lines.forEach((line, i) => {\n const x1 = line[0] * 0.01 * data.resolution[0];\n const y1 = line[1] * 0.01 * data.resolution[1];\n const x2 = line[2] * 0.01 * data.resolution[0];\n const y2 = line[3] * 0.01 * data.resolution[1];\n const color = this.getColor(i);\n const lineElement = this.createSVGElement(\"line\", {\n x1,\n y1,\n x2,\n y2,\n stroke: color,\n \"stroke-width\": \"1\",\n });\n group.appendChild(lineElement);\n });\n }\n },\n renderBoxes(group, data) {\n if (data?.boxes) {\n data.boxes.forEach((box, i) => {\n if (box?.length === 6) {\n const [x, y, w, h, score, tar] = box;\n const color = this.getColor(tar);\n const tarStr = data.labels?.[i] ?? `NA-${tar}`;\n const rect = this.createSVGElement(\"rect\", {\n x: x - w / 2,\n y: y - h / 2,\n width: w,\n height: h,\n fill: \"none\",\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(rect);\n\n const rectText = this.createSVGElement(\"rect\", {\n x: x - w / 2,\n y: y - h / 2 - 14,\n width: w,\n height: 16,\n fill: color,\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(rectText);\n\n const text = this.createSVGElement(\"text\", {\n x: x - w / 2 + 5,\n y: y - h / 2 - 2,\n \"font-size\": \"14\",\n fill: \"white\",\n \"font-family\": \"Arial\",\n });\n text.textContent = data?.tracks ? `#${data.tracks[i]}: ${tarStr}(${score})` : `${tarStr}(${score})`;\n group.appendChild(text);\n }\n });\n }\n },\n renderClasses(group, data) {\n if (data?.classes) {\n const rectHeight = data.resolution[1] / 16;\n data.classes.forEach(([score, tar], i) => {\n const tarStr = data.labels?.[i] ?? `NA-${tar}`;\n const rectWidth = data.resolution[0] / data.classes.length;\n const rect = this.createSVGElement(\"rect\", {\n x: rectWidth * i,\n y: 0,\n width: rectWidth,\n height: rectHeight,\n fill: this.getColor(tar),\n \"fill-opacity\": 0.3,\n });\n group.appendChild(rect);\n\n const text = this.createSVGElement(\"text\", {\n x: rectWidth * i,\n y: data.resolution[1] / 24,\n \"font-size\": data.resolution[1] / 24,\n \"font-weight\": \"bold\",\n \"font-family\": \"arial\",\n fill: \"#ffffff\",\n });\n text.textContent = `${tarStr}: ${score}`;\n group.appendChild(text);\n });\n }\n },\n renderSegments(group, data) {\n if (data?.segments) {\n data.segments.forEach((segment, i) => {\n const box = segment[0];\n const polygon = segment[1];\n let color = this.getColor(i);\n let rgba = this.getColor(i, 0.3);\n if (box?.length === 6) {\n const [x, y, w, h, score, tar] = box;\n color = this.getColor(tar);\n rgba = this.getColor(tar, 0.3);\n const tarStr = data.labels?.[i] ?? `NA-${tar}`;\n const rect = this.createSVGElement(\"rect\", {\n x: x - w / 2,\n y: y - h / 2,\n width: w,\n height: h,\n fill: \"none\",\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(rect);\n\n const rectText = this.createSVGElement(\"rect\", {\n x: x - w / 2,\n y: y - h / 2 - 14,\n width: w,\n height: 16,\n fill: color,\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(rectText);\n\n const text = this.createSVGElement(\"text\", {\n x: x - w / 2 + 5,\n y: y - h / 2 - 2,\n \"font-size\": \"14\",\n fill: \"white\",\n \"font-family\": \"Arial\",\n });\n text.textContent = data?.tracks ? `#${data.tracks[i]}: ${tarStr}(${score})` : `${tarStr}(${score})`;\n group.appendChild(text);\n }\n if (polygon) {\n function convertToPoints(polygon) {\n let points = \"\";\n for (let i = 0; i < polygon.length; i += 2) {\n points += `${polygon[i]},${polygon[i + 1]} `;\n }\n return points.trim();\n }\n\n // Convert the data array to SVG points format\n const points = convertToPoints(polygon);\n\n const polygonElement = this.createSVGElement(\"polygon\", {\n points: points,\n fill: rgba,\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(polygonElement);\n }\n });\n }\n },\n renderKeypoints(group, data) {\n if (!data?.keypoints) {\n return;\n }\n data.keypoints.forEach((keypoint, i) => {\n const box = keypoint[0];\n const keypoints = keypoint[1];\n let points = new Set();\n if (box?.length === 6) {\n const [x, y, w, h, score, tar] = box;\n const color = this.getColor(tar);\n const tarStr = data.labels?.[i] ?? `NA-${tar}`;\n const rect = this.createSVGElement(\"rect\", {\n x: x - w / 2,\n y: y - h / 2,\n width: w,\n height: h,\n fill: \"none\",\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(rect);\n\n const rectText = this.createSVGElement(\"rect\", {\n x: x - w / 2,\n y: y - h / 2 - 14,\n width: w,\n height: 16,\n fill: color,\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(rectText);\n\n const text = this.createSVGElement(\"text\", {\n x: x - w / 2 + 5,\n y: y - h / 2 - 2,\n \"font-size\": \"14\",\n fill: \"white\",\n stroke: \"white\",\n \"font-family\": \"Arial\",\n });\n text.textContent = data?.tracks ? `#${data.tracks[i]}: ${tarStr}(${score})` : `${tarStr}(${score})`;\n group.appendChild(text);\n }\n\n for (let j = 0; j < keypoints.length; j += 1) {\n const point = keypoints[j];\n const x = point[0];\n const y = point[1];\n const target = point[3] ? point[3] : j;\n // draw if point in the box\n if (x > box[0] - box[2] / 2 && x < box[0] + box[2] / 2 && y > box[1] - box[3] / 2 && y < box[1] + box[3] / 2) {\n points.add(target);\n }\n }\n\n if (keypoints?.length === 17) {\n // nose to left eye\n if (points.has(0) && points.has(1)) {\n const color = this.getColor(0);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[0][0],\n y1: keypoints[0][1],\n x2: keypoints[1][0],\n y2: keypoints[1][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // nose to right eye\n if (points.has(0) && points.has(2)) {\n const color = this.getColor(0);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[0][0],\n y1: keypoints[0][1],\n x2: keypoints[2][0],\n y2: keypoints[2][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left eye to left ear\n if (points.has(1) && points.has(3)) {\n const color = this.getColor(0);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[1][0],\n y1: keypoints[1][1],\n x2: keypoints[3][0],\n y2: keypoints[3][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // right eye to right ear\n if (points.has(2) && points.has(4)) {\n const color = this.getColor(0);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[2][0],\n y1: keypoints[2][1],\n x2: keypoints[4][0],\n y2: keypoints[4][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left ear to left shoulder\n if (points.has(3) && points.has(5)) {\n const color = this.getColor(0);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[3][0],\n y1: keypoints[3][1],\n x2: keypoints[5][0],\n y2: keypoints[5][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // right ear to right shoulder\n if (points.has(4) && points.has(6)) {\n const color = this.getColor(0);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[4][0],\n y1: keypoints[4][1],\n x2: keypoints[6][0],\n y2: keypoints[6][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left shoulder to right shoulder\n if (points.has(5) && points.has(6)) {\n const color = this.getColor(1);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[5][0],\n y1: keypoints[5][1],\n x2: keypoints[6][0],\n y2: keypoints[6][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left shoulder to left hip\n if (points.has(5) && points.has(11)) {\n const color = this.getColor(2);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[5][0],\n y1: keypoints[5][1],\n x2: keypoints[11][0],\n y2: keypoints[11][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // right shoulder to right hip\n if (points.has(6) && points.has(12)) {\n const color = this.getColor(2);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[6][0],\n y1: keypoints[6][1],\n x2: keypoints[12][0],\n y2: keypoints[12][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left hip to right hip\n if (points.has(11) && points.has(12)) {\n const color = this.getColor(2);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[11][0],\n y1: keypoints[11][1],\n x2: keypoints[12][0],\n y2: keypoints[12][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left shoulder to left elbow\n if (points.has(5) && points.has(7)) {\n const color = this.getColor(1);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[5][0],\n y1: keypoints[5][1],\n x2: keypoints[7][0],\n y2: keypoints[7][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left elbow to left wrist\n if (points.has(7) && points.has(9)) {\n const color = this.getColor(1);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[7][0],\n y1: keypoints[7][1],\n x2: keypoints[9][0],\n y2: keypoints[9][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // right shoulder to right elbow\n if (points.has(6) && points.has(8)) {\n const color = this.getColor(6);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[6][0],\n y1: keypoints[6][1],\n x2: keypoints[8][0],\n y2: keypoints[8][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // right elbow to right wrist\n if (points.has(8) && points.has(10)) {\n const color = this.getColor(1);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[8][0],\n y1: keypoints[8][1],\n x2: keypoints[10][0],\n y2: keypoints[10][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left hip to left knee\n if (points.has(11) && points.has(13)) {\n const color = this.getColor(3);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[11][0],\n y1: keypoints[11][1],\n x2: keypoints[13][0],\n y2: keypoints[13][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // left knee to left ankle\n if (points.has(13) && points.has(15)) {\n const color = this.getColor(3);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[13][0],\n y1: keypoints[13][1],\n x2: keypoints[15][0],\n y2: keypoints[15][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // right hip to right knee\n if (points.has(12) && points.has(14)) {\n const color = this.getColor(3);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[12][0],\n y1: keypoints[12][1],\n x2: keypoints[14][0],\n y2: keypoints[14][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n // right knee to right ankle\n if (points.has(14) && points.has(16)) {\n const color = this.getColor(3);\n const line = this.createSVGElement(\"line\", {\n x1: keypoints[14][0],\n y1: keypoints[14][1],\n x2: keypoints[16][0],\n y2: keypoints[16][1],\n stroke: color,\n \"stroke-width\": \"2\",\n });\n group.appendChild(line);\n }\n }\n\n for (let j = 0; j < keypoints.length; j += 1) {\n const point = keypoints[j];\n const x = point[0];\n const y = point[1];\n const target = point[3] ? point[3] : j;\n // draw if point in the box\n if (x > box[0] - box[2] / 2 && x < box[0] + box[2] / 2 && y > box[1] - box[3] / 2 && y < box[1] + box[3] / 2) {\n const color = this.getColor(target);\n const circle = this.createSVGElement(\"circle\", {\n cx: x,\n cy: y,\n r: 3,\n stroke: color,\n \"stroke-width\": \"2\",\n fill: color,\n });\n group.appendChild(circle);\n }\n }\n });\n },\n renderAll() {\n const container = document.getElementById(this.containerId);\n const svg = document.getElementById(this.svgId);\n if (!container || !svg) return;\n\n let group = document.getElementById(`image-output-group`);\n if (!group) {\n group = this.createSVGElement(\"g\", {\n id: `image-output-group`,\n transform: \"translate(0, 50)\",\n });\n svg.appendChild(group);\n }\n group.innerHTML = \"\"; // Clear existing content\n\n const previewData = this.msg?.payload?.data;\n if (!previewData) {\n return;\n }\n this.renderImage(svg, group, previewData);\n this.renderLines(group, previewData);\n this.renderBoxes(group, previewData);\n this.renderClasses(group, previewData);\n this.renderSegments(group, previewData);\n this.renderKeypoints(group, previewData);\n },\n },\n watch: {\n msg() {\n this.renderAll();\n },\n },\n };\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":781.4286003112793,"y":500.0000190734863,"wires":[[]]},{"id":"89ecdff83571f71a","type":"light","z":"35ee92b6dbd194c1","light":false,"x":490,"y":880,"wires":[]},{"id":"3b3e95a077d8b2f5","type":"switch","z":"35ee92b6dbd194c1","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"on","vt":"str"},{"t":"eq","v":"off","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":310,"y":880,"wires":[["89ecdff83571f71a"],["89ecdff83571f71a"]]},{"id":"17c017f615f08725","type":"inject","z":"35ee92b6dbd194c1","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"on","payloadType":"str","x":150,"y":800,"wires":[["3b3e95a077d8b2f5"]]},{"id":"566fbb014c8183f3","type":"inject","z":"35ee92b6dbd194c1","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"off","payloadType":"str","x":150,"y":900,"wires":[["3b3e95a077d8b2f5"]]},{"id":"6d2751f9f4ec8ad5","type":"function","z":"35ee92b6dbd194c1","name":"Send Confidence","func":"const tscore = Number((Number(msg.payload)/100).toFixed(2))\nmsg.payload = {tscore}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":351.4286003112793,"y":280.0000190734863,"wires":[["8e7dd2ac770921ac"]]},{"id":"7342757d0d19d85b","type":"function","z":"35ee92b6dbd194c1","name":"Send IoU","func":"const tiou = Number((Number(msg.payload)/100).toFixed(2))\nmsg.payload = {tiou}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":200,"wires":[["8e7dd2ac770921ac"]]},{"id":"e6a7de5ba8206343","type":"ui-text","z":"35ee92b6dbd194c1","group":"d9c66abde84c734d","order":5,"width":0,"height":0,"name":"Counting Result","label":"","format":"{{msg.payload}}","layout":"row-left","style":true,"font":"Courier,monospace","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":1021.4286003112793,"y":360.0000190734863,"wires":[]},{"id":"06067327cd71d385","type":"function","z":"35ee92b6dbd194c1","name":"Select Handle","func":"flow.set(\"option_model\", msg.payload)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1021.4286003112793,"y":300.0000190734863,"wires":[[]]},{"id":"9421501be712ec87","type":"function","z":"35ee92b6dbd194c1","name":"Model Info Handle","func":"const selectModel = flow.get(\"option_model\")\nlet currentModel = \"Current \"\nlet object = ''\nswitch(selectModel) {\n case \"0\":\n currentModel += \"People\";\n object = 'person'\n break;\n case \"1\":\n currentModel += \"Cat\";\n object = 'cat'\n break;\n case \"2\":\n currentModel += \"Dog\";\n object = 'dog'\n break;\n case \"3\":\n currentModel += \"Bottle\";\n object = 'bottle'\n break;\n default:\n currentModel = null\n}\nif (currentModel) {\n const labels = msg.payload?.data?.labels ?? []\n if (!Array.isArray(labels)) {\n return { payload: '' }\n }\n const num = labels.filter(label => String(label).toLowerCase() === object).length\n currentModel += ` number: ${num}`\n return {payload: currentModel}\n} else {\n return {payload: ''}\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":791.4286003112793,"y":360.0000190734863,"wires":[["e6a7de5ba8206343"]]},{"id":"aba92de76fd54560","type":"ui-text","z":"35ee92b6dbd194c1","group":"d9c66abde84c734d","order":1,"width":0,"height":0,"name":"","label":"Current Model is: ","format":"{{msg.payload}}","layout":"row-left","style":false,"font":"","fontSize":16,"color":"#717171","wrapText":false,"className":"","x":1031.4286003112793,"y":180.00001907348633,"wires":[]},{"id":"63f1c8b4b32c9895","type":"ui-template","z":"35ee92b6dbd194c1","group":"d9c66abde84c734d","page":"","ui":"","name":"Current Model","order":2,"width":"3","height":"1","head":"","format":"<template>\n <div style=\"display: none\"></div>\n</template>\n\n<script>\n export default {\n data() {\n // define variables available component-wide\n // (in <template> and component functions)\n return {\n name: 0\n }\n },\n watch: {\n // watch for any changes of \"count\"\n name: function () {\n this.send({payload: this.name})\n }\n },\n async mounted() {\n // const response = await fetch(`http://192.168.42.1/api/deviceMgr/getModelInfo`)\n const response = await fetch(`http://${window.location.hostname}/api/deviceMgr/getModelInfo`)\n const data = await response.json()\n const modelInfo = JSON.parse(data.data.model_info)\n this.name = modelInfo.model_name\n },\n }\n</script>\n<style>\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":781.4286003112793,"y":180.00001907348633,"wires":[["aba92de76fd54560"]]},{"id":"b45002094cd26740","type":"comment","z":"35ee92b6dbd194c1","name":"Light Instruction","info":"You can control the fill light by this node.\n\nYou can also change the previous nodes to other functions to control the light on/off based on time or other occations. ","x":160,"y":740,"wires":[]},{"id":"0ca4d145e0d94e60","type":"comment","z":"35ee92b6dbd194c1","name":"Preview Demo","info":"In this demo, we created sliders for IoU and Confidence that you can play with. We also created UI to display some counting demos.\nFeel free to adjust this page for your own needs.","x":131.4286003112793,"y":140.00001907348633,"wires":[]},{"id":"272ec8d7d65a4dc5","type":"subflow:39f2b91c983d671f","z":"35ee92b6dbd194c1","name":"","x":170,"y":580,"wires":[]},{"id":"486df3f9f5adb4f7","type":"subflow:13a0b285aa95568e","z":"35ee92b6dbd194c1","name":"","x":150,"y":660,"wires":[]}]
您还可以在 Github 和 SenseCraft 平台 上访问流程 JSON 文件。
技术支持与产品讨论
感谢您选择我们的产品!我们致力于为您提供多种支持,确保您在使用我们的产品时拥有尽可能顺畅的体验。我们提供多种沟通渠道,以满足不同的偏好和需求。