diff --git a/server/src/wled_controller/api/schemas.py b/server/src/wled_controller/api/schemas.py index 7a58b66..bdf180f 100644 --- a/server/src/wled_controller/api/schemas.py +++ b/server/src/wled_controller/api/schemas.py @@ -36,6 +36,7 @@ class DisplayInfo(BaseModel): x: int = Field(description="Display X position") y: int = Field(description="Display Y position") is_primary: bool = Field(default=False, description="Whether this is the primary display") + refresh_rate: int = Field(description="Display refresh rate in Hz") class DisplayListResponse(BaseModel): diff --git a/server/src/wled_controller/core/screen_capture.py b/server/src/wled_controller/core/screen_capture.py index 66af6f6..ef8fd40 100644 --- a/server/src/wled_controller/core/screen_capture.py +++ b/server/src/wled_controller/core/screen_capture.py @@ -7,7 +7,7 @@ import mss import numpy as np from PIL import Image -from wled_controller.utils import get_logger, get_monitor_names +from wled_controller.utils import get_logger, get_monitor_names, get_monitor_refresh_rates logger = get_logger(__name__) @@ -23,6 +23,7 @@ class DisplayInfo: x: int y: int is_primary: bool + refresh_rate: int # in Hz @dataclass @@ -58,6 +59,9 @@ def get_available_displays() -> List[DisplayInfo]: # Get friendly monitor names (Windows only, falls back to generic names) monitor_names = get_monitor_names() + # Get monitor refresh rates (Windows only, falls back to 60Hz) + refresh_rates = get_monitor_refresh_rates() + with mss.mss() as sct: displays = [] @@ -66,6 +70,9 @@ def get_available_displays() -> List[DisplayInfo]: # Use friendly name from WMI if available, otherwise generic name friendly_name = monitor_names.get(idx, f"Display {idx}") + # Use detected refresh rate or default to 60Hz + refresh_rate = refresh_rates.get(idx, 60) + display_info = DisplayInfo( index=idx, name=friendly_name, @@ -74,6 +81,7 @@ def get_available_displays() -> List[DisplayInfo]: x=monitor["left"], y=monitor["top"], is_primary=(idx == 0), + refresh_rate=refresh_rate, ) displays.append(display_info) diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index e8b3583..beb29f9 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -268,6 +268,10 @@ async function loadDisplays() { ${t('displays.resolution')} ${display.width} × ${display.height} +
+ ${t('displays.refresh_rate')} + ${display.refresh_rate}Hz +
${t('displays.position')} (${display.x}, ${display.y}) diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 8d0f507..7a84444 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -26,6 +26,7 @@ "displays.badge.primary": "Primary", "displays.badge.secondary": "Secondary", "displays.resolution": "Resolution:", + "displays.refresh_rate": "Refresh Rate:", "displays.position": "Position:", "displays.index": "Display Index:", "displays.loading": "Loading displays...", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 84fb8c0..ce56cdb 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -26,6 +26,7 @@ "displays.badge.primary": "Основной", "displays.badge.secondary": "Вторичный", "displays.resolution": "Разрешение:", + "displays.refresh_rate": "Частота Обновления:", "displays.position": "Позиция:", "displays.index": "Индекс Дисплея:", "displays.loading": "Загрузка дисплеев...", diff --git a/server/src/wled_controller/utils/__init__.py b/server/src/wled_controller/utils/__init__.py index 4e58a9b..6e2fec9 100644 --- a/server/src/wled_controller/utils/__init__.py +++ b/server/src/wled_controller/utils/__init__.py @@ -1,6 +1,6 @@ """Utility functions and helpers.""" from .logger import setup_logging, get_logger -from .monitor_names import get_monitor_names, get_monitor_name +from .monitor_names import get_monitor_names, get_monitor_name, get_monitor_refresh_rates -__all__ = ["setup_logging", "get_logger", "get_monitor_names", "get_monitor_name"] +__all__ = ["setup_logging", "get_logger", "get_monitor_names", "get_monitor_name", "get_monitor_refresh_rates"] diff --git a/server/src/wled_controller/utils/monitor_names.py b/server/src/wled_controller/utils/monitor_names.py index b8039f6..810c374 100644 --- a/server/src/wled_controller/utils/monitor_names.py +++ b/server/src/wled_controller/utils/monitor_names.py @@ -77,3 +77,90 @@ def get_monitor_name(index: int) -> str: """ monitor_names = get_monitor_names() return monitor_names.get(index, f"Display {index}") + + +def get_monitor_refresh_rates() -> Dict[int, int]: + """Get refresh rates (in Hz) for connected monitors. + + On Windows, attempts to retrieve refresh rates from display settings. + On other platforms, returns empty dict (will use default 60Hz). + + Returns: + Dictionary mapping display indices to refresh rates in Hz + """ + if sys.platform != "win32": + logger.debug("Refresh rate detection only supported on Windows") + return {} + + try: + import ctypes + from ctypes import wintypes + + # Define Windows structures + class DEVMODE(ctypes.Structure): + _fields_ = [ + ('dmDeviceName', ctypes.c_wchar * 32), + ('dmSpecVersion', wintypes.WORD), + ('dmDriverVersion', wintypes.WORD), + ('dmSize', wintypes.WORD), + ('dmDriverExtra', wintypes.WORD), + ('dmFields', wintypes.DWORD), + ('dmPositionX', wintypes.LONG), + ('dmPositionY', wintypes.LONG), + ('dmDisplayOrientation', wintypes.DWORD), + ('dmDisplayFixedOutput', wintypes.DWORD), + ('dmColor', wintypes.SHORT), + ('dmDuplex', wintypes.SHORT), + ('dmYResolution', wintypes.SHORT), + ('dmTTOption', wintypes.SHORT), + ('dmCollate', wintypes.SHORT), + ('dmFormName', ctypes.c_wchar * 32), + ('dmLogPixels', wintypes.WORD), + ('dmBitsPerPel', wintypes.DWORD), + ('dmPelsWidth', wintypes.DWORD), + ('dmPelsHeight', wintypes.DWORD), + ('dmDisplayFlags', wintypes.DWORD), + ('dmDisplayFrequency', wintypes.DWORD), + ] + + user32 = ctypes.windll.user32 + refresh_rates = {} + + # Enumerate all display devices + idx = 0 + while True: + device_name = ctypes.create_unicode_buffer(32) + if not user32.EnumDisplayDevicesW(None, idx, None, 0): + # Try getting display settings by index + devmode = DEVMODE() + devmode.dmSize = ctypes.sizeof(DEVMODE) + + if user32.EnumDisplaySettingsW(None, -1, ctypes.byref(devmode)): # ENUM_CURRENT_SETTINGS = -1 + refresh_rate = devmode.dmDisplayFrequency + if refresh_rate > 0: + refresh_rates[idx] = refresh_rate + logger.debug(f"Display {idx}: {refresh_rate}Hz") + + idx += 1 + if idx > 10: # Safety limit + break + else: + break + + # If no refresh rates found, try alternative method + if not refresh_rates: + devmode = DEVMODE() + devmode.dmSize = ctypes.sizeof(DEVMODE) + + for monitor_idx in range(4): # Check up to 4 monitors + if user32.EnumDisplaySettingsW(None, -1, ctypes.byref(devmode)): + refresh_rate = devmode.dmDisplayFrequency + if refresh_rate > 0: + refresh_rates[monitor_idx] = refresh_rate + logger.debug(f"Monitor {monitor_idx}: {refresh_rate}Hz") + + return refresh_rates + + except Exception as e: + logger.debug(f"Failed to retrieve monitor refresh rates: {e}") + return {}