LovyanGFXとJpgLoopAnimeでM5StackとM5Cameraの全画面WiFi動画ストリーミング実験

LovyanGFXとJpgLoopAnimeを使って、M5CameraとM5Stackの動画ストリーミング実験 M5Stack

操作方法

まず、WiFi環境を起動し、M5Camera を起動します。
1~2分後、M5Stackを起動して、WiFiルーターに接続します。
M5Stack のLCD画面に

WiFi Connected OK!

と表示されたら、Aボタンを長押しし、ボタンを放すとカメラ動画ストリーミングがスタートします。

今回のプログラミングは、全画面動画ストリーミングに特化しているので、ボタン操作の画面表示等のプログラミングは省きました。
操作に関しては前回記事と同様で、以下の感じです。
ただ、M5Stack公式ライブラリを使った今回のプログラミングは、ボタンを放した時点で確定になります。

【Aボタン】
瞬時押し→ 画角切り替え
長押し→  Stream Start or Stop

【Bボタン】
瞬時押し→ 画質切替
長押し→ M5Camera 強制リセット

【Cボタン】
瞬時押し→ 自動露出設定変更
長押し→ PCLK 分周変更

以上、最初に紹介した動画のように動作すればOKです。

M5Stack用電池モジュール(オプションバッテリー)使用について

M5Stack純正のLiPoバッテリーでは容量が少なすぎるので、M5Stack用電池モジュール(オプションバッテリー)を使ってみたら、私の環境では約3時間の長時間動画ストリーミングができました。

ただ、Twitterで @Ghz2000さん、@tnkmasayukiさん、その他の方々から、M5Stackのオプションバッテリーと純正のボトム常設バッテリーを併用してスタックすると、バッテリー寿命が低下するという情報を頂きました。

M5Stackとオプションバッテリーをスタックする場合、ボトムモジュールの間に装着しますが、そうすると単純に700mAh と 150mAhのLiPoバッテリーの並列接続になります。

すると、USB給電する時、一方のバッテリーが満充電になると、充電停止になると思います。
そこで、もう一方のバッテリーが満充電になっていない場合、USB給電を外すと、満充電のバッテリーから未充電のバッテリーに電流が流れます。
その時、LiPoバッテリーの許容充電電流の最大値を超える大きな電流が流れる場合があります。
ボトムのLiPoバッテリーの充電電流許容値は1時間で150mAです。

例えば、以下のスイッチサイエンスウェブショップに似たようなLiPoバッテリーがあります。
https://www.switch-science.com/catalog/3166/

このサイトを見ると、1Cを超える充電電流を流すと発火等の危険があると書いてあります。
すると、M5Stackのオプションバッテリーにはその危険性があるかも知れません。

LiPoバッテリーには保護回路が付いていますが、電流に対してはその保護回路が働かないという情報もありますので、充分注意しないといけないかも知れません。
備付けの保護回路は謎なところが多いので、この辺はいつか自分で調べてみたいですね。

また、バッテリーの並列接続回路に循環電流が流れている可能性があります。
つまり、一方が満充電で他方が未充電の場合、満充電のバッテリーが電源となって未充電のバッテリーに充電電流が流れます。そして、それが満充電になると、今度は電流が逆流して一方に充電し始め、それを繰り返します。
そうすると、バッテリーの寿命を知らない間に縮めている可能性があるらしいです。

ただ、M5Stack公式の以下のページ
https://m5stack.com/collections/m5-module/products/battery-module
には、LiPoバッテリーは並列接続できると書いてあり、代理店のスイッチサイエンスウェブショップの以下のページ
https://www.switch-science.com/catalog/3653/
では、ボトムモジュールとの併用の危険性は明記されていないので、正直なところ定かではありません。

この辺が疑問ならば、自分で実験して調べてみるしかないですね。
心配ならばボトムを外して使用した方が良いかも知れません。

新たに学んだこと

ローカル関数内の変数にstatic修飾子を付けたら、とっても便利だった。

高速化とは異なる話題です。
先のM5Stack側の設定のところで少し紹介しましたが、ローカル関数内で static修飾子を入れた変数を使うと、使い方によってはとっても便利だったので紹介します。
あまりに初歩的なので、熟練者の方は読み飛ばしてください。

私は今まで、変数の値について、関数を出てもそのままその値を保持させたい場合、グローバル変数をよく使っていました。

例えば、こんな感じで、Arduinoプログラミングでよく見るやつです。

int x = 5;

void setup() {
  Serial.begin(115200);
}

void loop() {
  func();
  delay(1000);
}

void func(){
  Serial.println(x);
  x++;
}

これを実行すると、シリアルモニターには以下のように表示されます。

プログラマーの世界ではグローバル変数は極力使うなと言われているやつを平気で使っていました。

では、ローカル関数 func内に以下のように static修飾子を付けて、関数内で初期化をするコードにしてみます。

void setup() {
  Serial.begin(115200);
}

void loop() {
  func();
  delay(1000);
}

void func(){
  static int i = 5;
  Serial.println(i);
  i++;
}

そして、コンパイル書き込み実行させると下図の様な結果になります。

このように、関数内の変数に static修飾子を付けると、その行を実行した時に初期化されて、その関数を抜けても値を保持し続けます。まるでグローバル変数の様な振る舞いですね。
これができれば、グローバル変数を劇的に減らせますね。
個人的には目から鱗で、かなり使える方法でしたね。

特に今回のように他者のライブラリを取り込んで、ほんのちょっと改変する場合は有効でしたね。
だだ、複数の関数にまたがる場合は使えないので注意です。

その他の注意点として、初期化で static修飾子を付けると、一回だけしか初期化できないということをよく頭に入れておくことです。
例として、以下のようにするとダメです。

