Initial commit: GSPro Remote MVP - Phase 1 complete
This commit is contained in:
commit
74ca4b38eb
50 changed files with 12818 additions and 0 deletions
193
backend/app/core/config.py
Normal file
193
backend/app/core/config.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"""
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue