使用Node-RED开发reCamera
Node-RED 简介
Node-RED 的目标是让任何人都能构建应用程序,用于收集、转换和可视化数据,进而构建能够自动化其生活的应用配置流。其低代码特性使其适用于各种背景的用户——无论是家庭自动化、工业控制系统还是其他应用。通过将 Node-RED 与 reCamera 集成,它提供了一种适合初学者的开发方式,让用户可以直接拖拽操作设备。
您可以在这里了解 Node-RED 概念 或观看 视频教程 进行入门。
在 reCamera 上,我们已安装了 Node-RED 的节点,包含以下内容:
- SSCMA Palette (所有 OS 版本)
- Dashboard Palette (所有 OS 版本)
- reCamera Hardware (OS 版本 0.1.6 及以上)
借助 Node-RED 默认提供的其他节点(如 function、debug、trigger、mqtt 等),您可以构建各种应用配置流以实现不同的计算机视觉应用。
将应用配置流导入 reCamera
导入应用配置流到 reCamera 有两种方式:
1. 从本地文件或 JSON 导入
- 步骤 1:点击右上角的
菜单图标
并选择导入

- 步骤 2:点击
导入
选项卡

步骤 3:粘贴应用配置流 JSON 代码或上传应用配置流 JSON 文件。您可以在社区或 Github 上找到可用的应用配置流,并将它们与 reCamera 集成。
步骤 4:点击
导入
按钮
2. 从 SenseCraft reCamera 官方软件平台 导入
- 步骤 1:在公共应用中找到感兴趣的应用配置流,然后点击
克隆
- 步骤 2:选择通过 USB 或网络方式连接到 reCamera。如果使用网络连接,请在文本框中输入正确的 reCamera IP 后点击连接。
- 步骤 3:公共应用将自动导入到 reCamera。您也可以将您的应用配置流贡献给社区,以激发其他用户的灵感。
将应用配置流部署到 reCamera
在工作区中添加、删除或修改节点和连线后,请务必点击右上角的 部署
按钮,将最新应用配置流部署到 reCamera。
SSCMA 节点

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

该节点用于启用摄像头,可用于捕捉摄像头模块的视频流。
配置
当您首次拖拽该节点时,会看到如下界面:

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

选择或添加 SSCMA 客户端后,该配置节点只需设置一次(后续如模型节点等均可复用),红色三角形也会消失。
输入和输出
您可以通过传入 msg.enabled = true
或 msg.enabled = false
来控制摄像头的开关。例如,可结合时间触发节点在特定时间启用摄像头,实现节能(仅适用于 OS 版本 0.1.5 及以上)。
摄像头节点可以与 stream
节点(用于 RTSP)、preview
节点或 model
节点(用于计算机视觉处理)连接。
模型节点
该节点使 reCamera 能够加载不同的视觉 AI 模型(例如 Yolo),并调整模型参数。

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

模型选择
在 reCamera 上部署不同模型有三种方式:
- 选择“设备内”模型。reCamera 默认内置了几个 Yolo 模型,详见 默认模型。
- 从
SenseCraft Zoo
中选择模型。有多个公共模型可供选择,如手势、果蔬等。用户也可以上传自己的模型,并将其公开贡献给社区。 - 上传您自己的模型。按照 将模型转换为 reCamera 格式 的说明,用户可以将自己的 AI 模型转换为 INT8 cvimodel 格式,以适配 reCamera。上传后,请在
Labels
字段中列出模型的所有类别。

模型参数
- Confidence 滑块:用于设置 AI 模型的置信度。置信度表示模型对某个预测的概率或确定性,范围为 0 到 1,值越高,模型过滤低置信预测的能力越强。
- IoU 滑块:用于设置 AI 模型的 IoU。IoU 衡量目标检测中预测边界框与真实边界框之间的重叠程度,其值在 0 到 1 之间,0 表示完全不重叠,1 表示完全匹配。较高的 IoU 阈值(例如 0.5 或 0.7)表示对检测准确性要求更高。
输出
- base64 image output 复选框:设置是否输出包含其他参数的 base64 图片代码。
- Trace 复选框:启用追踪模式,开启后检测对象会分配唯一 ID。
- Counting 复选框:启用计数模式,开启后节点会在控制台输出计数信息。
- Splitter 字段:用于设置计数线,在该区域内绘制任意线条,统计穿过该线的对象数量。
将模型节点连接到 debug 节点即可查看输出。 例如,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、mqtt、debug 或 Dashboard UI Palette 中的其他节点)进行进一步处理。
预览节点
该节点用于预览摄像头模块的视频流。您可以通过绿色开关启用或禁用预览。请注意,由于设备 CPU 限制,请勿同时拖出过多的预览和调试节点,否则打印大量调试信息会增加 CPU 负载。

