M5StackとM5Camera でWiFi TCP/IP 動画ストリーミングする実験

M5StackとM5CameraをWiFi,TCP/IP,でMJPEG動画ストリーミングする実験 M5Stack

こんばんは。

今回はM5Cameraのイメージセンサ画像をWiFi TCP/IP通信で、M5Stackへ動画ストリーミング送信する実験です。
しかも、M5Stackのボタン操作でM5Cameraのホワイトバランスや露出設定ができるので、ESP32同士のWiFi TCP リアルタイム双方向通信です。
パソコンやスマホブラウザ表示に切り替えることもできます。
個人的になかなかの力作です。

スポンサーリンク

ただ、みなさん、大丈夫ですか?
私は今のところコロナから逃れています。
しかし、こんな世界情勢が来るとは数か月前まで想像もしませんでした。
オタクの私にとって、Stay Home は都合が良いのですが、来月以降はさすがに仕事も家庭も大ピンチです。
というか全人類ピンチだと思うので、なんとか知恵を振り絞ってウイルスや災害に負けないものを生み出していかなきゃと思いました。
私自身、こんな記事の書き方をしていたら、身を滅ぼしかねないので、今回の記事を境に抜本的に変えていこうと思いました。
ということで、この不安を払拭するためにプログラミングに集中して、TCPについていろいろと実験しました。
そしたら、記事に載せる項目が多すぎてしまい、いつも以上に収拾がつかなくなって、逆にストレス溜まりまくりでした。
こんな長ったらしい記事書かなきゃいいのに、苦労した点がドサッと頭に浮かんできて、書き始めたら止まらなくなったという次第です。
このストレスだけは、いつも通り、何も変わらず安定でした、、、。

さて、気を取り直して、今回の実験の話に戻ります。

以前のこちらの記事こちらの記事では、UDPで動画ストリーミング(正しくはデータグラム)の実験をしましたが、M5Cameraの発熱が尋常じゃなかったので、冷却ファンが必要不可欠でした。
それに、UDPはパソコンやスマホのブラウザとの通信ができなかったので、個人的にはあまり使える用途が無いというのが実情でした。

そこで、今回のTCP/IP通信の場合、M5CameraやM5Stackの発熱は少なめで、ほんのり暖かい程度、つまり、通常のWiFi通信程度の発熱に抑えられました。
ただ、残念ながら、M5CameraとM5Stackのイメージセンサ動画ストリーミングのフレームレートは4fps程度でした。
(実は、イメージセンサを使わなければ、12fps出ました。後で詳しく説明します)
59.2kB のビットマップデータなので、将来的にJPEGにすればもっと高速表示できます。。

何はともあれ、イメージセンサを使った場合の以下の動画をご覧ください。

「よっしゃー!!!」
って感じです。
カメラ画像のM5Stack表示は4fpsしか速度出ませんでしたが、スマホ表示は8fps出せました。
(フレームレートは使うデバイスによって謎の挙動を示しました。後で詳しく説明しています)

ところで今回の個人的目玉は、動画ストリーミングというよりも、ボタン操作でコネクション切断できて、再接続できたというところを推したいです。

動画のタイムで言うと、2:11頃を見て頂くと、M5Stack表示からスマホ表示に切り替えています。
そして、2:38 では、スマホ表示側のエンディアンを切り替えて、通常表示になったところが注目です。

そしてそして、3:22から今度はM5Stackに強制的に切り替えてハックしています。
そして、M5Stack側でエンディアンを切り替えて正常表示させています。

ただ、スマホの高速表示から切り替えると、Interval (データ送信間隔)が速すぎるので、4:03にM5Stack側でIntervalを切り替えて、4fpsの安定した動画表示に切り替えているところです。
ここは、我ながらうまくできたと思っています。
ここまでできるようになるまでホント苦労しましたよ!
傍から見れば退屈な動画かも知れませんが、やっていることは個人的に自慢したいです。
将来的には全部自動で切り替えたいですね。

ただ、別の端末側からカメラをハックできてしまうというのは、セキュリティ的にヤバイやつですね。
でも、個人メイカーならばいろいろと使い道が考えられそうです。

例えば、自分の部屋で別の部屋にある3Dプリンターの印刷状況をM5Stackで監視して、台所で料理する時にスマホに切り替えて監視するとかですかね。

ということで、ESP32間のTCP通信で動画ストリーミングがようやくできるようになってきたわけですが、ここまでできるようになるまで2~3か月かかりました。
苦労した点が沢山あったので、ひたすら列挙してみました。
何か気付いた点があったら、コメント投稿でご連絡いただけると助かります。

因みに、私は独学素人アマチュアです。
誤りや勘違いが多々あると思いますのでご了承ください。

    【目次】

  1. 苦労した点、悩んだ点
  2. 使ったもの
  3. Arduino core for the ESP32 のインストール
  4. 自作ライブラリのインストール
  5. フォントを micro SDHC カードにコピーしておき、M5Stackにセットしておく
  6. M5Camera(送信側)にテスト用 MJPEG (BMP) 動画送信スケッチを入力
  7. M5Stack(受信側)のスケッチ(プログラムソースコード)の入力
  8. M5Camera (送信側)イメージセンサ動画ストリーミングのスケッチ入力
  9. WiFi長時間動画ストリーミング運転していると意味不明な動作出現
  10. 通信距離について
  11. 編集後記

苦労した点、悩んだ点

今回の実験はTCP通信の動画ストリーミングに苦しみ続けました。
UDPの比ではない難解で謎な症状に出くわしましたので、それを余すところなく列挙してみます。

M5Cameraのイメージセンサを使う時と使わない時で、MJPEGストリーミング速度が異なる謎?

先ほど紹介した動画でM5Stackのフレームレートは4fps、スマートフォンは8fpsでした。
では、前回記事のような生成ビットマップ画像を使った場合のフレームレートはどうなるかという実験をしてみました。
今回は前回記事よりも更に改良したテスト用プログラムを新たに作成しました。
以下の動画をご覧ください。

そして、Windows 10 パソコンのブラウザでテスト用プログラムを動かした場合のフレームレートは以下の動画の様な感じになりました。

以上からまとめると、各フレームレートはこのようになりました。
ビットマップ(BMP)ファイルの1フレームサイズは、59200 byte です。
interval はデータ送信間隔です。

【フレームレート(送信速度)の違い】
送信側デバイス 受信側デバイス フレームレート
M5Camera
イメージセンサ無し
生成BMP送信のみ
パソコンブラウザ 22~23fps
M5Camera
イメージセンサ無し
生成BMP送信のみ
スマホブラウザ 19~20fps
M5Camera
イメージセンサ無し
生成BMP送信のみ
M5Stack 12~13fps
M5Camera
イメージセンサ有り
interval 0
パソコンブラウザ 8fps
M5Camera
イメージセンサ有り
interval 0
スマホブラウザ 8fps
M5Camera
イメージセンサ有り
interval 60ms
M5Stack 約4fps

interval(データ送信間隔)については後述しています。

これから分かる通り、パソコンブラウザで受信する時に比べ、M5Stackで受信すると速度は半分にまで落ちています。
これはパソコンの処理能力に比べてM5Stackの方が遅いからだということは理解できます。

なぜか M5Camera ⇔ M5Stack という組み合わせのWiFi通信で、しかもイメージセンサで動画ストリーミングした場合だけ、極端に通信速度が遅かったんです。

M5Camera側イメージセンサのDMA(Direct Memory Access)や割り込み制御がWiFi動作に影響を及ぼしていると想像できます。
ただ謎なのは、スマホブラウザにイメージセンサ動画を送信した場合は8fps出せるんだから、M5Stack は12fps処理できる能力があるので、M5Stackにイメージセンサ動画を送信しても8fpsは出せるはずです。
それが、なぜか4fps止まりなのが納得いきません。
これは全く解明できませんでした。

M5Camera の画像データ送信間隔について

前節で述べたように、M5Cameraでイメージセンサを使わない生成ビットマップデータをM5Stack相手に送信すると転送速度が速いのに、M5Cameraのイメージセンサを同時使用すると速度がガクンと落ちます。
これはネットワークの輻輳とはちょっと違う理由な気がしていて、ただ単にM5StackのWiFi受信処理能力が追いついていないと想像できます。
そこで、M5Camera側のデータ送信間隔をいろいろ変えて実験しました。

動画送信側のM5Cameraスケッチでは、Arduino core for the ESP32 のhttpdライブラリを使っています。
M5Stack からGETリクエストで、
”/stream”
という文字列を検知したら、レスポンスヘッダを返して、無限whileループに入ります。
送信間隔が最速の場合はこんな感じです。

while(true){
  httpd_send(req, boundary_header.c_str(), boundary_header.length());
  httpd_send(req, (const char *)bmp_file_buf, file_size);
  httpd_send(req, "\r\n", 2);
  delay(1); //ウォッチドッグタイマ動作有効の場合は必要
}

