Develop reCamera with Node-RED
Nodered Introduction
Node-RED's goal is to enable anyone to build applications that collect, transform and visualize their data; building flows that can automate their world. Its low-code nature makes it accessible to users of any background, whether for home automation, industrial control systems or anything in between. By integrated Node-RED with reCamera, it provides a beginner-friendly development method that can let users drag and play with the device right the way.
You can learn the Node-RED Concept here or start with a video tutorial.
On reCamera, we have installed palette for Node-RED, which includes the following:
- SSCMA Palette (All OS version)
- Dashboard Palette (All OS version)
- reCamera Hardware (0.1.6 OS and above)
With other palette in Node-RED by default, such as function, debug, trigger, mqtt and so on, you can now use them to build your flows to achieve different computer vision applications.
Import Flow to reCamera
There are two ways to import flow to reCamera:
Import flow from local file or json.
Step1: Click on the
menu icon
in the upper right corner and select "Import".Step2: Click on the "Import" tab.
Step3: Either paste the flow json code or upload the flow json file. You can find flows in the community or github for useable flows and integrated them with reCamera.
Step4: Click on the "Import" button.
Import flow from SenseCraft reCamera public applications.
Step1: Find any interesting flow in the public applications. Then click
clone
.Step2: Choose your connect method to reCamera either via USB or Network. If you are using network connection, please enter the correct IP for reCamera in the text box then click connect.
Step3: The public application will be auto imported to reCamera. You can also contribute your flow to the community to make the platform more inspired to other users.
Deploy Flow to reCamera
Once you add, delete, or change nodes and wires in the workspace, please make sure you click the Deploy
button in the upper right corner to deploy the newest flow to reCamera.
SSCMA Palette

node-red-contrib-sscma
is a Node-RED node component designed to facilitate the quick deployment of AI models through flow-based programming. sscma
is short for Seeed SenseCraft Model Assistant. This allows for seamless integration of AI model outputs with other devices, enabling smart automation and intelligent workflows.
Installation
This palette is installed by default when you install Node-RED. If you want to install it manually, you can follow the steps below:
- Access to Node-RED Workspace by visiting
ip_address/#/workspace
. - Click on the
menu icon
in the upper right corner and select "Manage Palette." - Click on the "Install" tab.
- In the search bar, enter "node-red-contrib-sscma" and click on the "Install" button.
- Wait for the installation to complete. Please note that because the device limitation, the downloading time will be around 30 seconds to 5 minutes depends on the network speed.
Camera Node

This node is used to enable the camera. It can be used to capture the stream of the camera module.
Configuration
When first dragging out the node, you will see the following:

The audio selection means if you want the video stream to ouput with audio or not, and the volume of audio is adjustable. The red triangle on node means the node needs a client to connect with it. You can click on add icon
to add a SSCMA client.

Then you can add the sscma config
node by hitting the upper right corner "Add" button with the following default parameters. This config node is only neeeded once for other nodes like model node and so on. After the client is selected, the red triangle will be gone.
Input and Output
You can also input parameters to control whether the camera is on or not by parsing msg.enabled = true
or msg.enabled = false
to the node. An example will be using time trigger node to enable the camera at specific time to make a power-efficient camera. (Only with OS version 0.1.5 and above)
Camera node can wire to stream
node for RTSP, preview
node or model
node for computer vision processing.
Model Node
This model node enables reCamera to load different vision AI models such as Yolo and adjust the parameters of the model.

Configuration
Please also select sscma
for the client. After selecting, the red triangle will be gone.

Model Selection
There are 3 ways to deploy different models on reCamera:
- Choose
On Device
model. Several Yolo models are in the reCamera by default.
- Choose
- Select models from
SenseCraft Zoo
. There are several public models to choose such as gesture and fruits. Users can also upload their own models, and make it public to contribute the community.
- Select models from
Upload your own model
to reCamera. By following the instruction for [converting model to reCamera](https://wiki.seeedstudio.com/convert xxx), users can convert their own AI models to INT8 cvimodel format to adapt to reCamera. Then upload the model to reCamera for deployment. After the model is uploaded, please list the classies of the model in theLabels
field.
Model parameters
The Confidence
slider is used to set the confidence for AI model. Confidence refers to the probability or certainty that a model assigns to a particular prediction. it also provides a confidence score ranging from 0 to 1. A higher confidence indicates that the model will filter out less confident predictions.
The IoU
slider is used to set the IoU for AI model. IoU is a metric used to measure the overlap between the predicted bounding box and the ground truth bounding box in object detection tasks. It is calculated as the ratio of the intersection area of the two boxes to their union area. The IoU value ranges from 0 to 1, where 0 means no overlap and 1 means a perfect match. A higher IoU threshold (e.g., 0.5 or 0.7) indicates a stricter requirement for a correct detection.
Output
The base64 image ouput
tickbox is used to set if you want the base64 image code to be output with other parameters or not.
The Trace
tickbox is used to enable the tracking mode. When the tracking mode is enabled, the detected object will be assigned ID.
The Counting
tickbox is used to enable the counting mode. When the counting mode is enabled, the node will output the counting information to the console.
The Splitter
field is used to set the counting line. Draw any line in the box to count the number of objects that cross the line.
Wire model node to a debug node to see the output. Please Output object example for 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,
]
}
Model node can be wired to preview
node to preview effect in the Node-RED workspace. You can also parse the output to other nodes for further processing such as function node
, mqtt node
, debug node
or other nodes in the Dashboard UI Palette
.
Preview Node
This node is used to enable the preview of the camera module. It can be used to preview the video stream of the camera module. You can use the green toogle to enable or disable the preview. Please note that because of the device CPU limit, do not drag out too many preview nodes and debug nodes at the same time as CPU load will be heavier when printing the debug information to the console.

