初めに
以下の前回の記事で Brilliant Labs の Frame を Mac から動かして OLED に Hello World を表示するところまで動かしました。
今回は Frame のカメラで写真を撮って、Mac 側にファイルとして保存してみます。
開発環境
前回と同じです。
- macOS
- Python 3.12(uv 管理)
- frame-ble 1.1.1 / frame-msg 5.2.1
frame-msg を使う
カメラやマイクのように Lua 文字列を送るだけでは完結しない機能を扱うときは、低レベルの frame-ble ではなく高レベルの frame-msg を使います。
frame-msg はホスト側 Python とデバイス側 Lua のセットで動かす作りになっています。典型的な流れは以下の通りです。
- ホストから Frame に標準 Lua ライブラリ(
data,cameraなど)を転送する - ホストから「アプリ本体の Lua」を転送する
- ホストから
start_frame_app()でデバイス側のアプリを起動する - ホスト ↔ Frame で構造化メッセージをやり取りする
カメラ用のアプリ本体である camera_frame_app.lua は、公式の frame_examples_python(BSD-3-Clause)から流用します。リポジトリの frame_msg/lua/camera_frame_app.lua をそのままコピーして、自分のプロジェクトの samples/lua/ 配下に置きました。
ホスト側のコード
ホスト側の Python サンプルです。samples/take_photo.py として置いています。
"""Frameのカメラで写真を撮ってホスト側に保存する最小サンプル.""" from __future__ import annotations import asyncio from datetime import datetime from pathlib import Path from frame_msg import FrameMsg, RxPhoto, TxCaptureSettings LUA_APP = Path(__file__).parent / "lua" / "camera_frame_app.lua" CAPTURES_DIR = Path(__file__).resolve().parent.parent / "captures" CAPTURE_SETTINGS_MSG = 0x0d async def main() -> None: CAPTURES_DIR.mkdir(parents=True, exist_ok=True) frame = FrameMsg() try: await frame.connect() await frame.print_short_text("Loading...") await frame.upload_stdlua_libs(lib_names=["data", "camera"]) await frame.upload_frame_app(local_filename=str(LUA_APP)) frame.attach_print_response_handler() await frame.start_frame_app() rx_photo = RxPhoto() photo_queue = await rx_photo.attach(frame) print("Letting autoexposure loop run for 5 seconds to settle") await asyncio.sleep(5.0) print("Capturing a photo") await frame.send_message( CAPTURE_SETTINGS_MSG, TxCaptureSettings(resolution=720).pack(), ) jpeg_bytes = await asyncio.wait_for(photo_queue.get(), timeout=10.0) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") out_path = CAPTURES_DIR / f"photo_{timestamp}.jpg" out_path.write_bytes(jpeg_bytes) print(f"Saved {len(jpeg_bytes)} bytes to {out_path}") rx_photo.detach(frame) frame.detach_print_response_handler() await frame.stop_frame_app() except Exception as e: print(f"An error occurred: {e}") finally: await frame.disconnect() if __name__ == "__main__": asyncio.run(main())
ポイントは以下の通りです。
upload_stdlua_libs(['data', 'camera'])で標準 Lua ライブラリを Frame に送るupload_frame_app(...)でカメラアプリ本体を Frame に送る- 起動直後はオートエクスポージャが安定していないので 5 秒待つ
TxCaptureSettings(resolution=720)で撮影リクエストを送る(解像度 720×720 の最大値)RxPhotoのキューから JPEG バイトを受け取り、captures/配下に保存
TxCaptureSettings には他にも quality_index(0〜4 の段階指定で、0 が VERY_LOW、4 が VERY_HIGH)や pan、raw などのパラメータがありますが、今回はデフォルトのままで撮ります。
実行
実機を装着して、以下を実行します。
uv run python samples/take_photo.py
Frame の OLED に一瞬白いフラッシュが出て、約 5 秒のオートエクスポージャ待機の後に撮影、JPEG が BLE 経由で Mac に転送されてきます。
Frame app is running Letting autoexposure loop run for 5 seconds to settle Capturing a photo Saved 36279 bytes to /.../captures/photo_20260510_181153.jpg
縦縞のノイズが目立ちますが、被写体は判別できる程度には写っています。Frame の Bluetooth 帯域は 40kBps 程度しか出ないため、720×720 の JPEG 1 枚を受け取るだけでも数秒かかります。