Skip to main content

Node-REDでreCameraを開発する

Node-RED紹介

Node-REDの目標は、誰でもデータの収集、変換、可視化を行うアプリケーションを構築できるようにすることです。自動化フローを構築して、彼らの世界を自動化できます。ローコードの性質により、ホームオートメーション、産業制御システム、またはその間のあらゆる用途において、あらゆる背景のユーザーがアクセスできます。Node-REDをreCameraと統合することで、ユーザーがデバイスをドラッグアンドプレイで操作できる初心者向けの開発方法を提供します。

Node-REDの概念をここで学習するか、ビデオチュートリアルから始めることができます。

reCameraでは、Node-RED用のパレットをインストールしており、以下が含まれています:

  • SSCMAパレット(全OSバージョン)
  • ダッシュボードパレット(全OSバージョン)
  • reCameraハードウェア(OSバージョン0.1.6以上)

function、debug、trigger、mqttなど、Node-REDのデフォルトの他のパレットと組み合わせて、これらを使用してフローを構築し、さまざまなコンピュータビジョンアプリケーションを実現できます。

reCameraにフローをインポートする

reCameraにフローをインポートする方法は2つあります:

  • ローカルファイルまたはjsonからフローをインポートする。

    • ステップ1:右上角のメニューアイコンをクリックし、「Import」を選択します。

    • ステップ2:「Import」タブをクリックします。

    • ステップ3:フローのjsonコードを貼り付けるか、フローのjsonファイルをアップロードします。コミュニティやgithubで使用可能なフローを見つけて、reCameraと統合できます。

    • ステップ4:「Import」ボタンをクリックします。

  • SenseCraft reCameraパブリックアプリケーションからフローをインポートする。

    • ステップ1:パブリックアプリケーションで興味深いフローを見つけます。次にcloneをクリックします。

    • ステップ2:USBまたはネットワーク経由でreCameraへの接続方法を選択します。ネットワーク接続を使用している場合は、テキストボックスにreCameraの正しいIPを入力してから接続をクリックしてください。

    • ステップ3:パブリックアプリケーションが自動的にreCameraにインポートされます。また、あなたのフローをコミュニティに貢献して、プラットフォームを他のユーザーにとってより刺激的なものにすることもできます。

reCameraにフローをデプロイする

ワークスペースでノードやワイヤーを追加、削除、または変更したら、右上角のDeployボタンをクリックして、最新のフローをreCameraにデプロイしてください。

SSCMA パレット

node-red-contrib-sscma は、フローベースプログラミングを通じてAIモデルの迅速なデプロイメントを促進するために設計されたNode-REDノードコンポーネントです。sscma は Seeed SenseCraft Model Assistant の略です。これにより、AIモデルの出力を他のデバイスとシームレスに統合し、スマートオートメーションとインテリジェントワークフローを実現できます。

インストール

このパレットは、Node-REDをインストールする際にデフォルトでインストールされます。手動でインストールしたい場合は、以下の手順に従ってください:

  1. ip_address/#/workspace にアクセスしてNode-REDワークスペースにアクセスします。
  2. 右上角の メニューアイコン をクリックし、「パレットの管理」を選択します。
  3. 「インストール」タブをクリックします。
  4. 検索バーに「node-red-contrib-sscma」と入力し、「インストール」ボタンをクリックします。
  5. インストールが完了するまで待ちます。デバイスの制限により、ダウンロード時間はネットワーク速度に応じて約30秒から5分かかることにご注意ください。

カメラノード

このノードはカメラを有効にするために使用されます。カメラモジュールのストリームをキャプチャするために使用できます。

設定

ノードを初めてドラッグアウトすると、以下のように表示されます:

オーディオ選択は、ビデオストリームをオーディオ付きで出力するかどうかを意味し、オーディオの音量は調整可能です。ノード上の赤い三角形は、ノードが接続するクライアントを必要としていることを意味します。追加アイコン をクリックしてSSCMAクライアントを追加できます。

その後、右上角の「追加」ボタンを押して、以下のデフォルトパラメータで sscma config ノードを追加できます。この設定ノードは、モデルノードなどの他のノードに対して一度だけ必要です。クライアントが選択されると、赤い三角形は消えます。

入力と出力

