M5UnitVのカメラ映像をシリアル通信でM5Stackの液晶に表示する

# はじめに
* M5UnitVのカメラからQVGA（320 * 240）サイズの画像を撮影し、M5Stackの液晶画面に表示します。
 * UnitVは本来AIカメラですが、今回は単に外付けのカメラとして使用します。
 * M5UnitVとM5Stack(Gray)はGroveケーブルで接続し、JPEG形式で圧縮してシリアル転送します。


![IMG_3310D.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/e579b45d-a543-c682-c341-ef7e73e82fdb.jpeg)

# 参考にしたページ
* HomeMadeGarbage さんのページがとても参考になります。構成が多少異なるので、適宜読み替えます。
 * https://homemadegarbage.com/ai13
* グラフィックス系はLovyanGFXライブラリを使わせてもらっています。フレームレートが向上します。
 * https://github.com/lovyan03/LovyanGFX

# 本体を接続
* 接続には、M5UnitVに付いてきたGroveケーブルを使います。
 * UnitVにはバッテリが搭載されていませんが、M5Stackから電源供給できるのでこんな感じでシンプルな構成になります。
 * 自作でここまで最適化された構成を作るのはかなり難しいので、こう言う使い方が出来るところがM5Stackの良いところですね。 

![IMG_3312D.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/b50c6409-0e38-b3a7-c085-53f600e32bc7.jpeg)


# M5UnitV側のコード
* カメラの撮影画像サイズはQVGA（320 * 240）、JPEG圧縮率はとりあえず50%。
* Groveポートに割り当てられているGPIO34, 35を使ってシリアル通信します。
 * ポートレートは115200
 * JPEG画像データを送る前に、`uart.write(data_packet)`でPacketヘッダ10byteを先に送ります。
 * 先頭ビットは固定で`0xxFF, 0xD8, 0xEA, 0x01`、その後に画像サイズが続きます。
 * JPEG圧縮後の画像データは`uart.write(img_buf)`で送ります。
* UnitVは液晶画面が無く、ちゃんと動いてるか見た目で分からないためRGB LEDでステータスを表示します。
 * `class_ws2812 = ws2812(8,100)`で`GPIO8`と、（多分）明度の刻み`100`を指定します。
 * ブートしたら、3回青色に点滅後、点灯させます。
 * これでちゃんと動いてるかが分かるようになります。