int x = 8;

void setup() {
  Serial.begin(115200);
}

void loop() {
  func(x);
  x++;
  delay(1000);
}

void func(int i){
  static int iii = i;
  Serial.println(iii);
}

これの結果は以下のようになります。

初期化は1回のみというところが重要です。
最初の初期化の値を保持したままになってしまうわけです。
まぁ、そもそもこのコードは誤りなので、こういう使い方はしないようにしたいですね。

以上から、staticを付けると、その変数をメモリ内に保持し続けるけど、static を使わなければ、ローカル関数を抜ける時にそのメモリは一旦捨てられるので、メモリを消費して保持し続けるか、一旦捨ててメモリを節約するかという、用途に合わせて使い分けるといいかもですね。

FreeRTOS の Queue(キュー)制御を使ってみたけど、あまり効果無かった

今までずっと敬遠してきたことを今回初めてやってみました。

M5Stack側のスケッチで、WiFi受信している間にJPEGデコードしてLCD表示させるために、FreeRTOSの Queue(キュー)制御を使ってみました。
Arduino core for the ESP32 はそもそも FreeRTOSをベースに作られているので、FreeRTOS系関数はそのまま使えることが多いです。

Queue制御はネットで豊富に情報がありますが、Twitterでいつもアドバイスを頂く @tnkmasayukiさんの以下のサイトが参考になると思います。

ESP32のFreeRTOS入門 その5 キュー

ここでは、Queue(キュー)制御についての詳細は割愛します。
(ていうか、自分自身まだ理解していないです)

例えば、ザッとかいつまんでキュー処理だけピックアップすると、概略はこんな流れです。

typedef struct {
  uint32_t jpg_len;
  uint8_t *jpg_buf;
} recv_jpg_q_t;

QueueHandle_t sem;
TaskHandle_t taskClientStrm_handl;
const uint8_t queue_max = 2;

void setup() {
  sem = xQueueCreate(queue_max, sizeof(recv_jpg_q_t*));
  xTaskCreatePinnedToCore(&taskClientStream, "taskClientStream", 8192, NULL, 20, &taskClientStrm_handl, 0);
}

void loop() { //core 1
  static recv_jpg_q_t *q = NULL;
  if (xQueueReceive(sem, &q, 0)){
    //Queueが受信されたら、JPEGデコードとLCD描画
    //....中略.....
  }
}

void taskClientStream(void *pvParameters){
  //core 0 task
  while(true){
    //....中略.....
    receiveJpgData();
  }
}

void receiveJpgData(){
  //JPEGデータを受信するプログラム
  //....中略.....
  xQueueSend(sem, &q, 70);
}

JPEGデータはマルチタスクのcore 0 で受信し、core 1のメインloopでは JPEGデコードとLCD描画を処理します。

core 0 でJPEGデータを受信したら、xQueueSend でキュー合図を送って、メインloop内の xQueueReceiveでキューを受信したら、JPEGデコードとLCD描画する流れです。

xQueueSend はxQueueReceive処理が間に合わない場合、キューを貯め込むことができるのですが、今回はLCD描画がWiFi受信よりも超高速なので、キューが溜まりません。
それよりも、WiFiストリーミングの方が輻輳して遅れがちなので、マルチタスク化してキュー制御するほどのことでもなかったかなと思います。

このQueue制御は、以前の記事でも述べた FIFO(First-in First-out) 方式で、送ったキューの順番で読み出す方式です。
しかし、キューが貯まらないので、キューバッファの数は2で充分でした。
もしくはキュー制御無しでもOKだったと思います。

ということで、今回の実験ではキュー制御は有効では無かったのですが、何となく使い方が分ってきたので、今後は積極的に使っていきたいと思います。

今回の実験の課題点

今回の実験の課題点はこんな感じです。

全画面最高画質の場合、30秒に1回くらい、カクカクする

全画面 (320 x 240 pixel) でJPEG最高画質の場合、30秒に1回くらいは動画がカクカクしますが、1秒くらいで復帰します。

これはたぶん、WiFi通信の輻輳(混雑)かな?と想像しています。
画質を1段階落とすと、カクつく回数は少なくなります。

いつかこれを無くすことができれば良いですね。

全画面最高画質の場合、たまに10秒程ストリーミングがフリーズする

これも全画面 (320 x 240 pixel) でJPEG最高画質の場合、たまに10秒前後ストリーミングがフリーズします。
その後は復帰するので、個人的にはヨシとしています。

この原因もおそらくWiFi通信の輻輳かな?と想像しています。
画質を落とすほど、その回数は少なくなります。
ということは、やはり送信するデータ容量が大きすぎるのだと思います。
その他、WiFiルーターの性能によるものかも知れません。

シリアルモニターでログを注視していても、フリーズする前兆を捉えることができませんでした。
送信間隔が短すぎるのかな? とも思ったのですが、そうでも無さそうで、結局、原因不明でした。

本当なら、全画面、最高画質、常時25fps、ノンストップが実現できたら最高ですね。

編集後記

今まで自前ライブラリで M5Stack のLCDに表示させていましたが、今回、ガッツリと他人のLCDライブラリを使ってみて、こんなにも違うものなのかと愕然としましたね。

LCD描画の高速化なんて、ある程度できれば充分と思っていましたが、高速化を追求することによって、WiFi 動画ストリーミングも高速化できるし、TCPによる送信側の無駄な再送が減るので、何よりも省エネになるわけですね。
ほんと、プログラミングって奥が深くて、底なし沼で嫌になりますね。。。

ということで、長い間 M5Camera と M5Stack のカメラ動画ストリーミングを追求してきましたが、これで一旦完結できそうです。

ではまた・・・。

コメント

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