また、msg.enabled = true または msg.enabled = false をノードに解析することで、カメラのオン/オフを制御するパラメータを入力することもできます。例として、時間トリガーノードを使用して特定の時間にカメラを有効にし、電力効率の良いカメラを作ることができます。(OSバージョン0.1.5以上のみ)

カメラノードは、RTSP用の stream ノード、preview ノード、またはコンピュータビジョン処理用の model ノードに接続できます。

モデルノード

このモデルノードにより、reCameraはYoloなどの異なるビジョンAIモデルを読み込み、モデルのパラメータを調整できます。

設定

クライアントには sscma を選択してください。選択後、赤い三角形は消えます。

モデル選択

reCameraに異なるモデルをデプロイする方法は3つあります:

    1. On Device モデルを選択します。いくつかのYoloモデルがデフォルトでreCameraに含まれています。
    1. SenseCraft Zoo からモデルを選択します。ジェスチャーや果物など、選択できるいくつかのパブリックモデルがあります。ユーザーは独自のモデルをアップロードし、コミュニティに貢献するために公開することもできます。
    1. 独自のモデルをアップロード してreCameraに配置します。reCameraへのモデル変換の指示に従って、ユーザーは独自のAIモデルをreCameraに適応するINT8 cvimodel形式に変換できます。その後、モデルをreCameraにアップロードしてデプロイメントします。モデルがアップロードされた後、Labels フィールドにモデルのクラスをリストしてください。

モデルパラメータ

Confidence スライダーは、AIモデルの信頼度を設定するために使用されます。信頼度とは、モデルが特定の予測に割り当てる確率または確実性を指します。また、0から1の範囲の信頼度スコアも提供します。信頼度が高いほど、モデルは信頼度の低い予測をフィルタリングします。

IoU スライダーは、AIモデルのIoUを設定するために使用されます。IoUは、物体検出タスクにおいて予測されたバウンディングボックスとグラウンドトゥルースバウンディングボックス間の重複を測定するために使用されるメトリックです。これは、2つのボックスの交差領域とその結合領域の比率として計算されます。IoU値は0から1の範囲で、0は重複なし、1は完全一致を意味します。より高いIoU閾値(例:0.5または0.7)は、正しい検出に対するより厳しい要件を示します。

出力

base64 image ouput チェックボックスは、base64画像コードを他のパラメータと一緒に出力するかどうかを設定するために使用されます。

Trace チェックボックスは、トラッキングモードを有効にするために使用されます。トラッキングモードが有効になると、検出されたオブジェクトにIDが割り当てられます。

Counting チェックボックスは、カウンティングモードを有効にするために使用されます。カウンティングモードが有効になると、ノードはカウンティング情報をコンソールに出力します。

Splitter フィールドは、カウンティングラインを設定するために使用されます。ボックス内に任意の線を描いて、その線を横切るオブジェクトの数をカウントします。

モデルノードをデバッグノードに接続して出力を確認してください。 Yolo 11nの出力オブジェクトの例:

{
boxes: [
0: box_center_x,
1: box_center_y,
2: box_width,
3: box_height,
4: detected object score,
5: detected object class ID,
],
count: //inference numbers ,
image: //base64 image code,
labels: [
0: class name // e.g. person
],
perf: [
0: 0 fps, //pre-processing fps
1: 40 ms, //inference time
2: 20 ms, //post-processing time
],
resolution: [ //pixel size of the image
0: 640,
1: 640,
]
}

Model nodeはpreview nodeに接続して、Node-REDワークスペースでエフェクトをプレビューできます。また、出力を他のノードに解析して、function nodemqtt nodedebug node、またはDashboard UI Paletteの他のノードなどのさらなる処理に使用することもできます。

Preview Node

このノードはカメラモジュールのプレビューを有効にするために使用されます。カメラモジュールのビデオストリームをプレビューするために使用できます。緑色のトグルを使用してプレビューを有効または無効にできます。デバイスのCPU制限のため、デバッグ情報をコンソールに出力する際にCPU負荷が重くなるため、同時にあまり多くのpreview nodeとdebug nodeをドラッグしないでください。

Stream Node

このノードはカメラモジュールのストリーミングを有効にするために使用されます。カメラモジュールのビデオストリームをサーバーにストリーミングするために使用できます。