delay(1)は必ず必要です。
このブログで何度も述べてきましたが、WiFi送受信をマルチタスクのCPU core 0 で動かしているので、ウォッチドッグタイマ動作がデフォルトで有効になっています。
ですから、delay(1)が無いと強制リセットしてしまう可能性があるためです。

この最速設定ではパソコンやスマホのブラウザ相手ならば問題無くスムースな動画ストリーミングできます。

ただ先ほど述べたように、これでは M5Cameraのイメージセンサ同時使用でM5Stackへ送信する場合はうまく動作してくれません。
データを送ることはできるんですが、途中で頻繁に停止します。
おそらく、M5Stack側の受信処理が追い付かなくて、混雑しているものと思われます。

そこで、試しに以下のようにDelayを入れてみました。

while(true){
  httpd_send(req, boundary_header.c_str(), boundary_header.length());
  delay(15);
  httpd_send(req, (const char *)bmp_file_buf, file_size);
  delay(60);
  httpd_send(req, "\r\n", 2);
  delay(1);
}

実は、これでもうまく動作しませんでした。
無限ループを停止してしまったのが原因でしょうか?
なぜか、送信がうまく行かなかったんです。
やはりCPU動作させるためはループを止めてはいけないんでしょうか???

そこで、無限ループ動作を止めないで、設定時間が来たらデータを送信する方式に変えました。
以下の感じです。

uint32_t interval[3] = {1, 15, 60};
uint32_t last_time = 0;
uint8_t send_order = 0;
while(true){
  if(millis() - last_time > interval[send_order]){
    if(send_order == 0){
      httpd_send(req, boundary_header.c_str(), boundary_header.length());
      send_order = 1;
    }else if(send_order == 1){
      httpd_send(req, (const char *)bmp_file_buf, file_size);
      send_order = 2;
    }else if(send_order == 2){
      httpd_send(req, "\r\n", 2);
      send_order = 0;
    }
    last_time = millis();
  }
  delay(1); //ウォッチドッグタイマ動作有効の場合は必要
}

こうすると、画像データをある程度定期的に送信してくれるようになりました。
この1行目の
uint32_t interval[3] = {1, 15, 60};
というinterval値(データ送信間隔)は、私のWiFi環境では一番動作が安定しました。

他の環境ではうまく動くか分かりませんので、ご自分の環境に合わせていろいろ変えてみてください。
もっと良い方法があったら是非教えて下さい。

そして、最初に紹介したYouTube動画のように、受信端末 M5Stackからスマホへ切り替えることができるわけですが、そうすると、このM5Stack用のinterval値では遅いので、ボタン操作で
interval[0] = 0, interval[1] = 0, interval[2] = 0;
と変えられるようにしました。
スマホで見る時には、動画がスムースになりますね。
逆にその状態でM5Stack表示に切り替えると、処理が間に合わないのでカクカクしてしまいます。
その場合、M5Stackの右側ボタンを長押しして、interval値を変えれば良いです。

TCPストリーミングの方がUDPより発熱が抑えられた(Arduino core ESP32の場合)

以前のこちらこちらの記事でM5CameraからM5StackへUDP動画ストリーミング(正しくはデータグラム)を実験しましたが、TCPよりもUDPの方が圧倒的に高速フレームレートでした。
ですが、UDPの場合、受信側の処理が追い付かないと、その分、捨てられるパケットも多くなります。
そうなると、裏のプログラムでデータ再送したり、WiFiの電波強度を上げて電力を多量に消費しているものと考えられます。
(あくまで予想です)
だからCPU温度が65℃を超えたのだと思われます。
触ると、
「熱っ!!!」
っていう感じで、故障しかねない状態でした。
冷却ファンは不可欠でした。
ライブラリを解読して設定すれば発熱を抑えられるかもしれませんが、特に追求していません。

そんなこともあって、TCPならデータの到達確認システムがあるので、パケットロスは少ないだろうと思いました。
現にTwitterでらびやんさんがTCPの方が良い具合だとおっしゃっていたので、自分も試してみました。

そうしたらバッチリ!

M5CameraおよびM5Stack側の発熱がかなり抑えられ、ボディを触るとちょっと暖かい程度で済んでいます。
4時間連続運転でも発熱は抑えられていました。
やっぱりTCPは面倒だけど、良いっすね!

M5Camera側はhttpdライブラリを使い、M5Stack側はWiFiClientライブラリを使った

