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

ATOMS3R M12カメラキット(OV3660)を使ってみた。
小さい。単四電池と比べてみたらどのくらい小さいかわかる(24×24×23.8m)。

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点の修正を加える。具体的な修正箇所はソースコードを参照。



※画像のサイズは、QVGAである。試しにVGAに設定したら表示できなかった。
.frame_size    = FRAMESIZE_QVGA,

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

Arduino IDEのコード — camera_IP_FIXED

/* camera_IP_FIXED 
 * サンプルプログラムcamera_copyに固定IPのWiFi設定を行ったもの。  2025.03.25
 * SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
 *
 * SPDX-License-Identifier: MIT
 */

/**
 * @file camera.ino
 * @brief M5AtomS3R Cam Web Server
 * @version 1.0
 * @date 2024-09-27
 *
 *
 * @Hardwares: M5AtomS3R Cam
 * @Platform Version: Arduino M5Stack Board Manager v2.1.2
 * @Notes: Remember to turn on PSRAM, otherwise the camera can't be initialized normally.
 */

#include "camera_pins.h"
#include <WiFi.h>
#include "esp_camera.h"

#define STA_MODE
// #define AP_MODE

const char* ssid     = "aterm-2b4139-a";   //  WiFi設定
const char* password = "3e00cfa4ba409";    //  WiFi設定

IPAddress local_IP(192, 168, 0, 105);      // *** 25.3.25 ***
IPAddress gateway(192, 168, 0, 1);         // *** 25.3.25 *** 
IPAddress subnet(255, 255, 255, 0);        // *** 25.3.25 ***
IPAddress primaryDNS(192, 168, 0, 1);      // *** 25.3.25 ***

WiFiServer server(80);
camera_fb_t* fb    = NULL;
uint8_t* out_jpg   = NULL;
size_t out_jpg_len = 0;

static void jpegStream(WiFiClient* client);

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,
    .ledc_timer   = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,

    .pixel_format  = PIXFORMAT_RGB565,
    .frame_size    = FRAMESIZE_QVGA,
    .jpeg_quality  = 0,
    .fb_count      = 2,
    .fb_location   = CAMERA_FB_IN_PSRAM,
    .grab_mode     = CAMERA_GRAB_LATEST,
    .sccb_i2c_port = 0,
};

void setup() {
    Serial.begin(115200);
    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");
    }
    delay(100);

#ifdef STA_MODE

    WiFi.mode(WIFI_STA);

    // *** 25.3.25 *** for fixeed IP Address      ここから   
    if (!WiFi.config(local_IP, gateway, subnet, primaryDNS)) {
      Serial.println("STA Failed to configure");
    }
    // *** 25.3.25 *** for fixeed IP Address      ここかまで 

    WiFi.begin(ssid, password);
    WiFi.setSleep(false);
    Serial.println("");

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

    // Wait for connection
    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)
            ;
    }

    Serial.println("AP SSID:");
    Serial.println(ssid);
    Serial.println("AP PASSWORD:");
    Serial.println(password);

    IPAddress IP = WiFi.softAPIP();
    Serial.print("AP IP address: ");
    Serial.println(IP);
#endif

    server.begin();
}

void loop() {
    WiFiClient client = server.available();  // listen for incoming clients
    if (client) {                            // if you get a client,
        while (client.connected()) {         // loop while the client's connected
            if (client.available()) {        // if there's bytes to read from the
                jpegStream(&client);
            }
        }
        // close the connection:
        client.stop();
        Serial.println("Client Disconnected.");
    }
}

// used to image stream
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY     = "\r\n--" 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 satrt");
    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 (;;) {
        fb = esp_camera_fb_get();
        if (fb) {
            frame2jpg(fb, 255, &out_jpg, &out_jpg_len);

            Serial.printf("pic size: %d\n", out_jpg_len);
            client->print(_STREAM_BOUNDARY);
            client->printf(_STREAM_PART, out_jpg_len);
            int32_t to_sends    = out_jpg_len;
            int32_t now_sends   = 0;
            uint8_t* out_buf    = out_jpg;
            uint32_t packet_len = 8 * 1024;
            while (to_sends > 0) {
                now_sends = to_sends > packet_len ? packet_len : to_sends;
                if (client->write(out_buf, now_sends) == 0) {
                    goto client_exit;
                }
                out_buf += now_sends;
                to_sends -= packet_len;
            }

            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;
            }
            if (out_jpg) {
                free(out_jpg);
                out_jpg     = NULL;
                out_jpg_len = 0;
            }
        } else {
            Serial.println("Camera capture failed");
        }
    }

client_exit:
    if (fb) {
        esp_camera_fb_return(fb);
        fb = NULL;
    }
    client->stop();
    Serial.printf("Image stream end\r\n");
}

工場出荷の状態に戻す

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