Configuration

クライアントにはsscmaを選択してください。選択後、赤い三角形が消えます。

入力と出力

Input: camera nodeをstream nodeに接続してストリーミングを有効にします。

Output: その後、VLCなどの他のアプリケーションを使用して、reCameraからのRTSPストリームを表示できます。上記のスクリーンショットの例として、VLCでrtsp://admin:[email protected]:554/liveを使用すると、H.264ストリーミングビデオを見ることができます。

  • ビデオパラメータ: デフォルトで1920 1800 15fps。
  • レイテンシ: これは使用するエンドアプリケーションによって異なります。例えば、VLCは500msです。
note

デバイスプッシュストリームの安定性を考慮して、推奨する最高設定は1080p@15fpsビデオストリームです。これもデフォルト設定です。 異なる解像度を設定したい場合は、以下の方法でプリセットオプションを簡単に変更できます:

  1. recameraバックエンドターミナルに入る
  2. コマンドcd /home/recamera/.node-red/node_modules/node-red-contrib-sscma/nodesを入力
  3. コマンドsudo sed -i 's/option: n.option || 0,/option: n.option !== undefined ? parseInt(n.option) : 1,/' camera.jsを入力して720pビデオに設定。

option: n.option !== undefined ? parseInt(n.option) : 1

オプション設定の数値設定関係は以下の通りです:

if (option.find("1080p") != std::string::npos) {
option_ = 0;
} else if (option.find("720p") != std::string::npos) {
option_ = 1;
} else if (option.find("480p") != std::string::npos) {
option_ = 2;
}

詳細な情報については、以下のリンクを参照してください。sscma-nodeノードは、希望するビデオ解像度とフレームレートに合わせてカスタマイズできます。

ソースコードを変更するには、C++の確固たる基盤を持ち、クロスコンパイルの技術スタックに精通している必要があることに注意してください。「default」の設定を変更するだけです。

Save Node

このノードは、カメラモジュールの保存機能を有効にするために使用されます。カメラモジュールのビデオストリームを保存するために使用できます。

Configuration

クライアントにはsscmaを選択してください。選択後、赤い三角形が消えます。

Input

入力:cameraノードをsaveノードに接続して、保存を有効にします。

Save Parameters

ストレージ:

  • Local -> パス:/userdata/VIDEO
  • External -> SDカードに保存されます。

Start tickbox:チェックを入れると、保存が即座に開始されます。保存パラメータは、以下のslicedurationに基づきます。

Slice:保存したい各ファイルのビデオ時間の長さ。(バージョン0.1.6以降では、ドロップダウンメニューで単位を変更できます)

Duration:保存したいビデオの総時間の長さ。(バージョン0.1.6以降では、ドロップダウンメニューで単位を変更できます)

例:sliceが5分に設定され、durationが1時間に設定されている場合、ビデオは5分ずつの12個のファイルに保存されます。

SSCMA ノードを使用したフロー例

このフローは、Yolo 11n Detection モデルを使用してワークスペース内で検出されたオブジェクトをプレビューし、RTSP経由で元のビデオストリームをストリーミング出力します。