前回記事では、サーバー側はWiFiServerライブラリを使うよりも、httpdライブラリを使った方が通信速度は速かったのです。
ならば、クライアント側はesp_http_clientライブラリを使うと速くなるかなと想像しました。

しかし、実際プログラミングしてみると、WiFiClientライブラリで自力で構成した方が、結果的に通信速度が速かったです。
ただ、私のプログラミングが間違えているかもしれないので何とも言えません。

WiFiClientライブラリだけでプログラミングすると面倒ですが、サーバーとのやり取りが良く解って勉強になるので、これでヨシとしました。

M5StackとM5Camera のマルチタスク分けについて

ご存知だと思いますが、M5Stack も M5Camera もCPUがデュアルコアで、マルチタスクが使えます。
(過去記事参照)
Arduino – ESP32 のマルチタスク ( Dual Core ) を試す

今回の実験では以下のようにタスク分けしました。

【送信側 M5Camera】
●CPU core 0 — WiFi および httpd関数群
●CPU core 1 — イメージセンサ(OV2640) DMA制御

【受信側 M5Stack】
●CPU core 0 — WiFi port 80 テキストデータ送受信
●CPU core 0 — WiFi port 81 ビットマップ動画ストリーミング
●CPU core 1 — LCD(液晶ディスプレイ)表示

実は今までずっと思い込んでいたのですが、ESP32がデュアルコアだから、2タスクまでしかできないと思っていました。
でも、FreeRTOSの場合は同じコアでも複数タスクが組めるんですよね。
そんなわけで、受信側のM5Stackは3タスクにしました。

受信側M5Stackは、動画をストリーミング受信しながら、ボタン操作で制御コマンドを別ポートで送信しなければなりません。
それに加えてLCD(液晶ディスプレイ)表示させる必要があります。
そうすると、必然とタスクが3つ必要になるわけです。

一方、送信側 M5Cameraは何故2タスクかというと、httpdライブラリを使っているからです。
ライブラリでタスク分けを自動でやってくれているっぽいので、タスクを気にする必要は無さそうです。たぶん、、、。

WiFi動作は以前のこちらの記事で実験した結果から、CPU core 0 で動かした方が良いという結論でした。

すると、M5StackのLCD(液晶ディスプレイ)はSPI制御なので、WiFiの邪魔をしない様に core 1 で動かします。要するに、setup関数やメインloop関数で動かします。

WiFi動画ストリーミングと制御操作送受信は1タスクにまとめて出来ないことは無いのですが、プログラムが複雑になるし、ポート80番でGETリクエストを送ったら、M5Cameraサーバーからレスポンスが返って来るので、その処理をしている間は動画ストリーミングが中断してしまいます。
今回のTCP/IP通信で厄介なのが、パケットロスしたものは再送してきて、受信確認取れるまで再送してくるのです。
Arduino core for the ESP32 プログラミングではロスしたパケットを意図的に破棄できないので、その間通信が止まってしまいます。
受信応答が無ければタイムアウトするまで通信が止まりまってしまいます。

そこで、今回はM5Cameraのイメージセンサ(OV2640)の制御コマンド送受信のタスクと、MJPEG(BMP)動画ストリーミングのタスクを同じCPU core 0 にして、タスクを分けてみました。
そうしたら、意外とソースコードが見やすくなったし、効率よくなりました。

ただ、難しかったのが、コネクションを切断する時に、両方のポートを一気にstopしてしまうと、再接続ができなってしまいます。
コネクションの再接続は、なかなか苦労したので、それは後で紹介します。

M5StackのLCD表示とブラウザ表示はRGB565データの並びが逆(エンディアンが逆)

前々回の記事で述べましたが、RGB565タイプのビットマップ(BMP)データをパソコンおよびスマホブラウザに表示させる時は、リトルエンディアン(最下位バイトが先)でした。

しかし、M5StackのLCD( ILI9341 or ILI9342C )に表示させる場合は、デフォルトでビッグエンディアン(最上位バイトが先)となっています。

パソコンやスマホのブラウザでは、ビットマップ(BMP)ファイル表示のエンディアンを変えられませんでした。

M5StackのLCDは、ILI9342C のデータシートによると、エンディアンを変更できるレジスタコマンド(0xF6)が用意されていました。
しかし、16bit RGB565 データの場合、エンディアンを変えられないようです。(たぶん)

よって、受信側のM5Stackのボタン操作で、M5Camera のイメージセンサ(OV2640)のエンディアンを切り替えられるようにしました。