* このプログラムは、SDカード直下に`boot.py`と言うファイル名で配置します。
 * こうしておくと、UnitVの電源がONになったタイミングでこのファイルを読み込んで実行されます。
 * SDカードの抜かずにMaixPyから直接配置できる機能があるので、[こちらのページ](https://qiita.com/Nabeshin/items/7d49ca51cfdb10b70014#その４-bootpyの作成)を参考にしてください。

![スクリーンショット 2020-12-31 17.08.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/77d7cd2e-e62a-9143-ebb0-560d7d42a194.png)

```Python:boot.py
import sensor
import time
from machine import UART
from fpioa_manager import fm
from Maix import GPIO
from modules import ws2812

# RGB LED設定
class_ws2812 = ws2812(8,100)
BRIGHTNESS = 0x10

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_vflip(1)
sensor.set_hmirror(1)
sensor.run(1)
sensor.skip_frames()

# UART
fm.register(35, fm.fpioa.UART1_TX, force=True)
fm.register(34, fm.fpioa.UART1_RX, force=True)

# RGB LEDを光らせる
def RGB_LED(r,g,b):
    a = class_ws2812.set_led(0,(r,g,b))
    a = class_ws2812.display()
    time.sleep(0.3)
    a = class_ws2812.set_led(0,(0,0,0))
    a = class_ws2812.display()
    time.sleep(0.3)
    a = class_ws2812.set_led(0,(r,g,b))
    a = class_ws2812.display()

# 起動インジケータとしてblueで点滅、最後に点灯
RGB_LED(0,0,BRIGHTNESS)
RGB_LED(0,0,BRIGHTNESS)
RGB_LED(0,0,BRIGHTNESS)

uart = UART(UART.UART1, 115200,8,0,0, timeout=1000, read_buf_len=4096)

while(True):
    img = sensor.snapshot()
    # Jpeg圧縮率を上げると表示フレームレートは上がるが、綺麗さとトレードオフ
    img_buf = img.compress(quality=50)
    # Packetヘッダ
    img_size1 = (img.size()& 0xFF0000)>>16
    img_size2 = (img.size()& 0x00FF00)>>8
    img_size3 = (img.size()& 0x0000FF)>>0
    data_packet = bytearray([0xFF,0xD8,0xEA,0x01,img_size1,img_size2,img_size3,0x00,0x00,0x00])
    uart.write(data_packet)

    # imgデータ転送
    uart.write(img_buf)
```

# M5Stack側のコード
* M5Stack GrayのGroveポートに割り当てられているGPIOは21, 22なので、これを使ってシリアル通信します。
 * `Serial2.begin(115200, SERIAL_8N1, 21, 22);`でポートレートは送信側と同じ115200に合わせます。
* `loop()`でデータ受信と画面表示を行います。
 * Packetデータ10byteを読み込みます。
 * 先頭バイト列がフォーマットに合ってた場合、データサイズを読み込みます。
 * `Serial2.readBytes(jpeg_data.buf, jpeg_data.length);`でそのサイズ分のJPEGデータを読み込みます。
 * 最後にdrawJpgでM5Stackの液晶画面に表示します。
* drawJpgについて
 * 最初、`M5.Lcd.drawJpg`を使って描画したところ、30フレーム目くらいで更新がストップしてしまいました。
 * 高速表示できる事で有名なLovyanGFXに差し替える事で、ストップせずに表示し続けられるようになりました。
 * 素晴らしいライブラリを提供していただき、ありがとうございます。


![スクリーンショット 2020-12-31 17.08.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/7e62b4e8-d01c-8430-ae0a-33dc312b305f.png)

```Cpp
#include <M5Stack.h>

#define LGFX_M5STACK
#include <LovyanGFX.hpp>

static LGFX lcd;

typedef struct {
    uint32_t  length;  //Jpegデータサイズ
    uint8_t   *buf;    //Jpegデータバッファ
} jpeg_data_t;
 
jpeg_data_t jpeg_data;
static const int RX_BUF_SIZE = 100000;

// Packetヘッダ判別用固定バイト列
static const uint8_t packet_begin[3] = { 0xFF, 0xD8, 0xEA };

void setup() {
    M5.begin();

    Serial2.begin(115200, SERIAL_8N1, 21, 22);
    jpeg_data.buf = (uint8_t *) malloc(sizeof(uint8_t) * RX_BUF_SIZE);

    lcd.init();
    lcd.setRotation(1);
    lcd.setBrightness(255);
    lcd.setColorDepth(16);
    lcd.clear();
}
 
void loop() {
    if (Serial2.available()) {
        uint8_t rx_buffer[10];
        // 10byteずつ読み込む
        int rx_size = Serial2.readBytes(rx_buffer, 10);
        if (rx_size == 10) {
            // Packetヘッダのbyte列を特定
            if ((rx_buffer[0] == packet_begin[0]) && (rx_buffer[1] == packet_begin[1]) && (rx_buffer[2] == packet_begin[2])) {
                // データサイズを取得
                jpeg_data.length = (uint32_t)(rx_buffer[4] << 16) | (rx_buffer[5] << 8) | rx_buffer[6];
                Serial.println(jpeg_data.length);
                // データサイズ分バッファに読み込み
                int rx_size = Serial2.readBytes(jpeg_data.buf, jpeg_data.length);
            }

            // Jpegデータを画面に描画
            //M5.Lcd.drawJpg(jpeg_data.buf, jpeg_data.length,0, 0, 320, 240, 0, 0, JPEG_DIV_NONE);

            // LovyanGFXの方が高速
            lcd.drawJpg(jpeg_data.buf, jpeg_data.length,0, 0, 320, 240, 0, 0, ::JPEG_DIV_NONE);
        }
    }
}
```

# 実行してみる
* ポートレート115200でQVGAサイズのJPEG50%圧縮した画像で、フレームレート5fpsくらいで結構綺麗に画像が表示されます。
 * これで液晶付きのM5StickVを買わなくても、M5UnitVをM5Stackと組み合わせる事でイケてる表示環境が手に入りますね。 
 * フレームレートを上げたい場合は、JPEG圧縮率を上げると良いです。（画質はかなり落ちますが・・・）
* このガジェット感、良いな。

![IMG_3311D.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283854/1a4b7678-cda9-2929-b5a8-e27aacf8636a.jpeg)


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/H0meMadeGarbage?ref_src=twsrc%5Etfw">@H0meMadeGarbage</a> さんのM5UnitVの記事を参考に、M5Stack画面にUnitVからのカメラ画像を表示できた。<br>JPEG表示し続けると30フレーム目位で画面更新が止まってしまったけど<a href="https://twitter.com/lovyan03?ref_src=twsrc%5Etfw">@lovyan03</a> さんのLovyanGFX使ったら止まらなくなりました。感謝！ <a href="https://t.co/MaGyJ2Nk8f">pic.twitter.com/MaGyJ2Nk8f</a></p>&mdash; Nabeshin (@desmoquattro996) <a href="https://twitter.com/desmoquattro996/status/1344323806643376128?ref_src=twsrc%5Etfw">December 30, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>