Stream Node
This node is used to enable the streaming of the camera module. It can be used to stream the video stream of the camera module to the server.

Configuration
Please also select sscma
for the client. After selecting, the red triangle will be gone.
Input and Output
Input: Wire the camera
node to the stream
node to enable the streaming.
Output:
You can then use other applications such as VLC to view the RTSP stream out from reCamera. As an example in the above screenshot, you can use rtsp://admin:[email protected]:554/live
in VLC then you can see the streaming video.
- Video parameter: 1920 1800 30fps by default.
- Latency: This is different based on what end applications you are using. E.g, VLC is 500 ms.
Save Node
This node is used to enable the saving of the camera module. It can be used to save the video stream of the camera module.

Configuration
Please also select sscma
for the client. After selecting, the red triangle will be gone.
Input
Input: Wire the camera
node to the save
node to enable the saving.
Save Parameters
Storage:
- Local -> path:
recamera/userdata/VIDEO
- External -> Stored in SD card.

Start tickbox: Once ticked, the saving will start immediately. Saving parameter will based on the slice
and duration
below.
Slice: Video time length of each file you want to save. (You can change units in the dropdown menu with version 0.1.6 or above)
Duration: Total time length of the video you want to save.(You can change units in the dropdown menu with version 0.1.6 or above)
E.g. if slice is set to 5 minutes and duration is set to 1 hour, the video will be saved in 12 files with 5 minutes each.
Example Flow with SSCMA Nodes

This flow is using Yolo 11n Detection model to preview the detected object in the workspace, and stream out the original video stream via 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 Palette
Dashboard 2.0 paletter is made by Flowfuse based on the dashboard 1.0 palette (shout out to them for this amazing work). It is an easy to use collection of nodes for Node-RED that allows you to create data-driven dashboards & data visualisations. With this palette, you can create interactive dashboards that directly running on reCamera with components like buttons charts, text or slider and preview effects.
Installation
This palette is installed by default in the device. If you want to install it manually, you can follow the steps below:
- Access to Node-RED Workspace by visiting
ip_address/#/workspace
. - Click on the
menu icon
in the upper right corner and select "Manage Palette." - Click on the "Install" tab.
- In the search bar, enter "node-red-contrib-sscma" and click on the "Install" button.
- Wait for the installation to complete. Please note that because the device limitation, the downloading time will be around 30 seconds to 5 minutes depends on the network speed and the package size.
Dashboard Nodes

Popular nodes like button
, slider
, switch
, text
and templete
are very handy when it comes to building dashboard for reCamera. View detail docs of each node on their official website, or watch their beginner tutorial to get a better understanding of nodes and widgets in this palette.
Example Flow with Dashboard Nodes
With OS version 0.1.4 and above, a default dashboard flow is installed with the device as an unbox example for users to get started. Any OS version lower than 0.1.4 will not have the default dashboard flow.
The functionality of this flow is to preview the model output, provide different demos such as countinng pereson, dog, cat or bottle. It also provides an example of how to inframe basic webpage to the dashboard such as network, terminal, ssh pages and device info such as CPU, memory, disk usage and so on.

In this dashboard, the following nodes are used:
slider
node: Used to control the confidence and IoU of the model.dropdown
node: Used to select the Demo.text
node: Used to display models name and some textual information.template
node: Used to render the base64 image code and draw the bounding box on the image.function
node: Used to parse the output of the model node to the template node and add some logic to other nodes.

The json of this flow is as below:
[{"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":[]}]
You can also access the flow json on Github and SenseCraft Platform.
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.