Cボタンを長押しします。
ブラウザの場合は Change Endian ボタンを押します。

それによって、スマホやパソコンのブラウザで動画ストリーミングする時と、M5Stackで動画ストリーミングする時と、切り替えが可能になりました。
これは我ながら良い出来だと思っていて、プチ自慢です。

長い間操作しないと、勝手にタイムアウトしてしまい、制御不能になる問題

実は、WiFi に限らずイーサネットの TCP/IP の規格では、長時間クライアントからのアクションが無いとタイムアウトしてソケット通信が解除されてしまうようです。

今回はWiFiのポート80番でM5Cameraのイメージセンサ(OV2640)の操作を制御するコマンドを送っていて、ポート81番でMJPEG(BMP)動画ストリーミングしています。
動画ストリーミングは常時データをクライアント(M5Stack)へ送りつけていますが、イメージセンサ制御コマンドの場合、1回指令を出すと、それ以降しばらく操作しないことが多いと思います。
そうすると、タイムアウトしてしまい、再びボタン操作で動画ストリーミングを停止したり、ホワイトバランスや露出調整ができなくなり、操作不能に陥ります。

そこで私は考えました。

クライアント(M5Stack)側から定期的(例えば30秒毎)にM5CameraへHTTP GETリクエストを送れば良いということです。
要するにPing送信みたいなものです。
そうすればタイムアウトしないので、2~3時間の長時間動画ストリーミングしても、M5Stackからボタン操作でホワイトバランスや露出設定を変えられるし、動画の停止、再スタート操作も可能になるわけです。

このことは、ネットワークエンジニアならば当たり前に思い付くことかも知れませんね。
私はしばらくPing Pongを使っていなかったので、すっかり忘れていました。
思い付いた時には我ながら「良いアイデア!」と思ったんです。

一方、動画ストリーミングしているポート81番はタイムアウトしないんでしょうか?
実は、謎で意味不明な挙動をすることがありました。
これは最後の方で述べます。

readStringUntil関数は要注意!!!
MJPEG(BMP) 動画ストリーミングは、テキストデータとバイナリデータの検出分けが難しい。

前々回のこちらの記事で述べたように、MJPEG フォーマットの場合、MPEGとは異なり、時間軸の圧縮はありません。
よって、完成された画像1枚1枚を送信しています。
そして、合間にテキストデータも送信しています。
おさらいすると、こんな感じです。

1.boundary文字列(テキスト形式) 送信
2.HTTPヘッダ(テキスト形式) 送信
3.画像データ1フレーム(バイナリり形式) 送信
4.終了コード¥r¥n(テキスト形式) 送信

本来、M5Stackで動画処理する場合は、テキスト文字列は全て不要で、画像のバイナリデータだけ抽出すれば良いわけです。
しかし、実はそれが難しいんですよ!!!
とにかく画像データの先頭と末尾をしっかり検出できさえすれば全てうまくいくのですが、それが難しい。

まず、HTTPヘッダをテキスト形式で読み込む場合、Arduino関数の
readStringUntil(‘\n’)
を使えば、改行コードまでを順次読み込んで、最後の空行コード ‘\r\n’ までは検出可能です。

しかし、通信障害や輻輳で空行コードが検出できない場合、readStringUntil 関数が誤ってバイナリ形式の画像データまで読み込んでしまいます。
すると、改行コードと同じバイナリ値が検出できないと、ハングアップして強制リセットしてしまいます。

よって、テキストデータは何バイト送られてくるのか、はたまたバイナリデータは何バイト送られてくるのかを事前に把握できれば、readStringUntilのハングアップも回避できるかもしれません。
その場合、サンプルスケッチのCameraWebServerにあるように、送信パケットをchunk化するという方法も有りですね。
データをchunk化すると、データを分割して送る場合、各データの前にテキストデータで送るバイト数値を送信するのです。

でも実は、私はchunk化データ送受信を試してみたんです。
でも、とても面倒な割に、あまりフレームレートを上げられませんでした。
結局、chunk化はあきらめました。

そんなこともあって、サーバーとのコネクション処理まではreadStringUntil関数で処理して、実際にMJPEG動画の受信処理はreadStringUntil関数を使わないようにしました。

では、どうするかというと、送信されてきたパケットを1バイトずつチェックするしか思いつきませんでした。

boundary文字列の場合、

--myboundary\r\n

としていますが、最初の’-‘という文字を検知したら、その後13バイトまで読み込み、

-myboundary\r\n

という文字列と一致したら、次を検索するという方法です。

