ATOMS3R M12カメラキット(OV3660)を使ってみた

USB接続

オフィシャルサイトには、「工場出荷時のファームウェアには、UVC 機能と Wi-Fi 送信機能の両方が含まれています」との記載。
USB接続の画像が下の画像(右)。比較のための左の画像は、DELL Inspiron 15 3535のカメラ(0.92 メガピクセル)で撮影したもの。

Wi-Fi 接続

Wi-Fi 送信機能で接続してみる。PCのSSID「AtomS3R-M12-WiFi」に切り替え、192.168.4.1に接続した画面が下の画像。「Screen」ボタンを押すと、カメラ画像が表示される。なぜかWindows11からは接続できず、Ubuntuからは接続できた。どうしてWindows11から接続できないかは分かっていない。

上記は「工場出荷時の状態」を使った内容である。次のステップは自宅のルーターに接続することである。

参照サイト

ステーションモードでWi-Fiアクセスポイントに接続する

「工場出荷時の状態」では、ルーターに接続していないので実用的でない。この製品の唯一のオフィシャル例題を使ってルーターに接続する。

環境設定

例題プログラムを利用するためのArduino IDE環境設定は、オフィシャルページの通りに実施すればよい。ここではGOODMINI2のArduino IDEバージョン2.3.4を使用した。

Arduinoボード管理
「ファイル」→「基本設定」→「追加のボードマネージャのURL」に下記を入力する。
https://static-cdn.m5stack.com/resource/arduino/package_m5stack_index.json

サイドバーで「Board Manager」を選択し、検索欄に「M5Stack」を入力する。
表示された「M5Stack by M5Stack」のインストール・ボタンをクリックする。

メニューバーから以下の順に選択し開発ボードを設定する。
「ツール」→「ボード」→「M5Stack」→「M5AtomS3」

Arduino ライブラリ管理
サイドバーから「Library Manager」を選択する。
検索フィルターに「M5AtomS3」を入力する。
表示された「M5AtomS3 by M5Stack」のインストール・ボタンをクリックする。
依存関係として他のライブラリをインストールするように求められた場合は、Install Allボタンをクリックする。

AtomS3 プログラムのコンパイルとアップロードはサイトの説明の通り。
本体横のボタンを押してダウンロードモードするところがポイント

カメラの例題プログラムに修正を加える。

例題プログラムに2点の修正を加える。具体的な修正箇所はソースコードを参照。




.frame_size   = FRAMESIZE_VGA,    // 640×480
AGC, 露出, コントラス, トシャープネスの設定 —– 130行辺り

動作確認
ブラウザで192.168.0.105にアクセスする。

Arduino IDEのコード — ATOMS3RM12_Basic_mod.ino

/*
 * プログラム名: ATOMS3RM12_Basic_mod.ino
 * 更新日付: 2025-10-06
 * 処理概要:
 * M5StackのATOMS3R M12用基本コードに、OTA(Over-The-Air)アップデート機能と、
 * カメラの画質パラメータ(ゲイン、露出、ホワイトバランス等)を規定値に設定する機能を追加したもの。
 * 元のサンプルプログラムの出所: M5Stack
 */

/**
 * @Hardwares: AtomS3R-CAM / AtomS3R-M12
 * @Platform Version: Arduino M5Stack Board Manager v2.1.4
 */

// -------------------------------------------------------------------------
// ライブラリのインクルード
// -------------------------------------------------------------------------
#include "camera_pins.h"  // カメラモジュールのピン定義を読み込むヘッダーファイル
#include <WiFi.h>         // Wi-Fi機能を使用するためのライブラリ
#include "esp_camera.h"   // ESP32用カメラドライバライブラリ
#include <ArduinoOTA.h>   // ▼▼▼ 2025-10-06 OTA対応のため追加 ▼▼▼

// -------------------------------------------------------------------------
// コンパイル時の設定 (使用するハードウェアやモードを選択)
// -------------------------------------------------------------------------
// #define USE_ATOMS3R_CAM // 標準レンズ版を使う場合はこちらを有効化
#define USE_ATOMS3R_M12   // M12レンズ版を使う場合はこちらを有効化

