194 lines
6.7 KiB
Python
194 lines
6.7 KiB
Python
|
|
"""
|
||
|
|
Configuration management for GSPro Remote.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional
|
||
|
|
from functools import lru_cache
|
||
|
|
|
||
|
|
from pydantic import BaseModel, Field
|
||
|
|
from pydantic_settings import BaseSettings
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class ServerConfig(BaseModel):
|
||
|
|
"""Server configuration settings."""
|
||
|
|
|
||
|
|
host: str = Field("0.0.0.0", description="Server host address")
|
||
|
|
port: int = Field(5005, description="Server port")
|
||
|
|
mdns_enabled: bool = Field(True, description="Enable mDNS service discovery")
|
||
|
|
|
||
|
|
|
||
|
|
class CaptureConfig(BaseModel):
|
||
|
|
"""Screen capture configuration settings."""
|
||
|
|
|
||
|
|
fps: int = Field(30, description="Frames per second for streaming")
|
||
|
|
quality: int = Field(85, description="JPEG quality (0-100)")
|
||
|
|
resolution: str = Field("720p", description="Stream resolution")
|
||
|
|
region_x: int = Field(0, description="Map region X coordinate")
|
||
|
|
region_y: int = Field(0, description="Map region Y coordinate")
|
||
|
|
region_width: int = Field(640, description="Map region width")
|
||
|
|
region_height: int = Field(480, description="Map region height")
|
||
|
|
|
||
|
|
|
||
|
|
class GSProConfig(BaseModel):
|
||
|
|
"""GSPro application configuration settings."""
|
||
|
|
|
||
|
|
window_title: str = Field("GSPro", description="GSPro window title")
|
||
|
|
auto_focus: bool = Field(True, description="Auto-focus GSPro window before sending keys")
|
||
|
|
key_delay: float = Field(0.05, description="Default delay between key presses (seconds)")
|
||
|
|
|
||
|
|
|
||
|
|
class VisionConfig(BaseModel):
|
||
|
|
"""Computer vision configuration settings (for V2 features)."""
|
||
|
|
|
||
|
|
enabled: bool = Field(False, description="Enable vision features")
|
||
|
|
ocr_engine: str = Field("easyocr", description="OCR engine to use (easyocr or tesseract)")
|
||
|
|
confidence_threshold: float = Field(0.7, description="Minimum confidence for OCR detection")
|
||
|
|
|
||
|
|
|
||
|
|
class AppConfig(BaseSettings):
|
||
|
|
"""Main application configuration."""
|
||
|
|
|
||
|
|
server: ServerConfig = Field(default_factory=ServerConfig)
|
||
|
|
capture: CaptureConfig = Field(default_factory=CaptureConfig)
|
||
|
|
gspro: GSProConfig = Field(default_factory=GSProConfig)
|
||
|
|
vision: VisionConfig = Field(default_factory=VisionConfig)
|
||
|
|
|
||
|
|
config_path: Optional[Path] = None
|
||
|
|
debug: bool = Field(False, description="Enable debug mode")
|
||
|
|
|
||
|
|
class Config:
|
||
|
|
env_prefix = "GSPRO_REMOTE_"
|
||
|
|
env_nested_delimiter = "__"
|
||
|
|
case_sensitive = False
|
||
|
|
|
||
|
|
def __init__(self, **kwargs):
|
||
|
|
super().__init__(**kwargs)
|
||
|
|
if self.config_path is None:
|
||
|
|
self.config_path = self._get_default_config_path()
|
||
|
|
self.load()
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _get_default_config_path() -> Path:
|
||
|
|
"""Get the default configuration file path."""
|
||
|
|
import os
|
||
|
|
|
||
|
|
if os.name == "nt": # Windows
|
||
|
|
base_path = Path(os.environ.get("LOCALAPPDATA", ""))
|
||
|
|
if not base_path:
|
||
|
|
base_path = Path.home() / "AppData" / "Local"
|
||
|
|
else: # Unix-like
|
||
|
|
base_path = Path.home() / ".config"
|
||
|
|
|
||
|
|
config_dir = base_path / "GSPro Remote"
|
||
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
return config_dir / "config.json"
|
||
|
|
|
||
|
|
def load(self) -> None:
|
||
|
|
"""Load configuration from file."""
|
||
|
|
if self.config_path and self.config_path.exists():
|
||
|
|
try:
|
||
|
|
with open(self.config_path, "r") as f:
|
||
|
|
data = json.load(f)
|
||
|
|
|
||
|
|
# Update configuration with loaded data
|
||
|
|
if "server" in data:
|
||
|
|
self.server = ServerConfig(**data["server"])
|
||
|
|
if "capture" in data:
|
||
|
|
self.capture = CaptureConfig(**data["capture"])
|
||
|
|
if "gspro" in data:
|
||
|
|
self.gspro = GSProConfig(**data["gspro"])
|
||
|
|
if "vision" in data:
|
||
|
|
self.vision = VisionConfig(**data["vision"])
|
||
|
|
if "debug" in data:
|
||
|
|
self.debug = data["debug"]
|
||
|
|
|
||
|
|
logger.info(f"Configuration loaded from {self.config_path}")
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Failed to load configuration: {e}")
|
||
|
|
self.save() # Save default configuration
|
||
|
|
else:
|
||
|
|
# Create default configuration file
|
||
|
|
self.save()
|
||
|
|
logger.info(f"Created default configuration at {self.config_path}")
|
||
|
|
|
||
|
|
def save(self) -> None:
|
||
|
|
"""Save configuration to file."""
|
||
|
|
if self.config_path:
|
||
|
|
try:
|
||
|
|
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
data = {
|
||
|
|
"server": self.server.model_dump(),
|
||
|
|
"capture": self.capture.model_dump(),
|
||
|
|
"gspro": self.gspro.model_dump(),
|
||
|
|
"vision": self.vision.model_dump(),
|
||
|
|
"debug": self.debug,
|
||
|
|
}
|
||
|
|
|
||
|
|
with open(self.config_path, "w") as f:
|
||
|
|
json.dump(data, f, indent=2)
|
||
|
|
|
||
|
|
logger.info(f"Configuration saved to {self.config_path}")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to save configuration: {e}")
|
||
|
|
|
||
|
|
def update(self, **kwargs) -> None:
|
||
|
|
"""Update configuration with new values."""
|
||
|
|
for key, value in kwargs.items():
|
||
|
|
if hasattr(self, key):
|
||
|
|
if isinstance(value, dict):
|
||
|
|
# Update nested configuration
|
||
|
|
current = getattr(self, key)
|
||
|
|
if isinstance(current, BaseModel):
|
||
|
|
for sub_key, sub_value in value.items():
|
||
|
|
if hasattr(current, sub_key):
|
||
|
|
setattr(current, sub_key, sub_value)
|
||
|
|
else:
|
||
|
|
setattr(self, key, value)
|
||
|
|
self.save()
|
||
|
|
|
||
|
|
def reset(self) -> None:
|
||
|
|
"""Reset configuration to defaults."""
|
||
|
|
self.server = ServerConfig()
|
||
|
|
self.capture = CaptureConfig()
|
||
|
|
self.gspro = GSProConfig()
|
||
|
|
self.vision = VisionConfig()
|
||
|
|
self.debug = False
|
||
|
|
self.save()
|
||
|
|
|
||
|
|
def to_dict(self) -> dict:
|
||
|
|
"""Convert configuration to dictionary."""
|
||
|
|
return {
|
||
|
|
"server": self.server.model_dump(),
|
||
|
|
"capture": self.capture.model_dump(),
|
||
|
|
"gspro": self.gspro.model_dump(),
|
||
|
|
"vision": self.vision.model_dump(),
|
||
|
|
"debug": self.debug,
|
||
|
|
"config_path": str(self.config_path) if self.config_path else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# Global configuration instance
|
||
|
|
_config: Optional[AppConfig] = None
|
||
|
|
|
||
|
|
|
||
|
|
@lru_cache(maxsize=1)
|
||
|
|
def get_config() -> AppConfig:
|
||
|
|
"""Get the global configuration instance."""
|
||
|
|
global _config
|
||
|
|
if _config is None:
|
||
|
|
_config = AppConfig()
|
||
|
|
return _config
|
||
|
|
|
||
|
|
|
||
|
|
def reset_config() -> None:
|
||
|
|
"""Reset the global configuration instance."""
|
||
|
|
global _config
|
||
|
|
_config = None
|
||
|
|
get_config.cache_clear()
|