パケットロスして、boundary文字列を検知できないことも考えられましたが、あまり深く考えなくても、WiFiClientライブラリでしっかり検知してくれました。
ただ、たまにロスすることがあるので、boundary文字列は検知し易い文字列が良いと思います。

そこで、Arduino core for the ESP32 のサンプルスケッチ、CameraWebServer を見てみると、以下のようなboundary文字列でした。

--123456789000000000000987654321

これはスゲーと思いました。
7byteくらいまでパケットがズレても、問題無く検知できそうなんです。

最初の1234…. という並びがミソで、そこの9文字までをバイナリ値に変換すると、
0x31, 0x32, 0x33, 0x34….
というように、1ずつ増加しています。
すると、例えば連続した3文字だけ一致すればOKという判定にすると、7パターンのどこを取っても1ずつ増加しているのです。
例えば下図の様な感じです。

これは頭いいなぁと思いましたね。

ただ、実際のこの文字列で7byteズレ許容プログラムを組んでみても、あまりフレームレートを上げられませんでした。

結局、前回の
–myboundary
という文字列検知の方が速かったんです。

恐らく、WiFiのパケットロスは7byteどころではなく、もっと大きな量をロスしているし、TCPの場合は、ロスしたパケットが再送されて遅れて到達する場合もあるからだろうと思います。

結局はあまり深く考えず、ある程度ライブラリ任せにして良いと悟りました。
ただ、こういう文字列があることは勉強になったので、今後のプログラミングに生かしたいですね。

また、いくらTCPといえど、完全にロスしてしまうパケットもあるため、データの先頭を検知する目的で、boundary文字列をしっかり検知する必要があります。
検知できなければ、出来るだけ早いうちにそのパケットを捨てて、boundary文字列検知でリセットをかける感じが良いと思いました。

ところで、MJPEG フォーマットのもう一つの難しさは、最後の終了コード検知にもあります。

終了コード “¥r¥n” の検出は難しい

MJPEG フォーマットの動画ストリーミングの場合、以前のこちらの記事でも述べましたが、画像1フレームを送信した後、終了コード

\r\n

がM5Cameraから送られてきます。
バイナリ値では、
0x0D ,0x0A
です。

この値はビットマップ(BMP)の画像データの終了直前の値と誤検知してしまう可能性が高くなります。

では、どうするか。

アマチュアの私が考えられるのは、1フレームの画像データのサイズを予め調べておき、client.read(buf, size)
でサイズを指定して、まずは画像の実データをキッチリ受信することに尽きるということです。

ただ、それを指定しても、1回で受信できるデータ量が決まっているので、受信できなかった分はループ処理で返り値がゼロになるまで読み込むようにします。
client.readが-1という負の値を返す時も多々あるので、正の値になるまで繰り返し読み込むようにします。

そんなことも、M5Cameraからのデータ送信にインターバル時間を設けて(今回は60ms)送信を一時停止し、その間にclient.readを繰り返し読み込ませて、データサイズまで受信し切るという方法を取りました。

例えば、サイズが 59200 byte で、実際に読み込んだ返り値をretとして、
int ret = client.read(buf, 59200);
とします。

全部のサイズを受信し切れば、
ret = 59200
という数値が返って来るのですが、殆どが、
ret = 1490
とか、遙かに小さい数値です。
要するに一度に読み込める数値に限界があるわけです。

そこで、ループで何度も読み込ませて、返り値
ret == 0
になれば、1フレーム受信完了としました。

予め画像のサイズが分かっていればこれで良いのですが、分からない場合は、前節で述べたようにhttpd の chunk 化という手法しかないでしょうね。

そして、その後、2byte読み込んだ時に、\r\nという終了コードが検出できれば良いのです。
そこのループは以下の感じです。

while(true){
  if(client81.available()) {
    uint8_t cr[2] = {};
    if(client81.read(cr, 2) < 0){
       delay(1); continue;
     }
     if((char)cr[0] == '\r') { //0x0D = CR
       canDisplayLCD = true;
       return true;
     }else{
       canDisplayLCD = false;
       return false;
     }
   }
   delay(1);
}

ただ、たまにこの検出に失敗します。
その時には M5Stack のディスプレイに表示しない様にします。

ちょっと不思議なのが、このwhileループで
client81.available()
の返り値がゼロの場合が数百回もありました。
これは、M5Camera側の送信インターバルを60msにしているせいもありますが、インターバルを0にしても数十回~数百回あります。
インターバルがゼロならば、画像データを受信した後、すぐに終了コードを検知するはずですが、変に長い間がありました。

