Node-RED MCUを使ってみる その2(カメラノード、XIAO ESP32S3 Sense)
はじめに
今回はNode-RED MCU用に開発されたmcu_cameraノードを試してみました。
Node-RED MCUの書籍の執筆者メンバーであるkitazakiさんが、ひと月ほど前に開発されたノードです。
▼ポストされていました。
▼Qiitaにもまとめられていました。
https://qiita.com/kitazaki/items/011cbd2bc894238a6301
マイコンとNode-REDで通信して画像を取得できるようになると、さらに他のノードとつなげて使えそうです。
▼YOLOの物体検出と組み合わせたいなと思っています。
▼以前の記事はこちら
▼Node-REDとNode-RED MCUの簡単な紹介はこちら
環境を構築する
Node-RED MCUは予めインストールしておいてください。
▼最近Windows 11で環境を構築しました。
つい最近、Moddable公式からNode-RED MCUの開発者向けにアップデートしたというポストがありました。
▼一週間前です。
GitHubで見ていると、node-red-mcuのリポジトリが更新されたようでした。そのため、node-red-mcu-pluginを再インストールすることにしました。
▼Node-REDのパレットの管理の画面で、一度削除しました。

▼もう一度インストールしました。

この後カメラを利用するためにXIAO ESP32S3 Sense用にビルドすると、エラーが起きることがありました。
▼ビルドに失敗していました。

Macでは実行できるという話をkitazakiさんから聞いていました。
Node-RED MCUの書籍の執筆者メンバーであるNWLabさんから、Windowsのパスの最大長に関する問題について教えてもらいました。
▼以下のページに、パスの最大長の制限に関して書かれています。
https://learn.microsoft.com/ja-jp/windows/win32/fileio/maximum-file-path-limitation?tabs=registry
長いパスを有効にするため、Power Shellを管理者権限で起動して以下のコマンドを実行しました。
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
▼LongPathsEnabledが1になっています。

この状態だと、この後のビルドに成功しました。
ノードを追加する
mcu_cameraノードを追加します。
▼npmのページはこちら
https://www.npmjs.com/package/@kitazaki/node-red-contrib-mcu-camera
▼GitHubのページはこちら
https://github.com/kitazaki/node-red-contrib-mcu-camera#readme
Node-REDのパレットの管理から、ノードを追加しました。
▼@kitazaki/node-red-contrib-mcu-cameraを追加しました。

▼ノードが追加されました!

撮影してみる
サンプルフローを試してみる
Node-REDのノードは、ノードの開発に関するドキュメントに準拠していればexamplesフォルダにサンプルフローがあります。
今回もGitHubにサンプルフローがあったので、まずはそのフローを試してみました。
▼こちらのページにありました。
https://github.com/kitazaki/node-red-contrib-mcu-camera/blob/main/examples/flows.json
▼ノードが追加されました。

マイコンで撮影した画像を、HTTP通信で送信するようになっています。ビルドしてフローを書き込みます。
▼マイコンはXIAO ESP32S3 Senseを使いました。
▼ビルド対象はesp32/xiao_esp32s3_senseにしています。

http in/outノードで通信するので、Wi-Fiの設定も行います。Buildの横にあるShow more optionsから設定できます。
▼WiFiのSSIDとパスワードも設定しました。

ビルドして実行してみました。
▼実行が開始すると、xsbugにIPアドレスが表示されます。

http://<IPアドレス>/cameraにアクセスすると、カメラの画像が表示されます。
▼撮影できました!ブラウザからアクセスできます。


▼サンプルフローでは画像のサイズが176×144なので小さいです。

▼1280×720に変更してみました。

▼サイズを大きくしても撮影できました!

なお、動作中はマイコンがかなり熱くなります。Wi-Fiの環境によっては接続が不安定にもなっていました。
マイコンから画像を一定の時間間隔で送信する
サンプルフローでは、マイコンがブラウザからのHTTPリクエストを受け取ったときに撮影を行っていました。
逆にマイコン側が一定の時間間隔で撮影を行い、PCに画像を送信するようにしてみました。
▼フローはこちら