流媒体节点
该节点用于将摄像头模块的视频流传输到服务器。

配置
同样,请为客户端选择 sscma
。选择后红色三角形会消失。
输入和输出
输入:将
camera
节点连接到stream
节点以启用流媒体传输。输出
:您可以使用 VLC 等应用程序查看 reCamera 输出的 RTSP 流。例如,在上述截图中,您可以在 VLC 中使用
rtsp://admin:[email protected]:554/live
,从而看到 H.264 视频流。
- 视频参数:默认分辨率为 1920 × 1800,30fps。
- 延迟:具体取决于终端应用,例如 VLC 延迟约 500 毫秒。
保存节点
该节点用于保存摄像头模块的视频流。

配置
同样,请为客户端选择 sscma
。选择后红色三角形会消失。
输入
将 camera
节点连接到 save
节点以启用保存功能。
保存参数
存储方式:
- 本地 → 路径:
recamera/userdata/VIDEO
- 外部 → 存储在 SD 卡中
- 本地 → 路径:
Start 复选框:勾选后,保存将立即开始,保存参数依据下面的
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 节点
由 Flowfuse 制作的 Dashboard 2.0 节点 是基于 Dashboard 1.0 节点开发的(向他们致敬,感谢他们的出色工作)。这是一个易于使用的 Node-RED 节点集合,可帮助您创建数据驱动的仪表板和数据可视化。借助该节点,您可以构建在 reCamera 上直接运行的交互式仪表板,包含按钮、图表、文本、滑块及预览效果等组件。
安装
该节点在设备上默认安装。如果您想手动安装,请按照以下步骤操作:
- 访问 Node-RED 工作区,地址为
ip_address/#/workspace
。 - 点击右上角的
菜单图标
并选择 “管理节点”。 - 点击 “安装” 选项卡。
- 在搜索栏中输入
node-red-contrib-sscma
并点击 “安装” 按钮。 - 等待安装完成。请注意,由于设备限制,下载时间可能因网络速度和软件包大小而在 30 秒到 5 分钟之间。
仪表板节点

常用节点如 button
、slider
、switch
、text
和 template
在构建 reCamera 仪表板时非常实用。您可以在它们的 官方文档 查看各节点详细说明,或观看 初学者教程 更好地了解该节点中的节点和组件。
带有仪表板节点的示例应用配置流
对于 OS 版本 0.1.4 及以上,设备上默认安装了一个仪表板应用配置流,作为开箱示例供用户入门。低于 0.1.4 的 OS 版本则没有默认仪表板应用配置流。 该应用配置流功能包括预览模型输出、提供计数(如计数人、狗、猫或瓶子)等演示,同时也展示了如何将基本网页(如网络、终端、SSH 页面及设备信息:CPU、内存、磁盘使用情况等)嵌入到仪表板中。

在此仪表板中,使用了以下节点:
- slider 节点:用于控制模型的置信度和 IoU。
- dropdown 节点:用于选择演示。
- text 节点:用于显示模型名称及部分文本信息。
- template 节点:用于渲染 base64 图片代码并绘制图像边界框。
- function 节点:用于解析模型节点的输出传递给 template 节点,并为其他节点添加逻辑。
该应用配置流的 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</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 console.log(msg.interfaces, '---msg.interfaces---')\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":620,"y":480,"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: 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":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: 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":590,"y":540,"wires":[[]]}]
您也可以通过以下链接访问该应用配置流的 JSON 文件:Github 和 SenseCraft 平台。
Tech Support & Product Discussion
Thank you for choosing our products! We are here to provide you with different support to ensure that your experience with our products is as smooth as possible. We offer several communication channels to cater to different preferences and needs.