個人的な想像では、送信側M5Camera のhttpd送信では、ネットワークのTCP輻輳制御により、一度に多量のデータを送れず、パケットを分割して送信しているために、最後の終了コードを送るまで待機時間ができてしまうと思われます。(違っていたらスミマセン)

もっと不思議なのは、最初の動画にあるように、イメージセンサを使わずに、M5Cameraで生成したBMP画像を送信した場合は、インターバルゼロでも快適に高速フレームレートになります。
これは謎謎謎・・・・・です。
だれか教えて下さーーい!

因みに、この終了コードは、最初のboundary文字列と合体すれば速くなるかとも考えました。
こんな感じです。

\r\n--myboundary\r\n

でも、実際やってみると、ブラウザ相手なら問題無いのですが、大してフレームレートが上がらず、逆にフレームレートのバラつきが出たので止めました。

ストリーミング動画の一時停止は難しい

MJPEG(BMP)動画ストリーミングをする際、ボタン操作で一時停止したい時が多々あります。
ただ、前々回記事でも言いましたが、MJPEG方式はMPEG方式と異なり、各フレームの時間軸方向の繋がりが無いパラパラ漫画風動画なので、一時停止した際、再生する時のプログラミングがとても難しいです。

一時停止した地点を記憶しておくことはできるんですが、厄介なのがTCP規格のタイムアウトです。
一時停止して直ぐに再生すれば良いのですが、しばらく放置しておくとタイムアウトして通信不能になるのです。

だったらストリーミング用のport 81番にPing Pongして、コネクションを維持すれば良いと思いますよね。

それがですねぇ、、、httpdライブラリを使うと、ちょっと難しんです。
それをやるなら、WiFiServerライブラリを使った方が作り易いですね。

結局、そこにあまり時間を割けなかったので、今回は一時停止を見送ることにして、コネクション切断を選択しました。
実際、今回の実験の目的はイメージセンサ動画ストリーミングなので、一時停止は不要なんです。
コネクション切断した方が、何かと都合が良いのです。

動画ストリーミング中にストップボタンを押したら、確実にコネクション切断させること。そして再接続できるようにすること。

前節で述べたように、「Stream Stop」ボタンを押したら一時停止ではなく、コネクション切断する方式を選択しました。

その場合、動画ストリーミングの無限ループを確実に抜け出すことが大事です。

私のプログラミングでは、以下の4つのループにしました。

1.boundary文字列検知ループ
2.HTTPヘッダ文字列検知ループ
3.画像バイナリデータ受信ループ
4.終了コード検知ループ

「Stop」ボタンを押した時点でどこのループに入っているかは分からないので、どのループでも確実に抜け出すことです。
それにはそれぞれのループに

while(StatusStream == on_strm){
  ………
  delay(1);
}

という感じの条件にしてみましたが、もっと簡単に一括で抜け出す方法は無いもんですかねぇ?

いずれにせよ、確実にループを抜け出して、クライアントがコネクション切断してサーバーに知らせる必要があります。
クライアントの切断は、

client.stop();

があれば良いです。
ただし、ストリーミング用の port 81 番と、制御用の port 80番の両方を速やかに切断します。

そして、M5Cameraのサーバー側もstop_streamコマンドを受信したら、動画送信ループを抜け出すようにしました。

こうすれば、再接続もできるようになり、スマホなどの他のデバイスに変わっても接続できるようになります。

setSocketOption 関数で、TCP/IP 通信の細かい設定ができるらしい?

ちょっとした余談ですが、Arduino core for the ESP32 のWiFiClientライブラリでストリーミングをやろうとすると、TCPの再送設定やタイムアウト設定を変えたいと思いますよね。
実は、最近知ったのですが、WiFiClientライブラリに
setSocketOption
という関数がありました。

これは元々、lwIP(lightweight IP)というオープンソースのTCP/IPライブラリがあって、その中の
setsockopt
という関数を使い易くしたもののようです。
setsockopt関数は、Arduino core の sockets.h に定義されています。

この使い方は、ネットワーク技術のTCPを理解していないと超難解で、私には使いこなせませんでした。
ただ、コネクション切断して、再接続する場合、socketのアドレスを再利用する設定方法を見つけました。
以下の感じです。

int enable = 1;
int SO_REUSEADDR = 4;
client.setSocketOption(SO_REUSEADDR, (char*)&enable, sizeof(enable));

