M5stack Timer Camera X の camera を CircuitPython で動かしてみる


## はじめに
CAMERAはmicropython本家に対応してないがCircuitPythonなら対応してるみたいなので、M5stack Timer Camera Xで動かしてみた記録です。注意点は以下。

・M5stack Timer Camera XはESP32チップなのでCIRCUITPYドライブが使えません(https://learn.adafruit.com/welcome-to-circuitpython/the-circuitpy-drive#boards-without-circuitpy-3134472)

・M5stack Timer Camera X はcameraがOV3660なので、デフォルト設定だと色がいまいちです。(https://learn.adafruit.com/capturing-camera-images-with-circuitpython/overview)



## 使用機材
・M5stack Timer Camera X (https://www.switch-science.com/products/6742)
・CircuitPython 9.0.0-beta.0 (https://circuitpython.org/board/m5stack_timer_camera_x/)


## 1.M5stack Timer Camera XにCircuitPythonをインストール

CIRCUITPYドライブが使えないので、Web serial かesptools.pyの二択です。

Web Serial　でインストールするなら Edge , chrome , Opera　で https://circuitpython.org/board/m5stack_timer_camera_x/ を開いて「OPEN INSTALLER」のボタンを押します。今回は最新のCircuitPython 9.0.0-beta.0をインストールしました。

https://learn.adafruit.com/circuitpython-with-esp32-quick-start/overview


## 2. Thonny Editor をインストール
~~CIRCUITPYドライブが使えないので~~

https://learn.adafruit.com/welcome-to-circuitpython/the-circuitpy-drive#boards-without-circuitpy-3134472



## 3. WifiとWeb Workflow(WEB_API)　の設定 
M5stack Timer Camera X 上の /settings.toml でCIRCUITPY_WIFI_SSIDとCIRCUITPY_WIFI_PASSWORDを（Thonny Editorで）編集して再起動すればWifiに接続できます。


(追記)Web Workflow(WEB_API)は後述のhttpサーバと衝突しないようにCIRCUITPY_WEB_API_PORTを80以外（例えば8080）に設定します。

```/settings.toml
# To auto-connect to Wi-Fi
CIRCUITPY_WIFI_SSID="yourwifissid"
CIRCUITPY_WIFI_PASSWORD="yourpassword"

# To enable modifying files from the web. Change this too!
# Leave the User field blank when you type the password into the browser.
CIRCUITPY_WEB_API_PASSWORD="passw0rd"

CIRCUITPY_WEB_API_PORT=8080
```

https://learn.adafruit.com/getting-started-with-web-workflow-using-the-code-editor/device-setup#creating-a-settings-dot-toml-file-3125968


## 4.httpサーバ インストール
LCDがないので、簡単な表示のためにHTTP サーバを立ち上げて画像表示させます。

adafruit_httpserver というライブラリがあるのでこれをインストールします~~が、CIRCUITPYドライブが使えないのでCircupコマンド使えません~~。

https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer/releases からダウンロード＆展開して(Thonny Editor か WebWorkflowで)M5stack Timer camera X 上の/libへコピーします。

そのあとテスト用に[examples/httpserver_static_files_serving.py](https://docs.circuitpython.org/projects/httpserver/en/latest/examples.html#id6)を M5stack Timer camera X 上の /code.py にコピーします。

``` /code.py
# SPDX-FileCopyrightText: 2023 Michał Pokusa
#
# SPDX-License-Identifier: Unlicense


import socketpool
import wifi

from adafruit_httpserver import Server, MIMETypes


MIMETypes.configure(
    default_to="text/plain",
    # Unregistering unnecessary MIME types can save memory
    keep_for=[".html", ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico"],
    # If you need to, you can add additional MIME types
    register={".foo": "text/foo", ".bar": "text/bar"},
)

pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

# You don't have to add any routes, by default the server will serve files
# from it's root_path, which is set to "/static" in this example.

# If you don't set a root_path, the server will not serve any files.

server.serve_forever(str(wifi.radio.ipv4_address))
```

/static/index.html をつくります。あとでcamera.bmp を表示するために使います。
``` /static/index.html
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
　　　　<meta http-equiv="Cache-Control" content="no-cache">
        <title></title>
    </head>
    <body>
        <img src=camera.bmp>
    </body>
</html>
```

適当な画像を /static/camera.bmp にコピーしたあと、M5stack Timer Camera X を再起動して、IPアドレスにhttpアクセスして表示されていれば動作確認OKです。


## 5. espcamera 動作確認
cameraからのテスト信号（カラーバー）を出力させ、RGB565のbitmapでファイルに保存した後、先程のhttpserverで表示させて確認します。

・cameraの初期化はほとんど固定です
・pixel_format=espcamera.PixelFormat.RGB565 で出力をRGB565BMPにしてます。

・解像度 frame_size は(BMP生成が早くなるよう解像度の一番小さな)R96X96にしてます。

・カラーバーの出力のために cam.colorbar=True にします。

・httpserverの表示部はそのまま[examples/httpserver_static_files_serving.py](https://docs.circuitpython.org/projects/httpserver/en/latest/examples.html#id6)をコピーして使用します。

・/static/index.html も先程つくったままです。

・BMPファイルは[碧色工房さんのBMPファイルフォーマット](https://www.mm2d.net/main/prog/c/image_io-07.html)を参考にしました。

``` /code.py
#
# take a picture
#

import board
import espcamera

cam = espcamera.Camera(
    data_pins=board.D,
    pixel_clock_pin=board.PCLK,
    vsync_pin=board.VSYNC,
    href_pin=board.HREF,
    i2c=board.SSCB_I2C(),
    external_clock_pin=board.XCLK,
    reset_pin=board.RESET,
    pixel_format=espcamera.PixelFormat.RGB565,
    frame_size=espcamera.FrameSize.R96X96
    )

cam.colorbar=True # Test 

cam.reconfigure()
bitmap=cam.take(1)
print(type(bitmap))


#
# save RGB565BMP file
#

def RGB565toBMPfile(filename , bitmap):

    import struct
    import ulab.numpy as np


    output_file=open( filename  ,"wb" )

    # _bytes_per_row(width)
    pixel_bytes = 3* bitmap.width
    padding_bytes = (4 - (pixel_bytes % 4)) % 4
    bytes_per_row = pixel_bytes + padding_bytes

    filesize = 66 + bitmap.height * bytes_per_row


    # _write_bmp_header(output_file, filesize)
    output_file.write(bytes("BM", "ascii"))
    output_file.write(struct.pack("<I", filesize))
    output_file.write(b"\00\x00")
    output_file.write(b"\00\x00")
    output_file.write(struct.pack("<I", 66))


    #  _write_dib_header(output_file, width, height)
    output_file.write(struct.pack("<I", 40))
    output_file.write(struct.pack("<I", bitmap.width))
    output_file.write(struct.pack("<I", bitmap.height))
    output_file.write(struct.pack("<H", 1))
    output_file.write(struct.pack("<H", 16))  #bits per pixel
    for _ in range(24):
        output_file.write(b"\x00")

    output_file.write(b"\x00\xF8\x00\x00")  #RED mask
    output_file.write(b"\xE0\x07\x00\x00")  #GREEN mask
    output_file.write(b"\x1F\x00\x00\x00")  #BLUE mask

    #  _write_pixels(output_file, bitmap)
    swapped = np.frombuffer(bitmap, dtype=np.uint16)
    swapped.byteswap(inplace=True)
    output_file.write(swapped)


    output_file.close()

    return



print('Saving bitmap')

RGB565toBMPfile('/static/camera.bmp'  , bitmap )



#
# https://docs.circuitpython.org/projects/httpserver/en/latest/examples.html#id6
#
import socketpool
import wifi

from adafruit_httpserver import Server, MIMETypes


MIMETypes.configure(
    default_to="text/plain",
    # Unregistering unnecessary MIME types can save memory
    keep_for=[".html", ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico"],
    # If you need to, you can add additional MIME types
    register={".foo": "text/foo", ".bar": "text/bar"},
)

pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

# You don't have to add any routes, by default the server will serve files
# from it's root_path, which is set to "/static" in this example.

# If you don't set a root_path, the server will not serve any files.

server.serve_forever(str(wifi.radio.ipv4_address))

```


``` /static/index.html
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
　　　　<meta http-equiv="Cache-Control" content="no-cache">
        <title></title>
    </head>
    <body>
        <img src=camera.bmp>
    </body>
</html>
```

これで M5stack Timer Camera X のIPアドレスにアクセスしてカラーバーが見えたら動作確認OKです。(https://photos.app.goo.gl/ABUchkJuyR8g29aC9)




## 6.最後に
先程のソースコードでカラーバー出力をOFF（cam.colorbar=False）にしたら、cameraの画像がみえます。

解像度の設定やcameraの設定は[ここ](https://docs.circuitpython.org/en/latest/shared-bindings/espcamera/index.html)にありますのでお好みで設定してください。初期値はいまいちでした・・


``` /code.py
#
# take a picture
#

import board
import espcamera

cam = espcamera.Camera(
    data_pins=board.D,
    pixel_clock_pin=board.PCLK,
    vsync_pin=board.VSYNC,
    href_pin=board.HREF,
    i2c=board.SSCB_I2C(),
    external_clock_pin=board.XCLK,
    reset_pin=board.RESET,
    pixel_format=espcamera.PixelFormat.RGB565,
    frame_size=espcamera.FrameSize.R96X96
    )

cam.colorbar=False # Test 

cam.reconfigure()
bitmap=cam.take(1)
print(type(bitmap))


#
# save RGB565BMP file
#

def RGB565toBMPfile(filename , bitmap):

    import struct
    import ulab.numpy as np


    output_file=open( filename  ,"wb" )

    # _bytes_per_row(width)
    pixel_bytes = 3* bitmap.width
    padding_bytes = (4 - (pixel_bytes % 4)) % 4
    bytes_per_row = pixel_bytes + padding_bytes

    filesize = 66 + bitmap.height * bytes_per_row


    # _write_bmp_header(output_file, filesize)
    output_file.write(bytes("BM", "ascii"))
    output_file.write(struct.pack("<I", filesize))
    output_file.write(b"\00\x00")
    output_file.write(b"\00\x00")
    output_file.write(struct.pack("<I", 66))


    #  _write_dib_header(output_file, width, height)
    output_file.write(struct.pack("<I", 40))
    output_file.write(struct.pack("<I", bitmap.width))
    output_file.write(struct.pack("<I", bitmap.height))
    output_file.write(struct.pack("<H", 1))
    output_file.write(struct.pack("<H", 16))  #bits per pixel
    for _ in range(24):
        output_file.write(b"\x00")

    output_file.write(b"\x00\xF8\x00\x00")  #RED mask
    output_file.write(b"\xE0\x07\x00\x00")  #GREEN mask
    output_file.write(b"\x1F\x00\x00\x00")  #BLUE mask

    #  _write_pixels(output_file, bitmap)
    swapped = np.frombuffer(bitmap, dtype=np.uint16)
    swapped.byteswap(inplace=True)
    output_file.write(swapped)


    output_file.close()

    return



print('Saving bitmap')

RGB565toBMPfile('/static/camera.bmp'  , bitmap )



#
# https://docs.circuitpython.org/projects/httpserver/en/latest/examples.html#id6
#
import socketpool
import wifi

from adafruit_httpserver import Server, MIMETypes


MIMETypes.configure(
    default_to="text/plain",
    # Unregistering unnecessary MIME types can save memory
    keep_for=[".html", ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico"],
    # If you need to, you can add additional MIME types
    register={".foo": "text/foo", ".bar": "text/bar"},
)

pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

# You don't have to add any routes, by default the server will serve files
# from it's root_path, which is set to "/static" in this example.

# If you don't set a root_path, the server will not serve any files.

server.serve_forever(str(wifi.radio.ipv4_address))

```

``` /static/index.html
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
　　　　<meta http-equiv="Cache-Control" content="no-cache">
        <title></title>
    </head>
    <body>
        <img src=camera.bmp>
    </body>
</html>
```


以上