#define STA_MODE          // Wi-Fi子機モード(ルーターに接続)
// #define AP_MODE        // Wi-Fi親機モード(本機がアクセスポイントになる)

// -------------------------------------------------------------------------
// グローバル変数・定数の定義
// -------------------------------------------------------------------------
// --- Wi-Fi設定 ---
const char* ssid     = " ";   // 接続するWi-FiのSSID
const char* password = " ";    // Wi-Fiのパスワード

// --- 固定IPアドレス設定 ---
IPAddress local_IP(192, 168, 0, 105);      // 本機に割り当てるIPアドレス (*** 2025-10-06 修正 ***)
IPAddress gateway(192, 168, 0, 1);       // ルーターのIPアドレス (*** 2025-10-06 修正 ***)
IPAddress subnet(255, 255, 255, 0);      // サブネットマスク (*** 2025-10-06 修正 ***)
IPAddress primaryDNS(192, 168, 0, 1);      // DNSサーバーのIPアドレス (*** 2025-10-06 修正 ***)

// --- Webサーバー/カメラ関連 ---
WiFiServer server(80);      // Webサーバーをポート80 (HTTP)で起動
camera_fb_t* fb    = NULL;  // カメラのフレームバッファ(撮影した画像データ)を指すポインタ
uint8_t* out_jpg   = NULL;  // JPEGに変換後のデータへのポインタ
size_t out_jpg_len = 0;     // JPEGデータのサイズ

// --- プロトタイプ宣言 ---
static void jpegStream(WiFiClient* client); // JPEGストリーミング配信を行う関数

// -------------------------------------------------------------------------
// カメラの初期設定
// -------------------------------------------------------------------------
static camera_config_t camera_config = {
    // --- カメラモジュールのピン接続設定 ---
    .pin_pwdn     = PWDN_GPIO_NUM,
    .pin_reset    = RESET_GPIO_NUM,
    .pin_xclk     = XCLK_GPIO_NUM,
    .pin_sscb_sda = SIOD_GPIO_NUM,
    .pin_sscb_scl = SIOC_GPIO_NUM,
    .pin_d7       = Y9_GPIO_NUM,
    .pin_d6       = Y8_GPIO_NUM,
    .pin_d5       = Y7_GPIO_NUM,
    .pin_d4       = Y6_GPIO_NUM,
    .pin_d3       = Y5_GPIO_NUM,
    .pin_d2       = Y4_GPIO_NUM,
    .pin_d1       = Y3_GPIO_NUM,
    .pin_d0       = Y2_GPIO_NUM,
    .pin_vsync    = VSYNC_GPIO_NUM,
    .pin_href     = HREF_GPIO_NUM,
    .pin_pclk     = PCLK_GPIO_NUM,

    // --- カメラの動作設定 ---
    .xclk_freq_hz = 20000000,    // カメラへのマスタークロック周波数 (20MHz)
    .ledc_timer   = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,

// --- カメラごとの画像フォーマットと解像度設定 ---
#ifdef USE_ATOMS3R_CAM
    // 標準レンズ版の場合
    .pixel_format = PIXFORMAT_RGB565, // 非圧縮の生データ形式で取得
    .frame_size   = FRAMESIZE_QVGA,   // 320x240
#endif

#ifdef USE_ATOMS3R_M12
    // M12レンズ版の場合
    .pixel_format = PIXFORMAT_JPEG,   // カメラ側でJPEG圧縮済みの形式で取得
    .frame_size   = FRAMESIZE_VGA,    // 640x480 (UXGAから変更)
#endif

    // --- 共通の画質・メモリ設定 ---
    .jpeg_quality  = 10,  // JPEG圧縮品質 (0:最高画質, 63:最低画質)
    .fb_count      = 2,   // フレームバッファの数 (2にするとダブルバッファリングが有効になりスムーズになる)
    .fb_location   = CAMERA_FB_IN_PSRAM, // フレームバッファをPSRAMに確保 (必須)
    .grab_mode     = CAMERA_GRAB_LATEST, // 常に最新のフレームを取得するモード
    .sccb_i2c_port = 0,
};


