Desenvolver reCamera com Node-RED
Introdução ao Node-RED
O objetivo do Node-RED é permitir que qualquer pessoa crie aplicações que coletem, transformem e visualizem seus dados, construindo fluxos que podem automatizar o seu mundo. Sua natureza de low-code o torna acessível a usuários de qualquer formação, seja para automação residencial, sistemas de controle industrial ou qualquer coisa entre eles. Ao integrar o Node-RED com a reCamera, ele fornece um método de desenvolvimento amigável para iniciantes que permite aos usuários arrastar e usar o dispositivo diretamente.
Você pode aprender o Conceito do Node-RED aqui ou começar com um tutorial em vídeo.
Na reCamera, instalamos um palette para Node-RED, que inclui o seguinte:
- SSCMA Palette (todas as versões do SO)
- Dashboard Palette (todas as versões do SO)
- reCamera Hardware (SO 0.1.6 e superior)
Com outros palettes no Node-RED por padrão, como function, debug, trigger, mqtt e assim por diante, você pode agora usá-los para construir seus fluxos para alcançar diferentes aplicações de visão computacional.
Importar Flow para reCamera
Há duas maneiras de importar flow para a reCamera:
-
Importar flow de um arquivo local ou json.
-
Passo 1: Clique no
menu iconno canto superior direito e selecione "Import".
-
Passo 2: Clique na aba "Import".

-
Passo 3: Cole o código json do flow ou envie o arquivo json do flow. Você pode encontrar flows na comunidade ou no github para flows utilizáveis e integrá-los com a reCamera.
-
Passo 4: Clique no botão "Import".
-
-
Importar flow a partir das aplicações públicas da SenseCraft reCamera.
-
Passo 1: Encontre qualquer flow interessante nas aplicações públicas. Em seguida, clique em
clone.
-
Passo 2: Escolha seu método de conexão com a reCamera via USB ou Network. Se você estiver usando conexão de rede, por favor insira o IP correto da reCamera na caixa de texto e clique em connect.
-
Passo 3: A aplicação pública será importada automaticamente para a reCamera. Você também pode contribuir com o seu flow para a comunidade para tornar a plataforma mais inspiradora para outros usuários.
-
Fazer Deploy do Flow para reCamera
Assim que você adicionar, excluir ou alterar nós e conexões no workspace, certifique-se de clicar no botão Deploy no canto superior direito para fazer o deploy do flow mais recente para a reCamera.
SSCMA Palette

node-red-contrib-sscma é um componente de nó do Node-RED projetado para facilitar a implantação rápida de modelos de IA por meio de programação baseada em fluxos. sscma é a abreviação de Seeed SenseCraft Model Assistant. Isso permite a integração perfeita das saídas de modelos de IA com outros dispositivos, possibilitando automação inteligente e fluxos de trabalho inteligentes.
Instalação
Este palette é instalado por padrão quando você instala o Node-RED. Se você quiser instalá-lo manualmente, pode seguir os passos abaixo:
- Acesse o Workspace do Node-RED visitando
ip_address/#/workspace. - Clique no
menu iconno canto superior direito e selecione "Manage Palette." - Clique na aba "Install".
- Na barra de busca, digite "node-red-contrib-sscma" e clique no botão "Install".
- Aguarde a conclusão da instalação. Observe que, devido à limitação do dispositivo, o tempo de download será em torno de 30 segundos a 5 minutos, dependendo da velocidade da rede.
Camera Node

Este nó é usado para habilitar a câmera. Ele pode ser usado para capturar o stream do módulo de câmera.
Configuração
Ao arrastar o nó pela primeira vez, você verá o seguinte:

A seleção de áudio significa se você deseja que o stream de vídeo tenha saída com áudio ou não, e o volume do áudio é ajustável. O triângulo vermelho no nó significa que o nó precisa de um cliente para se conectar a ele. Você pode clicar no add icon para adicionar um cliente SSCMA.

Então você pode adicionar o nó sscma config clicando no botão "Add" no canto superior direito com os seguintes parâmetros padrão. Este nó de configuração é necessário apenas uma vez para outros nós como model node e assim por diante. Depois que o cliente for selecionado, o triângulo vermelho desaparecerá.
Input e Output
Você também pode inserir parâmetros para controlar se a câmera está ligada ou não, passando msg.enabled = true ou msg.enabled = false para o nó. Um exemplo será usar o nó time trigger para habilitar a câmera em um horário específico para fazer uma câmera com consumo de energia eficiente. (Somente com versão de SO 0.1.5 e superior)
O camera node pode ser conectado ao nó stream para RTSP, ao nó preview ou ao nó model para processamento de visão computacional.
Model Node
Este model node permite que a reCamera carregue diferentes modelos de visão de IA, como Yolo, e ajuste os parâmetros do modelo.