SO_REUSEADDR は、sockets.h に定義されていますが、コネクション確立した相手とのアドレスを使い回せるようにするTCPオプションです。

ただ、これが無くても再接続できるようになったので、今回は不要でしたが、このオプション設定を極めれば、もっといろいろな通信設定が可能かもしれません。

その他、setNoDelay関数や、setTimeout関数もありますが、どれもlwIPのsetsockoptで設定しているものです。
実際にそれらを使っても、フレームレートを上げることはできませんでした。

IRAM_ATTR属性付け忘れで、ハングアップしてリセットしてしまう問題

今回のプログラミング中にふと疑問が出てきました。
いつもの悪い癖です。

M5Cameraの動画ストリーミング中にWiFiルーター(アクセスポイント)の電源を切って通信を強制的に切断したら、M5Stackのボタンでストリーミング開始できるのかという疑問です。
それで実験してみました。

すると、M5Cameraがハングアップして、再起動したのです。

シリアルモニターのエラーメッセージは、

Guru Meditation Error: Core  0 panic'ed (Cache disabled but cached memory region accessed)

と出ていて、プラグインのException Decoder 解析では、
static inline void resetI2Sconf()
という関数のある行でハングアップしていました。
もしかしたら、WiFi関連関数がマルチタスクの Core 0 に影響しているのかも? と思いました。
でもそれは、勘違いでした。

エラーメッセージをネット検索すると、GitHubのArduino core for the ESP32 のissue 855 で解決策が出ていました。

これによると、開発チームのigrrさん曰く、

キャッシュが無効になっているときに割り込みが発生したことが原因

とのこと。そして、同じく開発チームの stickbreakerさん曰く、

割り込みによってトリガされたコードはIRAM_ATTR属性を持っていませんでした。
ほとんどのO/Sライブラリ関数は、ISRから呼び出すことができません。コードを見直して、すべての関数がローカルであること、IRAM_ATTR属性を持っていることを確認してください。例えば、Serial.print()をISRから呼び出すことはできません。

ということです。
意味わかりますか?

M5Camera などのイメージセンサ制御は、DMA(Direct Memory Access)と割り込みを使って制御しますが、割り込み制御は極力短時間で処理を済ませなければなりません。
そうしないと、他の制御に影響してしまうからです。

そこで、割り込み制御する関数にIRAM_ATTR属性というものを付けると、特別な専用のRAM領域に関数が配置されることが保証されて、処理を短時間で済ませることができるそうです。
ただ単に関数名の前に IRAM_ATTR と付けるだけなんですけどね。

実は、Arduino core for the ESP32 のライブラリにこのIRAM_ATTR属性が付いているところが多々あって、この意味がイマイチ良く解らなかったんですよね。
だから、邪魔だと思って自分勝手に消去したりしていたんです。

IRAM_ATTR属性については、Twitterでいつもお世話になっている@tnkmasayukiさんのブログ中の以下の記事が参考になると思います。
ESP32の高精度タイマー割り込みを調べる

そして、stickbreakerさん言うように、割り込み制御している関数の中にIRAM_ATTR属性を付けていないローカル関数があると、その関数は呼び出すことができません。
これが重要なところですね。
私の今回のソースコードでは、IRAM_ATTR属性を付けている vsync_isr関数の中に、IRAM_ATTR属性を付けていないresetI2Sconf関数を配置していたためにエラーが出たわけです。

なるほど!!!

要は、割り込み関連関数には全てIRAM_ATTR属性付けるべし!!!
コレに尽きます!!!

ということで、ESP32の割り込みとIRAM_ATTR属性について、ちょっと理解しました。

シリアルモニターはTeraTermを併用すると効率上がった

今まで、Arduino IDE のシリアルモニターを主に使っていましたが、今回の実験のように2台のデバイスのシリアルモニターを同時に使用したい場合は、フリーウェアのTeraTermを使うと良いと思います。
TeraTermはすでに開発が停止しているようですが、ものスゴイ重宝しますのでお勧めです。

一番良いのは、TeraTermを再起動するだけで、M5StackやM5Cameraも再起動してくれるところです。
私の場合は、M5Cameraのリセットスイッチを押しすぎて破損してしまったので、とても重宝しました。

TeraTermは窓の杜やVectorでダウンロードできると思います。

https://forest.watch.impress.co.jp/library/software/utf8teraterm/

こんな感じで使っています。

以上が今回いろいろ試行錯誤したことです。
では、次は実際にArduino IDEでソースコードを書き込んで動かす方法を紹介します。

コメント

タイトルとURLをコピーしました