// =========================================================================
// setup() - 初期化処理
// =========================================================================
void setup()
{
    // シリアル通信の開始 (デバッグメッセージ用)
    Serial.begin(1152200);

    // カメラモジュールの電源をONにする
    pinMode(POWER_GPIO_NUM, OUTPUT);
    digitalWrite(POWER_GPIO_NUM, LOW);
    delay(500);

    // カメラを初期化
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK) {
        Serial.println("Camera Init Fail"); // 失敗したら再起動
        delay(1000);
        esp_restart();
    } else {
        Serial.println("Camera Init Success");
    }

    // ▼▼▼ 2025-10-06 カメラの画質パラメータを設定(規定値)▼▼▼
    // センサーのインスタンスを取得
    sensor_t * s = esp_camera_sensor_get();
    if (s != NULL) {
        s->set_gain_ctrl(s, 1);      // 自動ゲインコントロールを有効化 (ON)
        s->set_exposure_ctrl(s, 1);  // 自動露出コントロールを有効化 (ON)
        s->set_whitebal(s, 1);       // 自動ホワイトバランスを有効化 (ON)
        s->set_saturation(s, 0);     // 彩度を中間値 (0) に設定
        s->set_contrast(s, 0);       // コントラストを中間値 (0) に設定
        s->set_sharpness(s, 0);      // シャープネスを中間値 (0) に設定
        Serial.println("Camera sensor settings set to defaults (Auto).");
    }
    // ▲▲▲ 2025-10-06 ここまで ▲▲▲

    delay(100);

// --- Wi-Fi接続処理 ---
#ifdef STA_MODE
    // Wi-Fiをステーション(子機)モードに設定
    WiFi.mode(WIFI_STA);

    // *** 2025-10-06 for fixed IP Address ***
    // 固定IPアドレスを設定
    if (!WiFi.config(local_IP, gateway, subnet, primaryDNS)) {
      Serial.println("STA Failed to configure");
    }

    // Wi-Fiアクセスポイントへ接続開始
    WiFi.begin(ssid, password);
    WiFi.setSleep(false); // スリープモードを無効化
    Serial.println("");

    Serial.print("Connecting to ");
    Serial.println(ssid);

    // 接続が完了するまで待機
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("");
    Serial.print("Connected to ");
    Serial.println(ssid);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
#endif

// --- アクセスポイントモードの処理 ---
#ifdef AP_MODE
    if (!WiFi.softAP(ssid, password)) {
        log_e("Soft AP creation failed.");
        while (1);
    }
    // ... (APモード時の情報表示)
#endif

    // ▼▼▼ 2025-10-06 OTA対応のため追加 ▼▼▼
    // OTAアップデートサービスの初期化と開始
    ArduinoOTA.begin();
    Serial.println("OTA Ready");
    // ▲▲▲ 2025-10-06 ここまで ▲▲▲

    // Webサーバーを開始
    server.begin();
}

// =========================================================================
// loop() - メインループ
// =========================================================================
void loop()
{
    // ▼▼▼ 2025-10-06 OTA対応のため追加 ▼▼▼
    // OTAのアップデート要求がないか常にチェックする
    ArduinoOTA.handle();
    // ▲▲▲ 2025-10-06 ここまで ▲▲▲

    // Webブラウザなどからの新しいクライアント接続を待つ
    WiFiClient client = server.available();
    if (client) { // クライアントが接続してきたら
        while (client.connected()) { // 接続が維持されている間ループ
            if (client.available()) { // クライアントからデータが送られてきたら
                // 映像ストリーミング処理を開始
                jpegStream(&client);
            }
        }
        // 接続が切れたらクライアントを停止
        client.stop();
        Serial.println("Client Disconnected.");
    }
}

// =========================================================================
// jpegStream() - JPEG映像をストリーミング配信する関数
// =========================================================================

// --- MJPEGストリーム用のHTTPヘッダー定義 ---
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY     = "--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART         = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