Configuração
Por favor, selecione também sscma para o cliente. Após a seleção, o triângulo vermelho desaparecerá.

Seleção de Modelo
Existem 3 maneiras de fazer o deploy de diferentes modelos na reCamera:
-
- Escolher modelo
On Device. Vários modelos Yolo estão na reCamera por padrão.
- Escolher modelo
-
- Selecionar modelos do
SenseCraft Zoo. Há vários modelos públicos para escolher, como gestos e frutas. Os usuários também podem enviar seus próprios modelos e torná-los públicos para contribuir com a comunidade.
- Selecionar modelos do
-
Upload your own modelpara a reCamera. Seguindo as instruções para converter modelo para reCamera, os usuários podem converter seus próprios modelos de IA para o formato INT8 cvimodel para se adaptar à reCamera. Em seguida, faça o upload do modelo para a reCamera para implantação. Depois que o modelo for enviado, liste as classes do modelo no campoLabels.

Parâmetros do Modelo
O controle deslizante Confidence é usado para definir a confiança para o modelo de IA. Confiança refere-se à probabilidade ou certeza que um modelo atribui a uma determinada previsão. Ele também fornece uma pontuação de confiança que varia de 0 a 1. Uma confiança mais alta indica que o modelo filtrará previsões menos confiáveis.
O controle deslizante IoU é usado para definir o IoU para o modelo de IA. IoU é uma métrica usada para medir a sobreposição entre a caixa delimitadora predita e a caixa delimitadora real em tarefas de detecção de objetos. Ele é calculado como a razão entre a área de interseção das duas caixas e a área de sua união. O valor de IoU varia de 0 a 1, onde 0 significa nenhuma sobreposição e 1 significa uma correspondência perfeita. Um limite de IoU mais alto (por exemplo, 0.5 ou 0.7) indica uma exigência mais rigorosa para uma detecção correta.
Output
A caixa de seleção base64 image ouput é usada para definir se você deseja que o código de imagem base64 seja emitido com outros parâmetros ou não.
A caixa de seleção Trace é usada para habilitar o modo de rastreamento. Quando o modo de rastreamento está habilitado, o objeto detectado receberá um ID.
A caixa de seleção Counting é usada para habilitar o modo de contagem. Quando o modo de contagem está habilitado, o nó enviará as informações de contagem para o console.
O campo Splitter é usado para definir a linha de contagem. Desenhe qualquer linha na caixa para contar o número de objetos que cruzam a linha.
Conecte o model node a um nó debug para ver a saída. Por favor Exemplo de objeto de saída para 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,
]
}
O model node pode ser conectado ao nó preview para pré-visualizar o efeito no workspace do Node-RED. Você também pode passar a saída para outros nós para processamento adicional, como function node, mqtt node, debug node ou outros nós no Dashboard UI Palette.
Preview Node
Este nó é usado para habilitar a pré-visualização do módulo de câmera. Ele pode ser usado para pré-visualizar o stream de vídeo do módulo de câmera. Você pode usar o toggle verde para habilitar ou desabilitar a pré-visualização. Observe que, devido ao limite de CPU do dispositivo, não arraste muitos preview nodes e debug nodes ao mesmo tempo, pois a carga da CPU será maior ao imprimir as informações de debug no console.

Stream Node
Este nó é usado para habilitar o streaming do módulo de câmera. Ele pode ser usado para transmitir o stream de vídeo do módulo de câmera para o servidor.

Configuração
Por favor, selecione também sscma para o cliente. Após a seleção, o triângulo vermelho desaparecerá.
Input e Output
Input: Conecte o nó camera ao nó stream para habilitar o streaming.
Output:
Você pode então usar outros aplicativos, como VLC, para visualizar o stream RTSP vindo da reCamera. Como exemplo na captura de tela acima, você pode usar rtsp://admin:[email protected]:554/live no VLC e então verá o vídeo em streaming H.264.
- Parâmetro de vídeo: 1920 1800 15fps por padrão.
- Latência: Isto é diferente com base nas aplicações finais que você está usando. Por exemplo, VLC é 500 ms.
Considerando a estabilidade do fluxo enviado pelo dispositivo, a configuração máxima que recomendamos é um fluxo de vídeo de 1080p@15fps. Esta também é a configuração padrão. Se você quiser definir uma resolução diferente, você pode simplesmente alterar as opções predefinidas por:
- Entrar no terminal de backend da recamera
- Inserir o comando
cd /home/recamera/.node-red/node_modules/node-red-contrib-sscma/nodes - Inserir o comando
sudo sed -i 's/option: n.option || 0,/option: n.option !== undefined ? parseInt(n.option) : 1,/' camera.jspara definir para vídeo em 720p.
option: n.option !== undefined ? parseInt(n.option) : 1
A relação de configuração numérica das opções é a seguinte:
if (option.find("1080p") != std::string::npos) {
option_ = 0;
} else if (option.find("720p") != std::string::npos) {
option_ = 1;
} else if (option.find("480p") != std::string::npos) {
option_ = 2;
}
Para informações detalhadas, consulte o link a seguir. O nó sscma-node pode ser personalizado para atender à resolução de vídeo e taxa de quadros desejadas.
Deve-se notar que modificar o código-fonte exige que você tenha uma base sólida em C++ e seja proficiente na stack técnica para compilação cruzada. Basta modificar a configuração de "default".

