From cc37d7054cb0830e65e21a3be533f88a982c7109 Mon Sep 17 00:00:00 2001 From: bartool Date: Thu, 9 Oct 2025 21:35:50 +0200 Subject: [PATCH] refactor: new mock gphoto --- core/camera/gphoto_camera.py | 2 +- core/camera/mock_gphoto.py | 204 +++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 core/camera/mock_gphoto.py diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index 003bfd5..d4e569a 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -8,7 +8,7 @@ from .base_camera import BaseCamera try: import gphoto2 as gp # type: ignore except: - import controllers.mock_gphoto as gp + import core.camera.mock_gphoto as gp camera_widget_types = { gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore diff --git a/core/camera/mock_gphoto.py b/core/camera/mock_gphoto.py new file mode 100644 index 0000000..28f3280 --- /dev/null +++ b/core/camera/mock_gphoto.py @@ -0,0 +1,204 @@ +""" +Mock gphoto2 module for Windows testing and development. +Simulates gphoto2 API behavior to allow the app to run without a camera. +""" + +import numpy as np +import cv2 +from dataclasses import dataclass + + +# --- Constants (simulate gphoto2 enums) --- + +GP_WIDGET_WINDOW = 0 +GP_WIDGET_SECTION = 1 +GP_WIDGET_TEXT = 2 +GP_WIDGET_RANGE = 3 +GP_WIDGET_TOGGLE = 4 +GP_WIDGET_RADIO = 5 +GP_WIDGET_MENU = 6 +GP_WIDGET_BUTTON = 7 +GP_WIDGET_DATE = 8 + +GP_OPERATION_NONE = 0x00 +GP_OPERATION_CAPTURE_IMAGE = 0x01 +GP_OPERATION_CAPTURE_VIDEO = 0x02 +GP_OPERATION_CAPTURE_AUDIO = 0x04 +GP_OPERATION_CAPTURE_PREVIEW = 0x08 +GP_OPERATION_CONFIG = 0x10 +GP_OPERATION_TRIGGER_CAPTURE = 0x20 + + +# --- Error class --- +class GPhoto2Error(Exception): + pass + + +# --- Mock camera configuration widget --- + +class MockWidget: + def __init__(self, name, label, wtype, value=None, choices=None): + self._name = name + self._label = label + self._type = wtype + self._value = value + self._choices = choices or [] + self._children = [] + + def get_id(self): + return id(self) + + def get_name(self): + return self._name + + def get_label(self): + return self._label + + def get_type(self): + return self._type + + def get_value(self): + return self._value + + def set_value(self, value): + if self._choices and value not in self._choices: + raise GPhoto2Error(f"Invalid value '{value}' for widget '{self._name}'") + self._value = value + + def get_choices(self): + return self._choices + + def count_children(self): + return len(self._children) + + def get_child(self, i): + return self._children[i] + + def add_child(self, child): + self._children.append(child) + + +# --- Mock classes for detection / abilities --- + +@dataclass +class MockCameraInfo: + name: str + port: str + + +class MockCameraList: + def __init__(self): + self._cameras = [ + MockCameraInfo("Mock Camera 1", "usb:001,002"), + MockCameraInfo("Mock Camera 2", "usb:001,003") + ] + + def count(self): + return len(self._cameras) + + def get_name(self, i): + return self._cameras[i].name + + def get_value(self, i): + return self._cameras[i].port + + +class CameraAbilities: + def __init__(self, operations): + self.operations = operations + + +class CameraAbilitiesList: + def load(self): + pass + + def lookup_model(self, name): + return 0 + + def get_abilities(self, index): + return CameraAbilities( + GP_OPERATION_CAPTURE_IMAGE + | GP_OPERATION_CAPTURE_PREVIEW + | GP_OPERATION_CONFIG + ) + + +class PortInfoList: + def load(self): + pass + + def lookup_path(self, path): + return 0 + + def __getitem__(self, index): + return f"MockPortInfo({index})" + + +# --- Mock Camera class --- + +class Camera: + def __init__(self): + self.initialized = False + self.port_info = None + + def init(self): + self.initialized = True + + def exit(self): + self.initialized = False + + def set_port_info(self, info): + self.port_info = info + + def get_config(self): + # Simulate config tree + root = MockWidget("root", "Root", GP_WIDGET_WINDOW) + + iso = MockWidget("iso", "ISO", GP_WIDGET_MENU, "100", ["100", "200", "400", "800"]) + shutter = MockWidget("shutter", "Shutter Speed", GP_WIDGET_MENU, "1/60", ["1/30", "1/60", "1/125"]) + wb = MockWidget("whitebalance", "White Balance", GP_WIDGET_RADIO, "Auto", ["Auto", "Daylight", "Tungsten"]) + + root.add_child(iso) + root.add_child(shutter) + root.add_child(wb) + + return root + + def set_single_config(self, name, widget): + # Simulate saving a setting + print(f"[mock_gphoto] Setting '{name}' = '{widget.get_value()}'") + + def capture_preview(self): + # Generate a fake image (OpenCV compatible) + frame = np.zeros((480, 640, 3), dtype=np.uint8) + cv2.putText(frame, "Mock Preview", (150, 240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) + _, buf = cv2.imencode(".jpg", frame) + + class MockFile: + def get_data_and_size(self_inner): + return buf.tobytes() + + return MockFile() + + +# --- Mock detection functions --- + +def gp_camera_autodetect(): + return MockCameraList() + + +def check_result(value): + # gphoto2.check_result usually raises error if return < 0 + return value + + +# --- API aliases to match gphoto2 --- + +GP_ERROR = -1 + +gp_camera_autodetect = gp_camera_autodetect +check_result = check_result +Camera = Camera +CameraAbilitiesList = CameraAbilitiesList +PortInfoList = PortInfoList +GPhoto2Error = GPhoto2Error