refactor: new mock gphoto
This commit is contained in:
@@ -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
|
||||
|
||||
204
core/camera/mock_gphoto.py
Normal file
204
core/camera/mock_gphoto.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user