見出し画像

M5Cameraで古代のデジカメを作る

M5PaperとM5Stackで遊んでいたんですが、M5シリーズには他にもいろいろあります。今回は中古でM5Camera Xというのを見つけました。

まあ販売終了なんですけど…M5Cameraシリーズはこんな感じでM5Stackと同じようにESP32を積んでおり、つまり単体でWiFiに接続してWebカメラっぽく使ったりできます。サンプルプログラムとしてはLAN内からWebブラウザでアクセスしストリーミング映像を見たり画像を撮影したりするものが入っています。

そういう単体でいろいろできるやつなんですが、逆に有線で繋いで画像を取り出す情報があまりありませんでした。やるとすればUARTなんですが、WiFiのほうが速いんでしょうね。多分ね。でもここにM5Stackと同じGroveコネクタがあって…物理的にケーブル直結するのは簡単なんですよ。やってみました。

M5CameraでJPEGを撮影する

サンプルプログラムはWebサーバ部分が大部分なので読みづらいですが、M5Cameraの使い方は簡単です。なんかモデルごとにPSRAMの有無だとか、信号線が違うとかがあるので場合分けが多いだけです。基本こんなふうにcamera_config_t構造体を作って

// Camera init
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = size;
  config.pixel_format = PIXFORMAT_JPEG; 
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

esp_camera_init()で初期化します。

// camera init
  esp_err_t err = esp_camera_init(&config);

あとはesp_camera_fb_get()で撮影し、フレームバッファを取得します。PIXFORMAT_JPEGを指定していればJPEG画像のデータがそのまま出来ます。

// capture image
  camera_fb_t *fb = esp_camera_fb_get();

フレームバッファ使用後はesp_camera_fb_return()で明示的にもう解放していいよと知らせる必要があるようです。

// return frame buffer after using
  esp_camera_fb_return(fb);

M5CameraとM5StackでUART通信する

M5CameraやM5StackにはGroveコネクタというコネクタが出ていますが、Grove自体はI2Cに使えたりUARTに使えたり自由です。ただしどのピンがGroveコネクタに出ているのかは機種によっても違うので、コード上でどのピンをTX(送信側)に使いどのピンをRX(受信側)に使うか指定する必要があります。

どのピンを使っているかのメモ。それぞれのTXを相手のRXに繋がるように指定する

Groveケーブルには5Vの電源ラインがあり、M5CameraとM5Stackを繋いだ場合M5Cameraはここから電源を取ってるような気がします。ただ、M5CameraとM5Stackがそれぞれ別の電源を取っているときは繋がない方がいいんでしょうね。デバッグの時は片方しかMacに繋がないようにしました。

UART通信にはSerialクラスを使いますが、「Serial」は通常書き込みやデバッグに使うので、「Serial1」と「Serial2」を使っています。多分M5CameraもM5Stackも1、2どちらでもいいはず。

ただのテキスト送信ならこうやって送信して

Serial1.println("CAPTURE:");

こうやって一行ずつ受信すれば簡単です。

if (Serial1.available() > 0)
  {
    String line = Serial1.readStringUntil('\n');

今回はJPEGバイナリを送りますが、撮像サイズの伝達とか撮像の開始とかそういうコマンドを送るのにテキストを使っています。流れとしてはこんな感じ

M5StackとM5Paperはそれぞれの画面サイズに合った撮像サイズを事前に伝達しています

上記のJPEGバイナリが出来たらまずテキストでサイズを伝えて、それからバイナリを送信しています。長いと一気にwrite出来ないかもしれないと思ってちゃんと送信バイト数によって繰り返し送るようにしています。

    // JPEG_SIZE: command used to send frame buffer size before trasfer JPEG image
    Serial2.printf("JPEG_SIZE:%d\n", fb->len);

    // JPEG_START: command used to notify start transfer binary data after \n 
    Serial2.println("JPEG_START:builtin");
    byte *buffer = fb->buf;
    size_t bytesToSend = fb->len;
    while (bytesToSend > 0)
    {
      size_t index = fb->len - bytesToSend;
      size_t sentBytes = Serial2.write((const char *)(buffer + index), bytesToSend);
      bytesToSend -= sentBytes;
    }

受信側はサイズを受け取ったらメモリを確保して、受信したバイトを書き込んでいきます。最初は受信したバイトをSDカードに書き込んでいたのですが、どうもそれだと速度が間に合わず取りこぼすようです。取りこぼしていた頃の名残でタイムアウトしたら受信をやめるようにしています。

if (line.startsWith("JPEG_SIZE:"))
    { // JPEG_SIZE: response contains byte length of captured JPEG image
      // Store byte length in jpegSize

      String sizeString = line.substring(strlen("JPEG_SIZE:"));
      jpegSize = sizeString.toInt();
      Serial.printf("jpeg size:%d\n", jpegSize);
    }

    else if (line.startsWith("JPEG_START:"))
    { // JPEG_START: response is marker before binary transfer
      // Start receiving binary

      // allocate buffer with JPEG_SIZE: byte length
      size_t receivedSize = 0;
      byte *buffer = (byte *)malloc(jpegSize * sizeof(byte));
      if (buffer == NULL)
      {
        Serial.println("malloc failed");
        return;
      }
      
      int timeoutCount = 0;
      while (receivedSize < jpegSize)
      {
        if (Serial1.available() > 0)
        { // Receive available bytes from Serial1
          size_t readLength = Serial1.readBytes(buffer + receivedSize, jpegSize - receivedSize);
            receivedSize += readLength;
            if (receivedSize == jpegSize)
              break;
          timeoutCount = 0;
        }

        else
        { // If no bytes available, increment timeout counter
          timeoutCount++;
          if (timeoutCount > 100)
          {
            Serial.println("Timeout");
            M5.Lcd.println("Timeout");
            free(buffer);
            return;
          }
          delay(1);
        }
      }
      Serial.printf("jpeg received:%d / %d\n", receivedSize, jpegSize);

長いな、誰も読まねえぞここ

古代のデジカメ、完成!

できました。

microSDカードがあるとそちらにも保存します

M5Stackの画面に合わせて320x240。なんてかわいいサイズなんだ

M5Paperに繋ぐと電子ペーパー白黒カメラという謎の存在に変わります

白黒でしか撮れなかった昭和のデジカメという嘘に使えそう
M5Paper用には800x600にしました。表示が白黒なだけで画像ファイル自体はカラー

M5Paperは(弱いけど)マグネットを内蔵しているので、M5Cameraの裏にスチールプレートを貼り付けて磁力で合体するようにしてみました。ツルツル滑って回転するのであまり意味がなかった

microSDカードの相性

あまり関係ないんですけど…M5Stack用のmicroSDカードって相性がありません?16GB以下とかアロケーションブロックサイズがどうとか聞くのでそこは合わせているんですが、100円ショップで買ってきたDigital Suppy Factoryとかいうところの16GBカードは何やっても認識したりしなかったりでした。

キオクシアの16GBはそのまま使えて不具合なしなのでこれを使っています。


この記事が気に入ったらサポートをしてみませんか?