feat: Add Camera Settings dialog for UVC controls and Qt parameters

- Implemented CameraSettingsDialog to manage UVC and Qt camera controls.
- Integrated UVC parameter sliders and auto controls for brightness, contrast, saturation, hue, sharpness, gamma, white balance, backlight compensation, and exposure.
- Added functionality to change white balance and exposure settings via Qt controls.
- Updated MainWindow to open CameraSettingsDialog and manage UVC controller lifecycle.
- Enhanced AppMenuBar to include a Camera Settings option.
- Created tests for UVC controller abstraction layer and parameter info.
- Documented camera specifications and supported features in new markdown files.
This commit is contained in:
2026-05-13 19:19:39 +02:00
parent d62416db4e
commit cdeac53555
12 changed files with 1756 additions and 118 deletions

117
tests/test_uvc.py Normal file
View File

@@ -0,0 +1,117 @@
"""Tests for the UVC controller abstraction layer."""
from __future__ import annotations
from app.camera.uvc.base import UvcParam, UvcParamInfo
from app.camera.uvc.stub import NullUvcController
class TestNullUvcController:
"""NullUvcController must implement the full interface, all as no-ops."""
def setup_method(self) -> None:
self.ctrl = NullUvcController()
def test_is_not_open(self) -> None:
assert not self.ctrl.is_open()
def test_open_returns_false(self) -> None:
assert self.ctrl.open("any device") is False
def test_close_does_not_raise(self) -> None:
self.ctrl.close() # must not raise
def test_get_param_info_returns_unsupported(self) -> None:
info = self.ctrl.get_param_info(UvcParam.BRIGHTNESS)
assert isinstance(info, UvcParamInfo)
assert info.supported is False
assert info.param is UvcParam.BRIGHTNESS
def test_get_all_params_covers_all_uvc_params(self) -> None:
infos = self.ctrl.get_all_params()
returned_params = {i.param for i in infos}
all_params = set(UvcParam)
assert returned_params == all_params
def test_get_all_params_all_unsupported(self) -> None:
for info in self.ctrl.get_all_params():
assert info.supported is False
def test_set_value_returns_false(self) -> None:
assert self.ctrl.set_value(UvcParam.CONTRAST, 50) is False
def test_set_auto_returns_false(self) -> None:
assert self.ctrl.set_auto(UvcParam.WHITE_BALANCE, True) is False
def test_set_value_does_not_raise(self) -> None:
for param in UvcParam:
self.ctrl.set_value(param, 0) # must not raise
def test_set_auto_does_not_raise(self) -> None:
for param in UvcParam:
self.ctrl.set_auto(param, True) # must not raise
self.ctrl.set_auto(param, False)
class TestUvcParamInfo:
"""UvcParamInfo dataclass sanity checks."""
def test_defaults(self) -> None:
info = UvcParamInfo(param=UvcParam.BRIGHTNESS, supported=True)
assert info.minimum == 0
assert info.maximum == 100
assert info.default == 50
assert info.current == 50
assert info.step == 1
assert info.auto_supported is False
assert info.auto_enabled is False
def test_unsupported_flag(self) -> None:
info = UvcParamInfo(param=UvcParam.GAMMA, supported=False)
assert info.supported is False
def test_custom_range(self) -> None:
info = UvcParamInfo(
param=UvcParam.HUE,
supported=True,
minimum=-180,
maximum=180,
default=0,
current=-45,
)
assert info.minimum == -180
assert info.maximum == 180
assert info.current == -45
class TestMakeUvcControllerFallback:
"""make_uvc_controller falls back to NullUvcController when no lib available."""
def test_returns_controller_instance(self) -> None:
from app.camera.uvc import make_uvc_controller
from app.camera.uvc.base import UvcControllerBase
ctrl = make_uvc_controller("Test Camera")
assert isinstance(ctrl, UvcControllerBase)
def test_stub_used_when_native_lib_absent(self, monkeypatch) -> None:
"""If the native import fails, should return NullUvcController."""
import builtins
real_import = builtins.__import__
def patched_import(name, *args, **kwargs):
if name in ("duvc_ctl", "uvc"):
raise ImportError(f"Mocked missing: {name}")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", patched_import)
# Re-import to exercise factory with patched import
import importlib
import app.camera.uvc as uvc_pkg
importlib.reload(uvc_pkg)
ctrl = uvc_pkg.make_uvc_controller("Test Camera")
# Should be functional (not raise), may be Null or platform controller
from app.camera.uvc.base import UvcControllerBase
assert isinstance(ctrl, UvcControllerBase)
# Reload back to normal
importlib.reload(uvc_pkg)