使用 Node-RED 开发 reCamera
Node-RED 介绍
Node-RED 的目标是让任何人都能构建收集、转换和可视化数据的应用程序;构建能够自动化他们世界的流程。其低代码特性使其对任何背景的用户都易于使用,无论是用于家庭自动化、工业控制系统还是其他任何应用。通过将 Node-RED 与 reCamera 集成,它提供了一种对初学者友好的开发方法,让用户可以直接拖拽和操作设备。
您可以在这里学习 Node-RED 概念 或从 视频教程 开始。
在 reCamera 上,我们已经为 Node-RED 安装了调色板,包括以下内容:
- SSCMA 调色板(所有操作系统版本)
- 仪表板调色板(所有操作系统版本)
- reCamera 硬件(0.1.6 操作系统及以上版本)
结合 Node-RED 默认的其他调色板,如 function、debug、trigger、mqtt 等,您现在可以使用它们构建流程来实现不同的计算机视觉应用。
将流程导入 reCamera
有两种方法将流程导入 reCamera:
-
从本地文件或 json 导入流程。
-
步骤1:点击右上角的
菜单图标
并选择"Import"。 -
步骤2:点击"Import"选项卡。
-
步骤3:粘贴流程 json 代码或上传流程 json 文件。您可以在社区或 github 中找到可用的流程并将它们与 reCamera 集成。
-
步骤4:点击"Import"按钮。
-
-
从 SenseCraft reCamera 公共应用程序 导入流程。
-
步骤1:在公共应用程序中找到任何有趣的流程。然后点击
clone
。 -
步骤2:选择通过 USB 或网络连接到 reCamera 的方法。如果您使用网络连接,请在文本框中输入 reCamera 的正确 IP,然后点击连接。
-
步骤3:公共应用程序将自动导入到 reCamera。您也可以将您的流程贡献给社区,让平台对其他用户更有启发性。
-
将流程部署到 reCamera
一旦您在工作区中添加、删除或更改节点和连线,请确保点击右上角的 Deploy
按钮,将最新的流程部署到 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 分钟之间。
摄像头节点

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

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

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

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

模型选择
在 reCamera 上部署不同模型有 3 种方式:
-
- 选择
On Device
模型。reCamera 默认包含几个 Yolo 模型。
- 选择
-
- 从
SenseCraft Zoo
选择模型。有几个公共模型可供选择,如手势和水果。用户也可以上传自己的模型,并将其公开以贡献社区。
- 从
-
上传您自己的模型
到 reCamera。通过遵循将模型转换为 reCamera 的说明,用户可以将自己的 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 ouput
复选框用于设置是否希望 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: //inference numbers ,
image: //base64 image code,
labels: [
0: class name // e.g. person
],
perf: [
0: 0 fps, //pre-processing fps
1: 40 ms, //inference time
2: 20 ms, //post-processing time
],
resolution: [ //pixel size of the image
0: 640,
1: 640,
]
}
模型节点可以连接到 preview
节点以在 Node-RED 工作区中预览效果。您也可以将输出解析到其他节点进行进一步处理,如 function node
、mqtt node
、debug node
或 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"的配置即可。

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

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

Start tickbox:一旦勾选,保存将立即开始。保存参数将基于下面的 slice
和 duration
。
Slice:您想要保存的每个文件的视频时长。(在版本 0.1.6 或更高版本中,您可以在下拉菜单中更改单位)
Duration:您想要保存的视频的总时长。(在版本 0.1.6 或更高版本中,您可以在下拉菜单中更改单位)
例如,如果 slice 设置为 5 分钟,duration 设置为 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":[]}]
Dashboard UI 调色板
Dashboard 2.0 调色板 是由 Flowfuse 基于 dashboard 1.0 调色板制作的(向他们的出色工作致敬)。它是一个易于使用的 Node-RED 节点集合,允许您创建数据驱动的仪表板和数据可视化。使用此调色板,您可以创建直接在 reCamera 上运行的交互式仪表板,包含按钮、图表、文本或滑块等组件并预览效果。
安装
此调色板默认安装在设备中。如果您想手动安装,可以按照以下步骤操作:
- 通过访问
ip_address/#/workspace
进入 Node-RED 工作区。 - 点击右上角的
菜单图标
并选择"管理调色板"。 - 点击"安装"选项卡。
- 在搜索栏中输入"node-red-contrib-sscma"并点击"安装"按钮。
- 等待安装完成。请注意,由于设备限制,下载时间将根据网络速度和包大小约为 30 秒到 5 分钟。
Dashboard 节点

像 button
、slider
、switch
、text
和 templete
这样的热门节点在为 reCamera 构建仪表板时非常方便。在他们的官方网站上查看每个节点的详细文档,或观看他们的初学者教程以更好地了解此调色板中的节点和小部件。
使用 Dashboard 节点的示例流程
在 OS 版本 0.1.4 及以上版本中,设备默认安装了一个仪表板流程作为开箱即用的示例,供用户入门使用。任何低于 0.1.4 的 OS 版本都不会有默认的仪表板流程。
此流程的功能是预览模型输出,提供不同的演示,如计数人、狗、猫或瓶子。它还提供了如何将基本网页嵌入到仪表板的示例,如网络、终端、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 Platform 上访问流程 json。
技术支持与产品讨论
感谢您选择我们的产品!我们在这里为您提供不同的支持,以确保您使用我们产品的体验尽可能顺畅。我们提供多种沟通渠道,以满足不同的偏好和需求。