Nó Save
Este nó é usado para habilitar o salvamento do módulo de câmera. Ele pode ser usado para salvar o fluxo de vídeo do módulo de câmera.

Configuração
Por favor, selecione também sscma para o cliente. Após selecionar, o triângulo vermelho desaparecerá.
Entrada
Entrada: Conecte o nó camera ao nó save para habilitar o salvamento.
Parâmetros de Salvamento
Armazenamento:
- Local -> caminho:
/userdata/VIDEO - Externo -> Armazenado no cartão SD.

Start tickbox: Uma vez marcado, o salvamento começará imediatamente. O parâmetro de salvamento será baseado em slice e duration abaixo.
Slice: Duração do vídeo de cada arquivo que você deseja salvar. (Você pode alterar as unidades no menu suspenso na versão 0.1.6 ou superior)
Duration: Duração total do vídeo que você deseja salvar. (Você pode alterar as unidades no menu suspenso na versão 0.1.6 ou superior)
Por exemplo, se slice estiver definido para 5 minutos e duration estiver definido para 1 hora, o vídeo será salvo em 12 arquivos com 5 minutos cada.
Fluxo de Exemplo com Nós SSCMA

Este fluxo está usando o modelo de Detecção Yolo 11n para pré-visualizar o objeto detectado no workspace e transmitir o fluxo de vídeo original 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":[]}]
Paleta de UI do Dashboard
Dashboard 2.0 paletter é feito pela Flowfuse com base na paleta dashboard 1.0 (agradecimentos a eles por esse trabalho incrível). É uma coleção fácil de usar de nós para Node-RED que permite criar dashboards orientados a dados e visualizações de dados. Com esta paleta, você pode criar dashboards interativos executando diretamente na reCamera com componentes como botões, gráficos, texto ou slider e visualizar efeitos.
Instalação
Esta paleta é instalada por padrão no dispositivo. Se você quiser instalá-la manualmente, pode seguir os passos abaixo:
- Acesse o Workspace do Node-RED visitando
ip_address/#/workspace. - Clique no
menu iconno canto superior direito e selecione "Manage Palette." - Clique na aba "Install".
- Na barra de pesquisa, digite "node-red-contrib-sscma" e clique no botão "Install".
- Aguarde a conclusão da instalação. Observe que, devido à limitação do dispositivo, o tempo de download será de cerca de 30 segundos a 5 minutos, dependendo da velocidade da rede e do tamanho do pacote.
Nós do Dashboard

Nós populares como button, slider, switch, text e templete são muito úteis na construção de dashboards para a reCamera. Veja a documentação detalhada de cada nó no site oficial deles ou assista ao tutorial para iniciantes para obter uma melhor compreensão dos nós e widgets desta paleta.
Fluxo de Exemplo com Nós do Dashboard
Com a versão do OS 0.1.4 e superior, um fluxo de dashboard padrão é instalado com o dispositivo como um exemplo de unboxing para os usuários começarem. Qualquer versão do OS inferior a 0.1.4 não terá o fluxo de dashboard padrão.
A funcionalidade deste fluxo é pré-visualizar a saída do modelo, fornecer diferentes demos como contar pessoa, cachorro, gato ou garrafa. Ele também fornece um exemplo de como incorporar uma página web básica ao dashboard, como páginas de rede, terminal, ssh e informações do dispositivo, como CPU, memória, uso de disco e assim por diante.

Neste dashboard, são usados os seguintes nós:
- nó
slider: Usado para controlar a confiança e o IoU do modelo. - nó
dropdown: Usado para selecionar a Demo. - nó
text: Usado para exibir o nome dos modelos e algumas informações textuais. - nó
template: Usado para renderizar o código de imagem base64 e desenhar a caixa delimitadora na imagem. - nó
function: Usado para analisar a saída do nó de modelo para o nó template e adicionar alguma lógica a outros nós.

O json deste fluxo é o seguinte:
[{"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":[]}]
Você também pode acessar o fluxo json no Github e na SenseCraft Platform.
Suporte Técnico & Discussão de Produto
Obrigado por escolher nossos produtos! Estamos aqui para oferecer diferentes tipos de suporte para garantir que sua experiência com nossos produtos seja a mais tranquila possível. Oferecemos vários canais de comunicação para atender a diferentes preferências e necessidades.