"""Android-inspired CameraManager for MicroPythonOS. Provides unified access to camera devices (back-facing, front-facing, external). Follows singleton pattern with class method delegation. Example usage: from mpos import 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 """ # 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, init=None, deinit=None, capture=None, apply_settings=None, rotation_degrees=0): """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) rotation_degrees: how many degrees the camera is rotated clockwise """ self.lens_facing = lens_facing self.name = name or "Camera" self.vendor = vendor or "Unknown" self.version = version or 1 self.init_function = init self.deinit_function = deinit self.capture_function = capture self.apply_settings_function = apply_settings self.rotation_degrees = rotation_degrees 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})" def init(self, width, height, colormode): if self.init_function: return self.init_function(width, height, colormode) def deinit(self, cam_obj=None): if self.deinit_function: return self.deinit_function(cam_obj) def capture(self, cam_obj, colormode=None): if self.capture_function: return self.capture_function(cam_obj, colormode) def apply_settings(self, cam_obj, prefs): if self.apply_settings_function: return self.apply_settings_function(cam_obj, prefs) def get_rotation_degrees(self): return self.rotation_degrees class CameraManager: """ Centralized camera device management service. Implements singleton pattern for unified camera access. Usage: from mpos import CameraManager # Register a camera CameraManager.add_camera(CameraManager.Camera( lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK, name="OV5640" )) # Get all cameras cameras = CameraManager.get_cameras() """ # Expose inner classes as class attributes Camera = Camera CameraCharacteristics = CameraCharacteristics _instance = None _cameras = [] # Class-level camera list for singleton def __init__(self): """Initialize CameraManager singleton instance.""" if CameraManager._instance: return CameraManager._instance = self self._initialized = False self.init() @classmethod def get(cls): """Get or create the singleton instance.""" if cls._instance is None: cls._instance = cls() return cls._instance def init(self): """Initialize CameraManager. Returns: bool: True if initialized successfully """ self._initialized = True return True def is_available(self): """Check if CameraManager is initialized. Returns: bool: True if CameraManager is initialized """ return self._initialized def add_camera(self, 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 # Check if camera with same facing already exists for existing in CameraManager._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) CameraManager._cameras.append(camera) print(f"[CameraManager] Registered camera: {camera}") return True def get_cameras(self): """Get list of all registered cameras. Returns: list: List of Camera objects (copy of internal list) """ return CameraManager._cameras.copy() if CameraManager._cameras else [] def get_camera_by_facing(self, 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 """ for camera in CameraManager._cameras: if camera.lens_facing == lens_facing: return camera return None def has_camera(self): """Check if any camera is registered. Returns: bool: True if at least one camera available """ return len(CameraManager._cameras) > 0 def get_camera_count(self): """Get number of registered cameras. Returns: int: Number of cameras """ return len(CameraManager._cameras) @staticmethod def resolution_to_framesize(width, height): """Map resolution (width, height) to FrameSize enum. Args: width: Image width in pixels height: Image height in pixels Returns: FrameSize enum value corresponding to the resolution, or R240X240 as default """ try: from camera import FrameSize except ImportError: print("Warning: camera module not available") return None # Format: (width, height): FrameSize resolution_map = { (96, 96): FrameSize.R96X96, (160, 120): FrameSize.QQVGA, (128, 128): FrameSize.R128X128, (176, 144): FrameSize.QCIF, (240, 176): FrameSize.HQVGA, (240, 240): FrameSize.R240X240, (320, 240): FrameSize.QVGA, (320, 320): FrameSize.R320X320, (400, 296): FrameSize.CIF, (480, 320): FrameSize.HVGA, (480, 480): FrameSize.R480X480, (640, 480): FrameSize.VGA, (640, 640): FrameSize.R640X640, (720, 720): FrameSize.R720X720, (800, 600): FrameSize.SVGA, (800, 800): FrameSize.R800X800, (1024, 768): FrameSize.XGA, (960, 960): FrameSize.R960X960, (1280, 720): FrameSize.HD, (1024, 1024): FrameSize.R1024X1024, # These are disabled in camera_settings.py because they use a lot of RAM: (1280, 1024): FrameSize.SXGA, (1280, 1280): FrameSize.R1280X1280, (1600, 1200): FrameSize.UXGA, (1920, 1080): FrameSize.FHD, } return resolution_map.get((width, height), FrameSize.R240X240) @staticmethod def ov_apply_camera_settings(cam, prefs): if not cam or not prefs: print("ov_apply_camera_settings: Skipping because invalid prefs or cam object") return try: # Basic image adjustments brightness = prefs.get_int("brightness") if brightness is not None: cam.set_brightness(brightness) contrast = prefs.get_int("contrast") if contrast is not None: cam.set_contrast(contrast) saturation = prefs.get_int("saturation") if saturation is not None: cam.set_saturation(saturation) # Orientation hmirror = prefs.get_bool("hmirror") if hmirror is not None: cam.set_hmirror(hmirror) vflip = prefs.get_bool("vflip") if vflip is not None: cam.set_vflip(vflip) # Special effect special_effect = prefs.get_int("special_effect") if special_effect is not None: cam.set_special_effect(special_effect) # Exposure control (apply master switch first, then manual value) exposure_ctrl = prefs.get_bool("exposure_ctrl") if exposure_ctrl is not None: cam.set_exposure_ctrl(exposure_ctrl) else: aec_value = prefs.get_int("aec_value") if aec_value is not None: cam.set_aec_value(aec_value) # Mode-specific default comes from constructor ae_level = prefs.get_int("ae_level") if ae_level is not None: cam.set_ae_level(ae_level) aec2 = prefs.get_bool("aec2") if aec2 is not None: cam.set_aec2(aec2) # Gain control (apply master switch first, then manual value) gain_ctrl = prefs.get_bool("gain_ctrl") if gain_ctrl is not None: cam.set_gain_ctrl(gain_ctrl) else: agc_gain = prefs.get_int("agc_gain") if agc_gain is None: cam.set_agc_gain(agc_gain) gainceiling = prefs.get_int("gainceiling") if gainceiling is not None: cam.set_gainceiling(gainceiling) # White balance (apply master switch first, then mode) whitebal = prefs.get_bool("whitebal") if whitebal is not None: cam.set_whitebal(whitebal) else: wb_mode = prefs.get_int("wb_mode") if wb_mode is not None: cam.set_wb_mode(wb_mode) awb_gain = prefs.get_bool("awb_gain") if awb_gain is not None: cam.set_awb_gain(awb_gain) # Sensor-specific settings (try/except for unsupported sensors) try: sharpness = prefs.get_int("sharpness") if sharpness is not None: cam.set_sharpness(sharpness) except: pass # Not supported on OV2640? try: denoise = prefs.get_int("denoise") if denoise is not None: cam.set_denoise(denoise) except: pass # Not supported on OV2640? # Advanced corrections colorbar = prefs.get_bool("colorbar") if colorbar is not None: cam.set_colorbar(colorbar) dcw = prefs.get_bool("dcw") if dcw is not None: cam.set_dcw(dcw) bpc = prefs.get_bool("bpc") if bpc is not None: cam.set_bpc(bpc) wpc = prefs.get_bool("wpc") if wpc is not None: cam.set_wpc(wpc) # Mode-specific default comes from constructor raw_gma = prefs.get_bool("raw_gma") if raw_gma is not None: print(f"applying raw_gma: {raw_gma}") cam.set_raw_gma(raw_gma) lenc = prefs.get_bool("lenc") if lenc is not None: cam.set_lenc(lenc) # JPEG quality (only relevant for JPEG format) #try: # quality = prefs.get_int("quality", 85) # if quality is not None: # cam.set_quality(quality) #except: # pass # Not in JPEG mode print("Camera settings applied successfully") except Exception as e: print(f"Error applying camera settings: {e}") # ============================================================================ # Class method delegation (at module level) # ============================================================================ _original_methods = {} _methods_to_delegate = [ 'init', 'is_available', 'add_camera', 'get_cameras', 'get_camera_by_facing', 'has_camera', 'get_camera_count' ] for method_name in _methods_to_delegate: _original_methods[method_name] = getattr(CameraManager, method_name) def _make_class_method(method_name): """Create a class method that delegates to the singleton instance.""" original_method = _original_methods[method_name] @classmethod def class_method(cls, *args, **kwargs): instance = cls.get() return original_method(instance, *args, **kwargs) return class_method for method_name in _methods_to_delegate: setattr(CameraManager, method_name, _make_class_method(method_name)) # Initialize on module load CameraManager.init()