static void jpegStream(WiFiClient* client)
{
    Serial.println("Image stream start");
    // --- ブラウザにHTTPヘッダーを送信 ---
    client->println("HTTP/1.1 200 OK");
    client->printf("Content-Type: %s\r\n", _STREAM_CONTENT_TYPE);
    client->println("Content-Disposition: inline; filename=capture.jpg");
    client->println("Access-Control-Allow-Origin: *");
    client->println();
    static int64_t last_frame = 0;
    if (!last_frame) {
        last_frame = esp_timer_get_time();
    }

    // --- 映像フレームを連続で送信する無限ループ ---
    for (;;) {
        // ▼▼▼ 2025-10-06 OTAポーリングのためjpegStreamを抜ける処理を追加 ▼▼▼
        // クライアントの接続が切れたらループを抜ける
        if (!client->connected()) {
            break;
        }
        // ▲▲▲ 2025-10-06 ここまで ▲▲▲

        // カメラから1フレーム取得
        fb = esp_camera_fb_get();
        if (fb) { // フレーム取得に成功したら
// --- カメラ機種ごとのデータ取得処理 ---
#ifdef USE_ATOMS3R_CAM
            // 標準CAMの場合、RGBデータをJPEGに変換する
            frame2jpg(fb, 255, &out_jpg, &out_jpg_len);
#endif

#ifdef USE_ATOMS3R_M12
            // M12の場合、既にJPEGなのでポインタを直接代入する
            out_jpg     = fb->buf;
            out_jpg_len = fb->len;
#endif

            Serial.printf("pic size: %d\n", out_jpg_len);
            // --- 取得したJPEGデータをクライアントに送信 ---
            client->print(_STREAM_BOUNDARY);
            client->printf(_STREAM_PART, out_jpg_len);
            
            // データを8KBずつのパケットに分割して送信
            int32_t to_sends    = out_jpg_len;
            uint8_t* out_buf    = out_jpg;
            while (to_sends > 0) {
                size_t now_sends = to_sends > (8*1024) ? (8*1024) : to_sends;
                if (client->write(out_buf, now_sends) == 0) {
                    goto client_exit; // 送信に失敗したら抜ける
                }
                out_buf += now_sends;
                to_sends -= now_sends;
            }

            // --- フレームレート計算・表示 ---
            int64_t fr_end     = esp_timer_get_time();
            int64_t frame_time = fr_end - last_frame;
            last_frame         = fr_end;
            frame_time /= 1000;
            Serial.printf("MJPG: %luKB %lums (%.1ffps)\r\n", (long unsigned int)(out_jpg_len / 1024),
                          (long unsigned int)frame_time, 1000.0 / (long unsigned int)frame_time);

            // --- メモリ解放処理 ---
            // フレームバッファを解放して次の撮影に備える (非常に重要)
            if (fb) {
                esp_camera_fb_return(fb);
                fb = NULL;
            }
#ifdef USE_ATOMS3R_CAM
            // 標準CAMの場合、JPEG変換で確保したメモリも解放
            if (out_jpg) {
                free(out_jpg);
                out_jpg     = NULL;
                out_jpg_len = 0;
            }
#endif
        } else {
            Serial.println("Camera capture failed");
        }
    }

client_exit: // 送信失敗時のジャンプ先
    // --- 終了時のメモリ解放処理 ---
    if (fb) {
        esp_camera_fb_return(fb);
        fb = NULL;
    }
#ifdef USE_ATOMS3R_CAM
    if (out_jpg) {
        free(out_jpg);
        out_jpg     = NULL;
        out_jpg_len = 0;
    }
#endif
    client->stop();
    Serial.printf("Image stream end\r\n");
}

ATOMS3RM12_Basic_mod.inoの場所
MINI2 C:\Users\%username%\Documents\Arduino\ATOMS3R_M12

工場出荷の状態に戻す

オフィシャルサイトの項目「イージーローダー」にあるダウンロードをクリックすると「AtomS3R-M12-Demo-V0.1.exe」がダウンロードされる。これを起動し、現れた画面のCOMを指定してから「Burn」をクリックすと初期状態に戻る。