You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Add CameraManager framework
This commit is contained in:
@@ -52,6 +52,7 @@ from . import net
|
||||
from . import content
|
||||
from . import time
|
||||
from . import sensor_manager
|
||||
from . import camera_manager
|
||||
from . import sdcard
|
||||
from . import battery_voltage
|
||||
from . import audio
|
||||
@@ -89,5 +90,5 @@ __all__ = [
|
||||
"get_all_widgets_with_text",
|
||||
# Submodules
|
||||
"apps", "ui", "config", "net", "content", "time", "sensor_manager",
|
||||
"sdcard", "battery_voltage", "audio", "hardware", "bootloader"
|
||||
"camera_manager", "sdcard", "battery_voltage", "audio", "hardware", "bootloader"
|
||||
]
|
||||
|
||||
@@ -122,6 +122,16 @@ import mpos.sensor_manager as SensorManager
|
||||
# (On Linux desktop, this will fail gracefully but set _initialized flag)
|
||||
SensorManager.init(None)
|
||||
|
||||
# === CAMERA HARDWARE ===
|
||||
import mpos.camera_manager as CameraManager
|
||||
|
||||
# Desktop builds can simulate a camera for testing
|
||||
CameraManager.add_camera(CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="Desktop Simulated Camera",
|
||||
vendor="MicroPythonOS"
|
||||
))
|
||||
|
||||
print("linux.py finished")
|
||||
|
||||
|
||||
|
||||
@@ -127,4 +127,14 @@ import mpos.sensor_manager as SensorManager
|
||||
# i2c_bus was created on line 75 for touch, reuse it for IMU
|
||||
SensorManager.init(i2c_bus, address=0x6B, mounted_position=SensorManager.FACING_EARTH)
|
||||
|
||||
# === CAMERA HARDWARE ===
|
||||
import mpos.camera_manager as CameraManager
|
||||
|
||||
# Waveshare ESP32-S3-Touch-LCD-2 has OV5640 camera
|
||||
CameraManager.add_camera(CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="OV5640",
|
||||
vendor="OmniVision"
|
||||
))
|
||||
|
||||
print("waveshare_esp32_s3_touch_lcd_2.py finished")
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Android-inspired CameraManager for MicroPythonOS.
|
||||
|
||||
Provides unified access to camera devices (back-facing, front-facing, external).
|
||||
Follows module-level singleton pattern (like SensorManager, AudioFlinger).
|
||||
|
||||
Example usage:
|
||||
import mpos.camera_manager as CameraManager
|
||||
|
||||
# In board init file:
|
||||
CameraManager.add_camera(CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="OV5640",
|
||||
vendor="OmniVision"
|
||||
))
|
||||
|
||||
# In app:
|
||||
cam_list = CameraManager.get_cameras()
|
||||
if len(cam_list) > 0:
|
||||
print("we have a camera!")
|
||||
|
||||
MIT License
|
||||
Copyright (c) 2024 MicroPythonOS contributors
|
||||
"""
|
||||
|
||||
try:
|
||||
import _thread
|
||||
_lock = _thread.allocate_lock()
|
||||
except ImportError:
|
||||
_lock = None
|
||||
|
||||
|
||||
# Camera lens facing constants (matching Android Camera2 API)
|
||||
class CameraCharacteristics:
|
||||
"""Camera characteristics and constants."""
|
||||
LENS_FACING_BACK = 0 # Back-facing camera (primary)
|
||||
LENS_FACING_FRONT = 1 # Front-facing camera (selfie)
|
||||
LENS_FACING_EXTERNAL = 2 # External USB camera
|
||||
|
||||
|
||||
class Camera:
|
||||
"""Camera metadata (lightweight data class, Android-inspired).
|
||||
|
||||
Represents a camera device with its characteristics.
|
||||
"""
|
||||
|
||||
def __init__(self, lens_facing, name=None, vendor=None, version=None):
|
||||
"""Initialize camera metadata.
|
||||
|
||||
Args:
|
||||
lens_facing: Camera orientation (LENS_FACING_BACK, LENS_FACING_FRONT, etc.)
|
||||
name: Human-readable camera name (e.g., "OV5640", "Front Camera")
|
||||
vendor: Camera vendor/manufacturer (e.g., "OmniVision")
|
||||
version: Driver version (default 1)
|
||||
"""
|
||||
self.lens_facing = lens_facing
|
||||
self.name = name or "Camera"
|
||||
self.vendor = vendor or "Unknown"
|
||||
self.version = version or 1
|
||||
|
||||
def __repr__(self):
|
||||
facing_names = {
|
||||
CameraCharacteristics.LENS_FACING_BACK: "BACK",
|
||||
CameraCharacteristics.LENS_FACING_FRONT: "FRONT",
|
||||
CameraCharacteristics.LENS_FACING_EXTERNAL: "EXTERNAL"
|
||||
}
|
||||
facing_str = facing_names.get(self.lens_facing, f"UNKNOWN({self.lens_facing})")
|
||||
return f"Camera({self.name}, facing={facing_str})"
|
||||
|
||||
|
||||
# Module state
|
||||
_initialized = False
|
||||
_cameras = [] # List of Camera objects
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize CameraManager.
|
||||
|
||||
Returns:
|
||||
bool: True if initialized successfully
|
||||
"""
|
||||
global _initialized
|
||||
_initialized = True
|
||||
return True
|
||||
|
||||
|
||||
def is_available():
|
||||
"""Check if CameraManager is initialized.
|
||||
|
||||
Returns:
|
||||
bool: True if CameraManager is initialized
|
||||
"""
|
||||
return _initialized
|
||||
|
||||
|
||||
def add_camera(camera):
|
||||
"""Register a camera device.
|
||||
|
||||
Args:
|
||||
camera: Camera object to register
|
||||
|
||||
Returns:
|
||||
bool: True if camera added successfully
|
||||
"""
|
||||
if not isinstance(camera, Camera):
|
||||
print(f"[CameraManager] Error: add_camera() requires Camera object, got {type(camera)}")
|
||||
return False
|
||||
|
||||
if _lock:
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
# Check if camera with same facing already exists
|
||||
for existing in _cameras:
|
||||
if existing.lens_facing == camera.lens_facing:
|
||||
print(f"[CameraManager] Warning: Camera with facing {camera.lens_facing} already registered")
|
||||
# Still add it (allow multiple cameras with same facing)
|
||||
|
||||
_cameras.append(camera)
|
||||
print(f"[CameraManager] Registered camera: {camera}")
|
||||
return True
|
||||
finally:
|
||||
if _lock:
|
||||
_lock.release()
|
||||
|
||||
|
||||
def get_cameras():
|
||||
"""Get list of all registered cameras.
|
||||
|
||||
Returns:
|
||||
list: List of Camera objects (copy of internal list)
|
||||
"""
|
||||
if _lock:
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
return _cameras.copy() if _cameras else []
|
||||
finally:
|
||||
if _lock:
|
||||
_lock.release()
|
||||
|
||||
|
||||
def get_camera_by_facing(lens_facing):
|
||||
"""Get first camera with specified lens facing.
|
||||
|
||||
Args:
|
||||
lens_facing: Camera orientation (LENS_FACING_BACK, LENS_FACING_FRONT, etc.)
|
||||
|
||||
Returns:
|
||||
Camera object or None if not found
|
||||
"""
|
||||
if _lock:
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
for camera in _cameras:
|
||||
if camera.lens_facing == lens_facing:
|
||||
return camera
|
||||
return None
|
||||
finally:
|
||||
if _lock:
|
||||
_lock.release()
|
||||
|
||||
|
||||
def has_camera():
|
||||
"""Check if any camera is registered.
|
||||
|
||||
Returns:
|
||||
bool: True if at least one camera available
|
||||
"""
|
||||
if _lock:
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
return len(_cameras) > 0
|
||||
finally:
|
||||
if _lock:
|
||||
_lock.release()
|
||||
|
||||
|
||||
def get_camera_count():
|
||||
"""Get number of registered cameras.
|
||||
|
||||
Returns:
|
||||
int: Number of cameras
|
||||
"""
|
||||
if _lock:
|
||||
_lock.acquire()
|
||||
|
||||
try:
|
||||
return len(_cameras)
|
||||
finally:
|
||||
if _lock:
|
||||
_lock.release()
|
||||
|
||||
|
||||
# Initialize on module load
|
||||
init()
|
||||
@@ -0,0 +1,298 @@
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
import mpos.camera_manager as CameraManager
|
||||
|
||||
class TestCameraClass(unittest.TestCase):
|
||||
"""Test Camera class functionality."""
|
||||
|
||||
def test_camera_creation_with_all_params(self):
|
||||
"""Test creating a camera with all parameters."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="OV5640",
|
||||
vendor="OmniVision",
|
||||
version=2
|
||||
)
|
||||
self.assertEqual(cam.lens_facing, CameraManager.CameraCharacteristics.LENS_FACING_BACK)
|
||||
self.assertEqual(cam.name, "OV5640")
|
||||
self.assertEqual(cam.vendor, "OmniVision")
|
||||
self.assertEqual(cam.version, 2)
|
||||
|
||||
def test_camera_creation_with_defaults(self):
|
||||
"""Test creating a camera with default parameters."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT
|
||||
)
|
||||
self.assertEqual(cam.lens_facing, CameraManager.CameraCharacteristics.LENS_FACING_FRONT)
|
||||
self.assertEqual(cam.name, "Camera")
|
||||
self.assertEqual(cam.vendor, "Unknown")
|
||||
self.assertEqual(cam.version, 1)
|
||||
|
||||
def test_camera_repr(self):
|
||||
"""Test Camera __repr__ method."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="TestCam"
|
||||
)
|
||||
repr_str = repr(cam)
|
||||
self.assertIn("TestCam", repr_str)
|
||||
self.assertIn("BACK", repr_str)
|
||||
|
||||
def test_camera_repr_front(self):
|
||||
"""Test Camera __repr__ with front-facing camera."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT,
|
||||
name="FrontCam"
|
||||
)
|
||||
repr_str = repr(cam)
|
||||
self.assertIn("FrontCam", repr_str)
|
||||
self.assertIn("FRONT", repr_str)
|
||||
|
||||
def test_camera_repr_external(self):
|
||||
"""Test Camera __repr__ with external camera."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_EXTERNAL,
|
||||
name="USBCam"
|
||||
)
|
||||
repr_str = repr(cam)
|
||||
self.assertIn("USBCam", repr_str)
|
||||
self.assertIn("EXTERNAL", repr_str)
|
||||
|
||||
|
||||
class TestCameraCharacteristics(unittest.TestCase):
|
||||
"""Test CameraCharacteristics constants."""
|
||||
|
||||
def test_lens_facing_constants(self):
|
||||
"""Test that lens facing constants are defined."""
|
||||
self.assertEqual(CameraManager.CameraCharacteristics.LENS_FACING_BACK, 0)
|
||||
self.assertEqual(CameraManager.CameraCharacteristics.LENS_FACING_FRONT, 1)
|
||||
self.assertEqual(CameraManager.CameraCharacteristics.LENS_FACING_EXTERNAL, 2)
|
||||
|
||||
def test_constants_are_unique(self):
|
||||
"""Test that all constants are unique."""
|
||||
constants = [
|
||||
CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
CameraManager.CameraCharacteristics.LENS_FACING_FRONT,
|
||||
CameraManager.CameraCharacteristics.LENS_FACING_EXTERNAL
|
||||
]
|
||||
self.assertEqual(len(constants), len(set(constants)))
|
||||
|
||||
|
||||
class TestCameraManagerFunctionality(unittest.TestCase):
|
||||
"""Test CameraManager core functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Clear cameras before each test."""
|
||||
# Reset the module state
|
||||
CameraManager._cameras = []
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test."""
|
||||
CameraManager._cameras = []
|
||||
|
||||
def test_is_available(self):
|
||||
"""Test is_available() returns True after initialization."""
|
||||
self.assertTrue(CameraManager.is_available())
|
||||
|
||||
def test_add_camera_single(self):
|
||||
"""Test adding a single camera."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="TestCam"
|
||||
)
|
||||
result = CameraManager.add_camera(cam)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(CameraManager.get_camera_count(), 1)
|
||||
|
||||
def test_add_camera_multiple(self):
|
||||
"""Test adding multiple cameras."""
|
||||
back_cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="BackCam"
|
||||
)
|
||||
front_cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT,
|
||||
name="FrontCam"
|
||||
)
|
||||
|
||||
CameraManager.add_camera(back_cam)
|
||||
CameraManager.add_camera(front_cam)
|
||||
|
||||
self.assertEqual(CameraManager.get_camera_count(), 2)
|
||||
|
||||
def test_add_camera_invalid_type(self):
|
||||
"""Test adding invalid object as camera."""
|
||||
result = CameraManager.add_camera("not a camera")
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(CameraManager.get_camera_count(), 0)
|
||||
|
||||
def test_get_cameras_empty(self):
|
||||
"""Test getting cameras when none registered."""
|
||||
cameras = CameraManager.get_cameras()
|
||||
self.assertEqual(len(cameras), 0)
|
||||
self.assertIsInstance(cameras, list)
|
||||
|
||||
def test_get_cameras_returns_copy(self):
|
||||
"""Test that get_cameras() returns a copy, not reference."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK
|
||||
)
|
||||
CameraManager.add_camera(cam)
|
||||
|
||||
cameras1 = CameraManager.get_cameras()
|
||||
cameras2 = CameraManager.get_cameras()
|
||||
|
||||
# Should be equal but not the same object
|
||||
self.assertEqual(len(cameras1), len(cameras2))
|
||||
self.assertIsNot(cameras1, cameras2)
|
||||
|
||||
def test_get_cameras_multiple(self):
|
||||
"""Test getting multiple cameras."""
|
||||
back_cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="BackCam"
|
||||
)
|
||||
front_cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT,
|
||||
name="FrontCam"
|
||||
)
|
||||
|
||||
CameraManager.add_camera(back_cam)
|
||||
CameraManager.add_camera(front_cam)
|
||||
|
||||
cameras = CameraManager.get_cameras()
|
||||
self.assertEqual(len(cameras), 2)
|
||||
names = [c.name for c in cameras]
|
||||
self.assertIn("BackCam", names)
|
||||
self.assertIn("FrontCam", names)
|
||||
|
||||
def test_get_camera_by_facing_back(self):
|
||||
"""Test getting back-facing camera."""
|
||||
back_cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="BackCam"
|
||||
)
|
||||
CameraManager.add_camera(back_cam)
|
||||
|
||||
found = CameraManager.get_camera_by_facing(
|
||||
CameraManager.CameraCharacteristics.LENS_FACING_BACK
|
||||
)
|
||||
self.assertIsNotNone(found)
|
||||
self.assertEqual(found.name, "BackCam")
|
||||
|
||||
def test_get_camera_by_facing_front(self):
|
||||
"""Test getting front-facing camera."""
|
||||
front_cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT,
|
||||
name="FrontCam"
|
||||
)
|
||||
CameraManager.add_camera(front_cam)
|
||||
|
||||
found = CameraManager.get_camera_by_facing(
|
||||
CameraManager.CameraCharacteristics.LENS_FACING_FRONT
|
||||
)
|
||||
self.assertIsNotNone(found)
|
||||
self.assertEqual(found.name, "FrontCam")
|
||||
|
||||
def test_get_camera_by_facing_not_found(self):
|
||||
"""Test getting camera that doesn't exist."""
|
||||
back_cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK
|
||||
)
|
||||
CameraManager.add_camera(back_cam)
|
||||
|
||||
found = CameraManager.get_camera_by_facing(
|
||||
CameraManager.CameraCharacteristics.LENS_FACING_FRONT
|
||||
)
|
||||
self.assertIsNone(found)
|
||||
|
||||
def test_get_camera_by_facing_returns_first(self):
|
||||
"""Test that get_camera_by_facing returns first matching camera."""
|
||||
# Add two back-facing cameras
|
||||
back_cam1 = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="BackCam1"
|
||||
)
|
||||
back_cam2 = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name="BackCam2"
|
||||
)
|
||||
|
||||
CameraManager.add_camera(back_cam1)
|
||||
CameraManager.add_camera(back_cam2)
|
||||
|
||||
found = CameraManager.get_camera_by_facing(
|
||||
CameraManager.CameraCharacteristics.LENS_FACING_BACK
|
||||
)
|
||||
self.assertEqual(found.name, "BackCam1")
|
||||
|
||||
def test_has_camera_empty(self):
|
||||
"""Test has_camera() when no cameras registered."""
|
||||
self.assertFalse(CameraManager.has_camera())
|
||||
|
||||
def test_has_camera_with_cameras(self):
|
||||
"""Test has_camera() when cameras registered."""
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK
|
||||
)
|
||||
CameraManager.add_camera(cam)
|
||||
self.assertTrue(CameraManager.has_camera())
|
||||
|
||||
def test_get_camera_count_empty(self):
|
||||
"""Test get_camera_count() when no cameras."""
|
||||
self.assertEqual(CameraManager.get_camera_count(), 0)
|
||||
|
||||
def test_get_camera_count_multiple(self):
|
||||
"""Test get_camera_count() with multiple cameras."""
|
||||
for i in range(3):
|
||||
cam = CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK,
|
||||
name=f"Camera{i}"
|
||||
)
|
||||
CameraManager.add_camera(cam)
|
||||
|
||||
self.assertEqual(CameraManager.get_camera_count(), 3)
|
||||
|
||||
|
||||
class TestCameraManagerUsagePattern(unittest.TestCase):
|
||||
"""Test the usage pattern from the task description."""
|
||||
|
||||
def setUp(self):
|
||||
"""Clear cameras before each test."""
|
||||
CameraManager._cameras = []
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test."""
|
||||
CameraManager._cameras = []
|
||||
|
||||
def test_task_usage_pattern(self):
|
||||
"""Test the exact usage pattern from the task description."""
|
||||
# Register a camera (as done in board init)
|
||||
CameraManager.add_camera(CameraManager.Camera(
|
||||
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK
|
||||
))
|
||||
|
||||
# App usage pattern
|
||||
cam_list = CameraManager.get_cameras()
|
||||
|
||||
if len(cam_list) > 0:
|
||||
has_camera = True
|
||||
else:
|
||||
has_camera = False
|
||||
|
||||
self.assertTrue(has_camera)
|
||||
|
||||
def test_task_usage_pattern_no_camera(self):
|
||||
"""Test usage pattern when no camera available."""
|
||||
cam_list = CameraManager.get_cameras()
|
||||
|
||||
if len(cam_list) > 0:
|
||||
has_camera = True
|
||||
else:
|
||||
has_camera = False
|
||||
|
||||
self.assertFalse(has_camera)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user