[{"id":"d72dbb768278d92b","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"291219139b4904ee","type":"sscma","host":"localhost","mqttport":"1883","apiport":"80","clientid":"recamera","username":"","password":""},{"id":"7ee52cad4723fbee","type":"camera","z":"d72dbb768278d92b","option":0,"client":"291219139b4904ee","audio":true,"volume":80,"x":120,"y":220,"wires":[["09b5621ae3fa9d71","0fcaef819aa764e6"]]},{"id":"09b5621ae3fa9d71","type":"model","z":"d72dbb768278d92b","name":"","uri":"/usr/share/supervisor/models/yolo11n_detection_cv181x_int8.cvimodel","model":"YOLO11n Detection","tscore":0.45,"tiou":0.25,"debug":false,"trace":false,"counting":false,"classes":"person,bicycle,car,motorcycle,airplane,bus,train,truck,boat,traffic light,fire hydrant,stop sign,parking meter,bench,bird,cat,dog,horse,sheep,cow,elephant,bear,zebra,giraffe,backpack,umbrella,handbag,tie,suitcase,frisbee,skis,snowboard,sports ball,kite,baseball bat,baseball glove,skateboard,surfboard,tennis racket,bottle,wine glass,cup,fork,knife,spoon,bowl,banana,apple,sandwich,orange,broccoli,carrot,hot dog,pizza,donut,cake,chair,couch,potted plant,bed,dining table,toilet,tv,laptop,mouse,remote,keyboard,cell phone,microwave,oven,toaster,sink,refrigerator,book,clock,vase,scissors,teddy bear,hair drier,toothbrush","splitter":"0,0,0,0","client":"291219139b4904ee","x":270,"y":220,"wires":[["9a4aacf197bedbaa"]]},{"id":"9a4aacf197bedbaa","type":"preview","z":"d72dbb768278d92b","name":"","active":true,"pass":false,"outputs":0,"x":440,"y":220,"wires":[]},{"id":"0fcaef819aa764e6","type":"stream","z":"d72dbb768278d92b","name":"stream","protocol":0,"port":554,"session":"live","username":"admin","password":"admin","client":"291219139b4904ee","x":270,"y":300,"wires":[]}]

Dashboard UI パレット

Dashboard 2.0 パレットFlowfuse によってダッシュボード 1.0 パレットをベースに作成されました(この素晴らしい作品に対して彼らに敬意を表します)。これは Node-RED 用の使いやすいノードコレクションで、データ駆動型ダッシュボードとデータ可視化を作成できます。このパレットを使用すると、ボタン、チャート、テキスト、スライダーなどのコンポーネントを使用して、reCamera 上で直接実行される対話型ダッシュボードを作成し、効果をプレビューできます。

インストール

このパレットはデバイスにデフォルトでインストールされています。手動でインストールしたい場合は、以下の手順に従ってください:

  1. ip_address/#/workspace にアクセスして Node-RED ワークスペースにアクセスします。
  2. 右上角の メニューアイコン をクリックし、「Manage Palette」を選択します。
  3. 「Install」タブをクリックします。
  4. 検索バーに「node-red-contrib-sscma」と入力し、「Install」ボタンをクリックします。
  5. インストールが完了するまで待ちます。デバイスの制限により、ダウンロード時間はネットワーク速度とパッケージサイズに応じて約 30 秒から 5 分かかることにご注意ください。

ダッシュボードノード

buttonsliderswitchtexttemplete などの人気ノードは、reCamera 用のダッシュボードを構築する際に非常に便利です。各ノードの詳細ドキュメント を公式ウェブサイトで確認するか、初心者向けチュートリアル を視聴して、このパレットのノードとウィジェットについてより深く理解してください。

ダッシュボードノードを使用したサンプルフロー

OS バージョン 0.1.4 以降では、ユーザーが開始できるように、デフォルトのダッシュボードフローがデバイスに開封時のサンプルとしてインストールされています。0.1.4 より低い OS バージョンには、デフォルトのダッシュボードフローは含まれていません。

このフローの機能は、モデル出力をプレビューし、人、犬、猫、ボトルのカウントなどの異なるデモを提供することです。また、ネットワーク、ターミナル、SSH ページなどの基本的なウェブページをダッシュボードに組み込む方法や、CPU、メモリ、ディスク使用量などのデバイス情報の例も提供します。

このダッシュボードでは、以下のノードが使用されています:

  • slider ノード:モデルの信頼度と IoU を制御するために使用されます。
  • dropdown ノード:デモを選択するために使用されます。
  • text ノード:モデル名といくつかのテキスト情報を表示するために使用されます。
  • template ノード:base64 画像コードをレンダリングし、画像上にバウンディングボックスを描画するために使用されます。
  • function ノード:モデルノードの出力をテンプレートノードに解析し、他のノードにロジックを追加するために使用されます。

このフローの json は以下の通りです:

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

フローのjsonはGithubSenseCraft Platformでもアクセスできます。

技術サポート & 製品ディスカッション

私たちの製品をお選びいただき、ありがとうございます!私たちは、お客様の製品体験が可能な限りスムーズになるよう、さまざまなサポートを提供しています。異なる好みやニーズに対応するため、複数のコミュニケーションチャンネルを用意しています。

Loading Comments...