Implement UDS Service 0x22 (Read Data By Identifier) with support for: - Engine ECU (0x7E0): boost pressure, fuel rail pressure, lambda, torque, wastegate position, ignition timing, knock correction per cylinder - DSG Transmission (0x7E1): current gear, selector position, oil temp - Instrument Cluster (0x714): RPM, ambient light sensor Also adds ECU_EMULATOR_SPEC.md with full protocol documentation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
106 lines
3.9 KiB
Python
106 lines
3.9 KiB
Python
"""
|
|
Конфигурация OBD2 эмулятора.
|
|
Поддержка физических CAN интерфейсов (can0, can1) и виртуальных (vcan0).
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class CANConfig(BaseModel):
|
|
"""Конфигурация CAN интерфейса."""
|
|
interface: str = Field(default="vcan0", description="CAN interface name")
|
|
bitrate: int = Field(default=500000, description="CAN bitrate (ignored for vcan)")
|
|
|
|
@field_validator("interface")
|
|
@classmethod
|
|
def validate_interface(cls, v: str) -> str:
|
|
valid_prefixes = ("can", "vcan")
|
|
if not any(v.startswith(prefix) for prefix in valid_prefixes):
|
|
raise ValueError(f"Interface must start with: {valid_prefixes}")
|
|
return v
|
|
|
|
@property
|
|
def is_virtual(self) -> bool:
|
|
"""Check if interface is virtual."""
|
|
return self.interface.startswith("vcan")
|
|
|
|
|
|
class VehicleConfig(BaseModel):
|
|
"""Конфигурация симулируемого автомобиля."""
|
|
# Базовые параметры двигателя
|
|
idle_rpm: int = Field(default=850, ge=600, le=1200)
|
|
max_rpm: int = Field(default=6500, ge=4000, le=9000)
|
|
redline_rpm: int = Field(default=6000, ge=3500, le=8500)
|
|
max_torque_nm: float = Field(default=350.0, ge=100, le=800, description="Max torque in Nm")
|
|
|
|
# Температуры
|
|
ambient_temp: float = Field(default=20.0, ge=-40, le=50)
|
|
target_coolant_temp: float = Field(default=90.0, ge=80, le=105)
|
|
|
|
# Топливо
|
|
fuel_tank_capacity: float = Field(default=58.0, ge=30, le=100)
|
|
initial_fuel_level: float = Field(default=75.0, ge=0, le=100)
|
|
|
|
# Характеристики авто
|
|
max_speed: int = Field(default=220, ge=100, le=350)
|
|
engine_displacement: float = Field(default=2.0, description="Engine displacement in liters")
|
|
|
|
|
|
class SimulatorConfig(BaseModel):
|
|
"""Конфигурация симулятора."""
|
|
update_rate_hz: int = Field(default=10, ge=1, le=100, description="Vehicle state update rate")
|
|
scenario: Literal["idle", "city", "highway", "warmup", "manual"] = Field(default="idle")
|
|
|
|
|
|
class LoggingConfig(BaseModel):
|
|
"""Конфигурация логирования."""
|
|
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field(default="INFO")
|
|
format: str = Field(default="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s")
|
|
log_can_frames: bool = Field(default=False, description="Log all CAN frames (verbose)")
|
|
|
|
|
|
class Config(BaseModel):
|
|
"""Главная конфигурация приложения."""
|
|
can: CANConfig = Field(default_factory=CANConfig)
|
|
vehicle: VehicleConfig = Field(default_factory=VehicleConfig)
|
|
simulator: SimulatorConfig = Field(default_factory=SimulatorConfig)
|
|
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
|
|
|
|
def load_config(config_path: Path | None = None) -> Config:
|
|
"""Загрузка конфигурации из файла или defaults."""
|
|
if config_path and config_path.exists():
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return Config.model_validate(data)
|
|
return Config()
|
|
|
|
|
|
# Глобальный экземпляр конфигурации
|
|
_config: Config | None = None
|
|
|
|
|
|
def get_config() -> Config:
|
|
"""Получить глобальную конфигурацию."""
|
|
global _config
|
|
if _config is None:
|
|
config_path = Path(__file__).parent / "config.json"
|
|
_config = load_config(config_path if config_path.exists() else None)
|
|
return _config
|
|
|
|
|
|
def set_config(config: Config) -> None:
|
|
"""Установить глобальную конфигурацию."""
|
|
global _config
|
|
_config = config
|
|
|
|
|
|
# Прокси для удобного доступа
|
|
class _ConfigProxy:
|
|
def __getattr__(self, name):
|
|
return getattr(get_config(), name)
|
|
|
|
config = _ConfigProxy()
|