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(受信側)に使うか指定する必要があります。
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バイナリを送りますが、撮像サイズの伝達とか撮像の開始とかそういうコマンドを送るのにテキストを使っています。流れとしてはこんな感じ
上記の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カードがあるとそちらにも保存します
M5Paperに繋ぐと電子ペーパー白黒カメラという謎の存在に変わります
M5Paperは(弱いけど)マグネットを内蔵しているので、M5Cameraの裏にスチールプレートを貼り付けて磁力で合体するようにしてみました。ツルツル滑って回転するのであまり意味がなかった
microSDカードの相性
あまり関係ないんですけど…M5Stack用のmicroSDカードって相性がありません?16GB以下とかアロケーションブロックサイズがどうとか聞くのでそこは合わせているんですが、100円ショップで買ってきたDigital Suppy Factoryとかいうところの16GBカードは何やっても認識したりしなかったりでした。
キオクシアの16GBはそのまま使えて不具合なしなのでこれを使っています。