QUICK start: M5Stack CamS3 Unit
みんな、こんにちは!この記事は、ほぼ毎週のようにリリースされるM5Stackさんの新商品の中から気になった商品を実際に使ってみて、使用感や簡単な使い方をまとめているよ。今日紹介するのはWi-FiカメラユニットのCamS3 Unit(以下CamS3)だよ。
CamS3の超ざっくりした紹介は以下の動画を見てみてね。
CamS3はスイッチサイエンスさんから購入可能だよ。
対象読者
- M5Stack大好き
- Wi-Fiカメラに興味がある
- 自分でWi-Fiカメラのプログラムを作成したい
やること
この記事ではCamS3の概要、デモの動作、プログラムの作成方法についてまとめているよ。プログラムの作成では、以下の動作ができるプログラムを作成しているよ。
- カメラの画像と映像をHTTP経由で見る
- カメラの画像をmicroSDカードに保存
- マイクの音声をmicroSDカードに保存
環境
- PlatformIO
- ESP Async WebServer @ 1.2.3
- esp32cam @ 0.0.20240110
- CamS3×1台
- microSDカード×1枚
microSDカードはESP32との相性があると思うから、事前にM5Stack core2などで動作確認済みのmicroSDカードがあるといいね。ボクが使ったのはSanDisk Ultra 32GB SDHC U1だよ。
CamS3を見てみよう
ここからは、CamS3を詳しくみていくよ。CamS3の開封からデモの使い方はYouTube Shortsで1分以内で分かるようにまとめているから、こちらもチェックしてみてね。
概要
CamS3の概要について説明するよ。CamS3はM5Stackさんが製造しているWi-Fiカメラユニットでスイッチサイエンスさんで販売されているよ。カメラの最大解像度は1600×1200ピクセルで、ESP32-S3を搭載しているから、電源さえ供給できればカメラ単体で動作が可能だよ。カメラセンサーはOmniVision TechnologiesさんのOV2640というモジュールが使用されていて、2005年からあるモジュールだからライブラリも整備されていて、簡単に使用できるようになっているよ。ESP32-S3はWi-Fiにも接続できるから、CamS3をWi-Fiに接続すると、遠隔地の動画撮影にも使うことができるよ。
特徴
- OV2640 (最大解像度1600×1200ピクセル)
- ESP32-S3搭載
- PDMマイク搭載
- マイクロSDカードスロット搭載
- ボディあり
M5Stackさんのカメラユニットは今までにもたくさん発売されているんだけど、マイクとマイクロSDカードスロットの両方が搭載された製品は初めてじゃないかな。あと、ESP32-S3が搭載されたカメラユニットも初めてだね。ボク個人的な感想だけど、Wi-Fiカメラに必要な機能がひと通り揃ってボディにも収まっているし、結構バランスの良い製品なんじゃないかな。
詳細な仕様はM5Stackさんのページを参照してね。
外観
CamS3の外観を見ていくよ。
正面
CamS3正面
CamS3を正面から見た写真だよ。上部にはカメラのレンズがあって、下部にはGrove端子があるよ。Grove端子の割り当てはGND,5V,USB D+,USB D-になっていて、付属のGrove2USB-Cアダプタを使うことで、CamS3への電源供給とファームウェアの書き込みを行うことができるよ。
背面
CamS3背面
CamS3の背面にはLEGOアダプタが最初から取り付けられているよ。下部にマイクロSDカードスロット、左右にそれぞれ3ピンのソケットがあって、ファームウェア書き込み用にライターを接続したり、GPIOとして使用したりすることができるよ。
microSDカード装着状態
microSDカードを刺した状態はこんな感じ。
microSDカードはスロットが短いから、microSDカードが半分以上外に出ているよ。
付属品
付属品
ジャンパワイヤ、Grove2USB-Cアダプタ、Groveケーブルが付属しているよ。ジャンパワイヤは、ファームウェアを書き込むときに背面のG0とGNDをショートさせてESP32-S3をdownload modeにするために使用するよ。
何ができるの?
CamS3を使うと何ができるのか、応用例を書いておくよ。M5Stackさんの公式サイトに記載された応用例は以下の通りだよ。遠隔で撮影してクラウドにアップしたり、一定間隔で写真を撮影するタイムラプス撮影をしたり、工場の自動化のために使用したりということが書かれているね。
- Remote Control
- Time-LapsePhotography
- Industrial Automation
あとはセキュリティカメラ的に使ったり、マイクもついているから音声をトリガーにして撮影したりとか、色々応用ができそうだね!
CamS3を動かしてみよう
ここでは、CamS3に最初から書き込まれているユーザデモの使い方と、esp32camというライブラリを使用したカメラの使用方法について説明するよ。
ユーザデモ
CamS3には最初からデモ用のファームウェアが書き込まれているから、まずはこれを使用してCamS3のハードウェアに初期不良が無いかどうかを確認するよ。
- CamS3のGrove端子にGroveケーブル>Grove2USB-Cアダプタ>USBケーブルを接続
- スマホかPCでWi-Fiのアクセスポイント画面を表示
- UnitCamS3-WiFiに接続
- ブラウザでhttp://192.168.4.1/を開く
ブラウザでUser Demoの画面が表示されたら、CameraのところでCaptureやStreamを押してみてね。カメラで撮影した画像が表示されるはずだよ。
ユーザデモの画面
esp32camで画像を取得
ここでは、esp32camというライブラリを使用してカメラモジュールから画像を取得するためのプログラムを作成する方法について説明しているよ。ここでは以下のようなプログラムを作成するよ。
画像と映像の取得
- ESP32-S3のWi-FiをAPモードで設定
- 非同期HTTPサーバーでテストページを表示
- カメラから画像を取得しJPG形式で表示
- カメラから逐次画像を取得しMotionJPEGとしてストリーム
ライブラリはこちら。
PlatformIOの設定
esp32camを使用してCamS3用のファームウェアを作成するときのplatformio.iniの例を以下に記載しておくよ。デバイスはユーザデモのサンプルコードを参考にesp32s3boxを選んだよ。非同期のHTTPサーバーを通じて画像を配信するためにAsyncWebServerというライブラリも使用しているよ。
設定の詳細は、以下のplatformio.iniの例に直接コメントを追加しているよ。
platformio.ini
; PlatformIOで新規プロジェクトを作成した時に自動的に作成されるよ。
; プロジェクト作成時のデバイスはesp32s3boxを選択したよ。
[env:esp32s3box]
platform = espressif32
board = esp32s3box
framework = arduino
; ESP32-S3でUSB経由でシリアルのログを見るにはUSB CDCモードにする必要があるよ。
; ARDUINO_USB_MODEとARDUINO_USB_CDC_ON_BOOTを指定することでCDCモードをOnにできるよ。
build_flags =
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
; 以下の2行はフラッシュのパーティションを設定しているよ。
; 1行目はWebサーバーでrootにアクセスしたときに表示されるページをフラッシュに保持するためのLittleFS領域を設定しているよ。
; 2行目はパーティションのファイルシステムとしてLittleFSを指定しているよ。
board_build.partitions = custom.csv
board_build.filesystem = littlefs
; LittleFSのイメージをフラッシュに書き込むときに必要な設定だよ。
; これを指定しないと書き込みに失敗するよ
upload_protocol = esptool
; ライブラリの設定だよ。ESP Async WebServerとesp32camを使用するよ。
lib_deps =
me-no-dev/ESP Async WebServer @ ^1.2.3
yoursunny/esp32cam @ ^0.0.20240110
monitor_speed = 115200
custom.csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
phy_init, data, phy, ,0x1000,
app0, app, ota_0, 0x10000, 2M,
spiffs, data, spiffs, , 2M
coredump, data, coredump,0xFF0000,0x10000,
画像と映像の取得
上でも説明したけど、ここではカメラから画像と映像を取得してESP32-S3上で構築した簡易Webサーバーを通してHTTPで配信するプログラムを作成するよ。ここで作成するプログラムの機能は以下の通りだよ。
- ESP32-S3のWi-FiをAPモードで設定
- 非同期HTTPサーバーでテストページを表示
- カメラから画像を取得しJPG形式で表示
- カメラから逐次画像を取得しMotionJPEGとしてストリーム
サンプルコードを以下に記載しておくよ。コードの説明は以下のコードに直接コメントとして書き込んできるから参考にしてね。
main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include "ESPAsyncWebServer.h"
#include <LittleFS.h>
#include <esp32cam.h>
#include <esp32cam-asyncweb.h>
// Wi-Fi関連の設定をしているよ。
// APモードでAPとなったときのアクスポイント名とパスワードを定義しているよ。
#define WIFI_SSID "UnitCamS3"
#define WIFI_PASSWORD ""
// Cameraの設定をしているよ。
// CamS3の回路図を元にCAMS3のピン割り当てを定義しているよ。
esp32cam::Resolution initialResolution;
constexpr esp32cam::Pins UnitCamS3{
D0: 6,
D1: 15,
D2: 16,
D3: 7,
D4: 5,
D5: 10,
D6: 4,
D7: 13,
XCLK: 11,
PCLK: 12,
VSYNC: 42,
HREF: 18,
SDA: 17,
SCL: 41,
RESET: 21,
PWDN: -1,
};
static void serveStill(AsyncWebServerRequest *request);
// Webサーバーの設定をしているよ。
// serverがWebサーバーのインスタンス、
// ipはサーバーのIPアドレス、subnetがサーバーのサブネットマスクのインスタンスだよ。
AsyncWebServer server(80);
IPAddress ip(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
void handleRoot(AsyncWebServerRequest *request);
void setup() {
// Serial setup
Serial.begin(115200);
// LittleFSを初期化
// LittleFSにはWebサーバーにアクセスした時に表示されるページindex.htmlを保存していて、それを読み出すためにLittleFSを初期化しているよ。
if(!LittleFS.begin()){
Serial.println("An Error has occurred while mounting LittleFS");
ESP.restart();
}
// Wi-Fiの設定
// ESP32-S3のWi-FiをAPモードに設定しているよ。
WiFi.mode(WIFI_AP);
// WI-Fi APのSSIDを設定しているよ。パスワードが必要な場合は2つ目の引数にWIFI_PASSWORDを指定するよ。
WiFi.softAP(WIFI_SSID);
delay(100);
// Wi-Fi APのIPアドレスとサブネットマスクを設定しているよ。
WiFi.softAPConfig(ip, ip, subnet);
// Cameraの設定
{
using namespace esp32cam;
// カメラから取得する画像の解像度を指定
initialResolution = Resolution::find(1600, 1200);
Config cfg;
cfg.setPins(UnitCamS3); // カメラモジュールのピン割り当てを設定
cfg.setResolution(initialResolution); // カメラから取得する画像の解像度を設定
cfg.setJpeg(80); // JPG画像の品質を設定
bool ok = Camera.begin(cfg);
if (!ok) {
Serial.println("camera initialize failure");
delay(5000);
ESP.restart();
}
Serial.println("camera initialize success");
}
// Serverの設定
// server.onは第1引数で与えられたアドレスにアクセスすると、第3引数で指定された関数が呼び出されるよ。
// ここでは、トップページにアクセスした時にhandleRoot、/stillにアクセスした時にserverStill、/mjpgにアクセスした時にMotionJPEGをストリーミングするようにしているよ。
server.on("/", HTTP_GET, handleRoot);
server.on("/still", HTTP_GET, serveStill);
server.on("/mjpg", esp32cam::asyncweb::handleMjpeg);
server.begin();
}
void loop() {
delay(1);
}
// トップページにアクセスした時の処理
// ここではWebサーバーでトップページにアクセスされた時にindex.htmlをフラッシュから読み込んで、レスポンスとして返す処理をしているよ。
void handleRoot(AsyncWebServerRequest *request){
File file = LittleFS.open("/index.html", "r");
AsyncWebServerResponse *response = request->beginResponse(file, "text/html");
response->setContentType("text/html"); // これはいらないかも。
request->send(response);
}
// JPG画像を返す
// ここではWebサーバーで/stillにアクセスされた時にカメラから画像を取得して、JPEG画像をレスポンスとして返す処理をしているよ。
static void serveStill(AsyncWebServerRequest *request) {
auto frame = esp32cam::capture(); // カメラから画像を取得
// frameにカメラから取得した画像データが格納されているから、画像に対して何か処理したいときは、このframeを使用してね。このサンプルでは、そのままHTTPのレスポンスとして返しているよ。
if (frame == nullptr) {
Serial.println("capture() failure");
request->send(500, "text/plain", "still capture error\n");
return;
}
Serial.printf("capture() success: %dx%d %zub\n", frame->getWidth(), frame->getHeight(), frame->size());
// JPEGのバッファをレスポンスとして返すにはAsyncWebServerRequestのbeginResponse_Pを使用するよ。
AsyncWebServerResponse *response = request->beginResponse_P(200, "image/jpeg", frame->data(), frame->size());
request->send(response);
}
index.html
index.htmlはsrcディレクトリと同じ階層にdataディレクトリを作成し、そこに入れておくよ。
<!DOCTYPE html>
<html>
<head>
<title>UnitCamS3</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>UnitCamS3</h1>
<div>testtest</div>
</body>
</html>
動作確認
サンプルコードをビルドしてESP32-S3に書き込んでみてね。
dataディレクトリの中身をLittleFS領域に書き込むには、PlatformIOのメニューから、PROJECT TASKS > Platform > Upload Filesystem Imageと選択するよ。
LittleFSへの書き込み
ファームウェアが書き込めたら、スマホやPCからCamS3にWi-Fiで接続するよ。アクセスポイント一覧からUnitCamS3を選択して接続してね。ブラウザでhttp://192.168.4.1/にアクセスするとテストページが表示されるよ。
/stillや/mjpgにもアクセスして、カメラの画像が動画が表示されることを確認してね。
microSDカードに画像を保存する
カメラから画像を取得して表示するサンプルコードはうまく動作したかな?ここからは、先ほどのサンプルコードに少し手を加えて、撮影した画像をマイクロSDカードに保存する処理を追加するよ。ここで作成する機能は以下の通りだよ。
- カメラから取得した画像をJPG形式でmicroSDカードに保存
サンプルコードを以下に記載しておくよ。以下のサンプルコードは画像と映像の取得で使用したコードの差分のみになっているよ。コードの詳細は先ほどと同様にコメントとして直接書き込んでいるよ。
main.cpp(変更点のみ)
..
#include <SPI.h>
#include <SD.h>
..
// SD cardの設定
// CamS3のラベルを参考にSPIのピン割り当てを定義しているよ。
enum {
sd_ss = 9,
sd_mosi = 38,
sd_miso = 40,
sd_sck = 39
};
..
// SDカードに撮影画像を保存するための関数を追加したので、宣言を追加しているよ。
void saveCapturedImageToSD(AsyncWebServerRequest *request);
void setup() {
..
//SD setup
//SPIの初期値ではSDカードの初期化に失敗したから、SPI.beginを呼び出して改めて正しいピン割り当てを指定して初期化しているよ。
SPI.end();
SPI.begin(sd_sck, sd_miso, sd_mosi, sd_ss);
if(!SD.begin(sd_ss,SPI,24000000)){
Serial.println("An Error has occurred while mounting SD card");
ESP.restart();
}
..
//savesdというページにアクセスするとカメラで撮影した画像をSDカードに書き込むよ。
server.on("/savesd", HTTP_GET, saveCapturedImageToSD);
}
..
// カメラで撮影した画像をSDカードに書き込むための処理だよ。
void saveCapturedImageToSD(AsyncWebServerRequest *request){
// カメラから画像を取得
auto frame = esp32cam::capture();
// SDカードでcaptured.jpgというファイルを書き込みモードで開くよ。
File file = SD.open("/captured.jpg", FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
request->send(500, "text/plain", "Failed to open file for writing\n");
return;
}
// SDカードに画像データを書き込むよ。
if(file.write(frame->data(), frame->size())){
Serial.println("Image saved to SD card");
request->send(200, "text/plain", "Image saved to SD card\n");
} else {
Serial.println("Failed to write image to file");
request->send(500, "text/plain", "Failed to write image to file\n");
}
// 最後にファイルをクローズするよ。
file.close();
}
動作確認
サンプルコードをビルドしてESP32-S3に書き込んでみてね。
画像取得のサンプルコードと同様にスマホかPCからCamS3にアクセスして、/savesdというページを開いてみてね。"Image saved to SD card"と表示されていたら成功だよ。SDカードをPCで読み込んで、captured.jpgを開いてみてね。
音声の取得
最後にCamS3に搭載されたPDMマイクから音声を録音して、録音した音声をmicroSDにWAV形式で保存するプログラムを作成するよ。ここで作成する機能は以下の通りだよ。
- PDMマイクから入力された3秒の音声をWAV(RIFF)形式でマイクロSDカードに保存
サンプルコードを以下に記載しておくよ。こちらも今までと同様に変更点のみを記載していて、コードの詳細はコメントとして直接書き込んでいるよ。
main.cpp(変更点のみ)
#include <driver/i2s.h>
..
// PDM mic
// マイクから入力した音声を3秒分保持するためのバッファだよ。
#define MIC_BUFFER_SIZE 96000
uint8_t mic_buffer[MIC_BUFFER_SIZE];
// WAVファイルのヘッダーを定義しているよ。音声データのサイズは96000に固定しているから、RIFF chunk sizeは計算済みの値を入れているよ。
#define WAV_HEADER_SIZE 44
uint8_t wav_header_template[WAV_HEADER_SIZE] = {
0x52, 0x49, 0x46, 0x46, // 'RIFF'
0x24, 0x77, 0x01, 0x00, // RIFF chunk size
0x57, 0x41, 0x56, 0x45, // 'WAVE'
0x66, 0x6D, 0x74, 0x20, // 'fmt'
0x10, 0x00, 0x00, 0x00, // fmt chunk size
0x01, 0x00, // Format
0x01, 0x00, // Number of channels
0x80, 0x3E, 0x00, 0x00, // Sampling rate
0x00, 0x7D, 0x00, 0x00, // bytes/sec
0x02, 0x00, // Block align
0x10, 0x00, // bits/sample
0x64, 0x61, 0x74, 0x61, // 'data'
0x00, 0x77, 0x01, 0x00 // Data size
};
void saveCapturedAudioToSD(AsyncWebServerRequest *request);
void setup() {
..
// PDM setup
// PDMマイクを使用するためのI2Sの設定だよ。ESP32はI2SでPDMも入力できるようになっているよ。
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 8,
.dma_buf_len = 512
};
// PDMマイクのピン割り当てを設定しているよ。
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_PIN_NO_CHANGE,
.ws_io_num = 47,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = 48
};
// I2Sを初期化しているよ。
if(i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK){
Serial.println("Error installing I2S driver");
ESP.restart();
}
// I2Sのピン割り当てを設定しているよ。
if(i2s_set_pin(I2S_NUM_0, &pin_config) != ESP_OK){
Serial.println("Error setting I2S pins");
ESP.restart();
}
..
// HTTPサーバーで/audiorecにアクセスした時、saveCapturedAudioToSDを呼び出すよ。
server.on("/audiorec", HTTP_GET, saveCapturedAudioToSD);
..
}
void loop() {
// PDMマイクから常に3秒分の音声を取得してバッファに格納しているよ。
uint32_t read_size = 0;
i2s_read(I2S_NUM_0, mic_buffer, MIC_BUFFER_SIZE, &read_size, portMAX_DELAY);
Serial.printf("MIC read_size: %d\n", read_size);
delay(1);
}
// HTTPサーバーで/audiorecにアクセスすると呼び出されるよ。
// ここでは、microSDカード上のcaptured.wavというファイルにPDMマイクから入力された音声を保存しているよ。
void saveCapturedAudioToSD(AsyncWebServerRequest *request){
// Open file for writing
File file = SD.open("/captured.wav", FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
request->send(500, "text/plain", "Failed to open file for writing\n");
return;
}
// Write WAVE header to file
// WAVファイルのヘッダーをファイルに書き込んでいるよ。
// chunk sizeなどは既に計算済みだから、ヘッダーのデータをそのまま書き込んでいるよ。
if(!file.write(wav_header_template, WAV_HEADER_SIZE)){
Serial.println("Failed to write WAVE header to file");
request->send(500, "text/plain", "Failed to write WAVE header to file\n");
return;
}
// Write audio data to file
// loop関数でバッファに格納された音声データをファイルに書き込んでいるよ。
if(file.write(mic_buffer, MIC_BUFFER_SIZE)){
Serial.println("Audio saved to SD card");
request->send(200, "text/plain", "Audio saved to SD card\n");
} else {
Serial.println("Failed to write audio to file");
request->send(500, "text/plain", "Failed to write audio to file\n");
}
file.close();
}
動作確認
上記のサンプルコードをビルドしてESP32-S3に書き込んでみてね。今までと同様にUnitCamS3にWi-Fiでアクセスするよ。3秒分の音声を常に録音しているから、喋ったり、音楽を流し続けた状態でスマホやPCから/audiorecを開いてみてね。"Audio saved to SD card"と表示されていれば、PDMマイクから録音された音声がmicroSDカードにcaptured.wavというファイル名で保存されているから、PCで再生してみてね。
上記のサンプルコードだと録音された音声の音量がちょっと小さいけど、何が悪いのかまだ特定できていないから、分かったら内容を更新するね。もし、何が悪いのか分かったらXやコメントで教えてもらえると嬉しいな。
まとめ
CamS3の概要とユーザデモの使い方、CamS3に搭載されたカメラ、マイク、Wi-Fiの機能を使用するためのプログラムの作成方法についてまとめたよ。CamS3はプログラムを書き込むことで、単体で動作できるから、遠隔地の動画撮影とかマイクを使って音声も録音したり、撮影のトリガーにしたりとか色々応用ができそうだね。このサンプルをベースにCamS3を色々と活用してみてね。もしよかったら、X(@nananauno)のフォローもよろしくね!
じゃあ、またね!
Discussion