[{"id":"04c5ba2d08c7584a","type":"inject","z":"f59b6baa4bc2bd8d","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"test","payloadType":"str","x":2510,"y":840,"wires":[["ad7479e8d98ef636"]]},{"id":"ad7479e8d98ef636","type":"debug","z":"f59b6baa4bc2bd8d","name":"debug 1","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2670,"y":840,"wires":[]},{"id":"58a1ec4dc4e83e06","type":"http request","z":"f59b6baa4bc2bd8d","name":"","method":"POST","ret":"txt","paytoqs":"ignore","url":"http://172.20.10.5:8000/api/camera","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":2850,"y":900,"wires":[[]]},{"id":"8daab312d588877b","type":"inject","z":"f59b6baa4bc2bd8d","name":"","props":[],"repeat":"5","crontab":"","once":true,"onceDelay":"1","topic":"","x":2510,"y":900,"wires":[["3a28a436e1ab05ce"]]},{"id":"3a28a436e1ab05ce","type":"mcu_camera","z":"f59b6baa4bc2bd8d","name":"","imagetype":"jpeg","width":"176","height":"144","moddable_manifest":{"include":"manifest.json"},"x":2670,"y":900,"wires":[["58a1ec4dc4e83e06"]]}]
▼injectノードで5秒間隔で送信するようにしています。

▼PC側のIPアドレスのURLに送信するようにしています。

PC側で受信してからファイルに保存するようにしました。
▼フローはこちら

[{"id":"872898ee66e043f8","type":"http in","z":"f59b6baa4bc2bd8d","name":"","url":"/camera","method":"post","upload":true,"swaggerDoc":"","x":2370,"y":600,"wires":[["a7eb1f4de951f444","c446183e39799944"]]},{"id":"6c39d239fd867d0a","type":"http response","z":"f59b6baa4bc2bd8d","name":"","statusCode":"","headers":{},"x":2750,"y":600,"wires":[]},{"id":"8ffb1be549b3e0f2","type":"file","z":"f59b6baa4bc2bd8d","name":"","filename":"filename","filenameType":"msg","appendNewline":false,"createDir":false,"overwriteFile":"false","encoding":"none","x":2720,"y":720,"wires":[[]]},{"id":"2bd0c597aaafaa54","type":"change","z":"f59b6baa4bc2bd8d","name":"","rules":[{"t":"set","p":"filename","pt":"msg","to":"","tot":"date"}],"action":"","property":"","from":"","to":"","reg":false,"x":2770,"y":660,"wires":[["3a58751e1cde2cb0"]]},{"id":"a7eb1f4de951f444","type":"change","z":"f59b6baa4bc2bd8d","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"OK","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2580,"y":600,"wires":[["6c39d239fd867d0a"]]},{"id":"3a58751e1cde2cb0","type":"template","z":"f59b6baa4bc2bd8d","name":"","field":"filename","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{{filename}}.jpeg","output":"str","x":2560,"y":720,"wires":[["8ffb1be549b3e0f2"]]},{"id":"c446183e39799944","type":"function","z":"f59b6baa4bc2bd8d","name":"function 13","func":"let data = msg.payload; // JSONの10進数データ\nlet buffer = Buffer.from(Object.values(data)); // 10進数データをバイナリに変換\n\nmsg.payload = buffer;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2570,"y":660,"wires":[["2bd0c597aaafaa54"]]}]
▼受信したデータを画像ファイルの形式で保存できるように、ChatGPTにfunctionノードのコードを書いてもらいました。

▼ファイル名は時間の数値を利用しています。

▼templateノードで.jpegを付け足しています。

実際に撮影を行ってみました。
▼大量に保存されています。

▼ちゃんと表示できる形式で保存されていました。

なお、撮影間隔が短いと処理が追い付かないのか、エラーが起きていました。
▼撮影が1秒間隔だと、途中で以下のエラーが起きました。

最後に
IoT用途の定点観測とかであれば、簡単に使えると思います。ロボットのようにリアルタイム性が求められる用途では、マイコンのカメラは性能不足のような気がしています。
2年前に、Node-RED MCUのノードを誰かが作って、ノードが増えるともっと使いやすくなるという記事を書いていました。その後、少しずつノードが増えているようです。
▼こちらの記事でNode-RED MCU用のmcu_servoノードについて書いていました。
https://qiita.com/background/items/9b820251aa9dda5a3167
なお、カメラノードを複数配置するとエラーが起きるようだったのでご注意ください。
▼camera init failedというエラーが起きます。


追記
私が修正したコードのPull Requestがマージされたので、複数のノードを置いても実行できるようになりました。ただし、現状では幅と高さが同じでないとエラーが出るのでご注意ください。
▼こちらのPull Requestです。
https://github.com/kitazaki/node-red-contrib-mcu-camera/pull/1