commit ef3d9212b518c052a786483230888f0507182928
Author: 9700X_PC <9700X_PC@gmail.com>
Date: Sun Jan 18 10:52:59 2026 +0900
first commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e4312f3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+venv/
+build/
+dist/
\ No newline at end of file
diff --git a/TrackInfo.xlsx b/TrackInfo.xlsx
new file mode 100644
index 0000000..63de304
Binary files /dev/null and b/TrackInfo.xlsx differ
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..3b2f96e
Binary files /dev/null and b/app/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ai/__init__.py b/app/ai/__init__.py
new file mode 100644
index 0000000..8248cb3
--- /dev/null
+++ b/app/ai/__init__.py
@@ -0,0 +1,32 @@
+"""
+AI 모듈 패키지
+AI 프로바이더 관리 및 통합 인터페이스를 제공합니다.
+"""
+
+from .base import AIProviderType, AIMessage, AIResponse, BaseAIProvider
+from .ai_client import AIClient, get_ai_client
+from .providers import (
+ OpenAIProvider,
+ OpenRouterProvider,
+ GeminiProvider,
+ XAIProvider,
+)
+
+__all__ = [
+ # Core
+ 'AIProviderType',
+ 'AIMessage',
+ 'AIResponse',
+ 'BaseAIProvider',
+
+ # Client
+ 'AIClient',
+ 'get_ai_client',
+
+ # Providers
+ 'OpenAIProvider',
+ 'OpenRouterProvider',
+ 'GeminiProvider',
+ 'XAIProvider',
+]
+
diff --git a/app/ai/__pycache__/__init__.cpython-311.pyc b/app/ai/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..4cbf8d6
Binary files /dev/null and b/app/ai/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ai/__pycache__/ai_client.cpython-311.pyc b/app/ai/__pycache__/ai_client.cpython-311.pyc
new file mode 100644
index 0000000..6d0f1a0
Binary files /dev/null and b/app/ai/__pycache__/ai_client.cpython-311.pyc differ
diff --git a/app/ai/__pycache__/base.cpython-311.pyc b/app/ai/__pycache__/base.cpython-311.pyc
new file mode 100644
index 0000000..55accb0
Binary files /dev/null and b/app/ai/__pycache__/base.cpython-311.pyc differ
diff --git a/app/ai/__pycache__/maintenance_kb.cpython-311.pyc b/app/ai/__pycache__/maintenance_kb.cpython-311.pyc
new file mode 100644
index 0000000..e498dc3
Binary files /dev/null and b/app/ai/__pycache__/maintenance_kb.cpython-311.pyc differ
diff --git a/app/ai/ai_client.py b/app/ai/ai_client.py
new file mode 100644
index 0000000..449553b
--- /dev/null
+++ b/app/ai/ai_client.py
@@ -0,0 +1,340 @@
+"""
+AI Client - AI 프로바이더 관리 및 통합 인터페이스
+
+사용 예시:
+ from app.ai.ai_client import AIClient, AIProviderType
+
+ # 클라이언트 생성
+ client = AIClient()
+
+ # 프로바이더 설정
+ client.set_provider(AIProviderType.OPENAI, api_key="sk-...")
+
+ # 또는 설정에서 자동 로드
+ client.load_from_settings()
+
+ # 채팅 요청
+ response = client.chat("안녕하세요!")
+"""
+
+from typing import Optional, Dict, List, Type
+from .base import BaseAIProvider, AIProviderType, AIMessage, AIResponse
+from .providers import OpenAIProvider, OpenRouterProvider, GeminiProvider, XAIProvider
+
+
+class AIClient:
+ """
+ AI 클라이언트 매니저
+ 여러 AI 프로바이더를 관리하고 통합 인터페이스를 제공합니다.
+ """
+
+ # 프로바이더 타입별 클래스 매핑
+ PROVIDER_CLASSES: Dict[AIProviderType, Type[BaseAIProvider]] = {
+ AIProviderType.OPENAI: OpenAIProvider,
+ AIProviderType.OPENROUTER: OpenRouterProvider,
+ AIProviderType.GEMINI: GeminiProvider,
+ AIProviderType.XAI: XAIProvider,
+ }
+
+ def __init__(self):
+ self._current_provider: Optional[BaseAIProvider] = None
+ self._providers: Dict[AIProviderType, BaseAIProvider] = {}
+ self._settings = None
+
+ @property
+ def current_provider(self) -> Optional[BaseAIProvider]:
+ """현재 활성화된 프로바이더"""
+ return self._current_provider
+
+ @property
+ def current_provider_type(self) -> Optional[AIProviderType]:
+ """현재 프로바이더 타입"""
+ if self._current_provider:
+ return self._current_provider.provider_type
+ return None
+
+ @property
+ def available_provider_types(self) -> List[AIProviderType]:
+ """사용 가능한 프로바이더 타입 목록"""
+ return list(self.PROVIDER_CLASSES.keys())
+
+ def get_provider_name(self, provider_type: AIProviderType) -> str:
+ """프로바이더 타입에 해당하는 표시 이름 반환"""
+ names = {
+ AIProviderType.OPENAI: "OpenAI",
+ AIProviderType.OPENROUTER: "OpenRouter",
+ AIProviderType.GEMINI: "Google Gemini",
+ AIProviderType.XAI: "xAI (Grok)",
+ }
+ return names.get(provider_type, provider_type.value)
+
+ def set_provider(
+ self,
+ provider_type: AIProviderType,
+ api_key: str,
+ model: Optional[str] = None
+ ) -> bool:
+ """
+ 프로바이더 설정 및 활성화
+
+ Args:
+ provider_type: 프로바이더 타입
+ api_key: API 키
+ model: 사용할 모델 (None이면 기본 모델)
+
+ Returns:
+ 초기화 성공 여부
+ """
+ provider_class = self.PROVIDER_CLASSES.get(provider_type)
+ if not provider_class:
+ raise ValueError(f"지원하지 않는 프로바이더: {provider_type}")
+
+ # 프로바이더 인스턴스 생성
+ provider = provider_class(api_key=api_key, model=model)
+
+ # 초기화 시도
+ if provider.initialize():
+ self._providers[provider_type] = provider
+ self._current_provider = provider
+ return True
+
+ return False
+
+ def switch_provider(self, provider_type: AIProviderType) -> bool:
+ """
+ 이미 등록된 프로바이더로 전환
+
+ Args:
+ provider_type: 전환할 프로바이더 타입
+
+ Returns:
+ 전환 성공 여부
+ """
+ if provider_type in self._providers:
+ self._current_provider = self._providers[provider_type]
+ return True
+ return False
+
+ def update_api_key(self, provider_type: AIProviderType, api_key: str) -> bool:
+ """
+ 프로바이더의 API 키 업데이트
+
+ Args:
+ provider_type: 프로바이더 타입
+ api_key: 새 API 키
+
+ Returns:
+ 업데이트 및 재초기화 성공 여부
+ """
+ if provider_type in self._providers:
+ provider = self._providers[provider_type]
+ provider.update_api_key(api_key)
+ return provider.initialize()
+ return False
+
+ def update_model(self, model: str) -> bool:
+ """
+ 현재 프로바이더의 모델 변경
+
+ Args:
+ model: 새 모델명
+
+ Returns:
+ 변경 성공 여부
+ """
+ if self._current_provider:
+ self._current_provider.model = model
+ return True
+ return False
+
+ def get_available_models(self) -> List[str]:
+ """현재 프로바이더의 사용 가능한 모델 목록"""
+ if self._current_provider:
+ return self._current_provider.available_models
+ return []
+
+ def load_from_settings(self, settings: 'AISettings') -> bool:
+ """
+ 설정에서 프로바이더 정보 로드
+
+ Args:
+ settings: AISettings 인스턴스
+
+ Returns:
+ 로드 성공 여부
+ """
+ self._settings = settings
+
+ # 현재 선택된 프로바이더 가져오기
+ current_type = settings.get_current_provider()
+ if not current_type:
+ return False
+
+ # API 키 가져오기
+ api_key = settings.get_api_key(current_type)
+ if not api_key:
+ return False
+
+ # 모델 가져오기
+ model = settings.get_model(current_type)
+
+ return self.set_provider(current_type, api_key, model)
+
+ def chat(
+ self,
+ user_message: str,
+ system_prompt: Optional[str] = None,
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """
+ 간편 채팅 메서드 (동기)
+
+ Args:
+ user_message: 사용자 메시지
+ system_prompt: 시스템 프롬프트 (선택)
+ temperature: 응답 다양성
+ max_tokens: 최대 토큰
+
+ Returns:
+ AI 응답
+ """
+ if not self._current_provider:
+ raise RuntimeError("프로바이더가 설정되지 않았습니다.")
+
+ messages = []
+ if system_prompt:
+ messages.append(AIMessage(role="system", content=system_prompt))
+ messages.append(AIMessage(role="user", content=user_message))
+
+ return self._current_provider.chat_sync(
+ messages=messages,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ **kwargs
+ )
+
+ async def chat_async(
+ self,
+ user_message: str,
+ system_prompt: Optional[str] = None,
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """
+ 간편 채팅 메서드 (비동기)
+
+ Args:
+ user_message: 사용자 메시지
+ system_prompt: 시스템 프롬프트 (선택)
+ temperature: 응답 다양성
+ max_tokens: 최대 토큰
+
+ Returns:
+ AI 응답
+ """
+ if not self._current_provider:
+ raise RuntimeError("프로바이더가 설정되지 않았습니다.")
+
+ messages = []
+ if system_prompt:
+ messages.append(AIMessage(role="system", content=system_prompt))
+ messages.append(AIMessage(role="user", content=user_message))
+
+ return await self._current_provider.chat(
+ messages=messages,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ **kwargs
+ )
+
+ def chat_with_history(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """
+ 대화 히스토리와 함께 채팅 (동기)
+
+ Args:
+ messages: 메시지 리스트
+ temperature: 응답 다양성
+ max_tokens: 최대 토큰
+
+ Returns:
+ AI 응답
+ """
+ if not self._current_provider:
+ raise RuntimeError("프로바이더가 설정되지 않았습니다.")
+
+ return self._current_provider.chat_sync(
+ messages=messages,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ **kwargs
+ )
+
+ async def chat_with_history_async(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """
+ 대화 히스토리와 함께 채팅 (비동기)
+
+ Args:
+ messages: 메시지 리스트
+ temperature: 응답 다양성
+ max_tokens: 최대 토큰
+
+ Returns:
+ AI 응답
+ """
+ if not self._current_provider:
+ raise RuntimeError("프로바이더가 설정되지 않았습니다.")
+
+ return await self._current_provider.chat(
+ messages=messages,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ **kwargs
+ )
+
+ def is_ready(self) -> bool:
+ """프로바이더가 사용 준비되었는지 확인"""
+ return self._current_provider is not None and self._current_provider._is_initialized
+
+ def get_status(self) -> Dict:
+ """현재 상태 정보 반환"""
+ if self._current_provider:
+ return {
+ "ready": self.is_ready(),
+ "provider": self._current_provider.provider_name,
+ "model": self._current_provider.model,
+ "api_key": self._current_provider.api_key, # 마스킹됨
+ }
+ return {
+ "ready": False,
+ "provider": None,
+ "model": None,
+ "api_key": None,
+ }
+
+
+# 싱글톤 인스턴스
+_ai_client_instance: Optional[AIClient] = None
+
+
+def get_ai_client() -> AIClient:
+ """AI 클라이언트 싱글톤 인스턴스 반환"""
+ global _ai_client_instance
+ if _ai_client_instance is None:
+ _ai_client_instance = AIClient()
+ return _ai_client_instance
+
diff --git a/app/ai/base.py b/app/ai/base.py
new file mode 100644
index 0000000..05dc7ef
--- /dev/null
+++ b/app/ai/base.py
@@ -0,0 +1,158 @@
+"""
+AI Provider 기본 클래스 정의
+모든 AI 프로바이더는 이 기본 클래스를 상속받아 구현합니다.
+"""
+
+from abc import ABC, abstractmethod
+from typing import Optional, Dict, Any, List
+from dataclasses import dataclass
+from enum import Enum
+
+
+class AIProviderType(Enum):
+ """지원하는 AI 프로바이더 타입"""
+ OPENAI = "openai"
+ OPENROUTER = "openrouter"
+ GEMINI = "gemini"
+ XAI = "xai"
+
+
+@dataclass
+class AIMessage:
+ """AI 메시지 데이터 클래스"""
+ role: str # 'system', 'user', 'assistant'
+ content: str
+
+
+@dataclass
+class AIResponse:
+ """AI 응답 데이터 클래스"""
+ content: str
+ model: str
+ provider: str
+ usage: Optional[Dict[str, int]] = None
+ raw_response: Optional[Any] = None
+
+
+class BaseAIProvider(ABC):
+ """
+ AI Provider 추상 기본 클래스
+ 모든 AI 프로바이더는 이 클래스를 상속받아 구현해야 합니다.
+ """
+
+ def __init__(self, api_key: str, model: Optional[str] = None):
+ """
+ Args:
+ api_key: API 키
+ model: 사용할 모델명 (None이면 기본 모델 사용)
+ """
+ self._api_key = api_key
+ self._model = model or self.default_model
+ self._is_initialized = False
+
+ @property
+ @abstractmethod
+ def provider_type(self) -> AIProviderType:
+ """프로바이더 타입 반환"""
+ pass
+
+ @property
+ @abstractmethod
+ def provider_name(self) -> str:
+ """프로바이더 이름 반환"""
+ pass
+
+ @property
+ @abstractmethod
+ def default_model(self) -> str:
+ """기본 모델명 반환"""
+ pass
+
+ @property
+ @abstractmethod
+ def available_models(self) -> List[str]:
+ """사용 가능한 모델 목록 반환"""
+ pass
+
+ @property
+ def model(self) -> str:
+ """현재 사용 중인 모델명"""
+ return self._model
+
+ @model.setter
+ def model(self, value: str):
+ """모델 변경"""
+ self._model = value
+
+ @property
+ def api_key(self) -> str:
+ """API 키 (마스킹된 값)"""
+ if self._api_key:
+ return self._api_key[:8] + "..." + self._api_key[-4:]
+ return ""
+
+ def update_api_key(self, api_key: str):
+ """API 키 업데이트"""
+ self._api_key = api_key
+ self._is_initialized = False
+
+ @abstractmethod
+ def initialize(self) -> bool:
+ """
+ 프로바이더 초기화 (클라이언트 생성 등)
+ Returns:
+ 초기화 성공 여부
+ """
+ pass
+
+ @abstractmethod
+ async def chat(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """
+ 채팅 요청 (비동기)
+
+ Args:
+ messages: 대화 메시지 리스트
+ temperature: 응답 다양성 (0.0 ~ 1.0)
+ max_tokens: 최대 토큰 수
+ **kwargs: 추가 파라미터
+
+ Returns:
+ AI 응답
+ """
+ pass
+
+ @abstractmethod
+ def chat_sync(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """
+ 채팅 요청 (동기)
+
+ Args:
+ messages: 대화 메시지 리스트
+ temperature: 응답 다양성 (0.0 ~ 1.0)
+ max_tokens: 최대 토큰 수
+ **kwargs: 추가 파라미터
+
+ Returns:
+ AI 응답
+ """
+ pass
+
+ def validate_api_key(self) -> bool:
+ """API 키 유효성 검사 (간단한 형식 체크)"""
+ return bool(self._api_key and len(self._api_key) > 10)
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}(model={self._model}, initialized={self._is_initialized})"
+
diff --git a/app/ai/interface.py b/app/ai/interface.py
new file mode 100644
index 0000000..a05b668
--- /dev/null
+++ b/app/ai/interface.py
@@ -0,0 +1,115 @@
+import polars as pl
+import json
+from typing import List, Dict, Any, Optional
+from app.core.reference_data import ref_data
+
+class MMIAIInterface:
+ """
+ Interface for AI to interact with MMI Data.
+ Provides tools and data access methods.
+ """
+
+ def __init__(self, parquet_path: str):
+ self.parquet_path = parquet_path
+ self.df: Optional[pl.DataFrame] = None
+ self._load_data()
+
+ def _load_data(self):
+ try:
+ self.df = pl.read_parquet(self.parquet_path)
+ except Exception as e:
+ print(f"[AI Interface] Load Error: {e}")
+ self.df = None
+
+ def get_tools_schema(self) -> List[Dict[str, Any]]:
+ """
+ Returns the JSON schema for tools available to the AI.
+ """
+ return [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_error_stats",
+ "description": "Get statistics of errors (e.g., ATC status, Warnings) for the current log.",
+ "parameters": {
+ "type": "object",
+ "properties": {},
+ "required": []
+ }
+ }
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "search_station_event",
+ "description": "Find events happening at a specific station.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "station_name": {"type": "string", "description": "Name of the station (e.g., '범어사')"}
+ },
+ "required": ["station_name"]
+ }
+ }
+ }
+ ]
+
+ def get_error_stats(self) -> Dict[str, Any]:
+ """
+ Tool: Get Error Statistics
+ """
+ if self.df is None:
+ return {"error": "No data loaded"}
+
+ # Example: Count ATC Status != 'ATC ACTIVE' (64)
+ # Note: atc_byte 64 is ACTIVE.
+ # We need to filter where system_id == 1 (Main) usually?
+ # Let's return stats for Main system (1)
+
+ main_df = self.df.filter(pl.col("system_id") == 1)
+ total_rows = len(main_df)
+
+ # Count non-active frames
+ # 64 is ATC ACTIVE.
+ non_active_count = main_df.filter(pl.col("atc_byte") != 64).height
+
+ return {
+ "total_frames": total_rows,
+ "non_active_frames": non_active_count,
+ "active_ratio": (total_rows - non_active_count) / total_rows if total_rows > 0 else 0
+ }
+
+ def search_station_event(self, station_name: str) -> Dict[str, Any]:
+ """
+ Tool: Search events at a station
+ """
+ if self.df is None:
+ return {"error": "No data loaded"}
+
+ # Filter by station name
+ # We assume 'station_name' column exists (added by FastParser)
+ station_df = self.df.filter(pl.col("station_name") == station_name)
+
+ if station_df.is_empty():
+ return {"message": f"No events found at {station_name}"}
+
+ # Return summary of that station visit
+ # e.g. avg speed, time range
+ return {
+ "station": station_name,
+ "visit_count": station_df.height,
+ "avg_speed": station_df["trainspeed"].mean(),
+ "start_time": str(station_df["timestamp"].min()),
+ "end_time": str(station_df["timestamp"].max())
+ }
+
+ def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
+ """
+ Dispatcher for tool execution.
+ """
+ if tool_name == "get_error_stats":
+ return self.get_error_stats()
+ elif tool_name == "search_station_event":
+ return self.search_station_event(arguments.get("station_name", ""))
+ else:
+ return {"error": "Unknown tool"}
diff --git a/app/ai/llm_engine.py b/app/ai/llm_engine.py
new file mode 100644
index 0000000..1ff6c7d
--- /dev/null
+++ b/app/ai/llm_engine.py
@@ -0,0 +1 @@
+# LangChain / Ollama Integration
diff --git a/app/ai/maintenance_kb.py b/app/ai/maintenance_kb.py
new file mode 100644
index 0000000..c4d07a3
--- /dev/null
+++ b/app/ai/maintenance_kb.py
@@ -0,0 +1,157 @@
+"""
+정비지침서(또는 추가 지식) JSON 로딩/검색 기반
+
+목표:
+- 사용자가 JSON 형태로 정비 지침서를 추가해두면 앱이 읽어서 AI 프롬프트에 관련 발췌를 넣을 수 있게 함
+
+기본 경로:
+- resources/maintenance_kb/*.json
+- ~/.mmi_analyzer/maintenance_kb/*.json
+
+JSON 포맷(권장, 유연하게 파싱):
+[
+ {
+ "id": "door_001",
+ "title": "출입문 불일치 대응",
+ "tags": ["door", "psd", "fault"],
+ "keywords": ["출입문", "PSD", "불일치", "DOORSTAT"],
+ "content": "정비 절차 ...",
+ "source": "정비지침서 v1.2"
+ }
+]
+"""
+
+from __future__ import annotations
+
+import json
+import re
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
+
+
+def _tokenize(text: str) -> List[str]:
+ text = (text or "").lower()
+ # 한글/영문/숫자 중심 토큰화 (아주 단순)
+ tokens = re.findall(r"[0-9a-z가-힣]{2,}", text)
+ return tokens
+
+
+@dataclass
+class KBDoc:
+ doc_id: str
+ title: str
+ content: str
+ tags: List[str]
+ keywords: List[str]
+ source: str
+ raw: Dict[str, Any]
+
+
+class MaintenanceKnowledgeBase:
+ def __init__(self):
+ self._docs: List[KBDoc] = []
+
+ self._resource_dir = Path(__file__).resolve().parents[2] / "resources" / "maintenance_kb"
+ self._user_dir = Path.home() / ".mmi_analyzer" / "maintenance_kb"
+
+ self._resource_dir.mkdir(parents=True, exist_ok=True)
+ self._user_dir.mkdir(parents=True, exist_ok=True)
+
+ def count(self) -> int:
+ return len(self._docs)
+
+ def reload(self) -> None:
+ self._docs = []
+ for base in [self._resource_dir, self._user_dir]:
+ for p in sorted(base.glob("*.json")):
+ self._load_file(p)
+
+ def _load_file(self, path: Path) -> None:
+ try:
+ data = json.loads(path.read_text(encoding="utf-8"))
+ except Exception:
+ return
+
+ items = data if isinstance(data, list) else [data]
+ for i, item in enumerate(items):
+ if not isinstance(item, dict):
+ continue
+ title = str(item.get("title") or item.get("name") or path.stem)
+ content = str(item.get("content") or item.get("text") or "")
+ tags = item.get("tags") or []
+ keywords = item.get("keywords") or []
+ source = str(item.get("source") or str(path.name))
+ doc_id = str(item.get("id") or f"{path.stem}:{i}")
+ if not content and item.get("steps"):
+ # steps 배열을 content로 합치기
+ try:
+ content = "\n".join([str(s) for s in item.get("steps")])
+ except Exception:
+ content = ""
+
+ self._docs.append(
+ KBDoc(
+ doc_id=doc_id,
+ title=title,
+ content=content,
+ tags=list(tags) if isinstance(tags, list) else [str(tags)],
+ keywords=list(keywords) if isinstance(keywords, list) else [str(keywords)],
+ source=source,
+ raw=item,
+ )
+ )
+
+ def search(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]:
+ q_tokens = set(_tokenize(query))
+ if not q_tokens or not self._docs:
+ return []
+
+ scored: List[Tuple[float, KBDoc]] = []
+ for d in self._docs:
+ hay = " ".join([d.title, d.content, " ".join(d.tags), " ".join(d.keywords)]).lower()
+ h_tokens = _tokenize(hay)
+ if not h_tokens:
+ continue
+
+ # 단순 점수: 토큰 매칭 수 + 키워드/태그 가중치
+ base = 0.0
+ for t in q_tokens:
+ if t in hay:
+ base += 1.0
+ for kw in d.keywords:
+ if kw and kw.lower() in query.lower():
+ base += 2.0
+ scored.append((base, d))
+
+ scored.sort(key=lambda x: x[0], reverse=True)
+ results = []
+ for score, d in scored[: max(0, top_k)]:
+ if score <= 0:
+ continue
+ snippet = d.content.strip().replace("\r\n", "\n")
+ if len(snippet) > 600:
+ snippet = snippet[:600] + "..."
+ results.append(
+ {
+ "id": d.doc_id,
+ "title": d.title,
+ "score": score,
+ "source": d.source,
+ "snippet": snippet,
+ }
+ )
+ return results
+
+
+_kb_singleton: Optional[MaintenanceKnowledgeBase] = None
+
+
+def get_maintenance_kb() -> MaintenanceKnowledgeBase:
+ global _kb_singleton
+ if _kb_singleton is None:
+ _kb_singleton = MaintenanceKnowledgeBase()
+ _kb_singleton.reload()
+ return _kb_singleton
+
+
diff --git a/app/ai/providers/__init__.py b/app/ai/providers/__init__.py
new file mode 100644
index 0000000..2c3a569
--- /dev/null
+++ b/app/ai/providers/__init__.py
@@ -0,0 +1,17 @@
+"""
+AI Providers 패키지
+각 AI 서비스 프로바이더 구현을 포함합니다.
+"""
+
+from .openai_provider import OpenAIProvider
+from .openrouter_provider import OpenRouterProvider
+from .gemini_provider import GeminiProvider
+from .xai_provider import XAIProvider
+
+__all__ = [
+ 'OpenAIProvider',
+ 'OpenRouterProvider',
+ 'GeminiProvider',
+ 'XAIProvider'
+]
+
diff --git a/app/ai/providers/__pycache__/__init__.cpython-311.pyc b/app/ai/providers/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..1ed0410
Binary files /dev/null and b/app/ai/providers/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ai/providers/__pycache__/gemini_provider.cpython-311.pyc b/app/ai/providers/__pycache__/gemini_provider.cpython-311.pyc
new file mode 100644
index 0000000..0605826
Binary files /dev/null and b/app/ai/providers/__pycache__/gemini_provider.cpython-311.pyc differ
diff --git a/app/ai/providers/__pycache__/openai_provider.cpython-311.pyc b/app/ai/providers/__pycache__/openai_provider.cpython-311.pyc
new file mode 100644
index 0000000..a1291cd
Binary files /dev/null and b/app/ai/providers/__pycache__/openai_provider.cpython-311.pyc differ
diff --git a/app/ai/providers/__pycache__/openrouter_provider.cpython-311.pyc b/app/ai/providers/__pycache__/openrouter_provider.cpython-311.pyc
new file mode 100644
index 0000000..b1b5974
Binary files /dev/null and b/app/ai/providers/__pycache__/openrouter_provider.cpython-311.pyc differ
diff --git a/app/ai/providers/__pycache__/xai_provider.cpython-311.pyc b/app/ai/providers/__pycache__/xai_provider.cpython-311.pyc
new file mode 100644
index 0000000..f49313f
Binary files /dev/null and b/app/ai/providers/__pycache__/xai_provider.cpython-311.pyc differ
diff --git a/app/ai/providers/gemini_provider.py b/app/ai/providers/gemini_provider.py
new file mode 100644
index 0000000..1a7645f
--- /dev/null
+++ b/app/ai/providers/gemini_provider.py
@@ -0,0 +1,156 @@
+"""
+Google Gemini Provider 구현
+"""
+
+import asyncio
+from typing import Optional, List, Dict, Any
+
+from ..base import BaseAIProvider, AIProviderType, AIMessage, AIResponse
+
+
+class GeminiProvider(BaseAIProvider):
+ """Google Gemini API 프로바이더"""
+
+ AVAILABLE_MODELS = [
+ "gemini-2.0-flash-exp",
+ "gemini-1.5-pro",
+ "gemini-1.5-flash",
+ "gemini-1.5-flash-8b",
+ "gemini-1.0-pro",
+ ]
+
+ def __init__(self, api_key: str, model: Optional[str] = None):
+ super().__init__(api_key, model)
+ self._client = None
+
+ @property
+ def provider_type(self) -> AIProviderType:
+ return AIProviderType.GEMINI
+
+ @property
+ def provider_name(self) -> str:
+ return "Google Gemini"
+
+ @property
+ def default_model(self) -> str:
+ return "gemini-1.5-flash"
+
+ @property
+ def available_models(self) -> List[str]:
+ return self.AVAILABLE_MODELS.copy()
+
+ def initialize(self) -> bool:
+ """Gemini 클라이언트 초기화"""
+ if not self.validate_api_key():
+ return False
+
+ try:
+ import google.generativeai as genai
+
+ genai.configure(api_key=self._api_key)
+ self._client = genai
+ self._is_initialized = True
+ return True
+ except ImportError:
+ print("Google Generative AI 라이브러리가 설치되지 않았습니다. pip install google-generativeai 를 실행하세요.")
+ return False
+ except Exception as e:
+ print(f"Gemini 초기화 실패: {e}")
+ return False
+
+ def _convert_messages_to_gemini(self, messages: List[AIMessage]) -> tuple:
+ """
+ AIMessage를 Gemini 형식으로 변환
+ Returns:
+ (system_instruction, history, last_user_message)
+ """
+ system_instruction = None
+ history = []
+ last_user_message = None
+
+ for msg in messages:
+ if msg.role == "system":
+ system_instruction = msg.content
+ elif msg.role == "user":
+ last_user_message = msg.content
+ # 이전 user 메시지는 history에 추가
+ if len([m for m in messages if m.role == "user"]) > 1:
+ history.append({"role": "user", "parts": [msg.content]})
+ elif msg.role == "assistant":
+ history.append({"role": "model", "parts": [msg.content]})
+
+ # 마지막 user 메시지를 history에서 제거 (send_message로 별도 전송)
+ if history and history[-1].get("role") == "user":
+ history.pop()
+
+ return system_instruction, history, last_user_message
+
+ async def chat(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """비동기 채팅 요청 (Gemini는 sync를 async로 래핑)"""
+ # Gemini SDK는 기본적으로 동기식이므로 executor에서 실행
+ loop = asyncio.get_event_loop()
+ return await loop.run_in_executor(
+ None,
+ lambda: self.chat_sync(messages, temperature, max_tokens, **kwargs)
+ )
+
+ def chat_sync(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """동기 채팅 요청"""
+ if not self._is_initialized:
+ if not self.initialize():
+ raise RuntimeError("Gemini 초기화 실패")
+
+ system_instruction, history, last_user_message = self._convert_messages_to_gemini(messages)
+
+ # 모델 생성 설정
+ generation_config = {
+ "temperature": temperature,
+ }
+ if max_tokens:
+ generation_config["max_output_tokens"] = max_tokens
+
+ # 모델 인스턴스 생성
+ model = self._client.GenerativeModel(
+ model_name=self._model,
+ generation_config=generation_config,
+ system_instruction=system_instruction
+ )
+
+ # 대화 시작
+ if history:
+ chat = model.start_chat(history=history)
+ else:
+ chat = model.start_chat()
+
+ # 메시지 전송
+ response = chat.send_message(last_user_message or "")
+
+ # 토큰 사용량 추출 시도
+ usage = None
+ if hasattr(response, 'usage_metadata'):
+ usage = {
+ "prompt_tokens": getattr(response.usage_metadata, 'prompt_token_count', 0),
+ "completion_tokens": getattr(response.usage_metadata, 'candidates_token_count', 0),
+ "total_tokens": getattr(response.usage_metadata, 'total_token_count', 0),
+ }
+
+ return AIResponse(
+ content=response.text,
+ model=self._model,
+ provider=self.provider_name,
+ usage=usage,
+ raw_response=response
+ )
+
diff --git a/app/ai/providers/openai_provider.py b/app/ai/providers/openai_provider.py
new file mode 100644
index 0000000..2d6177c
--- /dev/null
+++ b/app/ai/providers/openai_provider.py
@@ -0,0 +1,141 @@
+"""
+OpenAI Provider 구현
+"""
+
+import asyncio
+from typing import Optional, List, Dict, Any
+
+from ..base import BaseAIProvider, AIProviderType, AIMessage, AIResponse
+
+
+class OpenAIProvider(BaseAIProvider):
+ """OpenAI API 프로바이더"""
+
+ AVAILABLE_MODELS = [
+ "gpt-4o",
+ "gpt-4o-mini",
+ "gpt-4-turbo",
+ "gpt-4",
+ "gpt-3.5-turbo",
+ "o1-preview",
+ "o1-mini",
+ ]
+
+ def __init__(self, api_key: str, model: Optional[str] = None):
+ super().__init__(api_key, model)
+ self._client = None
+ self._async_client = None
+
+ @property
+ def provider_type(self) -> AIProviderType:
+ return AIProviderType.OPENAI
+
+ @property
+ def provider_name(self) -> str:
+ return "OpenAI"
+
+ @property
+ def default_model(self) -> str:
+ return "gpt-4o-mini"
+
+ @property
+ def available_models(self) -> List[str]:
+ return self.AVAILABLE_MODELS.copy()
+
+ def initialize(self) -> bool:
+ """OpenAI 클라이언트 초기화"""
+ if not self.validate_api_key():
+ return False
+
+ try:
+ from openai import OpenAI, AsyncOpenAI
+
+ self._client = OpenAI(api_key=self._api_key)
+ self._async_client = AsyncOpenAI(api_key=self._api_key)
+ self._is_initialized = True
+ return True
+ except ImportError:
+ print("OpenAI 라이브러리가 설치되지 않았습니다. pip install openai 를 실행하세요.")
+ return False
+ except Exception as e:
+ print(f"OpenAI 초기화 실패: {e}")
+ return False
+
+ def _convert_messages(self, messages: List[AIMessage]) -> List[Dict[str, str]]:
+ """AIMessage를 OpenAI 형식으로 변환"""
+ return [{"role": msg.role, "content": msg.content} for msg in messages]
+
+ async def chat(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """비동기 채팅 요청"""
+ if not self._is_initialized:
+ if not self.initialize():
+ raise RuntimeError("OpenAI 초기화 실패")
+
+ converted_messages = self._convert_messages(messages)
+
+ params = {
+ "model": self._model,
+ "messages": converted_messages,
+ "temperature": temperature,
+ }
+ if max_tokens:
+ params["max_tokens"] = max_tokens
+ params.update(kwargs)
+
+ response = await self._async_client.chat.completions.create(**params)
+
+ return AIResponse(
+ content=response.choices[0].message.content,
+ model=response.model,
+ provider=self.provider_name,
+ usage={
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ "total_tokens": response.usage.total_tokens,
+ } if response.usage else None,
+ raw_response=response
+ )
+
+ def chat_sync(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """동기 채팅 요청"""
+ if not self._is_initialized:
+ if not self.initialize():
+ raise RuntimeError("OpenAI 초기화 실패")
+
+ converted_messages = self._convert_messages(messages)
+
+ params = {
+ "model": self._model,
+ "messages": converted_messages,
+ "temperature": temperature,
+ }
+ if max_tokens:
+ params["max_tokens"] = max_tokens
+ params.update(kwargs)
+
+ response = self._client.chat.completions.create(**params)
+
+ return AIResponse(
+ content=response.choices[0].message.content,
+ model=response.model,
+ provider=self.provider_name,
+ usage={
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ "total_tokens": response.usage.total_tokens,
+ } if response.usage else None,
+ raw_response=response
+ )
+
diff --git a/app/ai/providers/openrouter_provider.py b/app/ai/providers/openrouter_provider.py
new file mode 100644
index 0000000..9472928
--- /dev/null
+++ b/app/ai/providers/openrouter_provider.py
@@ -0,0 +1,155 @@
+"""
+OpenRouter Provider 구현
+OpenRouter는 다양한 AI 모델에 대한 통합 API를 제공합니다.
+"""
+
+import asyncio
+from typing import Optional, List, Dict, Any
+
+from ..base import BaseAIProvider, AIProviderType, AIMessage, AIResponse
+
+
+class OpenRouterProvider(BaseAIProvider):
+ """OpenRouter API 프로바이더"""
+
+ BASE_URL = "https://openrouter.ai/api/v1"
+
+ AVAILABLE_MODELS = [
+ "openai/gpt-4o",
+ "openai/gpt-4o-mini",
+ "anthropic/claude-3.5-sonnet",
+ "anthropic/claude-3-opus",
+ "google/gemini-pro-1.5",
+ "google/gemini-flash-1.5",
+ "meta-llama/llama-3.1-70b-instruct",
+ "meta-llama/llama-3.1-8b-instruct",
+ "mistralai/mixtral-8x7b-instruct",
+ "deepseek/deepseek-chat",
+ ]
+
+ def __init__(self, api_key: str, model: Optional[str] = None):
+ super().__init__(api_key, model)
+ self._client = None
+ self._async_client = None
+
+ @property
+ def provider_type(self) -> AIProviderType:
+ return AIProviderType.OPENROUTER
+
+ @property
+ def provider_name(self) -> str:
+ return "OpenRouter"
+
+ @property
+ def default_model(self) -> str:
+ return "openai/gpt-4o-mini"
+
+ @property
+ def available_models(self) -> List[str]:
+ return self.AVAILABLE_MODELS.copy()
+
+ def initialize(self) -> bool:
+ """OpenRouter 클라이언트 초기화 (OpenAI 호환 API 사용)"""
+ if not self.validate_api_key():
+ return False
+
+ try:
+ from openai import OpenAI, AsyncOpenAI
+
+ self._client = OpenAI(
+ api_key=self._api_key,
+ base_url=self.BASE_URL
+ )
+ self._async_client = AsyncOpenAI(
+ api_key=self._api_key,
+ base_url=self.BASE_URL
+ )
+ self._is_initialized = True
+ return True
+ except ImportError:
+ print("OpenAI 라이브러리가 설치되지 않았습니다. pip install openai 를 실행하세요.")
+ return False
+ except Exception as e:
+ print(f"OpenRouter 초기화 실패: {e}")
+ return False
+
+ def _convert_messages(self, messages: List[AIMessage]) -> List[Dict[str, str]]:
+ """AIMessage를 OpenAI 형식으로 변환"""
+ return [{"role": msg.role, "content": msg.content} for msg in messages]
+
+ async def chat(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """비동기 채팅 요청"""
+ if not self._is_initialized:
+ if not self.initialize():
+ raise RuntimeError("OpenRouter 초기화 실패")
+
+ converted_messages = self._convert_messages(messages)
+
+ params = {
+ "model": self._model,
+ "messages": converted_messages,
+ "temperature": temperature,
+ }
+ if max_tokens:
+ params["max_tokens"] = max_tokens
+
+ # OpenRouter 특화 헤더는 extra_headers로 전달 가능
+ params.update(kwargs)
+
+ response = await self._async_client.chat.completions.create(**params)
+
+ return AIResponse(
+ content=response.choices[0].message.content,
+ model=response.model,
+ provider=self.provider_name,
+ usage={
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ "total_tokens": response.usage.total_tokens,
+ } if response.usage else None,
+ raw_response=response
+ )
+
+ def chat_sync(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """동기 채팅 요청"""
+ if not self._is_initialized:
+ if not self.initialize():
+ raise RuntimeError("OpenRouter 초기화 실패")
+
+ converted_messages = self._convert_messages(messages)
+
+ params = {
+ "model": self._model,
+ "messages": converted_messages,
+ "temperature": temperature,
+ }
+ if max_tokens:
+ params["max_tokens"] = max_tokens
+ params.update(kwargs)
+
+ response = self._client.chat.completions.create(**params)
+
+ return AIResponse(
+ content=response.choices[0].message.content,
+ model=response.model,
+ provider=self.provider_name,
+ usage={
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ "total_tokens": response.usage.total_tokens,
+ } if response.usage else None,
+ raw_response=response
+ )
+
diff --git a/app/ai/providers/xai_provider.py b/app/ai/providers/xai_provider.py
new file mode 100644
index 0000000..16b64c3
--- /dev/null
+++ b/app/ai/providers/xai_provider.py
@@ -0,0 +1,146 @@
+"""
+xAI (Grok) Provider 구현
+xAI API는 OpenAI 호환 형식을 사용합니다.
+"""
+
+import asyncio
+from typing import Optional, List, Dict, Any
+
+from ..base import BaseAIProvider, AIProviderType, AIMessage, AIResponse
+
+
+class XAIProvider(BaseAIProvider):
+ """xAI (Grok) API 프로바이더"""
+
+ BASE_URL = "https://api.x.ai/v1"
+
+ AVAILABLE_MODELS = [
+ "grok-beta",
+ "grok-2-1212",
+ "grok-2-vision-1212",
+ ]
+
+ def __init__(self, api_key: str, model: Optional[str] = None):
+ super().__init__(api_key, model)
+ self._client = None
+ self._async_client = None
+
+ @property
+ def provider_type(self) -> AIProviderType:
+ return AIProviderType.XAI
+
+ @property
+ def provider_name(self) -> str:
+ return "xAI (Grok)"
+
+ @property
+ def default_model(self) -> str:
+ return "grok-beta"
+
+ @property
+ def available_models(self) -> List[str]:
+ return self.AVAILABLE_MODELS.copy()
+
+ def initialize(self) -> bool:
+ """xAI 클라이언트 초기화 (OpenAI 호환 API 사용)"""
+ if not self.validate_api_key():
+ return False
+
+ try:
+ from openai import OpenAI, AsyncOpenAI
+
+ self._client = OpenAI(
+ api_key=self._api_key,
+ base_url=self.BASE_URL
+ )
+ self._async_client = AsyncOpenAI(
+ api_key=self._api_key,
+ base_url=self.BASE_URL
+ )
+ self._is_initialized = True
+ return True
+ except ImportError:
+ print("OpenAI 라이브러리가 설치되지 않았습니다. pip install openai 를 실행하세요.")
+ return False
+ except Exception as e:
+ print(f"xAI 초기화 실패: {e}")
+ return False
+
+ def _convert_messages(self, messages: List[AIMessage]) -> List[Dict[str, str]]:
+ """AIMessage를 OpenAI 형식으로 변환"""
+ return [{"role": msg.role, "content": msg.content} for msg in messages]
+
+ async def chat(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """비동기 채팅 요청"""
+ if not self._is_initialized:
+ if not self.initialize():
+ raise RuntimeError("xAI 초기화 실패")
+
+ converted_messages = self._convert_messages(messages)
+
+ params = {
+ "model": self._model,
+ "messages": converted_messages,
+ "temperature": temperature,
+ }
+ if max_tokens:
+ params["max_tokens"] = max_tokens
+ params.update(kwargs)
+
+ response = await self._async_client.chat.completions.create(**params)
+
+ return AIResponse(
+ content=response.choices[0].message.content,
+ model=response.model,
+ provider=self.provider_name,
+ usage={
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ "total_tokens": response.usage.total_tokens,
+ } if response.usage else None,
+ raw_response=response
+ )
+
+ def chat_sync(
+ self,
+ messages: List[AIMessage],
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ **kwargs
+ ) -> AIResponse:
+ """동기 채팅 요청"""
+ if not self._is_initialized:
+ if not self.initialize():
+ raise RuntimeError("xAI 초기화 실패")
+
+ converted_messages = self._convert_messages(messages)
+
+ params = {
+ "model": self._model,
+ "messages": converted_messages,
+ "temperature": temperature,
+ }
+ if max_tokens:
+ params["max_tokens"] = max_tokens
+ params.update(kwargs)
+
+ response = self._client.chat.completions.create(**params)
+
+ return AIResponse(
+ content=response.choices[0].message.content,
+ model=response.model,
+ provider=self.provider_name,
+ usage={
+ "prompt_tokens": response.usage.prompt_tokens,
+ "completion_tokens": response.usage.completion_tokens,
+ "total_tokens": response.usage.total_tokens,
+ } if response.usage else None,
+ raw_response=response
+ )
+
diff --git a/app/analysis/__init__.py b/app/analysis/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/analysis/correlation.py b/app/analysis/correlation.py
new file mode 100644
index 0000000..9251b91
--- /dev/null
+++ b/app/analysis/correlation.py
@@ -0,0 +1 @@
+# Signal Correlation Logic
diff --git a/app/analysis/pattern_match.py b/app/analysis/pattern_match.py
new file mode 100644
index 0000000..cbfd481
--- /dev/null
+++ b/app/analysis/pattern_match.py
@@ -0,0 +1 @@
+# Pattern Matching Logic
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/__pycache__/__init__.cpython-311.pyc b/app/core/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..eea890f
Binary files /dev/null and b/app/core/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/core/__pycache__/reference_data.cpython-311.pyc b/app/core/__pycache__/reference_data.cpython-311.pyc
new file mode 100644
index 0000000..42f6907
Binary files /dev/null and b/app/core/__pycache__/reference_data.cpython-311.pyc differ
diff --git a/app/core/__pycache__/reference_data_defaults.cpython-311.pyc b/app/core/__pycache__/reference_data_defaults.cpython-311.pyc
new file mode 100644
index 0000000..8e5e997
Binary files /dev/null and b/app/core/__pycache__/reference_data_defaults.cpython-311.pyc differ
diff --git a/app/core/__pycache__/settings.cpython-311.pyc b/app/core/__pycache__/settings.cpython-311.pyc
new file mode 100644
index 0000000..b862898
Binary files /dev/null and b/app/core/__pycache__/settings.cpython-311.pyc differ
diff --git a/app/core/__pycache__/sync_controller.cpython-311.pyc b/app/core/__pycache__/sync_controller.cpython-311.pyc
new file mode 100644
index 0000000..ee31355
Binary files /dev/null and b/app/core/__pycache__/sync_controller.cpython-311.pyc differ
diff --git a/app/core/ref_cache.db b/app/core/ref_cache.db
new file mode 100644
index 0000000..0a8ebda
Binary files /dev/null and b/app/core/ref_cache.db differ
diff --git a/app/core/reference_data.py b/app/core/reference_data.py
new file mode 100644
index 0000000..d46087f
--- /dev/null
+++ b/app/core/reference_data.py
@@ -0,0 +1,255 @@
+"""
+통합 레퍼런스 데이터 관리자
+SQLite 로컬 캐시 + Supabase 동기화 준비
+"""
+
+import os
+import sqlite3
+from typing import Dict, Optional, List
+from dataclasses import dataclass
+from enum import Enum
+from pathlib import Path
+
+from app.core.reference_data_defaults import (
+ DEFAULT_STATIONS,
+ DEFAULT_ERROR_CODES,
+ REFERENCE_DATA_VERSION
+)
+
+
+class DoorDirection(Enum):
+ DW = "DW" # Door West (Left)
+ DE = "DE" # Door East (Right)
+
+
+@dataclass
+class StationMeta:
+ code: int
+ name: str
+ pos: float
+ door_dir: DoorDirection
+ station_no: int
+ track_line: str # 'TC1' or 'TC2'
+
+
+class ReferenceDataManager:
+ """
+ 통합 레퍼런스 데이터 관리자
+ - SQLite 로컬 캐싱
+ - 메모리 캐시로 빠른 조회
+ - Supabase 동기화 준비
+ """
+ _instance = None
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super(ReferenceDataManager, cls).__new__(cls)
+ cls._instance._initialized = False
+ return cls._instance
+
+ def __init__(self):
+ if self._initialized:
+ return
+
+ self._initialized = True
+
+ # DB 경로 설정
+ self.db_path = Path(__file__).parent / "ref_cache.db"
+
+ # 메모리 캐시
+ self.tc1_stations: Dict[int, StationMeta] = {}
+ self.tc2_stations: Dict[int, StationMeta] = {}
+ self.station_name_map: Dict[str, int] = {}
+ self.error_code_map: Dict[str, str] = {}
+
+ self._is_loaded = False
+
+ # 초기화
+ self._init_db()
+ self._load_to_memory()
+
+ def _init_db(self):
+ """DB 초기화 (첫 실행 시 기본 데이터로 생성)"""
+ db_exists = self.db_path.exists()
+
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+
+ # 테이블 생성
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS stations (
+ code INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ pos REAL NOT NULL,
+ door_dir TEXT NOT NULL,
+ station_no INTEGER NOT NULL,
+ track_line TEXT NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS metadata (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS error_codes (
+ code TEXT PRIMARY KEY,
+ description TEXT NOT NULL
+ )
+ """)
+
+ # 인덱스 생성
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_station_name ON stations(name)")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_track_line ON stations(track_line)")
+
+ # 첫 실행 시 기본 데이터 삽입
+ if not db_exists:
+ print("[RefData] Initializing database with default data...")
+
+ # 역 정보 삽입
+ for station in DEFAULT_STATIONS:
+ cursor.execute("""
+ INSERT INTO stations (code, name, pos, door_dir, station_no, track_line)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """, (
+ station["code"],
+ station["name"],
+ station["pos"],
+ station["door_dir"],
+ station["station_no"],
+ station["track_line"]
+ ))
+
+ # 에러 코드 삽입
+ for code, desc in DEFAULT_ERROR_CODES.items():
+ cursor.execute("""
+ INSERT INTO error_codes (code, description)
+ VALUES (?, ?)
+ """, (code, desc))
+
+ # 버전 정보 삽입
+ cursor.execute("""
+ INSERT INTO metadata (key, value)
+ VALUES ('version', ?)
+ """, (REFERENCE_DATA_VERSION,))
+
+ conn.commit()
+ print(f"[RefData] Database initialized with version {REFERENCE_DATA_VERSION}")
+
+ conn.close()
+
+ def _load_to_memory(self):
+ """SQLite에서 메모리로 로드 (빠른 조회를 위한 캐싱)"""
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+
+ # 역 정보 로드
+ cursor.execute("SELECT code, name, pos, door_dir, station_no, track_line FROM stations")
+ rows = cursor.fetchall()
+
+ for row in rows:
+ code, name, pos, door_dir, station_no, track_line = row
+
+ station = StationMeta(
+ code=code,
+ name=name,
+ pos=pos,
+ door_dir=DoorDirection(door_dir),
+ station_no=station_no,
+ track_line=track_line
+ )
+
+ if track_line == "TC1":
+ self.tc1_stations[code] = station
+ elif track_line == "TC2":
+ self.tc2_stations[code] = station
+
+ # 이름 매핑 (TC1 우선)
+ if track_line == "TC1":
+ self.station_name_map[name] = code
+
+ # TC2 이름 매핑 (TC1에 없는 경우만)
+ for code, station in self.tc2_stations.items():
+ if station.name not in self.station_name_map:
+ self.station_name_map[station.name] = code
+
+ # 에러 코드 로드
+ cursor.execute("SELECT code, description FROM error_codes")
+ for code, desc in cursor.fetchall():
+ self.error_code_map[code] = desc
+
+ conn.close()
+
+ self._is_loaded = True
+ print(f"[RefData] Loaded from cache. TC1: {len(self.tc1_stations)}, TC2: {len(self.tc2_stations)}")
+
+ # ========== 조회 메서드 (기존 API 호환) ==========
+
+ def get_station_name(self, code: int) -> str:
+ """역 코드로 역 이름 조회"""
+ if code in self.tc1_stations:
+ return self.tc1_stations[code].name
+ if code in self.tc2_stations:
+ return self.tc2_stations[code].name
+ return ""
+
+ def get_station_info(self, code: int) -> Optional[StationMeta]:
+ """역 코드로 역 정보 조회"""
+ if code in self.tc1_stations:
+ return self.tc1_stations[code]
+ if code in self.tc2_stations:
+ return self.tc2_stations[code]
+ return None
+
+ def get_station_code(self, name: str) -> int:
+ """역 이름으로 역 코드 조회"""
+ return self.station_name_map.get(name, 0)
+
+ def get_station_distance(self, station_name: str, is_tc1: bool) -> float:
+ """역 이름으로 거리 조회"""
+ target_list = self.tc1_stations if is_tc1 else self.tc2_stations
+ for station in target_list.values():
+ if station.name == station_name:
+ return station.pos
+ return 0.0
+
+ def get_door_direction(self, station_name_or_code, is_tc1: bool) -> DoorDirection:
+ """역 이름 또는 코드로 도어 방향 조회"""
+ target_list = self.tc1_stations if is_tc1 else self.tc2_stations
+
+ for station in target_list.values():
+ if isinstance(station_name_or_code, str):
+ if station.name == station_name_or_code:
+ return station.door_dir
+ else:
+ if station.code == station_name_or_code:
+ return station.door_dir
+
+ return DoorDirection.DW
+
+ # ========== Supabase 동기화 (추후 구현) ==========
+
+ async def sync_from_supabase(self):
+ """
+ Supabase에서 최신 데이터 동기화
+ TODO: 추후 구현
+ """
+ pass
+
+ def get_version(self) -> str:
+ """현재 데이터 버전 조회"""
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+ cursor.execute("SELECT value FROM metadata WHERE key = 'version'")
+ result = cursor.fetchone()
+ conn.close()
+ return result[0] if result else "unknown"
+
+
+# 전역 인스턴스
+ref_data = ReferenceDataManager()
diff --git a/app/core/reference_data_defaults.py b/app/core/reference_data_defaults.py
new file mode 100644
index 0000000..2533200
--- /dev/null
+++ b/app/core/reference_data_defaults.py
@@ -0,0 +1,29 @@
+"""
+기본 레퍼런스 데이터 정의
+첫 실행 시 SQLite DB 초기화에 사용됨
+"""
+
+from typing import List, Dict, Any
+
+# 역 정보 기본 데이터
+DEFAULT_STATIONS: List[Dict[str, Any]] = [
+ # TC1 (하선) - 예시 데이터, 실제 데이터로 교체 필요
+ {"code": 1001, "name": "역1", "pos": 0.0, "door_dir": "DW", "station_no": 1, "track_line": "TC1"},
+ {"code": 1002, "name": "역2", "pos": 1500.0, "door_dir": "DE", "station_no": 2, "track_line": "TC1"},
+ {"code": 1003, "name": "역3", "pos": 3200.0, "door_dir": "DW", "station_no": 3, "track_line": "TC1"},
+
+ # TC2 (상선) - 예시 데이터, 실제 데이터로 교체 필요
+ {"code": 2001, "name": "역1", "pos": 0.0, "door_dir": "DE", "station_no": 1, "track_line": "TC2"},
+ {"code": 2002, "name": "역2", "pos": 1500.0, "door_dir": "DW", "station_no": 2, "track_line": "TC2"},
+ {"code": 2003, "name": "역3", "pos": 3200.0, "door_dir": "DE", "station_no": 3, "track_line": "TC2"},
+]
+
+# 에러 코드 매핑 (예시)
+DEFAULT_ERROR_CODES: Dict[str, str] = {
+ "E001": "통신 오류",
+ "E002": "센서 이상",
+ "E003": "제동 시스템 오류",
+}
+
+# 메타데이터 버전 정보
+REFERENCE_DATA_VERSION = "1.0.0"
diff --git a/app/core/settings.py b/app/core/settings.py
new file mode 100644
index 0000000..7d46394
--- /dev/null
+++ b/app/core/settings.py
@@ -0,0 +1,255 @@
+"""
+Settings management
+애플리케이션 설정 관리 모듈
+
+- 일반 앱 설정: ~/.mmi_analyzer/settings.json
+- AI 설정(키 포함): ~/.mmi_analyzer/ai_settings.json (별도 파일)
+"""
+
+import json
+from pathlib import Path
+from typing import Optional, Dict, Any
+from dataclasses import dataclass, field
+
+
+@dataclass
+class AIProviderConfig:
+ """개별 AI 프로바이더 설정"""
+ api_key: str = ""
+ model: str = ""
+ enabled: bool = False
+
+
+@dataclass
+class AISettings:
+ """AI 관련 설정"""
+ current_provider: str = "" # 현재 선택된 프로바이더
+ providers: Dict[str, Dict[str, Any]] = field(default_factory=dict)
+
+ def __post_init__(self):
+ # 기본 프로바이더 설정 초기화
+ default_providers = {
+ "openai": {"api_key": "", "model": "gpt-4o-mini", "enabled": False},
+ "openrouter": {"api_key": "", "model": "openai/gpt-4o-mini", "enabled": False},
+ "gemini": {"api_key": "", "model": "gemini-1.5-flash", "enabled": False},
+ "xai": {"api_key": "", "model": "grok-beta", "enabled": False},
+ }
+ for key, value in default_providers.items():
+ if key not in self.providers:
+ self.providers[key] = value
+
+ def get_api_key(self, provider_type: str) -> str:
+ """프로바이더의 API 키 반환"""
+ provider_key = provider_type.value if hasattr(provider_type, 'value') else provider_type
+ if provider_key in self.providers:
+ return self.providers[provider_key].get("api_key", "")
+ return ""
+
+ def set_api_key(self, provider_type: str, api_key: str):
+ """프로바이더의 API 키 설정"""
+ provider_key = provider_type.value if hasattr(provider_type, 'value') else provider_type
+ if provider_key not in self.providers:
+ self.providers[provider_key] = {}
+ self.providers[provider_key]["api_key"] = api_key
+ if api_key:
+ self.providers[provider_key]["enabled"] = True
+
+ def get_model(self, provider_type: str) -> str:
+ """프로바이더의 모델 반환"""
+ provider_key = provider_type.value if hasattr(provider_type, 'value') else provider_type
+ if provider_key in self.providers:
+ return self.providers[provider_key].get("model", "")
+ return ""
+
+ def set_model(self, provider_type: str, model: str):
+ """프로바이더의 모델 설정"""
+ provider_key = provider_type.value if hasattr(provider_type, 'value') else provider_type
+ if provider_key not in self.providers:
+ self.providers[provider_key] = {}
+ self.providers[provider_key]["model"] = model
+
+ def get_current_provider(self) -> Optional[str]:
+ """현재 선택된 프로바이더 타입 반환"""
+ return self.current_provider if self.current_provider else None
+
+ def set_current_provider(self, provider_type: str):
+ """현재 프로바이더 설정"""
+ provider_key = provider_type.value if hasattr(provider_type, 'value') else provider_type
+ self.current_provider = provider_key
+
+ def is_provider_configured(self, provider_type: str) -> bool:
+ """프로바이더가 설정되었는지 확인"""
+ provider_key = provider_type.value if hasattr(provider_type, 'value') else provider_type
+ if provider_key in self.providers:
+ api_key = self.providers[provider_key].get("api_key", "")
+ return bool(api_key)
+ return False
+
+
+@dataclass
+class AppSettings:
+ """전체 애플리케이션 설정"""
+ ai: AISettings = field(default_factory=AISettings)
+ theme: str = "dark"
+ language: str = "ko"
+ window_geometry: Dict[str, int] = field(default_factory=lambda: {
+ "x": 100, "y": 100, "width": 1800, "height": 1000
+ })
+ recent_files: list = field(default_factory=list)
+ max_recent_files: int = 10
+
+
+class SettingsManager:
+ """
+ 설정 관리자
+ 설정 파일 저장/로드 및 관리
+ """
+
+ DEFAULT_SETTINGS_DIR = Path.home() / ".mmi_analyzer"
+ APP_SETTINGS_FILE = "settings.json"
+ AI_SETTINGS_FILE = "ai_settings.json"
+
+ def __init__(self, settings_dir: Optional[Path] = None):
+ self._settings_dir = settings_dir or self.DEFAULT_SETTINGS_DIR
+ self._app_settings_file = self._settings_dir / self.APP_SETTINGS_FILE
+ self._ai_settings_file = self._settings_dir / self.AI_SETTINGS_FILE
+ self._settings: Optional[AppSettings] = None
+
+ # 설정 디렉토리 생성
+ self._settings_dir.mkdir(parents=True, exist_ok=True)
+
+ @property
+ def settings(self) -> AppSettings:
+ """현재 설정 반환 (없으면 로드)"""
+ if self._settings is None:
+ self.load()
+ return self._settings
+
+ @property
+ def ai_settings(self) -> AISettings:
+ """AI 설정 반환"""
+ return self.settings.ai
+
+ def load(self) -> AppSettings:
+ """설정 파일에서 로드"""
+ app_data: Dict[str, Any] = {}
+ ai_data: Dict[str, Any] = {}
+
+ # 1) 앱 설정 로드
+ if self._app_settings_file.exists():
+ try:
+ with open(self._app_settings_file, "r", encoding="utf-8") as f:
+ app_data = json.load(f) or {}
+ except Exception as e:
+ print(f"앱 설정 로드 실패: {e}")
+ app_data = {}
+
+ # 2) AI 설정 로드 (별도 파일)
+ if self._ai_settings_file.exists():
+ try:
+ with open(self._ai_settings_file, "r", encoding="utf-8") as f:
+ ai_data = json.load(f) or {}
+ except Exception as e:
+ print(f"AI 설정 로드 실패: {e}")
+ ai_data = {}
+ else:
+ # 3) 마이그레이션: 과거 settings.json 안의 ai 섹션이 있으면 ai_settings.json로 이동
+ legacy_ai = (app_data or {}).get("ai")
+ if isinstance(legacy_ai, dict) and legacy_ai:
+ ai_data = legacy_ai
+ try:
+ # 저장 후 app_data에서 ai 섹션 제거
+ with open(self._ai_settings_file, "w", encoding="utf-8") as f:
+ json.dump(ai_data, f, indent=2, ensure_ascii=False)
+ app_data.pop("ai", None)
+ with open(self._app_settings_file, "w", encoding="utf-8") as f:
+ json.dump(app_data, f, indent=2, ensure_ascii=False)
+ except Exception as e:
+ print(f"AI 설정 마이그레이션 실패: {e}")
+
+ # AI 설정 복원
+ ai_settings = AISettings(
+ current_provider=ai_data.get("current_provider", "") if isinstance(ai_data, dict) else "",
+ providers=ai_data.get("providers", {}) if isinstance(ai_data, dict) else {},
+ )
+
+ self._settings = AppSettings(
+ ai=ai_settings,
+ theme=app_data.get("theme", "dark"),
+ language=app_data.get("language", "ko"),
+ window_geometry=app_data.get("window_geometry", {}),
+ recent_files=app_data.get("recent_files", []),
+ max_recent_files=app_data.get("max_recent_files", 10),
+ )
+
+ return self._settings
+
+ def save(self):
+ """설정을 파일에 저장"""
+ if self._settings is None:
+ return
+
+ # 앱 설정 저장 (AI 제외)
+ try:
+ app_data = {
+ "theme": self._settings.theme,
+ "language": self._settings.language,
+ "window_geometry": self._settings.window_geometry,
+ "recent_files": self._settings.recent_files,
+ "max_recent_files": self._settings.max_recent_files,
+ }
+ with open(self._app_settings_file, "w", encoding="utf-8") as f:
+ json.dump(app_data, f, indent=2, ensure_ascii=False)
+ except Exception as e:
+ print(f"앱 설정 저장 실패: {e}")
+
+ # AI 설정 저장 (별도)
+ try:
+ ai_data = {
+ "current_provider": self._settings.ai.current_provider,
+ "providers": self._settings.ai.providers,
+ }
+ with open(self._ai_settings_file, "w", encoding="utf-8") as f:
+ json.dump(ai_data, f, indent=2, ensure_ascii=False)
+ except Exception as e:
+ print(f"AI 설정 저장 실패: {e}")
+
+ def reset(self):
+ """설정 초기화"""
+ self._settings = AppSettings()
+ self.save()
+
+ def add_recent_file(self, file_path: str):
+ """최근 파일 추가"""
+ if file_path in self.settings.recent_files:
+ self.settings.recent_files.remove(file_path)
+
+ self.settings.recent_files.insert(0, file_path)
+
+ # 최대 개수 제한
+ while len(self.settings.recent_files) > self.settings.max_recent_files:
+ self.settings.recent_files.pop()
+
+ self.save()
+
+
+# 싱글톤 인스턴스
+_settings_manager: Optional[SettingsManager] = None
+
+
+def get_settings_manager() -> SettingsManager:
+ """설정 관리자 싱글톤 인스턴스 반환"""
+ global _settings_manager
+ if _settings_manager is None:
+ _settings_manager = SettingsManager()
+ return _settings_manager
+
+
+def get_settings() -> AppSettings:
+ """현재 설정 반환"""
+ return get_settings_manager().settings
+
+
+def get_ai_settings() -> AISettings:
+ """AI 설정 반환"""
+ return get_settings_manager().ai_settings
diff --git a/app/core/sync_controller.py b/app/core/sync_controller.py
new file mode 100644
index 0000000..ef0fe97
--- /dev/null
+++ b/app/core/sync_controller.py
@@ -0,0 +1,39 @@
+from PySide6.QtCore import QObject, Signal
+
+class SyncController(QObject):
+ # 시점 변경 시그널 (인덱스, 데이터행, 보낸사람ID)
+ time_changed = Signal(int, object, str)
+
+ # X축 범위 변경 시그널 (min_ms, max_ms, 보낸사람ID)
+ # 밀리초(ms)는 1.7e12 수준이라 32-bit int를 초과할 수 있어 object로 받음(오버플로우 방지)
+ x_range_changed = Signal(object, object, object)
+
+ # 동기화 활성화 여부
+ is_sync_enabled = True
+
+ def __init__(self):
+ super().__init__()
+ self._total_records = 0
+
+ def request_sync(self, index, data_row, source_id):
+ """
+ 특정 패널에서 시간 변경 요청이 오면,
+ 동기화가 켜져 있을 때만 다른 패널들에게 전파
+ """
+ if self.is_sync_enabled:
+ # Sync가 켜져있으면 모두에게 알림 (source_id 포함)
+ self.time_changed.emit(index, data_row, source_id)
+ else:
+ # Sync가 꺼져있으면 아무것도 안 함 (각자 놈)
+ pass
+
+ def request_x_range_sync(self, min_ms: int, max_ms: int, source_id):
+ """
+ 특정 패널에서 X축(시간축) 범위 변경 요청이 오면,
+ 동기화가 켜져 있을 때만 다른 패널들에게 전파
+ """
+ if self.is_sync_enabled:
+ # int 캐스팅은 유지하되 시그널 타입은 object라 안전(파이썬 int 그대로 전달)
+ self.x_range_changed.emit(int(min_ms), int(max_ms), source_id)
+
+sync_manager = SyncController()
\ No newline at end of file
diff --git a/app/core/theme_manager.py.PY b/app/core/theme_manager.py.PY
new file mode 100644
index 0000000..f815684
--- /dev/null
+++ b/app/core/theme_manager.py.PY
@@ -0,0 +1,37 @@
+from PySide6.QtGui import QColor, QPalette
+from PySide6.QtWidgets import QApplication
+
+class ThemeManager:
+ """애플리케이션 전체의 테마(Light/Dark)를 관리"""
+
+ LIGHT_THEME = {
+ "bg": "#FFFFFF", "fg": "#000000", "panel": "#F0F0F0",
+ "accent": "#0078D7", "border": "#CCCCCC", "text": "#333333"
+ }
+
+ DARK_THEME = {
+ "bg": "#1E1E1E", "fg": "#FFFFFF", "panel": "#2D2D30",
+ "accent": "#007ACC", "border": "#3E3E42", "text": "#D4D4D4"
+ }
+
+ @staticmethod
+ def apply_theme(app: QApplication, theme="dark"):
+ palette = QPalette()
+ colors = ThemeManager.DARK_THEME if theme == "dark" else ThemeManager.LIGHT_THEME
+
+ # 기본 팔레트 설정 (Qt 기본 위젯용)
+ # (상세 팔레트 설정 코드는 생략하고 스타일시트 위주로 적용)
+
+ # 공통 스타일시트 (Fusion 스타일 + 커스텀)
+ qss = f"""
+ QMainWindow {{ background-color: {colors['bg']}; color: {colors['text']}; }}
+ QWidget {{ background-color: {colors['bg']}; color: {colors['text']}; font-family: 'Malgun Gothic', 'Segoe UI'; font-size: 10pt; }}
+ QTabWidget::pane {{ border: 1px solid {colors['border']}; background: {colors['panel']}; }}
+ QTabBar::tab {{ background: {colors['bg']}; border: 1px solid {colors['border']}; padding: 8px 16px; margin-right: 2px; }}
+ QTabBar::tab:selected {{ background: {colors['accent']}; color: white; font-weight: bold; }}
+ QSplitter::handle {{ background-color: {colors['border']}; width: 2px; }}
+ QPushButton {{ background-color: {colors['panel']}; border: 1px solid {colors['border']}; padding: 6px 12px; border-radius: 4px; }}
+ QPushButton:hover {{ background-color: {colors['accent']}; color: white; }}
+ QStatusBar {{ background: {colors['panel']}; color: {colors['text']}; border-top: 1px solid {colors['border']}; }}
+ """
+ app.setStyleSheet(qss)
\ No newline at end of file
diff --git a/app/data/__init__.py b/app/data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/data/__pycache__/__init__.cpython-311.pyc b/app/data/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..711cea3
Binary files /dev/null and b/app/data/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/data/__pycache__/data_processor.cpython-311.pyc b/app/data/__pycache__/data_processor.cpython-311.pyc
new file mode 100644
index 0000000..dd73d91
Binary files /dev/null and b/app/data/__pycache__/data_processor.cpython-311.pyc differ
diff --git a/app/data/__pycache__/fast_parser.cpython-311.pyc b/app/data/__pycache__/fast_parser.cpython-311.pyc
new file mode 100644
index 0000000..d7405ca
Binary files /dev/null and b/app/data/__pycache__/fast_parser.cpython-311.pyc differ
diff --git a/app/data/__pycache__/log_parser.cpython-311.pyc b/app/data/__pycache__/log_parser.cpython-311.pyc
new file mode 100644
index 0000000..63dec11
Binary files /dev/null and b/app/data/__pycache__/log_parser.cpython-311.pyc differ
diff --git a/app/data/__pycache__/station_codes.cpython-311.pyc b/app/data/__pycache__/station_codes.cpython-311.pyc
new file mode 100644
index 0000000..715a2ad
Binary files /dev/null and b/app/data/__pycache__/station_codes.cpython-311.pyc differ
diff --git a/app/data/__pycache__/station_info.cpython-311.pyc b/app/data/__pycache__/station_info.cpython-311.pyc
new file mode 100644
index 0000000..3ee960e
Binary files /dev/null and b/app/data/__pycache__/station_info.cpython-311.pyc differ
diff --git a/app/data/db_handler.py b/app/data/db_handler.py
new file mode 100644
index 0000000..962882b
--- /dev/null
+++ b/app/data/db_handler.py
@@ -0,0 +1 @@
+# SQLite/JSON handler
diff --git a/app/data/fast_parser.py b/app/data/fast_parser.py
new file mode 100644
index 0000000..5ec27d4
--- /dev/null
+++ b/app/data/fast_parser.py
@@ -0,0 +1,254 @@
+import os
+import numpy as np
+import os
+import numpy as np
+import polars as pl
+from typing import Optional
+
+class FastLogParser:
+ CHUNK_SIZE = 84
+
+ @staticmethod
+ def parse_to_parquet(input_path: str, output_path: str = None) -> Optional[str]:
+ """
+ Numpy 벡터 연산을 사용하여 MMI 바이너리 로그를 초고속으로 파싱 후 Parquet 저장.
+ (VDI/VDO 포함 모든 필드 완벽 구현)
+ """
+ if not os.path.exists(input_path):
+ return None
+
+ file_size = os.path.getsize(input_path)
+ n_records = file_size // FastLogParser.CHUNK_SIZE
+
+ if n_records == 0:
+ return None
+
+ try:
+ with open(input_path, 'rb') as f:
+ # 파일 전체를 한번에 메모리로 로드 (가장 빠름)
+ raw_data = np.fromfile(f, dtype=np.uint8)
+
+ # 잘린 마지막 청크 제거 및 Reshape
+ limit = n_records * FastLogParser.CHUNK_SIZE
+ raw_data = raw_data[:limit]
+ # (Row수, 84바이트) 형태의 2차원 배열로 변환
+ m = raw_data.reshape(n_records, FastLogParser.CHUNK_SIZE)
+
+ # --- [1. Basic Info] ---
+ seq = m[:, 1]
+ source = m[:, 3]
+ # 16(0x10) or 80(0x50) -> 1계, 그 외 -> 2계
+ system_id = np.where((source == 16) | (source == 80), 1, 2).astype(np.uint8)
+ # 이중계 판단 (32가 아니면 Break/Standby)
+ is_break = (source != 32)
+
+ # Time (BCD-like hex display: 0x25 -> 25)
+ # (val // 16) * 10 + (val % 16) 로직이 맞음
+ def bcd(arr): return (arr // 16) * 10 + (arr % 16)
+
+ year = bcd(m[:, 4]).astype(np.int16) + 2000
+ month = bcd(m[:, 5])
+ day = bcd(m[:, 6])
+ hour = bcd(m[:, 7])
+ minute = bcd(m[:, 8])
+ second = bcd(m[:, 9])
+
+ # --- [2. Speed & Analog] ---
+ # uint8 두 개를 합쳐 uint16으로 변환 시 반드시 먼저 astype(uint16)을 해야 함 (안 그러면 오버플로우)
+ trainspeed = ((m[:, 10].astype(np.uint16) << 8) | m[:, 11].astype(np.uint16)) / 10.0
+ limitspeed = m[:, 12]
+ pwm_value = m[:, 22]
+ ato_limitSpeed = m[:, 33]
+ tasc_value = m[:, 33]
+
+ # --- [3. Digital Flags (Bitwise Vectorized)] ---
+ # Byte 13: DO ATC
+ b13 = m[:, 13]
+ do_zvr = (b13 & 32) > 0; do_edl = (b13 & 16) > 0; do_edr = (b13 & 8) > 0
+ do_fsb = (b13 & 4) > 0; do_ebm = (b13 & 2) > 0; do_ebp = (b13 & 1) > 0
+
+ # Byte 14: Status
+ b14 = m[:, 14]
+ system_active = (b14 & 128) > 0; over_spd_warning = (b14 & 64) > 0
+ tcr = (b14 & 32) > 0; hcr = (b14 & 16) > 0
+ door_open = (b14 & 8) > 0; door_close = (b14 & 4) > 0
+ psd_open = (b14 & 2) > 0; psd_close = (b14 & 1) > 0
+
+ # Byte 15: Mode
+ b15 = m[:, 15]
+ fa = (b15 & 128) > 0; auto = (b15 & 64) > 0; mcs = (b15 & 32) > 0
+ yard = (b15 & 16) > 0; fmc = (b15 & 8) > 0
+ reverser_rvs = (b15 & 4) > 0; reverser_fwd = (b15 & 2) > 0; reverser_neu = (b15 & 1) > 0
+
+ # Byte 16: Mascon
+ b16 = m[:, 16]
+ ato_start_btn = (b16 & 128) > 0; ato_eb_req = (b16 & 64) > 0
+ tacho_dir_a = (b16 & 32) > 0; tacho_dir_b = (b16 & 16) > 0
+ mascon_dr = (b16 & 2) > 0; mascon_br = (b16 & 4) > 0; mascon_eb = (b16 & 8) > 0
+
+ # Byte 19: ATC Status
+ b19 = m[:, 19]
+ val_19 = b19 & 192 # 0, 192, 64, 128
+ wheelcheck = (b19 & 32) > 0
+
+ # Byte 20: Fail
+ b20 = m[:, 20]
+ fail_atcr = (b20 & 128) > 0; fail_atoc = (b20 & 64) > 0; fail_tcms = (b20 & 32) > 0
+ fail_tacho2 = (b20 & 2) > 0; fail_tacho1 = (b20 & 1) > 0
+
+ # Byte 21: Marker
+ b21 = m[:, 21]
+ recovery = (b21 & 128) > 0; nomal = (b21 & 64) > 0; tasc = (b21 & 32) > 0
+ marker_val = b21 & 31 # 16, 8, 4, 2, 1
+
+ # Byte 23: RLY ATO
+ b23 = m[:, 23]
+ trac_dr = (b23 & 128) > 0; trac_br = (b23 & 64) > 0; trac_cs = (b23 & 32) > 0
+ ador = (b23 & 16) > 0; adol = (b23 & 8) > 0; adc = (b23 & 4) > 0
+ start_enable = (b23 & 2) > 0; trainberth = (b23 & 1) > 0
+
+ # Byte 24: TCMS
+ b24 = m[:, 24]
+ tc2 = (b24 & 128) > 0; tc1 = (b24 & 64) > 0; tascdb = (b24 & 32) > 0
+
+ # Byte 25: ETC
+ b25 = m[:, 25]
+ pre_brake = (b25 & 128) > 0; limit_drive = (b25 & 64) > 0
+ ov_stop1 = (b25 & 8) > 0; ov_stop2 = (b25 & 4) > 0
+ sh_stop1 = (b25 & 2) > 0; sh_stop2 = (b25 & 1) > 0
+
+ # Byte 26-27: Train No
+ trainno_int = (m[:, 26].astype(np.uint16) << 8) | m[:, 27].astype(np.uint16)
+
+ # Byte 28-30: Stations
+ pstn = m[:, 28]; nstn = m[:, 29]; dstn = m[:, 30]
+
+ # Byte 31-32: DTG (Distance to Go) - 정밀 로직 구현
+ num_dtg = (m[:, 31].astype(np.uint16) << 8) | m[:, 32].astype(np.uint16)
+
+ # DTG 부호 처리: uint16을 int16으로 해석해서 음수 여부 판단
+ # C# 로직: if (num_dtg & 32768) != 0: num_dtg -= 65536
+ # Numpy에서는 그냥 .view(np.int16)하거나 astype(np.int16)하면 32768(0x8000) 이상은 자동으로 음수가 됨.
+ dtg_signed = num_dtg.astype(np.int16)
+
+ # 조건: TASC 모드가 아니면서 속도가 0이고 역행(Dr)이 아닐 때 부호 반전
+ cond_reverse = (~tasc) & (trainspeed == 0.0) & (~trac_dr)
+
+ # np.where(조건, 참일때값, 거짓일때값)
+ # 참일 때: (값 * -1) / 100.0
+ # 거짓일 때: TASC면 / 100.0, 아니면 / 10.0
+ dtg = np.where(
+ cond_reverse,
+ dtg_signed * -1.0 / 100.0,
+ np.where(tasc, num_dtg / 100.0, num_dtg / 10.0)
+ )
+
+ # Byte 34: TWC
+ b34 = m[:, 34]
+ twct_enable = (b34 & 16) > 0; door_close_warning = (b34 & 8) > 0; wrongdoor = (b34 & 4) > 0
+
+ # Byte 35: ATC Code
+ b35 = m[:, 35]
+ atc_idx = (b35 & 240) >> 4
+ osc_f0_ok = (b35 & 8) > 0
+
+ # Byte 36-39, 55, 58: Freq (XOR)
+ atc_code_carrier_f = (((m[:, 36]^65).astype(np.uint16) << 8) | (m[:, 37]^82).astype(np.uint16)) * 10.0
+ atc_code_f = (((m[:, 38]^99).astype(np.uint16) << 8) | (m[:, 39]^116).astype(np.uint16)) / 10.0
+ osc_f = (((m[:, 55]^99).astype(np.uint16) << 8) | (m[:, 58]^100).astype(np.uint16)) / 10.0
+
+ # --- [Missing Fields Restored: VDI/VDO] ---
+ # VDI A (63-65)
+ vdia_rvs = (m[:, 63] & 128) > 0; vdia_neu = (m[:, 63] & 64) > 0; vdia_fwd = (m[:, 63] & 32) > 0
+ vdia_mascondr = (m[:, 63] & 16) > 0; vdia_masconbr = (m[:, 63] & 8) > 0; vdia_masconeb = (m[:, 63] & 4) > 0
+ vdia_doorclose = (m[:, 63] & 2) > 0; vdia_dooropen = (m[:, 63] & 1) > 0
+ vdia_fmc = (m[:, 64] & 64) > 0; vdia_yard = (m[:, 64] & 32) > 0; vdia_mcs = (m[:, 64] & 16) > 0
+ vdia_auto = (m[:, 64] & 8) > 0; vdia_fa = (m[:, 64] & 4) > 0
+
+ # VDI B (66-68)
+ vdib_rvs = (m[:, 66] & 128) > 0; vdib_neu = (m[:, 66] & 64) > 0; vdib_fwd = (m[:, 66] & 32) > 0
+ vdib_mascondr = (m[:, 66] & 16) > 0; vdib_masconbr = (m[:, 66] & 8) > 0; vdib_masconeb = (m[:, 66] & 4) > 0
+ vdib_doorclose = (m[:, 66] & 2) > 0; vdib_dooropen = (m[:, 66] & 1) > 0
+
+ # VDI C (69-71)
+ vdic_tc2 = (m[:, 69] & 128) > 0; vdic_tc1 = (m[:, 69] & 64) > 0
+ vdic_edlfb = (m[:, 69] & 32) > 0; vdic_edrfb = (m[:, 69] & 16) > 0
+ vdic_psdclose = (m[:, 70] & 2) > 0; vdic_psdopen = (m[:, 70] & 1) > 0
+
+ # VDO A (75-76)
+ vdoa_edl = (m[:, 75] & 32) > 0; vdoa_edr = (m[:, 75] & 16) > 0
+ vdoa_zvr = (m[:, 75] & 8) > 0; vdoa_fsb = (m[:, 75] & 4) > 0
+
+ # --- [4. Create DataFrame] ---
+ data_dict = {
+ "seq": seq, "source": source, "system_id": system_id, "is_break": is_break,
+ "year": year, "month": month, "day": day, "hour": hour, "minute": minute, "second": second,
+ "trainspeed": trainspeed, "limitspeed": limitspeed, "pwm_value": pwm_value,
+ "ato_limitSpeed": ato_limitSpeed, "tasc_value": tasc_value,
+ "do_zvr": do_zvr, "do_edl": do_edl, "do_edr": do_edr, "do_fsb": do_fsb, "do_ebm": do_ebm, "do_ebp": do_ebp,
+ "system_active": system_active, "over_spd_warning": over_spd_warning, "tcr": tcr, "hcr": hcr,
+ "door_open": door_open, "door_close": door_close, "psd_open": psd_open, "psd_close": psd_close,
+ "fa": fa, "auto": auto, "mcs": mcs, "yard": yard, "fmc": fmc,
+ "reverser_rvs": reverser_rvs, "reverser_fwd": reverser_fwd, "reverser_neu": reverser_neu,
+ "ato_start_btn": ato_start_btn, "ato_eb_req": ato_eb_req,
+ "tacho_dir_a": tacho_dir_a, "tacho_dir_b": tacho_dir_b,
+ "mascon_dr": mascon_dr, "mascon_br": mascon_br, "mascon_eb": mascon_eb,
+ "atc_status_code": val_19,
+ "fail_atcr": fail_atcr, "fail_atoc": fail_atoc, "fail_tcms": fail_tcms,
+ "recovery": recovery, "nomal": nomal, "tasc": tasc, "marker_val": marker_val,
+ "trac_dr": trac_dr, "trac_br": trac_br, "trac_cs": trac_cs,
+ "tc2": tc2, "tc1": tc1, "tascdb": tascdb,
+ "pre_brake": pre_brake, "limit_drive": limit_drive,
+ "trainno_int": trainno_int,
+ "pstn": pstn, "nstn": nstn, "dstn": dstn,
+ "dtg": dtg,
+ "twct_enable": twct_enable, "door_close_warning": door_close_warning, "wrongdoor": wrongdoor,
+ "atc_idx": atc_idx, "osc_f0_ok": osc_f0_ok,
+ "atc_code_carrier_f": atc_code_carrier_f, "atc_code_f": atc_code_f, "osc_f": osc_f,
+
+ # 복원된 VDI/VDO
+ "vdia_rvs": vdia_rvs, "vdia_neu": vdia_neu, "vdia_fwd": vdia_fwd,
+ "vdia_mascondr": vdia_mascondr, "vdia_masconbr": vdia_masconbr,
+ "vdia_doorclose": vdia_doorclose, "vdia_dooropen": vdia_dooropen,
+ "vdib_doorclose": vdib_doorclose, "vdib_dooropen": vdib_dooropen,
+ "vdoa_edl": vdoa_edl, "vdoa_edr": vdoa_edr, "vdoa_zvr": vdoa_zvr
+ }
+
+ df = pl.DataFrame(data_dict)
+
+ # --- [5. Post-Processing] ---
+ # Polars의 강력한 문자열 처리 기능 사용 (Lambda 루프 제거 -> 속도 향상)
+
+ # 시간 문자열 생성
+ df = df.with_columns(
+ pl.format("{}-{}-{} {}:{}:{}",
+ pl.col("year"),
+ pl.col("month").cast(pl.String).str.zfill(2),
+ pl.col("day").cast(pl.String).str.zfill(2),
+ pl.col("hour").cast(pl.String).str.zfill(2),
+ pl.col("minute").cast(pl.String).str.zfill(2),
+ pl.col("second").cast(pl.String).str.zfill(2)
+ ).alias("time")
+ )
+
+ # 열차번호 Hex String 변환
+ df = df.with_columns(
+ pl.col("trainno_int")
+ .map_elements(lambda x: f"{x:04X}", return_dtype=pl.String)
+ .alias("trainno")
+ )
+
+ # [Optional] 여기서 역 이름 매핑이나 코드 변환을 수행할 수 있음
+ # (이전 대화의 Processor 로직을 여기에 통합해도 됨)
+
+ if output_path is None:
+ output_path = input_path.replace(".dat", ".parquet")
+
+ df.write_parquet(output_path, compression="zstd")
+ return output_path
+
+ except Exception as e:
+ print(f"[FastParser] Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return None
\ No newline at end of file
diff --git a/app/data/log_manager.py b/app/data/log_manager.py
new file mode 100644
index 0000000..bb6609b
--- /dev/null
+++ b/app/data/log_manager.py
@@ -0,0 +1 @@
+# Data caching and management
diff --git a/app/data/log_parser.py b/app/data/log_parser.py
new file mode 100644
index 0000000..2829951
--- /dev/null
+++ b/app/data/log_parser.py
@@ -0,0 +1,448 @@
+import struct
+import os
+from dataclasses import dataclass, field
+from typing import List, Optional
+
+# -------------------------------------------------------------------------
+# 1. 데이터 구조 정의 (C# DOSItoMMIClass 완벽 대응)
+# -------------------------------------------------------------------------
+@dataclass
+class DOSItoMMIClass:
+ # [기본 정보]
+ seq: int = 0
+ source: int = 0 # 16/80: 1계, 그외: 2계
+ time: str = ""
+ trainno: str = "" # 열차번호
+
+ # [속도 및 아날로그 값]
+ trainspeed: float = 0.0 # 현재 속도
+ limitspeed: int = 0 # 제한 속도
+ pwm_value: int = 0 # PWM
+ dtg: float = 0.0 # 잔여 거리
+ ato_limitSpeed: int = 0 # ATO 제한 속도
+ tasc_value: int = 0 # 그래프용(ato_limitSpeed와 동일)
+
+ # [ATC/ATO 주파수 및 코드 (XOR 복호화 데이터)]
+ osc_f: float = 0.0 # OSC 주파수
+ atc_code_carrier_f: float = 0.0 # ATC Carrier Freq
+ atc_code_f: float = 0.0 # ATC Code Freq
+ atc_carrier_f: str = "-" # F1~F4 문자열
+
+ # [ATC 카드 상태 - Byte 13, 14, 19]
+ do_zvr: bool = False; do_edl: bool = False; do_edr: bool = False
+ do_fsb: bool = False; do_ebm: bool = False; do_ebp: bool = False
+
+ system_active: bool = False; over_spd_warning: bool = False
+ tcr: bool = False; hcr: bool = False
+ door_open: bool = False; door_close: bool = False
+ psd_open: bool = False; psd_close: bool = False
+
+ atc_status: str = "-" # INITIAL, MANUAL, ACTIVE, STANDBY
+ wheelcheck: bool = False
+ mpdt: str = "-" # START, NG, OK
+ ipdt: str = "-" # NG, OK
+
+ # [운전 모드 및 마스콘 - Byte 15, 16]
+ fa: bool = False; auto: bool = False; mcs: bool = False
+ yard: bool = False; fmc: bool = False
+
+ reverser_rvs: bool = False; reverser_fwd: bool = False; reverser_neu: bool = False
+
+ ato_start_btn: bool = False; ato_eb_req: bool = False
+ tacho_dir_a: bool = False; tacho_dir_b: bool = False
+ mascon_neu: bool = False; mascon_dr: bool = False
+ mascon_br: bool = False; mascon_eb: bool = False
+
+ # [FAIL 카드 정보 - Byte 20]
+ fail_atcr: bool = False; fail_atoc: bool = False; fail_tcms: bool = False
+ fail_tacho2: bool = False; fail_tacho1: bool = False
+
+ # [마커 및 TWC - Byte 21, 28-30, 34]
+ recovery: bool = False; nomal: bool = False; tasc: bool = False
+ marker: str = "-" # ATS, PGX, PG3-2, PG2, PG1
+
+ pstn: int = 0; nstn: int = 0; dstn: int = 0 # 전역, 현역, 종착역
+ twct_enable: bool = False # TWC 수신 여부
+ door_close_warning: bool = False # DCW
+ wrongdoor: bool = False
+ nextdoor: str = "-"
+ station_name: str = "" # 역 이름 (TrackInfo 매핑)
+
+ # [ATO RLY 및 기타 - Byte 23, 24, 25]
+ trac_dr: bool = False; trac_br: bool = False; trac_cs: bool = False
+ ador: bool = False; adol: bool = False; adc: bool = False
+ start_enable: bool = False; trainberth: bool = False
+
+ tc2: bool = False; tc1: bool = False; tascdb: bool = False
+ tcmsdoor: str = "-" # LDO, RDO
+ doormod: str = "-" # A/A, A/M, M/M
+
+ pre_brake: bool = False; limit_drive: bool = False
+ ov_stop1: bool = False; ov_stop2: bool = False
+ sh_stop1: bool = False; sh_stop2: bool = False
+
+ # [ATC 코드 상세 - Byte 35]
+ atc_code: str = "-" # 01, 02, 25 ...
+ atc_code_str: str = "-" # 호환용(동일 값)
+ atc_code_val: int = 0 # 그래프용 속도값
+ osc_f0_ok: bool = False
+
+ # [에러 메시지 모음]
+ diagnostic_err_msg: str = ""
+ atc_err_msg: str = ""
+ interface_err_msg: str = ""
+ ato_err_msg: str = ""
+
+ # [VDI/VDO 카드 (DIO) - Byte 63~78]
+ # VDI A
+ vdia_stat: str = ""; vdia_input: str = ""
+ vdia_rvs: bool = False; vdia_neu: bool = False; vdia_fwd: bool = False
+ vdia_mascondr: bool = False; vdia_masconbr: bool = False; vdia_masconeb: bool = False
+ vdia_doorclose: bool = False; vdia_dooropen: bool = False
+ vdia_fmc: bool = False; vdia_yard: bool = False; vdia_mcs: bool = False
+ vdia_auto: bool = False; vdia_fa: bool = False; vdia_tcr: bool = False; vdia_hcr: bool = False
+
+ # VDI B
+ vdib_stat: str = ""; vdib_input: str = ""
+ vdib_rvs: bool = False; vdib_neu: bool = False; vdib_fwd: bool = False
+ vdib_mascondr: bool = False; vdib_masconbr: bool = False; vdib_masconeb: bool = False
+ vdib_doorclose: bool = False; vdib_dooropen: bool = False
+ vdib_fmc: bool = False; vdib_yard: bool = False; vdib_mcs: bool = False
+ vdib_auto: bool = False; vdib_fa: bool = False; vdib_tcr: bool = False; vdib_hcr: bool = False
+
+ # VDI C
+ vdic_stat: str = ""; vdic_input: str = ""
+ vdic_tc2: bool = False; vdic_tc1: bool = False
+ vdic_edlfb: bool = False; vdic_edrfb: bool = False; vdic_zvrfb: bool = False
+ vdic_fsbfb: bool = False; vdic_ebmfb: bool = False; vdic_ebpfb: bool = False
+ vdic_unit1: bool = False; vdic_startbtn: bool = False
+ vdic_psdclose: bool = False; vdic_psdopen: bool = False
+
+ # VDI D
+ vdid_stat: str = ""; vdid_input: str = ""
+ vdid_tc2: bool = False; vdid_tc1: bool = False
+ vdid_edlfb: bool = False; vdid_edrfb: bool = False; vdid_zvrfb: bool = False
+ vdid_fsbfb: bool = False; vdid_ebmfb: bool = False; vdid_ebpfb: bool = False
+ vdid_unit1: bool = False; vdid_startbtn: bool = False
+ vdid_psdclose: bool = False; vdid_psdopen: bool = False
+
+ # VDO A/B
+ vdoa_stat: str = ""; vdob_stat: str = ""
+ vdoa_edl: bool = False; vdoa_edr: bool = False; vdoa_zvr: bool = False
+ vdoa_fsb: bool = False; vdoa_ebm: bool = False; vdoa_ebp: bool = False
+ vdob_edl: bool = False; vdob_edr: bool = False; vdob_zvr: bool = False
+ vdob_fsb: bool = False; vdob_ebm: bool = False; vdob_ebp: bool = False
+
+
+# -------------------------------------------------------------------------
+# 2. 핵심 파싱 로직 (C# DDOSItoMMI.cs 정밀 이식)
+# -------------------------------------------------------------------------
+class DDOSItoMMI:
+ @staticmethod
+ def set_data(data: bytes) -> DOSItoMMIClass:
+ mmi = DOSItoMMIClass()
+
+ # 데이터 길이 보호
+ if len(data) < 84:
+ return mmi
+
+ # [Header]
+ mmi.seq = data[1]
+ mmi.source = data[3]
+
+ # [Time] "20YY.MM.DD HH:mm:ss"
+ try:
+ mmi.time = (f"20{data[4]:02x}.{data[5]:02x}.{data[6]:02x} "
+ f"{data[7]:02x}:{data[8]:02x}:{data[9]:02x}")
+ except:
+ mmi.time = "Invalid Time"
+
+ # [Speed] Byte 10-11
+ speed_raw = (data[10] << 8) | data[11]
+ mmi.trainspeed = speed_raw / 10.0
+ mmi.limitspeed = data[12]
+
+ # [DO ATC / Byte 13]
+ mmi.do_zvr = (data[13] & 32) > 0
+ mmi.do_edl = (data[13] & 16) > 0
+ mmi.do_edr = (data[13] & 8) > 0
+ mmi.do_fsb = (data[13] & 4) > 0
+ mmi.do_ebm = (data[13] & 2) > 0
+ mmi.do_ebp = (data[13] & 1) > 0
+
+ # [Status / Byte 14]
+ mmi.system_active = (data[14] & 128) > 0
+ mmi.over_spd_warning = (data[14] & 64) > 0
+ mmi.tcr = (data[14] & 32) > 0
+ mmi.hcr = (data[14] & 16) > 0
+ mmi.door_open = (data[14] & 8) > 0
+ mmi.door_close = (data[14] & 4) > 0
+ mmi.psd_open = (data[14] & 2) > 0
+ mmi.psd_close = (data[14] & 1) > 0
+
+ # [Mode / Byte 15]
+ mmi.fa = (data[15] & 128) > 0
+ mmi.auto = (data[15] & 64) > 0
+ mmi.mcs = (data[15] & 32) > 0
+ mmi.yard = (data[15] & 16) > 0
+ mmi.fmc = (data[15] & 8) > 0
+ mmi.reverser_rvs = (data[15] & 4) > 0
+ mmi.reverser_fwd = (data[15] & 2) > 0
+ mmi.reverser_neu = (data[15] & 1) > 0
+
+ # [Mascon & ATO / Byte 16]
+ mmi.ato_start_btn = (data[16] & 128) > 0
+ mmi.ato_eb_req = (data[16] & 64) > 0
+ mmi.tacho_dir_a = (data[16] & 32) > 0
+ mmi.tacho_dir_b = (data[16] & 16) > 0
+ mmi.mascon_neu = (data[16] & 0) > 0 # Logic Copy
+ mmi.mascon_dr = (data[16] & 2) > 0
+ mmi.mascon_br = (data[16] & 4) > 0
+ mmi.mascon_eb = (data[16] & 8) > 0
+
+ # [ATC Status / Byte 19]
+ val_19 = data[19] & 192
+ if val_19 == 0: mmi.atc_status = "INITIAL PDT"
+ elif val_19 == 192: mmi.atc_status = "MANUAL PDT"
+ elif val_19 == 64: mmi.atc_status = "ATC ACTIVE"
+ elif val_19 == 128: mmi.atc_status = "ATC STANDBY"
+
+ mmi.wheelcheck = (data[19] & 32) > 0
+
+ val_mpdt = data[19] & 28
+ if val_mpdt == 16: mmi.mpdt = "START"
+ elif val_mpdt == 8: mmi.mpdt = "NG"
+ elif val_mpdt == 4: mmi.mpdt = "OK"
+
+ val_ipdt = data[19] & 3
+ if val_ipdt == 2: mmi.ipdt = "NG"
+ elif val_ipdt == 1: mmi.ipdt = "OK"
+
+ # [Fail / Byte 20]
+ mmi.fail_atcr = (data[20] & 128) > 0
+ mmi.fail_atoc = (data[20] & 64) > 0
+ mmi.fail_tcms = (data[20] & 32) > 0
+ mmi.fail_tacho2 = (data[20] & 2) > 0
+ mmi.fail_tacho1 = (data[20] & 1) > 0
+
+ # [Marker / Byte 21]
+ mmi.recovery = (data[21] & 128) > 0
+ mmi.nomal = (data[21] & 64) > 0
+ mmi.tasc = (data[21] & 32) > 0
+
+ val_21 = data[21] & 31
+ if val_21 == 16: mmi.marker = "ATS"
+ elif val_21 == 8: mmi.marker = "PGX"
+ elif val_21 == 4: mmi.marker = "PG3-2"
+ elif val_21 == 2: mmi.marker = "PG2"
+ elif val_21 == 1: mmi.marker = "PG1"
+
+ # [PWM / Byte 22]
+ mmi.pwm_value = data[22]
+
+ # [RLY ATO / Byte 23]
+ mmi.trac_dr = (data[23] & 128) > 0
+ mmi.trac_br = (data[23] & 64) > 0
+ mmi.trac_cs = (data[23] & 32) > 0
+ mmi.ador = (data[23] & 16) > 0
+ mmi.adol = (data[23] & 8) > 0
+ mmi.adc = (data[23] & 4) > 0
+ mmi.start_enable = (data[23] & 2) > 0
+ mmi.trainberth = (data[23] & 1) > 0
+
+ # [TCMS / Byte 24]
+ mmi.tc2 = (data[24] & 128) > 0
+ mmi.tc1 = (data[24] & 64) > 0
+ mmi.tascdb = (data[24] & 32) > 0
+
+ val_tcms_door = data[24] & 24
+ if val_tcms_door == 16: mmi.tcmsdoor = "LDO"
+ elif val_tcms_door == 8: mmi.tcmsdoor = "RDO"
+ elif val_tcms_door == 24: mmi.tcmsdoor = "L,RDO"
+
+ val_door_mode = data[24] & 7
+ if val_door_mode == 4: mmi.doormod = "A / A"
+ elif val_door_mode == 2: mmi.doormod = "A / M"
+ elif val_door_mode == 1: mmi.doormod = "M / M"
+
+ # [ETC / Byte 25]
+ mmi.pre_brake = (data[25] & 128) > 0
+ mmi.limit_drive = (data[25] & 64) > 0
+ mmi.ov_stop1 = (data[25] & 8) > 0
+ mmi.ov_stop2 = (data[25] & 4) > 0
+ mmi.sh_stop1 = (data[25] & 2) > 0
+ mmi.sh_stop2 = (data[25] & 1) > 0
+
+ # [Train No / Byte 26-27]
+ mmi.trainno = f"{(data[26] << 8 | data[27]):04X}"
+
+ # [Stations / Byte 28-30]
+ mmi.pstn = data[28]
+ mmi.nstn = data[29]
+ mmi.dstn = data[30]
+
+ # [DTG / Byte 31-32]
+ num_dtg = (data[31] << 8) | data[32]
+ if mmi.tasc:
+ mmi.dtg = num_dtg / 100.0
+ else:
+ if mmi.trainspeed == 0.0 and not mmi.trac_dr:
+ if (num_dtg & 32768) != 0:
+ num_dtg -= 65536
+ mmi.dtg = (num_dtg / 100.0) * -1.0
+ else:
+ mmi.dtg = num_dtg / 10.0
+
+ # [ATO Limit / Byte 33]
+ mmi.ato_limitSpeed = data[33]
+ mmi.tasc_value = data[33]
+
+ # [TWC / Byte 34]
+ # kur(128), osc(64), inching(32), twct_enable(16), dcw(8), wrongdoor(4)
+ mmi.twct_enable = (data[34] & 16) > 0
+ mmi.door_close_warning = (data[34] & 8) > 0
+ mmi.wrongdoor = (data[34] & 4) > 0
+
+ val_nextdoor = data[34] & 3
+ if val_nextdoor == 2: mmi.nextdoor = "RIGHT"
+ elif val_nextdoor == 1: mmi.nextdoor = "LEFT"
+
+ # [ATC Code / Byte 35]
+ # 상위 4비트: Code Index
+ atc_idx = (data[35] & 240) >> 4
+ atc_str_map = {0:"02", 1:"01", 2:"25", 3:"40", 4:"55", 5:"65", 6:"75", 7:"DE", 8:"DW"}
+ mmi.atc_code = atc_str_map.get(atc_idx, "-")
+ mmi.atc_code_str = mmi.atc_code
+
+ # 그래프용 ATC 코드 속도 매핑 유지
+ atc_speed_map = {0: 0, 1: 0, 2: 25, 3: 40, 4: 55, 5: 65, 6: 75, 7: -1, 8: -1}
+ mmi.atc_code_val = atc_speed_map.get(atc_idx, 0)
+
+ mmi.osc_f0_ok = (data[35] & 8) > 0
+
+ # 하위 3비트: Carrier Freq
+ carr_idx = data[35] & 7
+ if carr_idx == 1: mmi.atc_carrier_f = "F1"
+ elif carr_idx == 2: mmi.atc_carrier_f = "F2"
+ elif carr_idx == 3: mmi.atc_carrier_f = "F3"
+ elif carr_idx == 4: mmi.atc_carrier_f = "F4"
+
+ # [Frequency Calculations (XOR Obfuscation) / Byte 36-39, 55, 58]
+ mmi.atc_code_carrier_f = ((data[36] ^ 65) << 8 | (data[37] ^ 82)) * 10.0
+ mmi.atc_code_f = ((data[38] ^ 99) << 8 | (data[39] ^ 116)) / 10.0
+ mmi.osc_f = ((data[55] ^ 99) << 8 | (data[58] ^ 100)) / 10.0
+
+ # [VDI A - Byte 63, 64, 65]
+ mmi.vdia_rvs = (data[63] & 128) > 0
+ mmi.vdia_neu = (data[63] & 64) > 0
+ mmi.vdia_fwd = (data[63] & 32) > 0
+ mmi.vdia_mascondr = (data[63] & 16) > 0
+ mmi.vdia_masconbr = (data[63] & 8) > 0
+ mmi.vdia_masconeb = (data[63] & 4) > 0
+ mmi.vdia_doorclose = (data[63] & 2) > 0
+ mmi.vdia_dooropen = (data[63] & 1) > 0
+
+ mmi.vdia_fmc = (data[64] & 64) > 0
+ mmi.vdia_yard = (data[64] & 32) > 0
+ mmi.vdia_mcs = (data[64] & 16) > 0
+ mmi.vdia_auto = (data[64] & 8) > 0
+ mmi.vdia_fa = (data[64] & 4) > 0
+ mmi.vdia_tcr = (data[64] & 2) > 0
+ mmi.vdia_hcr = (data[64] & 1) > 0
+ mmi.vdia_stat = f"{data[65]:02X}"
+
+ # [VDI B - Byte 66, 67, 68]
+ mmi.vdib_rvs = (data[66] & 128) > 0
+ mmi.vdib_neu = (data[66] & 64) > 0
+ mmi.vdib_fwd = (data[66] & 32) > 0
+ mmi.vdib_mascondr = (data[66] & 16) > 0
+ mmi.vdib_masconbr = (data[66] & 8) > 0
+ mmi.vdib_masconeb = (data[66] & 4) > 0
+ mmi.vdib_doorclose = (data[66] & 2) > 0
+ mmi.vdib_dooropen = (data[66] & 1) > 0
+
+ mmi.vdib_fmc = (data[67] & 64) > 0
+ mmi.vdib_yard = (data[67] & 32) > 0
+ mmi.vdib_mcs = (data[67] & 16) > 0
+ mmi.vdib_auto = (data[67] & 8) > 0
+ mmi.vdib_fa = (data[67] & 4) > 0
+ mmi.vdib_tcr = (data[67] & 2) > 0
+ mmi.vdib_hcr = (data[67] & 1) > 0
+ mmi.vdib_stat = f"{data[68]:02X}"
+
+ # [VDI C - Byte 69, 70, 71]
+ mmi.vdic_tc2 = (data[69] & 128) > 0
+ mmi.vdic_tc1 = (data[69] & 64) > 0
+ mmi.vdic_edlfb = (data[69] & 32) > 0
+ mmi.vdic_edrfb = (data[69] & 16) > 0
+ mmi.vdic_zvrfb = (data[69] & 8) > 0
+ mmi.vdic_fsbfb = (data[69] & 4) > 0
+ mmi.vdic_ebmfb = (data[69] & 2) > 0
+ mmi.vdic_ebpfb = (data[69] & 1) > 0
+
+ mmi.vdic_unit1 = (data[70] & 128) > 0
+ mmi.vdic_startbtn = (data[70] & 4) > 0
+ mmi.vdic_psdclose = (data[70] & 2) > 0
+ mmi.vdic_psdopen = (data[70] & 1) > 0
+ mmi.vdic_stat = f"{data[71]:02X}"
+
+ # [VDI D - Byte 72, 73, 74]
+ mmi.vdid_tc2 = (data[72] & 128) > 0
+ mmi.vdid_tc1 = (data[72] & 64) > 0
+ mmi.vdid_edlfb = (data[72] & 32) > 0
+ mmi.vdid_edrfb = (data[72] & 16) > 0
+ mmi.vdid_zvrfb = (data[72] & 8) > 0
+ mmi.vdid_fsbfb = (data[72] & 4) > 0
+ mmi.vdid_ebmfb = (data[72] & 2) > 0
+ mmi.vdid_ebpfb = (data[72] & 1) > 0
+
+ mmi.vdid_unit1 = (data[73] & 128) > 0
+ mmi.vdid_startbtn = (data[73] & 4) > 0
+ mmi.vdid_psdclose = (data[73] & 2) > 0
+ mmi.vdid_psdopen = (data[73] & 1) > 0
+ mmi.vdid_stat = f"{data[74]:02X}"
+
+ # [VDO A - Byte 75, 76]
+ mmi.vdoa_edl = (data[75] & 32) > 0
+ mmi.vdoa_edr = (data[75] & 16) > 0
+ mmi.vdoa_zvr = (data[75] & 8) > 0
+ mmi.vdoa_fsb = (data[75] & 4) > 0
+ mmi.vdoa_ebm = (data[75] & 2) > 0
+ mmi.vdoa_ebp = (data[75] & 1) > 0
+ mmi.vdoa_stat = f"{data[76]:02X}"
+
+ # [VDO B - Byte 77, 78]
+ mmi.vdob_edl = (data[77] & 32) > 0
+ mmi.vdob_edr = (data[77] & 16) > 0
+ mmi.vdob_zvr = (data[77] & 8) > 0
+ mmi.vdob_fsb = (data[77] & 4) > 0
+ mmi.vdob_ebm = (data[77] & 2) > 0
+ mmi.vdob_ebp = (data[77] & 1) > 0
+ mmi.vdob_stat = f"{data[78]:02X}"
+
+ return mmi
+
+# -------------------------------------------------------------------------
+# 3. 파일 로더
+# -------------------------------------------------------------------------
+class LogLoader:
+ def load(self, filepath: str) -> List[DOSItoMMIClass]:
+ result_list = []
+ if not os.path.exists(filepath):
+ return result_list
+
+ file_size = os.path.getsize(filepath)
+ with open(filepath, 'rb') as f:
+ CHUNK_SIZE = 84
+ while f.tell() < file_size:
+ chunk = f.read(CHUNK_SIZE)
+ if len(chunk) < CHUNK_SIZE: break
+
+ # 오류 방지용 try-except 추가 권장
+ try:
+ mmi_data = DDOSItoMMI.set_data(chunk)
+ result_list.append(mmi_data)
+ except Exception as e:
+ print(f"Error parsing chunk at {f.tell()}: {e}")
+
+ return result_list
\ No newline at end of file
diff --git a/app/data/w_mmiParser.py b/app/data/w_mmiParser.py
new file mode 100644
index 0000000..7d4927f
--- /dev/null
+++ b/app/data/w_mmiParser.py
@@ -0,0 +1,106 @@
+import struct
+import os
+import polars as pl
+from typing import Dict, List, Any
+
+class MMIParser:
+ """
+ MMI 바이너리 로그를 파싱하여 Polars DataFrame으로 반환하는 클래스
+ """
+ CHUNK_SIZE = 84
+
+ @staticmethod
+ def parse_chunk(data: bytes) -> Dict[str, Any]:
+ """84바이트 청크 하나를 딕셔너리로 변환"""
+ if len(data) < 84:
+ return None
+
+ # 기본 파싱 (비트 연산 로직 유지)
+ # 속도를 위해 dataclass 대신 dict 사용
+ row = {}
+
+ # [Header]
+ row["seq"] = data[1]
+ row["source"] = data[3]
+ # 이중계 판단 로직: 소스가 32(0x20)가 아니면 Break(고장/대기)로 간주
+ # (사용자 정의 로직에 따라 16, 32 외의 값 처리도 필요할 수 있음)
+ row["is_break"] = (data[3] != 32)
+
+ # [Time]
+ try:
+ row["time"] = (f"20{data[4]:02x}.{data[5]:02x}.{data[6]:02x} "
+ f"{data[7]:02x}:{data[8]:02x}:{data[9]:02x}")
+ except:
+ row["time"] = None
+
+ # [Speed & Analog]
+ row["trainspeed"] = ((data[10] << 8) | data[11]) / 10.0
+ row["limitspeed"] = data[12]
+ row["pwm_value"] = data[22]
+
+ # [DTG Calculation]
+ tasc = (data[21] & 32) > 0
+ trac_dr = (data[23] & 128) > 0
+ num_dtg = (data[31] << 8) | data[32]
+
+ if tasc:
+ row["dtg"] = num_dtg / 100.0
+ else:
+ if row["trainspeed"] == 0.0 and not trac_dr:
+ if (num_dtg & 32768) != 0:
+ num_dtg -= 65536
+ row["dtg"] = (num_dtg / 100.0) * -1.0
+ else:
+ row["dtg"] = num_dtg / 10.0
+
+ # [Frequency & XOR Decoding]
+ row["atc_code_carrier_f"] = ((data[36] ^ 65) << 8 | (data[37] ^ 82)) * 10.0
+ row["atc_code_f"] = ((data[38] ^ 99) << 8 | (data[39] ^ 116)) / 10.0
+
+ # [Bitmasks - 주요 신호만 예시로 포함, 필요 시 전체 추가]
+ # bitmasking은 polars의 map_elements보다 여기서 처리해서 넘기는게 빠름
+ row["do_zvr"] = (data[13] & 32) > 0
+ row["do_ebm"] = (data[13] & 2) > 0
+ row["system_active"] = (data[14] & 128) > 0
+ row["door_open"] = (data[14] & 8) > 0
+ row["door_close"] = (data[14] & 4) > 0
+
+ # [Station Codes for Semantic Mapping]
+ row["pstn"] = data[28] # 이전역
+ row["nstn"] = data[29] # 현재/다음역
+ row["dstn"] = data[30] # 종착역
+
+ # [ATC Code Parsing]
+ atc_idx = (data[35] & 240) >> 4
+ # 매핑은 여기서 문자열로 변환하지 않고 숫자(Index)만 넘깁니다.
+ # (문자열 변환은 Processor에서 일괄 처리하는 게 속도상 유리)
+ row["atc_code_idx"] = atc_idx
+
+ return row
+
+ def load_to_dataframe(self, filepath: str) -> pl.DataFrame:
+ """파일 전체를 읽어 Polars DataFrame으로 반환"""
+ if not os.path.exists(filepath):
+ raise FileNotFoundError(f"File not found: {filepath}")
+
+ raw_data = []
+ file_size = os.path.getsize(filepath)
+
+ with open(filepath, 'rb') as f:
+ # 파일 전체를 메모리에 로드 (50만개 * 84byte = 약 42MB, 메모리에 충분함)
+ # IO 속도 향상을 위해 한 번에 읽음
+ buffer = f.read()
+
+ # 청크 단위 파싱
+ total_chunks = len(buffer) // self.CHUNK_SIZE
+ for i in range(total_chunks):
+ start = i * self.CHUNK_SIZE
+ end = start + self.CHUNK_SIZE
+ chunk = buffer[start:end]
+
+ parsed_row = self.parse_chunk(chunk)
+ if parsed_row:
+ raw_data.append(parsed_row)
+
+ # Polars DataFrame 생성 (고속)
+ return pl.DataFrame(raw_data)
\ No newline at end of file
diff --git a/app/data/w_mmiProcessor.py b/app/data/w_mmiProcessor.py
new file mode 100644
index 0000000..d1c07a1
--- /dev/null
+++ b/app/data/w_mmiProcessor.py
@@ -0,0 +1,56 @@
+import polars as pl
+from mmi_parser import MMIParser
+
+class MMIProcessor:
+ def __init__(self):
+ # 역 코드 매핑 테이블 (예시)
+ self.station_map = {
+ 134: "범어사",
+ 135: "남산",
+ 136: "두실",
+ # ... 실제 매핑 데이터 추가
+ }
+
+ # ATC 코드 매핑
+ self.atc_code_map = {
+ 0: "02", 1: "01", 2: "25", 3: "40", 4: "55"
+ }
+
+ def process_and_save(self, input_path: str, output_parquet_path: str):
+ # 1. 파싱 (Raw Data 로드)
+ parser = MMIParser()
+ df = parser.load_to_dataframe(input_path)
+
+ print(f"Loaded {len(df)} rows.")
+
+ # 2. 이중계 필터링 (Redundancy Logic)
+ # 로직: 같은 seq 중에서 is_break가 False(정상)인 것을 우선 선택
+ # sort: seq 오름차순, is_break 오름차순 (False=0, True=1 이므로 정상 데이터가 위로 옴)
+ df_clean = (
+ df.sort(["seq", "is_break"])
+ .unique(subset=["seq"], keep="first") # 그룹별 첫 번째(정상)만 유지
+ )
+
+ print(f"Filtered redundancy. {len(df)} -> {len(df_clean)} rows.")
+
+ # 3. 시멘틱 데이터 보강 (Semantic Enrichment)
+ # map_dict를 사용하여 고속 매핑
+ df_enriched = df_clean.with_columns([
+ pl.col("nstn").replace(self.station_map).alias("station_name"),
+ pl.col("atc_code_idx").replace(self.atc_code_map).alias("atc_code_str")
+ ])
+
+ # 4. Parquet 저장 (압축 사용)
+ df_enriched.write_parquet(output_parquet_path, compression="zstd")
+ print(f"Saved to {output_parquet_path}")
+
+ return df_enriched
+
+# --- 실행 예시 ---
+if __name__ == "__main__":
+ processor = MMIProcessor()
+ # 로우 데이터 경로와 저장할 경로 지정
+ df = processor.process_and_save("raw_log.bin", "processed_data.parquet")
+
+ # 결과 확인
+ print(df.head())
\ No newline at end of file
diff --git a/app/ui/__init__.py b/app/ui/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/ui/__pycache__/__init__.cpython-311.pyc b/app/ui/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..f7305bc
Binary files /dev/null and b/app/ui/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ui/__pycache__/analysis_panel.cpython-311.pyc b/app/ui/__pycache__/analysis_panel.cpython-311.pyc
new file mode 100644
index 0000000..6ac9fdf
Binary files /dev/null and b/app/ui/__pycache__/analysis_panel.cpython-311.pyc differ
diff --git a/app/ui/__pycache__/main_window.cpython-311.pyc b/app/ui/__pycache__/main_window.cpython-311.pyc
new file mode 100644
index 0000000..4011a04
Binary files /dev/null and b/app/ui/__pycache__/main_window.cpython-311.pyc differ
diff --git a/app/ui/analysis_panel.py b/app/ui/analysis_panel.py
new file mode 100644
index 0000000..64a16a3
--- /dev/null
+++ b/app/ui/analysis_panel.py
@@ -0,0 +1,322 @@
+from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QLabel, QPushButton, QHBoxLayout, QFileDialog
+from PySide6.QtCore import Signal
+from app.ui.views.graph_view import GraphView
+# 추후 구현할 뷰들: from app.ui.views.dashboard_view import DashboardView
+from app.data.log_parser import LogLoader
+from app.core.sync_controller import sync_manager
+from app.ui.views.dashboard_view import DashboardView
+from app.ui.views.ai_view import AIDiagnosisView
+import polars as pl
+from dataclasses import fields
+from app.data.fast_parser import FastLogParser
+from app.data.log_parser import DOSItoMMIClass
+
+class AnalysisPanel(QWidget):
+ # 데이터 로드 완료 시그널
+ data_loaded = Signal()
+
+ def __init__(self, panel_id, parent=None):
+ super().__init__(parent)
+ self.panel_id = panel_id # "left" or "right"
+ self.loader = None # 지연 초기화 - 실제 사용 시점에 생성
+ self.raw_data = []
+ self.data_date = None # 데이터 날짜 (YYYY.MM.DD 형식)
+ self.file_path = None
+ self.current_index = None
+ self.current_row = None
+
+ self.setup_ui()
+
+ # 동기화 신호 받기
+ sync_manager.time_changed.connect(self.on_sync_received)
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # [상단] 개별 파일 로드 버튼 (A/B 비교를 위해)
+ top_bar = QHBoxLayout()
+ self.lbl_file = QLabel("No File Loaded")
+ self.btn_load = QPushButton(f"Load File ({self.panel_id.upper()})")
+ self.btn_load.clicked.connect(self.open_file)
+
+ top_bar.addWidget(self.lbl_file)
+ top_bar.addWidget(self.btn_load)
+ layout.addLayout(top_bar)
+
+ # [중앙] 탭 위젯
+ self.tabs = QTabWidget()
+
+ # 탭 1: 그래프 뷰
+ self.graph_view = GraphView(self.panel_id)
+ self.tabs.addTab(self.graph_view, "📊 Graph")
+
+ # 탭 2: 기존 뷰어 (껍데기)
+ self.dashboard_view = DashboardView(self.panel_id)
+ self.tabs.addTab(self.dashboard_view, "📟 Legacy Viewer")
+
+ # 탭 3: 신호 목록 (껍데기)
+ self.tabs.addTab(QWidget(), "📜 Signal List")
+
+ # 탭 4: AI 분석 (GraphView 컨텍스트 포함)
+ self.ai_view = AIDiagnosisView(self.panel_id, graph_view=self.graph_view)
+ self.tabs.addTab(self.ai_view, "🤖 AI Diagnosis")
+
+ # 그래프 선택 구간 → AI 요청 연결
+ self.graph_view.range_ai_requested.connect(self._on_range_ai_requested)
+
+ layout.addWidget(self.tabs)
+
+ def open_file(self):
+ file_name, _ = QFileDialog.getOpenFileName(self, "Open Log", "", "Log Files (*.dat)")
+ if file_name:
+ self.load_data(file_name)
+
+ def load_data(self, file_path):
+ self.file_path = file_path
+ self.lbl_file.setText(file_path.split("/")[-1])
+ # 1. FastLogParser 시도 (Polars)
+ parquet_path = file_path.replace(".dat", ".parquet")
+ try:
+ # 파싱 및 Parquet 저장 (이미 있으면 스킵 가능하지만, 로직상 호출)
+ # FastLogParser 내부에서 파일 존재 체크 등을 수행함
+ output_path = FastLogParser.parse_to_parquet(file_path)
+
+ if output_path:
+ # Polars로 읽기
+ df = pl.read_parquet(output_path)
+
+ # DataFrame -> List[DOSItoMMIClass] 변환
+ # 1) 필드 목록 추출
+ valid_fields = {f.name for f in fields(DOSItoMMIClass)}
+
+ # 2) 딕셔너리 변환 (Polars -> Python List of Dicts)
+ # rows = df.to_dicts() # 이 방식은 빠름
+
+ # 3) 객체 생성 (불필요한 컬럼 제거)
+ # 벡터화된 방식은 아니지만, Python 루프보다는 빠름 (파싱 비용 제거됨)
+ # 50만건 기준 약 1~2초 소요 예상
+ self.raw_data = [
+ DOSItoMMIClass(**{k: v for k, v in row.items() if k in valid_fields})
+ for row in df.to_dicts()
+ ]
+ print(f"[AnalysisPanel] Loaded {len(self.raw_data)} records via FastLogParser")
+
+ else:
+ raise Exception("FastLogParser returned None")
+
+ except Exception as e:
+ print(f"[AnalysisPanel] FastLogParser failed, falling back to legacy loader: {e}")
+ # LogLoader 지연 초기화
+ if self.loader is None:
+ self.loader = LogLoader()
+ self.raw_data = self.loader.load(file_path)
+
+ # 레거시 로더 사용 시 역 정보 매핑 (기존 로직 유지)
+ from app.core.reference_data import ref_data
+ if ref_data._is_loaded and self.raw_data:
+ for item in self.raw_data:
+ if item.pstn > 0:
+ st_name = ref_data.get_station_name(item.pstn)
+ if st_name:
+ item.station_name = st_name
+
+
+ # 데이터 날짜 추출 (첫 번째 레코드의 time 필드에서)
+ if self.raw_data and len(self.raw_data) > 0:
+ time_str = getattr(self.raw_data[0], 'time', '')
+ # time 형식: "20YY.MM.DD HH:mm:ss" -> 날짜 부분만 추출
+ if time_str and len(time_str) >= 10:
+ self.data_date = time_str[:10] # "20YY.MM.DD"
+ else:
+ self.data_date = None
+ else:
+ self.data_date = None
+
+ # 하위 뷰들에게 데이터 전달
+ self.graph_view.set_data(self.raw_data)
+ self.dashboard_view.set_data(self.raw_data) # Legacy Viewer에도 전달
+ self.ai_view.set_file_context(file_path)
+ self.ai_view.set_data(self.raw_data)
+
+ # 기본 커서 포인트(첫 레코드) 설정
+ if self.raw_data:
+ self.current_index = 0
+ self.current_row = self.raw_data[0]
+
+ # 첫 번째 패널이 로드되면 전체 레코드 수 설정 (임시)
+ if self.panel_id == "left":
+ sync_manager._total_records = len(self.raw_data)
+
+ # 데이터 로드 완료 시그널 발생
+ self.data_loaded.emit()
+
+ def on_sync_received(self, index, data, source_id):
+ """
+ SyncController로부터 "시간 바꿔!" 명령이 오면 실행
+ 단, 내가 보낸 신호가 아닐 때만 반응해야 함 (Loop 방지)
+ """
+ # 내 패널에서 움직인 커서도 기록해둬야 AI/플로팅 채팅에서 현재 시점을 알 수 있음
+ if source_id == self.panel_id:
+ self.current_index = index
+ self.current_row = data
+ return
+
+ if source_id != self.panel_id:
+ # 타임라인/커서 이동
+ self.graph_view.set_index(index)
+ # 추후 다른 뷰들도 이동 (예: self.dashboard.set_values(data))
+
+ def _on_range_ai_requested(self, start_ms: int, end_ms: int, start_idx: int, end_idx: int):
+ """그래프에서 선택한 구간을 AI 분석으로 전달"""
+ if hasattr(self, "ai_view") and self.ai_view:
+ self.ai_view.run_range_analysis(start_idx, end_idx, start_ms, end_ms)
+
+ def set_slave_mode(self, is_slave):
+ # Sync 모드일 때 로드 버튼 숨기기
+ self.btn_load.setVisible(not is_slave)
+ self.lbl_file.setText("Linked to Left Panel" if is_slave else "No File")
+
+ def receive_shared_data(self, data_list):
+ # 다른 패널에서 데이터 받아오기
+ self.raw_data = data_list
+ self.graph_view.set_data(data_list)
+ self.dashboard_view.set_data(data_list) # Legacy Viewer에도 전달
+ self.ai_view.set_data(data_list)
+
+ if self.raw_data:
+ self.current_index = 0
+ self.current_row = self.raw_data[0]
+
+ def get_ai_context(self):
+ """플로팅 AI/외부 호출용: 현재 패널의 AI 컨텍스트 묶음"""
+ ctx = {
+ "panel_id": self.panel_id,
+ "file_path": self.file_path,
+ "data_date": self.data_date,
+ "current_index": self.current_index,
+ }
+ try:
+ if hasattr(self, "graph_view") and self.graph_view and hasattr(self.graph_view, "get_ai_context"):
+ ctx["graph"] = self.graph_view.get_ai_context(self.current_index)
+ except Exception:
+ ctx["graph"] = {}
+
+ try:
+ if self.current_row is not None:
+ d = self.current_row
+ ctx["record"] = {
+ "time": getattr(d, "time", ""),
+ "trainno": getattr(d, "trainno", ""),
+ "trainspeed": getattr(d, "trainspeed", None),
+ "dtg": getattr(d, "dtg", None),
+ "atc_status": getattr(d, "atc_status", ""),
+ "marker": getattr(d, "marker", ""),
+ "pstn": getattr(d, "pstn", None),
+ }
+ except Exception:
+ pass
+ return ctx
+
+ def compute_pg_at_station(self, marker_name: str = "PG3-2"):
+ """
+ '각 역마다 PG3-2 위치에서의 속도/TASC' 같은 질의를 위해
+ 역 구간별(역 이벤트 ~ 다음 역 이벤트)로 PG 마커를 매칭해 지표를 추출한다.
+ """
+ if not self.raw_data or not hasattr(self, "graph_view") or not self.graph_view:
+ return []
+
+ gv = self.graph_view
+ if not hasattr(gv, "chart_view") or not getattr(gv, "start_timestamp", 0):
+ return []
+
+ # 역 이벤트(시간, 역명)
+ try:
+ stations = list(gv.chart_view.event_markers.get("STATION", []))
+ except Exception:
+ stations = []
+ stations = [(int(ts), str(name)) for ts, name in stations if ts is not None]
+ stations.sort(key=lambda x: x[0])
+ if not stations:
+ return []
+
+ # PG Rx 마커(시간, 타입)
+ try:
+ pgs = list(gv.chart_view.event_markers.get("PG", []))
+ except Exception:
+ pgs = []
+ pgs = [(int(ts), str(name)) for ts, name in pgs if ts is not None]
+ pgs = [p for p in pgs if p[1] == marker_name]
+ pgs.sort(key=lambda x: x[0])
+
+ results = []
+ for i, (st_ts, st_name) in enumerate(stations):
+ seg_start = st_ts
+ seg_end = stations[i + 1][0] if i + 1 < len(stations) else 10**18
+
+ # 구간 내 첫 PG3-2 (없으면 None)
+ pg_ts = None
+ for t, n in pgs:
+ if seg_start <= t < seg_end:
+ pg_ts = t
+ break
+
+ row = {
+ "station": st_name,
+ "station_ts": st_ts,
+ "pg_marker": marker_name,
+ "pg_ts": pg_ts,
+ }
+
+ if pg_ts is not None:
+ idx = int(round((pg_ts - gv.start_timestamp) / 1000.0))
+ idx = max(0, min(len(self.raw_data) - 1, idx))
+ d = self.raw_data[idx]
+ # raw 값
+ row.update(
+ {
+ "index": idx,
+ "time": getattr(d, "time", ""),
+ "raw_trainspeed": getattr(d, "trainspeed", None),
+ "raw_tasc_value": getattr(d, "tasc_value", None),
+ "raw_dtg": getattr(d, "dtg", None),
+ "raw_atc_status": getattr(d, "atc_status", ""),
+ }
+ )
+
+ # filtered(그래프 표시) 값도 함께
+ try:
+ sp = gv.signals_map.get("speed", {}).get("data", [])
+ ta = gv.signals_map.get("tasc", {}).get("data", [])
+ dg = gv.signals_map.get("dtg", {}).get("data", [])
+ if 0 <= idx < len(sp):
+ row["filtered_speed"] = sp[idx]
+ if 0 <= idx < len(ta):
+ row["filtered_tasc"] = ta[idx]
+ if 0 <= idx < len(dg):
+ row["filtered_dtg"] = dg[idx]
+ except Exception:
+ pass
+
+ # all-zero 프레임 여부
+ try:
+ if hasattr(gv, "_all_zero_mask") and gv._all_zero_mask and 0 <= idx < len(gv._all_zero_mask):
+ row["all_zero_frame"] = bool(gv._all_zero_mask[idx])
+ except Exception:
+ pass
+
+ try:
+ # 우선 filtered로 차이 계산, 없으면 raw로 폴백
+ s = row.get("filtered_speed", row.get("raw_trainspeed"))
+ t = row.get("filtered_tasc", row.get("raw_tasc_value"))
+ if s is not None and t is not None:
+ row["speed_minus_tasc"] = float(s) - float(t)
+ except Exception:
+ pass
+
+ results.append(row)
+
+ return results
+
+ def get_data_date(self):
+ """현재 로드된 데이터의 날짜 반환"""
+ return self.data_date
diff --git a/app/ui/components/__init__.py b/app/ui/components/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/ui/components/__pycache__/__init__.cpython-311.pyc b/app/ui/components/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..faa500e
Binary files /dev/null and b/app/ui/components/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ui/components/__pycache__/circular_gauge.cpython-311.pyc b/app/ui/components/__pycache__/circular_gauge.cpython-311.pyc
new file mode 100644
index 0000000..4e76b90
Binary files /dev/null and b/app/ui/components/__pycache__/circular_gauge.cpython-311.pyc differ
diff --git a/app/ui/components/__pycache__/clickableLabel.cpython-311.pyc b/app/ui/components/__pycache__/clickableLabel.cpython-311.pyc
new file mode 100644
index 0000000..9152a3c
Binary files /dev/null and b/app/ui/components/__pycache__/clickableLabel.cpython-311.pyc differ
diff --git a/app/ui/components/__pycache__/signal_components.cpython-311.pyc b/app/ui/components/__pycache__/signal_components.cpython-311.pyc
new file mode 100644
index 0000000..8344563
Binary files /dev/null and b/app/ui/components/__pycache__/signal_components.cpython-311.pyc differ
diff --git a/app/ui/components/__pycache__/speedometer.cpython-311.pyc b/app/ui/components/__pycache__/speedometer.cpython-311.pyc
new file mode 100644
index 0000000..2668716
Binary files /dev/null and b/app/ui/components/__pycache__/speedometer.cpython-311.pyc differ
diff --git a/app/ui/components/__pycache__/status_lamp.cpython-311.pyc b/app/ui/components/__pycache__/status_lamp.cpython-311.pyc
new file mode 100644
index 0000000..2d0a2da
Binary files /dev/null and b/app/ui/components/__pycache__/status_lamp.cpython-311.pyc differ
diff --git a/app/ui/components/__pycache__/toggle_button.cpython-311.pyc b/app/ui/components/__pycache__/toggle_button.cpython-311.pyc
new file mode 100644
index 0000000..1175bd4
Binary files /dev/null and b/app/ui/components/__pycache__/toggle_button.cpython-311.pyc differ
diff --git a/app/ui/components/__pycache__/track_map_view.cpython-311.pyc b/app/ui/components/__pycache__/track_map_view.cpython-311.pyc
new file mode 100644
index 0000000..57ad0e7
Binary files /dev/null and b/app/ui/components/__pycache__/track_map_view.cpython-311.pyc differ
diff --git a/app/ui/components/circular_gauge.py b/app/ui/components/circular_gauge.py
new file mode 100644
index 0000000..31f19d4
--- /dev/null
+++ b/app/ui/components/circular_gauge.py
@@ -0,0 +1,74 @@
+from PySide6.QtWidgets import QWidget
+from PySide6.QtCore import Qt, QPoint, QRectF
+from PySide6.QtGui import QPainter, QColor, QPen, QFont, QPolygon
+
+class CircularGauge(QWidget):
+ def __init__(self, title="SPEED", min_val=0, max_val=120, unit="km/h", color="#00E5FF"):
+ super().__init__()
+ self.value = 0
+ self.target_value = 0 # 목표 속도 (빨간 바늘 등)
+ self.min_val = min_val
+ self.max_val = max_val
+ self.title = title
+ self.unit = unit
+ self.accent_color = QColor(color)
+ self.setMinimumSize(200, 200)
+
+ def set_value(self, val, target=None):
+ self.value = max(self.min_val, min(val, self.max_val))
+ if target is not None:
+ self.target_value = max(self.min_val, min(target, self.max_val))
+ self.update()
+
+ def paintEvent(self, event):
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ w, h = self.width(), self.height()
+ side = min(w, h)
+ painter.translate(w/2, h/2)
+ painter.scale(side/200.0, side/200.0)
+
+ # 1. 배경
+ painter.setPen(QPen(QColor("#222"), 4))
+ painter.drawEllipse(-90, -90, 180, 180)
+
+ # 2. 눈금 (Scale)
+ start_angle = 135
+ span_angle = 270
+
+ painter.setPen(QPen(Qt.white, 2))
+ painter.setFont(QFont("Arial", 8))
+
+ step = (self.max_val - self.min_val) / 10
+ for i in range(11):
+ val = self.min_val + i * step
+ angle = start_angle + (i * (span_angle / 10))
+
+ painter.save()
+ painter.rotate(angle)
+ painter.drawLine(80, 0, 90, 0) # 눈금
+ painter.restore()
+
+ # 숫자 (삼각함수로 위치 계산 생략하고 간략히 처리하거나 추가 구현)
+
+ # 3. 메인 바늘 (Value)
+ val_pct = (self.value - self.min_val) / (self.max_val - self.min_val)
+ angle = start_angle + (val_pct * span_angle)
+
+ painter.save()
+ painter.rotate(angle)
+ painter.setBrush(self.accent_color)
+ painter.setPen(Qt.NoPen)
+ painter.drawConvexPolygon([QPoint(0, -3), QPoint(0, 3), QPoint(85, 0)])
+ painter.restore()
+
+ # 4. 텍스트 표시
+ painter.setPen(self.accent_color)
+ painter.setFont(QFont("Arial", 16, QFont.Bold))
+ painter.drawText(QRectF(-50, 20, 100, 30), Qt.AlignCenter, f"{self.value:.1f}")
+
+ painter.setPen(Qt.gray)
+ painter.setFont(QFont("Arial", 9))
+ painter.drawText(QRectF(-50, 50, 100, 20), Qt.AlignCenter, self.unit)
+ painter.drawText(QRectF(-50, -60, 100, 20), Qt.AlignCenter, self.title)
\ No newline at end of file
diff --git a/app/ui/components/clickableLabel.py b/app/ui/components/clickableLabel.py
new file mode 100644
index 0000000..3962530
--- /dev/null
+++ b/app/ui/components/clickableLabel.py
@@ -0,0 +1,445 @@
+from PySide6.QtWidgets import QLabel, QMenu, QVBoxLayout, QWidget
+from PySide6.QtCore import Qt,Signal, QPoint
+from PySide6.QtGui import QMouseEvent, QEnterEvent, QDragLeaveEvent, QCursor
+import logging
+
+logger = logging.getLogger(__name__)
+
+class ClickableLabel(QLabel):
+ """
+ 클릭 / 더블클릭 / 우클릭 컨텍스트 메뉴 / 마우스 호버링 / 드래그 / 그리드 스냅 지원 QLabel
+
+ [Signals]
+ - clicked(): 좌클릭
+ - double_clicked(): 좌측 더블클릭
+ - right_clicked(pos: QPoint): 우클릭 (로컬 좌표)
+ - hover_entered(): 마우스 진입
+ - hover_left(): 마우스 이탈
+ - drag_started(pos: QPoint): 드래그 시작
+ - dragging(delta: QPoint): 드래그 중 (이동량)
+ - drag_finished(): 드래그 종료
+
+ /사용예시
+ label = ClickableLabel("클릭해보세요")
+ label.clicked.connect(lambda: logger.info("원클릭"))
+ label.double_clicked.connect(lambda: logger.info("더블클릭"))
+ label.hover_entered.connect(lambda: logger.info("마우스 진입"))
+ label.hover_left.connect(lambda: logger.info("마우스 이탈"))
+
+ /우클릭 컨텍스트
+ label.right_clicked.connect(
+ lambda pos: logger.info(f"우클릭 위치: {pos}")
+ )
+
+ /드래그
+ label.drag_started.connect(
+ lambda pos: logger.info(f"드래그 시작 위치: {pos}")
+ )
+ label.dragging.connect(
+ lambda delta: logger.info(f"드래그 이동량: {delta}")
+ )
+ label.drag_finished.connect(lambda: logger.info("드래그 종료"))
+
+ def on_drag(delta: QPoint):
+ label.move(label.pos() + delta)
+ label.dragging.connect(on_drag)
+
+ /드래그 시작 시 UI 반응 정의 : 배경색 변경
+ label.drag_started.connect(lambda _: label.setStyleSheet("background:#d0ebff"))
+ label.drag_finished.connect(lambda: label.setStyleSheet(""))
+
+ 호버링 (호버링 사용시 setMouseTracking(True) 필요)
+ def on_hover_enter():
+ label.setStyleSheet("background-color: #f0f0f0;")
+
+ def on_hover_leave():
+ label.setStyleSheet("background-color: none;")
+
+ label.hover_entered.connect(on_hover_enter)
+ label.hover_left.connect(on_hover_leave)
+
+
+ """
+
+ clicked = Signal() # 좌클릭
+ double_clicked = Signal() # 더블클릭
+ right_clicked = Signal(QPoint) # 우클릭
+
+ hover_entered = Signal() # 마우스 진입
+ hover_left = Signal() # 마우스 이탈
+
+ drag_started = Signal(QPoint) # 드래그 시작 위치
+ dragging = Signal(QPoint) # 드래그 이동량
+ drag_finished = Signal() # 드래그 종료
+
+ def __init__(
+ self,
+ text: str = "",
+ parent=None,
+ hover_text=None,
+ *,
+ enable_click: bool = True,
+ enable_double_click: bool = True,
+ enable_right_click: bool = False,
+ enable_hover: bool = True,
+ enable_drag: bool = False,
+ enable_context_menu: bool = False,
+ context_menu_builder=None,
+ enable_grid_snap: bool = False,
+ grid_size: int = 20,
+ **kwargs
+ ):
+
+ super().__init__(text, parent)
+
+
+ # -----------------------------
+ # 기능 활성화 플래그
+ # -----------------------------
+ self.enable_click = enable_click
+ self.enable_double_click = enable_double_click
+ self.enable_right_click = enable_right_click
+ self.enable_hover = enable_hover
+ self.enable_drag = enable_drag
+ self.enable_context_menu = enable_context_menu
+ self._context_menu_builder = context_menu_builder
+ self.enable_grid_snap = enable_grid_snap
+
+ # -----------------------------
+ # 드래그 / 그리드 설정
+ # -----------------------------
+ self.grid_size = grid_size
+ self._drag_threshold = 5 # px (클릭 vs 드래그 구분)
+
+ self._press_pos: QPoint | None = None
+ self._dragging = False
+
+ # -----------------------------
+ # UI 기본 설정
+ # -----------------------------
+ self.setCursor(Qt.PointingHandCursor)
+
+ # 텍스트 중앙 정렬
+ self.setAlignment(Qt.AlignCenter)
+
+ self._hover_text = hover_text
+ self._hover_text_provider: callable | None = None
+ self._hover_popup = None
+
+ # hover는 mouse tracking 필수
+ if self.enable_hover:
+ self.setMouseTracking(True)
+ else:
+ self.setMouseTracking(False)
+
+ # -------------------------------------------------
+ # 마우스 누름
+ # -------------------------------------------------
+ def mousePressEvent(self, event: QMouseEvent):
+ if event.button() == Qt.LeftButton:
+ if self.enable_click:
+ self.clicked.emit()
+
+ if self.enable_drag:
+ self._press_pos = event.pos()
+ self._dragging = False
+
+ elif event.button() == Qt.RightButton:
+ if self.enable_right_click:
+ self.right_clicked.emit(event.pos())
+
+ if self.enable_context_menu:
+ self._show_context_menu(event.globalPos())
+
+ super().mousePressEvent(event)
+
+ # -------------------------------------------------
+ # 마우스 이동 (드래그)
+ # -------------------------------------------------
+ def mouseMoveEvent(self, event: QMouseEvent):
+ if not self.enable_drag or self._press_pos is None:
+ super().mouseMoveEvent(event)
+ return
+
+ delta = event.pos() - self._press_pos
+
+ if not self._dragging and delta.manhattanLength() >= self._drag_threshold:
+ self._dragging = True
+ self.drag_started.emit(self._press_pos)
+
+ if self._dragging:
+ self.dragging.emit(delta)
+ self._move_with_grid_snap(delta)
+
+ super().mouseMoveEvent(event)
+
+ # -------------------------------------------------
+ # 마우스 해제
+ # -------------------------------------------------
+ def mouseReleaseEvent(self, event: QMouseEvent):
+ if event.button() == Qt.LeftButton and self.enable_drag:
+ if self._dragging:
+ self.drag_finished.emit()
+
+ self._dragging = False
+ self._press_pos = None
+
+ super().mouseReleaseEvent(event)
+
+ # -------------------------------------------------
+ # 더블클릭
+ # -------------------------------------------------
+ def mouseDoubleClickEvent(self, event: QMouseEvent):
+ if event.button() == Qt.LeftButton and self.enable_double_click:
+ self.double_clicked.emit()
+ super().mouseDoubleClickEvent(event)
+
+ # -------------------------------------------------
+ # 호버
+
+ # 사용법 (주의:provider 함수에서 DB 조회, 네트워크 요청, 긴 계산 등을 포함하면 안됨,필요한 경우 캐시+갱신 이벤트구조로 구현)
+
+ # label.set_hover_text_provider(
+ # lambda: f"""
+ # 상태: {node.status}
+ # 전압: {node.voltage}V
+ # 최근 점검: {node.last_check}
+ # """.strip()
+ # )
+
+ # -------------------------------------------------
+ def enterEvent(self, event: QEnterEvent):
+ if self.enable_hover:
+ self.hover_entered.emit()
+ text = self._get_hover_text()
+ if text:
+ self._hover_popup = HoverInfoPopup(text)
+ pos = QCursor.pos() + QPoint(10, 10) # 마우스 커서 위치에서 약간 오프셋
+ self._hover_popup.move(pos)
+ self._hover_popup.show()
+
+ super().enterEvent(event)
+
+ def leaveEvent(self, event: QDragLeaveEvent):
+
+ if not self.enable_hover:
+ super().leaveEvent(event)
+ return
+
+ # popup이 있고, 마우스가 popup 안에 있으면 닫지 않음
+ if self._hover_popup and self._hover_popup.is_mouse_inside():
+ return
+
+ self.hover_left.emit()
+
+ if self.enable_hover:
+ if self._hover_popup:
+ self._hover_popup.close()
+ self._hover_popup = None
+ super().leaveEvent(event)
+
+ def set_hover_text(self, text: str):
+ """
+ hover 정보 텍스트 설정 (즉시 반영)
+ """
+ self._hover_text = text
+ self._hover_text_provider = None
+
+ if self._hover_popup:
+ self._hover_popup.set_text(text)
+
+ def _get_hover_text(self) -> str | None:
+ if self._hover_text_provider:
+ return self._hover_text_provider()
+ return self._hover_text
+
+ def clear_hover_text(self):
+ """
+ hover 정보 제거
+ """
+ self._hover_text = None
+ self._hover_text_provider = None
+
+ if self._hover_popup:
+ self._hover_popup.close()
+ self._hover_popup = None
+
+ def set_hover_text_provider(self, provider: callable):
+ """
+ hover 시점에 호출되는 함수 등록
+ """
+ self._hover_text_provider = provider
+ self._hover_text = None
+
+ if self._hover_popup:
+ self._hover_popup.set_text(provider())
+
+
+ # =================================================
+ # 내부 기능
+ # =================================================
+ def _show_context_menu(self, global_pos):
+ if not self._context_menu_builder:
+ return
+
+ menu = QMenu(self)
+ self._context_menu_builder(menu, self) # 컨텍스트 메뉴 빌더 호출
+ menu.exec(global_pos)
+
+ ''' 외부에서 컨텍스트 메뉴 빌더 정의
+ def build_label_menu(menu: QMenu, label: ClickableLabel):
+ menu.addAction("편집", lambda: edit_label(label))
+ menu.addAction("삭제", lambda: delete_label(label))
+ menu.addSeparator()
+ menu.addAction("정보", lambda: show_info(label))
+
+ def edit_label(label):
+ logger.info(f"편집: {label.text()}")
+
+ def delete_label(label):
+ label.deleteLater()
+
+ def show_info(label):
+ logger.info(f"위치: {label.pos()}")
+
+ 객체 생성시 컨텍스트 메뉴 빌더 전달
+ label = ClickableLabel(
+ "NODE-1",
+ context_menu_builder=build_label_menu
+ )
+ '''
+
+ def _move_with_grid_snap(self, delta: QPoint):
+ new_pos = self.pos() + delta
+
+ if self.enable_grid_snap:
+ x = round(new_pos.x() / self.grid_size) * self.grid_size
+ y = round(new_pos.y() / self.grid_size) * self.grid_size
+ self.move(QPoint(x, y))
+ else:
+ self.move(new_pos)
+
+
+
+class PopupTheme:
+ LIGHT = "light"
+ DARK = "dark"
+
+
+class HoverInfoPopup(QWidget):
+ """
+ 마우스 호버 시 정보 팝업 표시 QWidget
+
+ 사용예시
+ popup = HoverInfoPopup("호버 정보")
+ popup.show()
+ """
+ def __init__(self, text: str, theme=PopupTheme.DARK, parent=None):
+ super().__init__(parent)
+
+ # -----------------------------
+ # 테마 설정
+ # -----------------------------
+ self._theme = theme
+ self.setWindowFlags(
+ Qt.ToolTip |
+ Qt.FramelessWindowHint |
+ Qt.BypassWindowManagerHint
+ )
+
+ self.setAttribute(Qt.WA_ShowWithoutActivating)
+
+ self.apply_theme(theme)
+
+ # -----------------------------
+ # 윈도우 설정
+ # -----------------------------
+ self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
+
+ # -----------------------------
+ # 드래그 설정
+ # -----------------------------
+ self._drag_start_pos = None
+
+ # -----------------------------
+ # UI 설정
+ # -----------------------------
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(8, 6, 8, 6)
+
+ # -----------------------------
+ # 라벨 설정
+ # -----------------------------
+ self.label = QLabel(text)
+ self.label.setTextInteractionFlags(
+ Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard
+ )
+
+ layout.addWidget(self.label)
+
+ self.setStyleSheet("""
+ QWidget {
+ background-color: #ffffe1;
+ border: 1px solid #c8c8a9;
+ border-radius: 4px;
+ font-size: 12px;
+ }
+ """)
+
+ # -----------------------------
+ # 드래그 이동
+ # -----------------------------
+ def mousePressEvent(self, event: QMouseEvent):
+ if event.button() == Qt.LeftButton:
+ self._drag_start_pos = event.globalPos() - self.frameGeometry().topLeft()
+
+ def mouseMoveEvent(self, event: QMouseEvent):
+ if self._drag_start_pos:
+ self.move(event.globalPos() - self._drag_start_pos)
+
+ def mouseReleaseEvent(self, event: QMouseEvent):
+ self._drag_start_pos = None
+
+ # -------------------------------------------------
+ # 텍스트 설정
+ # -------------------------------------------------
+ def set_text(self, text: str):
+ self.label.setText(text)
+ self.adjustSize() # 내용 바뀌면 크기 자동 조정
+
+ # -------------------------------------------------
+ # 마우스 위치 확인
+ # -------------------------------------------------
+ def is_mouse_inside(self) -> bool:
+ pos = QCursor.pos()
+ return self.geometry().contains(pos)
+
+ # -------------------------------------------------
+ # 마우스 이탈 시 팝업 닫기
+ # -------------------------------------------------
+ def leaveEvent(self, event):
+ self.close()
+ super().leaveEvent(event)
+
+ # -------------------------------------------------
+ # 테마 적용
+ # -------------------------------------------------
+ def apply_theme(self, theme: str):
+ if theme == PopupTheme.DARK:
+ self.setStyleSheet("""
+ QWidget {
+ background-color: #2b2b2b;
+ color: #eaeaea;
+ border: 1px solid #555;
+ border-radius: 6px;
+ }
+ """)
+ else:
+ self.setStyleSheet("""
+ QWidget {
+ background-color: #ffffe1;
+ color: #202020;
+ border: 1px solid #c8c8a9;
+ border-radius: 6px;
+ }
+ """)
\ No newline at end of file
diff --git a/app/ui/components/signal_components.py b/app/ui/components/signal_components.py
new file mode 100644
index 0000000..cdcd7ee
--- /dev/null
+++ b/app/ui/components/signal_components.py
@@ -0,0 +1,466 @@
+"""
+신호 표시용 컴포넌트들
+- OnOffSignalLabel: ON/OFF 신호 표시
+- ModeSignalLabel: MODE 신호 표시 (여러 상태 중 하나)
+- DataSignalLabel: 데이터 신호 표시 (신호명 + 값)
+- SignalCard: 신호들을 그룹화하는 카드
+"""
+
+from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QFrame, QMenu, QGridLayout, QScrollArea, QSizePolicy)
+from PySide6.QtCore import Qt, Signal, QPoint
+from PySide6.QtGui import QCursor, QMouseEvent
+
+# ============================================================================
+# 1. ON/OFF 신호 라벨
+# ============================================================================
+class OnOffSignalLabel(QLabel):
+ """
+ ON/OFF 신호 표시용 라벨
+ - ON: 연두색 배경 + 검은색 글자
+ - OFF: 어두운 회색 배경 + 흰색 글자
+ """
+
+ clicked = Signal()
+ right_clicked = Signal(QPoint)
+
+ # 스타일 상수
+ STYLE_ON = "background-color: #76FF03; color: black; border-radius: 3px; padding: 2px 6px; font-weight: bold;"
+ STYLE_OFF = "background-color: #3C3C3C; color: #AAAAAA; border-radius: 3px; padding: 2px 6px;"
+
+ def __init__(self, signal_name: str, display_name: str = None,
+ description: str = "", on_list: list = None, off_list: list = None, parent=None):
+ super().__init__(display_name or signal_name, parent)
+
+ self.signal_name = signal_name
+ self.display_name = display_name or signal_name
+ self.description = description
+ self.on_list = on_list or [] # ON일 때 목록
+ self.off_list = off_list or [] # OFF일 때 목록
+ self._style_on_override = None
+ self._style_off_override = None
+
+ self._is_on = False
+
+ # UI 설정
+ self.setAlignment(Qt.AlignCenter)
+ self.setCursor(Qt.PointingHandCursor)
+ self.setMinimumSize(40, 18)
+ self.setMaximumHeight(22)
+ self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
+
+ # 툴팁 설정
+ if description:
+ self.setToolTip(description)
+
+ self.set_status(False)
+
+ def set_status(self, is_on: bool):
+ """신호 상태 설정"""
+ self._is_on = is_on
+ if is_on:
+ self.setStyleSheet(self._style_on_override or self.STYLE_ON)
+ else:
+ self.setStyleSheet(self._style_off_override or self.STYLE_OFF)
+
+ def set_style_override(self, on_style: str | None = None, off_style: str | None = None):
+ """ON/OFF 스타일 오버라이드"""
+ self._style_on_override = on_style
+ self._style_off_override = off_style
+ # 현재 상태 즉시 반영
+ self.set_status(self._is_on)
+
+ def mousePressEvent(self, event: QMouseEvent):
+ if event.button() == Qt.LeftButton:
+ self.clicked.emit()
+ elif event.button() == Qt.RightButton:
+ self.right_clicked.emit(event.pos())
+ self._show_context_menu(event.globalPosition().toPoint())
+ super().mousePressEvent(event)
+
+ def _show_context_menu(self, global_pos):
+ menu = QMenu(self)
+ menu.setStyleSheet("""
+ QMenu { background-color: #2D2D30; color: white; border: 1px solid #555; }
+ QMenu::item:selected { background-color: #0078D7; }
+ """)
+
+ # ON일 때 목록
+ if self.on_list:
+ on_menu = menu.addMenu("🟢 ON 목록")
+ for item in self.on_list:
+ on_menu.addAction(item)
+
+ # OFF일 때 목록
+ if self.off_list:
+ off_menu = menu.addMenu("⚫ OFF 목록")
+ for item in self.off_list:
+ off_menu.addAction(item)
+
+ menu.addSeparator()
+
+ # 신호 설명
+ if self.description:
+ desc_action = menu.addAction("ℹ️ 신호 설명")
+ desc_action.triggered.connect(self._show_description_popup)
+
+ menu.exec(global_pos)
+
+ def _show_description_popup(self):
+ from app.ui.components.clickableLabel import HoverInfoPopup
+ popup = HoverInfoPopup(f"{self.display_name}
{self.description}")
+ popup.move(QCursor.pos() + QPoint(10, 10))
+ popup.show()
+
+
+# ============================================================================
+# 2. MODE 신호 라벨 (여러 상태 중 하나)
+# ============================================================================
+class ModeSignalLabel(QLabel):
+ """
+ MODE 신호 표시용 라벨
+ - 신호 없음: OFF (어두운 회색 배경 + 흰색 글자)
+ - 신호 있음: 해당 모드 표시 (연보라색 배경 + 검은색 글자)
+ """
+
+ clicked = Signal()
+ right_clicked = Signal(QPoint)
+
+ STYLE_OFF = "background-color: #3C3C3C; color: #AAAAAA; border-radius: 3px; padding: 2px 6px;"
+ STYLE_ON = "background-color: #B388FF; color: black; border-radius: 3px; padding: 2px 6px; font-weight: bold;"
+
+ def __init__(self, signal_name: str, mode_list: list = None,
+ description: str = "", parent=None):
+ super().__init__("OFF", parent)
+
+ self.signal_name = signal_name
+ self.mode_list = mode_list or [] # 가능한 모드 목록
+ self.description = description
+ self._current_mode = None
+
+ # UI 설정
+ self.setAlignment(Qt.AlignCenter)
+ self.setCursor(Qt.PointingHandCursor)
+ self.setMinimumSize(45, 18)
+ self.setMaximumHeight(22)
+ self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
+
+ if description:
+ self.setToolTip(description)
+
+ self.set_mode(None)
+
+ def set_mode(self, mode: str):
+ """현재 모드 설정"""
+ self._current_mode = mode
+ if mode:
+ self.setText(mode)
+ self.setStyleSheet(self.STYLE_ON)
+ else:
+ self.setText("OFF")
+ self.setStyleSheet(self.STYLE_OFF)
+
+ def mousePressEvent(self, event: QMouseEvent):
+ if event.button() == Qt.LeftButton:
+ self.clicked.emit()
+ elif event.button() == Qt.RightButton:
+ self.right_clicked.emit(event.pos())
+ self._show_context_menu(event.globalPosition().toPoint())
+ super().mousePressEvent(event)
+
+ def _show_context_menu(self, global_pos):
+ menu = QMenu(self)
+ menu.setStyleSheet("""
+ QMenu { background-color: #2D2D30; color: white; border: 1px solid #555; }
+ QMenu::item:selected { background-color: #0078D7; }
+ """)
+
+ # 모드 목록
+ if self.mode_list:
+ mode_menu = menu.addMenu("📋 모드 목록")
+ for mode in self.mode_list:
+ action = mode_menu.addAction(mode)
+ if mode == self._current_mode:
+ action.setEnabled(False)
+
+ menu.addSeparator()
+
+ if self.description:
+ desc_action = menu.addAction("ℹ️ 신호 설명")
+ desc_action.triggered.connect(self._show_description_popup)
+
+ menu.exec(global_pos)
+
+ def _show_description_popup(self):
+ from app.ui.components.clickableLabel import HoverInfoPopup
+ popup = HoverInfoPopup(f"{self.signal_name}
{self.description}")
+ popup.move(QCursor.pos() + QPoint(10, 10))
+ popup.show()
+
+
+# ============================================================================
+# 3. DATA 신호 라벨 (신호명 + 값) - 세로 배치
+# ============================================================================
+class DataSignalLabel(QFrame):
+ """
+ 데이터 신호 표시용 라벨 (신호명 + 값 2개 세로 조합)
+ - 상단: 신호명 (밝은 파랑 배경 + 흰색 글자)
+ - 하단: 신호값 (남색 배경 + 흰색 글자)
+ """
+
+ clicked = Signal()
+ right_clicked = Signal(QPoint)
+
+ STYLE_NAME = "background-color: #2196F3; color: white; border-radius: 3px 0 0 3px; padding: 2px 6px; font-size: 9px;"
+ STYLE_VALUE = "background-color: #1A237E; color: white; border-radius: 0 3px 3px 0; padding: 2px 6px; font-weight: bold; font-size: 10px;"
+
+ def __init__(self, signal_name: str, display_name: str = None,
+ unit: str = "", possible_values: list = None,
+ description: str = "", parent=None):
+ super().__init__(parent)
+
+ self.signal_name = signal_name
+ self.display_name = display_name or signal_name
+ self.unit = unit
+ self.possible_values = possible_values or []
+ self.description = description
+
+ self._setup_ui()
+
+ if description:
+ self.setToolTip(description)
+
+ def _setup_ui(self):
+ # 가로 배치 (이름 | 값)
+ layout = QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+
+ # 좌측: 신호명 라벨
+ self.lbl_name = QLabel(self.display_name)
+ self.lbl_name.setStyleSheet(self.STYLE_NAME)
+ self.lbl_name.setAlignment(Qt.AlignCenter)
+ self.lbl_name.setMinimumWidth(40)
+ self.lbl_name.setFixedHeight(22)
+
+ # 우측: 신호값 라벨
+ self.lbl_value = QLabel("-")
+ self.lbl_value.setStyleSheet(self.STYLE_VALUE)
+ self.lbl_value.setAlignment(Qt.AlignCenter)
+ self.lbl_value.setMinimumWidth(50)
+ self.lbl_value.setFixedHeight(22)
+
+ layout.addWidget(self.lbl_name)
+ layout.addWidget(self.lbl_value)
+
+ self.setFixedHeight(24) # 1행 높이
+ self.setCursor(Qt.PointingHandCursor)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+
+ def set_value(self, value):
+ """신호 값 설정"""
+ if self.unit:
+ self.lbl_value.setText(f"{value}{self.unit}")
+ else:
+ self.lbl_value.setText(str(value))
+
+ def set_value_style(self, style: str):
+ """값 라벨 스타일 커스텀 설정"""
+ self.lbl_value.setStyleSheet(self.STYLE_VALUE + style)
+
+ def mousePressEvent(self, event: QMouseEvent):
+ if event.button() == Qt.LeftButton:
+ self.clicked.emit()
+ elif event.button() == Qt.RightButton:
+ self.right_clicked.emit(event.pos())
+ self._show_context_menu(event.globalPosition().toPoint())
+ super().mousePressEvent(event)
+
+ def _show_context_menu(self, global_pos):
+ menu = QMenu(self)
+ menu.setStyleSheet("""
+ QMenu { background-color: #2D2D30; color: white; border: 1px solid #555; }
+ QMenu::item:selected { background-color: #0078D7; }
+ """)
+
+ # 가능한 값 목록
+ if self.possible_values:
+ val_menu = menu.addMenu("📋 가능한 값")
+ for val in self.possible_values:
+ val_menu.addAction(str(val))
+
+ menu.addSeparator()
+
+ if self.description:
+ desc_action = menu.addAction("ℹ️ 신호 설명")
+ desc_action.triggered.connect(self._show_description_popup)
+
+ menu.exec(global_pos)
+
+ def _show_description_popup(self):
+ from app.ui.components.clickableLabel import HoverInfoPopup
+ popup = HoverInfoPopup(f"{self.display_name}
{self.description}")
+ popup.move(QCursor.pos() + QPoint(10, 10))
+ popup.show()
+
+
+# ============================================================================
+# 4. 신호 카드 (그룹화)
+# ============================================================================
+class SignalCard(QFrame):
+ """
+ 신호들을 그룹화하는 카드 컴포넌트
+ - 타이틀 바
+ - ON/OFF, MODE 컴포넌트: 1행 1열
+ - DATA 컴포넌트: 1행에 2~4열 차지 (가로로 넓게)
+ """
+
+ def __init__(self, title: str, parent=None):
+ super().__init__(parent)
+
+ self.title = title
+ self.signals = {} # signal_name -> widget
+ self._onoff_widgets = [] # ON/OFF, MODE 위젯들
+ self._data_widgets = [] # DATA 위젯들
+ self._horizontal_data_mode = False # DATA 가로 배치 모드
+
+ self._setup_ui()
+
+ def _setup_ui(self):
+ self.setStyleSheet("""
+ SignalCard {
+ background-color: #252526;
+ border: 1px solid #3E3E42;
+ border-radius: 4px;
+ }
+ """)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ self.setMinimumWidth(150)
+
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(4, 4, 4, 4)
+ layout.setSpacing(2)
+
+ # 타이틀
+ self.lbl_title = QLabel(self.title)
+ self.lbl_title.setFixedHeight(20) # 제목 높이 고정
+ self.lbl_title.setStyleSheet("""
+ font-weight: bold;
+ color: #00B0FF;
+ font-size: 10px;
+ padding: 1px 3px;
+ border-bottom: 1px solid #3E3E42;
+ """)
+ layout.addWidget(self.lbl_title)
+
+ # 신호 컨테이너 (GridLayout 사용)
+ self.signal_container = QWidget()
+ self.signal_grid = QGridLayout(self.signal_container)
+ self.signal_grid.setContentsMargins(0, 2, 0, 0)
+ self.signal_grid.setSpacing(2)
+
+ layout.addWidget(self.signal_container)
+
+ # 현재 그리드 위치 추적
+ self._grid_row = 0
+ self._grid_col = 0
+ self._max_cols = 4 # 4열 배치 (DATA가 2~4열 차지 가능)
+ self._data_colspan = 2 # DATA 위젯의 기본 colspan (2~4열 조정 가능)
+
+ def set_horizontal_data_mode(self, enabled: bool):
+ """DATA 위젯 가로 배치 모드 설정"""
+ self._horizontal_data_mode = enabled
+
+ def add_signal(self, widget, signal_name: str = None):
+ """신호 위젯 추가 - 타입에 따라 배치 방식 다름"""
+ if isinstance(widget, DataSignalLabel):
+ if self._horizontal_data_mode:
+ # DATA 가로 배치 모드: 1행 1열 (ON/OFF처럼)
+ self.signal_grid.addWidget(widget, self._grid_row, self._grid_col)
+ self._grid_col += 1
+ if self._grid_col >= self._max_cols:
+ self._grid_col = 0
+ self._grid_row += 1
+ else:
+ # 기본 모드: DATA 위젯은 1행에 2~4열 차지 (가로로 넓게)
+ # 현재 열이 0이 아니면 다음 행으로 이동
+ if self._grid_col != 0:
+ self._grid_row += 1
+ self._grid_col = 0
+
+ # colspan 계산 (남은 공간에 따라 2~4열)
+ colspan = min(self._data_colspan, self._max_cols - self._grid_col)
+ self.signal_grid.addWidget(widget, self._grid_row, self._grid_col, 1, colspan)
+ self._grid_col += colspan
+ if self._grid_col >= self._max_cols:
+ self._grid_col = 0
+ self._grid_row += 1
+ self._data_widgets.append(widget)
+ else:
+ # ON/OFF, MODE 위젯: 1행 1열
+ self.signal_grid.addWidget(widget, self._grid_row, self._grid_col)
+ self._grid_col += 1
+ if self._grid_col >= self._max_cols:
+ self._grid_col = 0
+ self._grid_row += 1
+ self._onoff_widgets.append(widget)
+
+ if signal_name:
+ self.signals[signal_name] = widget
+
+ def get_signal(self, signal_name: str):
+ """신호 위젯 가져오기"""
+ return self.signals.get(signal_name)
+
+ def finalize_layout(self):
+ """레이아웃 마무리 - 남는 공간에 stretch 추가"""
+ # 마지막 행 다음에 stretch 추가
+ if self._grid_col > 0:
+ self._grid_row += 1
+ self.signal_grid.setRowStretch(self._grid_row, 1)
+
+
+# ============================================================================
+# 5. FlowLayout (하위 호환용 - 더 이상 사용하지 않음)
+# ============================================================================
+class FlowLayout(QVBoxLayout):
+ """
+ 간단한 FlowLayout 구현 (레거시 호환용)
+ """
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._widgets = []
+ self._row_layouts = []
+ self._spacing = 4
+ self.setSpacing(4)
+
+ def setSpacing(self, spacing):
+ self._spacing = spacing
+ super().setSpacing(spacing)
+
+ def addWidget(self, widget):
+ self._widgets.append(widget)
+ self._reflow()
+
+ def _reflow(self):
+ # 기존 레이아웃 정리
+ for row_layout in self._row_layouts:
+ while row_layout.count():
+ item = row_layout.takeAt(0)
+
+ for row_layout in self._row_layouts:
+ self.removeItem(row_layout)
+ self._row_layouts.clear()
+
+ # 새 행 생성
+ current_row = QHBoxLayout()
+ current_row.setSpacing(self._spacing)
+ self._row_layouts.append(current_row)
+ super().addLayout(current_row)
+
+ for widget in self._widgets:
+ current_row.addWidget(widget)
+
+ current_row.addStretch()
+
diff --git a/app/ui/components/speedometer.py b/app/ui/components/speedometer.py
new file mode 100644
index 0000000..981dbd4
--- /dev/null
+++ b/app/ui/components/speedometer.py
@@ -0,0 +1,139 @@
+from PySide6.QtWidgets import QWidget
+from PySide6.QtCore import Qt, QRectF
+from PySide6.QtGui import QPainter, QColor, QPen, QFont, QBrush, QPainterPath
+
+class SpeedometerWidget(QWidget):
+ def __init__(self, title="SPEED", unit="km/h", max_value=120, parent=None):
+ super().__init__(parent)
+ self.title = title
+ self.unit = unit
+ self.max_value = max_value
+ self.current_value = 0.0
+ self.limit_value = 0.0
+ self.atc_code = 0 # ATC CODE 추가
+ self.setMinimumSize(120, 120)
+
+ def set_value(self, value):
+ try:
+ self.current_value = float(value)
+ except ValueError:
+ self.current_value = 0.0
+ self.update()
+
+ def set_limit(self, limit):
+ try:
+ self.limit_value = float(limit)
+ except ValueError:
+ self.limit_value = 0.0
+ self.update()
+
+ def set_atc_code(self, code):
+ """ATC CODE 설정"""
+ try:
+ self.atc_code = int(code)
+ except (ValueError, TypeError):
+ self.atc_code = 0
+ self.update()
+
+ def paintEvent(self, event):
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ width = self.width()
+ height = self.height()
+ side = min(width, height)
+
+ # 중앙 정렬
+ painter.translate(width / 2, height / 2)
+ painter.scale(side / 200.0, side / 200.0)
+
+ # 배경 (어두운 원)
+ painter.setPen(Qt.NoPen)
+ painter.setBrush(QColor(30, 30, 30))
+ painter.drawEllipse(-100, -100, 200, 200)
+
+ # 눈금 그리기 (Start: 135도, Span: 270도)
+ start_angle = 135 * 16
+ span_angle = -270 * 16
+
+ # 게이지 배경 (회색 아크)
+ pen = QPen(QColor(60, 60, 60))
+ pen.setWidth(15)
+ pen.setCapStyle(Qt.FlatCap)
+ painter.setPen(pen)
+ painter.drawArc(-80, -80, 160, 160, start_angle, span_angle)
+
+ # 현재 속도 아크 (녹색/노란색/빨간색)
+ if self.current_value > self.limit_value and self.limit_value > 0:
+ color = QColor(255, 82, 82) # 초과 시 빨강
+ elif self.current_value > 0:
+ color = QColor(0, 208, 132) # 평소 녹색
+ else:
+ color = QColor(60, 60, 60)
+
+ if self.current_value > 0:
+ val_ratio = min(self.current_value / self.max_value, 1.0)
+ val_span = int(span_angle * val_ratio)
+
+ pen.setColor(color)
+ painter.setPen(pen)
+ painter.drawArc(-80, -80, 160, 160, start_angle, val_span)
+
+ # 제한 속도 표시 (작은 마커)
+ if self.limit_value > 0:
+ limit_ratio = min(self.limit_value / self.max_value, 1.0)
+ limit_angle = 135 + (270 * limit_ratio)
+
+ painter.save()
+ painter.rotate(limit_angle)
+ painter.setPen(Qt.NoPen)
+ painter.setBrush(QColor(255, 165, 0)) # 주황색 마커
+ painter.drawRect(-2, -95, 4, 10) # 눈금 위치에 마커
+ painter.restore()
+
+ # ATC CODE 바늘 및 동그라미 표시
+ if self.atc_code > 0:
+ # ATC CODE에 해당하는 각도 계산 (0~max_value 범위)
+ atc_ratio = min(self.atc_code / self.max_value, 1.0)
+ atc_angle = 135 + (270 * atc_ratio)
+
+ painter.save()
+ painter.rotate(atc_angle)
+
+ # 빨간 바늘 (외부에서 내부로)
+ pen = QPen(QColor(255, 50, 50))
+ pen.setWidth(3)
+ painter.setPen(pen)
+ painter.drawLine(0, -95, 0, -70) # 외부(-95)에서 내부(-70)로
+
+ # 바늘 외부에 동그라미
+ painter.setPen(Qt.NoPen)
+ painter.setBrush(QColor(255, 50, 50))
+ painter.drawEllipse(-8, -110, 16, 16) # 바늘 외부에 원
+
+ # 동그라미 안에 ATC CODE 텍스트
+ painter.setPen(QColor(255, 255, 255))
+ font = QFont("Arial", 8, QFont.Bold)
+ painter.setFont(font)
+ painter.drawText(QRectF(-8, -110, 16, 16), Qt.AlignCenter, str(self.atc_code))
+
+ painter.restore()
+
+ # 텍스트 표시
+ painter.setPen(QColor(255, 255, 255))
+
+ # 현재 값 (큰 글씨)
+ font = QFont("Arial", 28, QFont.Bold)
+ painter.setFont(font)
+ painter.drawText(QRectF(-100, -20, 200, 40), Qt.AlignCenter, f"{self.current_value:.1f}")
+
+ # 단위 (작은 글씨)
+ font.setPointSize(10)
+ painter.setFont(font)
+ painter.drawText(QRectF(-100, 20, 200, 20), Qt.AlignCenter, self.unit)
+
+ # 타이틀 (상단)
+ font.setPointSize(12)
+ painter.setFont(font)
+ painter.setPen(QColor(180, 180, 180))
+ painter.drawText(QRectF(-100, -70, 200, 20), Qt.AlignCenter, self.title)
\ No newline at end of file
diff --git a/app/ui/components/status_lamp.py b/app/ui/components/status_lamp.py
new file mode 100644
index 0000000..5415173
--- /dev/null
+++ b/app/ui/components/status_lamp.py
@@ -0,0 +1,38 @@
+from PySide6.QtWidgets import QLabel
+from PySide6.QtCore import Qt
+
+class StatusLamp(QLabel):
+ def __init__(self, text, on_color="#00FF00", off_color="#333333", text_color="white", font_size=10, shape="rect"):
+ super().__init__(text)
+ self.setAlignment(Qt.AlignCenter)
+ self.on_color = on_color
+ self.off_color = off_color
+ self.text_color = text_color
+ self.shape = shape
+
+ # 기본 스타일 정의
+ radius = "12px" if shape == "circle" else "2px"
+ padding = "2px"
+
+ self.base_style = f"""
+ QLabel {{
+ color: {self.text_color};
+ border: 1px solid #555555;
+ border-radius: {radius};
+ font-family: 'Malgun Gothic';
+ font-weight: bold;
+ font-size: {font_size}pt;
+ padding: {padding};
+ }}
+ """
+ self.set_status(False)
+
+ def set_status(self, is_on: bool):
+ bg = self.on_color if is_on else self.off_color
+ # 켜졌을 때 글자색을 검정으로 할지 흰색으로 할지 (가독성)
+ fg = "black" if (is_on and self.on_color in ["#00FF00", "#FFFF00", "#00BFFF", "#FFA500"]) else self.text_color
+
+ self.setStyleSheet(f"""
+ {self.base_style}
+ QLabel {{ background-color: {bg}; color: {fg}; }}
+ """)
\ No newline at end of file
diff --git a/app/ui/components/time_slider.py b/app/ui/components/time_slider.py
new file mode 100644
index 0000000..54c7bd1
--- /dev/null
+++ b/app/ui/components/time_slider.py
@@ -0,0 +1 @@
+# Timeline Slider
diff --git a/app/ui/components/toggle_button.py b/app/ui/components/toggle_button.py
new file mode 100644
index 0000000..d26523f
--- /dev/null
+++ b/app/ui/components/toggle_button.py
@@ -0,0 +1,70 @@
+from PySide6.QtWidgets import QCheckBox
+from PySide6.QtCore import Property, QSize, Qt, QRect
+from PySide6.QtGui import QPainter, QColor, QBrush, QPen
+
+class ToggleButton(QCheckBox):
+ """아이폰 스타일의 커스텀 토글 스위치"""
+ def __init__(self, width=50, bg_color="#777", circle_color="#DDD", active_color="#00BCff",
+ disabled_color="#444"):
+ super().__init__()
+ self.setFixedSize(width, 28)
+ self.setCursor(Qt.PointingHandCursor)
+
+ self._bg_color = bg_color
+ self._circle_color = circle_color
+ self._active_color = active_color
+ self._disabled_color = disabled_color
+ self._circle_position = 3
+
+ self.stateChanged.connect(self.start_animation)
+
+ def start_animation(self, state):
+ self.update() # 애니메이션 없이 즉시 변경 (필요시 QPropertyAnimation 추가 가능)
+
+ def mousePressEvent(self, event):
+ """마우스 클릭 이벤트 명시적 처리"""
+ if event.button() == Qt.LeftButton and self.isEnabled():
+ self.setChecked(not self.isChecked())
+ event.accept()
+ else:
+ super().mousePressEvent(event)
+
+ def paintEvent(self, e):
+ p = QPainter(self)
+ p.setRenderHint(QPainter.Antialiasing)
+
+ rect = self.rect()
+
+ # 비활성화 상태
+ if not self.isEnabled():
+ p.setBrush(QColor(self._disabled_color))
+ p.setPen(Qt.NoPen)
+ p.drawRoundedRect(0, 0, rect.width(), rect.height(), rect.height() / 2, rect.height() / 2)
+
+ # 어두운 원
+ circle_dia = rect.height() - 6
+ p.setBrush(QColor("#555"))
+ p.drawEllipse(3, 3, circle_dia, circle_dia)
+ p.end()
+ return
+
+ # 배경 그리기 (Capsule 모양)
+ if self.isChecked():
+ p.setBrush(QColor(self._active_color))
+ p.setPen(Qt.NoPen)
+ else:
+ p.setBrush(QColor(self._bg_color))
+ p.setPen(Qt.NoPen)
+
+ p.drawRoundedRect(0, 0, rect.width(), rect.height(), rect.height() / 2, rect.height() / 2)
+
+ # 원 그리기
+ circle_dia = rect.height() - 6
+ p.setBrush(QColor(self._circle_color))
+
+ if self.isChecked():
+ p.drawEllipse(rect.width() - circle_dia - 3, 3, circle_dia, circle_dia)
+ else:
+ p.drawEllipse(3, 3, circle_dia, circle_dia)
+
+ p.end()
\ No newline at end of file
diff --git a/app/ui/dialogs/__init__.py b/app/ui/dialogs/__init__.py
new file mode 100644
index 0000000..d8d35bd
--- /dev/null
+++ b/app/ui/dialogs/__init__.py
@@ -0,0 +1,8 @@
+"""
+UI 다이얼로그 패키지
+"""
+
+from .api_key_dialog import APIKeyDialog
+
+__all__ = ['APIKeyDialog']
+
diff --git a/app/ui/dialogs/__pycache__/__init__.cpython-311.pyc b/app/ui/dialogs/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..e4cbac5
Binary files /dev/null and b/app/ui/dialogs/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ui/dialogs/__pycache__/api_key_dialog.cpython-311.pyc b/app/ui/dialogs/__pycache__/api_key_dialog.cpython-311.pyc
new file mode 100644
index 0000000..03e7b9f
Binary files /dev/null and b/app/ui/dialogs/__pycache__/api_key_dialog.cpython-311.pyc differ
diff --git a/app/ui/dialogs/api_key_dialog.py b/app/ui/dialogs/api_key_dialog.py
new file mode 100644
index 0000000..0ed1e67
--- /dev/null
+++ b/app/ui/dialogs/api_key_dialog.py
@@ -0,0 +1,521 @@
+"""
+API 키 설정 다이얼로그
+AI 서비스 API 키 입력 및 관리 UI
+"""
+
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
+ QLabel, QLineEdit, QComboBox, QPushButton,
+ QTabWidget, QWidget, QGroupBox, QMessageBox,
+ QFrame, QSizePolicy
+)
+from PySide6.QtCore import Qt, Signal
+from PySide6.QtGui import QFont, QIcon
+
+from app.core.settings import get_settings_manager, AISettings
+from app.ai import AIProviderType, get_ai_client
+
+
+class ProviderTab(QWidget):
+ """개별 프로바이더 설정 탭"""
+
+ api_key_changed = Signal(str, str) # provider_type, api_key
+
+ def __init__(self, provider_type: str, provider_name: str, available_models: list, parent=None):
+ super().__init__(parent)
+ self.provider_type = provider_type
+ self.provider_name = provider_name
+ self.available_models = available_models
+
+ self.setup_ui()
+ self.load_settings()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+ layout.setSpacing(16)
+ layout.setContentsMargins(20, 20, 20, 20)
+
+ # 프로바이더 정보
+ info_group = QGroupBox(f"{self.provider_name} 설정")
+ info_group.setStyleSheet("""
+ QGroupBox {
+ font-size: 14px;
+ font-weight: bold;
+ border: 1px solid #3d3d3d;
+ border-radius: 8px;
+ margin-top: 12px;
+ padding-top: 12px;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 12px;
+ padding: 0 8px;
+ }
+ """)
+ info_layout = QGridLayout(info_group)
+ info_layout.setSpacing(12)
+ info_layout.setContentsMargins(16, 24, 16, 16)
+
+ # API Key 입력
+ api_key_label = QLabel("API Key:")
+ api_key_label.setStyleSheet("font-weight: bold;")
+ self.api_key_input = QLineEdit()
+ self.api_key_input.setPlaceholderText("API 키를 입력하세요...")
+ self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
+ self.api_key_input.setMinimumHeight(36)
+ self.api_key_input.setStyleSheet("""
+ QLineEdit {
+ padding: 8px 12px;
+ border: 1px solid #555;
+ border-radius: 6px;
+ background: #2d2d2d;
+ color: #fff;
+ font-size: 13px;
+ }
+ QLineEdit:focus {
+ border-color: #0078d4;
+ }
+ """)
+
+ # API Key 보기/숨기기 버튼
+ self.toggle_visibility_btn = QPushButton("👁")
+ self.toggle_visibility_btn.setFixedSize(36, 36)
+ self.toggle_visibility_btn.setCheckable(True)
+ self.toggle_visibility_btn.setStyleSheet("""
+ QPushButton {
+ border: 1px solid #555;
+ border-radius: 6px;
+ background: #2d2d2d;
+ }
+ QPushButton:checked {
+ background: #3d3d3d;
+ }
+ QPushButton:hover {
+ background: #404040;
+ }
+ """)
+ self.toggle_visibility_btn.clicked.connect(self.toggle_api_key_visibility)
+
+ api_key_row = QHBoxLayout()
+ api_key_row.addWidget(self.api_key_input)
+ api_key_row.addWidget(self.toggle_visibility_btn)
+
+ info_layout.addWidget(api_key_label, 0, 0)
+ info_layout.addLayout(api_key_row, 0, 1)
+
+ # 모델 선택
+ model_label = QLabel("모델:")
+ model_label.setStyleSheet("font-weight: bold;")
+ self.model_combo = QComboBox()
+ self.model_combo.addItems(self.available_models)
+ self.model_combo.setMinimumHeight(36)
+ self.model_combo.setStyleSheet("""
+ QComboBox {
+ padding: 8px 12px;
+ border: 1px solid #555;
+ border-radius: 6px;
+ background: #2d2d2d;
+ color: #fff;
+ font-size: 13px;
+ }
+ QComboBox:hover {
+ border-color: #666;
+ }
+ QComboBox::drop-down {
+ border: none;
+ width: 30px;
+ }
+ QComboBox::down-arrow {
+ image: none;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 6px solid #888;
+ margin-right: 10px;
+ }
+ QComboBox QAbstractItemView {
+ background: #2d2d2d;
+ border: 1px solid #555;
+ selection-background-color: #0078d4;
+ }
+ """)
+
+ info_layout.addWidget(model_label, 1, 0)
+ info_layout.addWidget(self.model_combo, 1, 1)
+
+ info_layout.setColumnStretch(1, 1)
+
+ layout.addWidget(info_group)
+
+ # 테스트 버튼
+ test_layout = QHBoxLayout()
+ test_layout.addStretch()
+
+ self.test_btn = QPushButton("🔗 연결 테스트")
+ self.test_btn.setMinimumHeight(40)
+ self.test_btn.setMinimumWidth(140)
+ self.test_btn.setStyleSheet("""
+ QPushButton {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 6px;
+ background: #0078d4;
+ color: white;
+ font-size: 13px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background: #1084d8;
+ }
+ QPushButton:pressed {
+ background: #006cc1;
+ }
+ QPushButton:disabled {
+ background: #555;
+ color: #888;
+ }
+ """)
+ self.test_btn.clicked.connect(self.test_connection)
+
+ test_layout.addWidget(self.test_btn)
+ layout.addLayout(test_layout)
+
+ # 상태 표시
+ self.status_label = QLabel("")
+ self.status_label.setStyleSheet("color: #888; font-size: 12px;")
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ layout.addWidget(self.status_label)
+
+ layout.addStretch()
+
+ def toggle_api_key_visibility(self, checked):
+ """API 키 표시/숨기기 토글"""
+ if checked:
+ self.api_key_input.setEchoMode(QLineEdit.EchoMode.Normal)
+ self.toggle_visibility_btn.setText("🔒")
+ else:
+ self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
+ self.toggle_visibility_btn.setText("👁")
+
+ def load_settings(self):
+ """설정에서 값 로드"""
+ settings = get_settings_manager().ai_settings
+
+ api_key = settings.get_api_key(self.provider_type)
+ if api_key:
+ self.api_key_input.setText(api_key)
+
+ model = settings.get_model(self.provider_type)
+ if model:
+ index = self.model_combo.findText(model)
+ if index >= 0:
+ self.model_combo.setCurrentIndex(index)
+
+ def save_settings(self):
+ """설정 저장"""
+ settings_manager = get_settings_manager()
+ settings = settings_manager.ai_settings
+
+ settings.set_api_key(self.provider_type, self.api_key_input.text())
+ settings.set_model(self.provider_type, self.model_combo.currentText())
+
+ settings_manager.save()
+
+ def test_connection(self):
+ """연결 테스트"""
+ api_key = self.api_key_input.text().strip()
+ if not api_key:
+ self.status_label.setText("❌ API 키를 입력하세요")
+ self.status_label.setStyleSheet("color: #ff5252; font-size: 12px;")
+ return
+
+ self.status_label.setText("⏳ 연결 테스트 중...")
+ self.status_label.setStyleSheet("color: #ffc107; font-size: 12px;")
+ self.test_btn.setEnabled(False)
+
+ # 테스트 실행 (간단한 동기 테스트)
+ try:
+ from app.ai import AIClient, AIProviderType
+
+ client = AIClient()
+ provider_type = AIProviderType(self.provider_type)
+ model = self.model_combo.currentText()
+
+ if client.set_provider(provider_type, api_key, model):
+ # 간단한 테스트 메시지
+ response = client.chat("Hello", max_tokens=10)
+ self.status_label.setText(f"✅ 연결 성공! ({response.model})")
+ self.status_label.setStyleSheet("color: #4caf50; font-size: 12px;")
+ else:
+ self.status_label.setText("❌ 초기화 실패")
+ self.status_label.setStyleSheet("color: #ff5252; font-size: 12px;")
+ except Exception as e:
+ error_msg = str(e)[:50] + "..." if len(str(e)) > 50 else str(e)
+ self.status_label.setText(f"❌ 오류: {error_msg}")
+ self.status_label.setStyleSheet("color: #ff5252; font-size: 12px;")
+ finally:
+ self.test_btn.setEnabled(True)
+
+ def get_api_key(self) -> str:
+ return self.api_key_input.text().strip()
+
+ def get_model(self) -> str:
+ return self.model_combo.currentText()
+
+
+class APIKeyDialog(QDialog):
+ """API 키 설정 다이얼로그"""
+
+ settings_changed = Signal()
+
+ # 프로바이더별 정보
+ PROVIDERS = [
+ {
+ "type": "openai",
+ "name": "OpenAI",
+ "models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo", "o1-preview", "o1-mini"],
+ },
+ {
+ "type": "openrouter",
+ "name": "OpenRouter",
+ "models": [
+ "openai/gpt-4o", "openai/gpt-4o-mini",
+ "anthropic/claude-3.5-sonnet", "anthropic/claude-3-opus",
+ "google/gemini-pro-1.5", "google/gemini-flash-1.5",
+ "meta-llama/llama-3.1-70b-instruct", "meta-llama/llama-3.1-8b-instruct",
+ "mistralai/mixtral-8x7b-instruct", "deepseek/deepseek-chat",
+ ],
+ },
+ {
+ "type": "gemini",
+ "name": "Google Gemini",
+ "models": ["gemini-2.0-flash-exp", "gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.5-flash-8b", "gemini-1.0-pro"],
+ },
+ {
+ "type": "xai",
+ "name": "xAI (Grok)",
+ "models": ["grok-beta", "grok-2-1212", "grok-2-vision-1212"],
+ },
+ ]
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("AI API 키 설정")
+ self.setMinimumSize(550, 450)
+ self.setModal(True)
+
+ self.provider_tabs = {}
+
+ self.setup_ui()
+ self.load_current_provider()
+
+ def setup_ui(self):
+ self.setStyleSheet("""
+ QDialog {
+ background: #1e1e1e;
+ color: #fff;
+ }
+ QLabel {
+ color: #ddd;
+ }
+ QTabWidget::pane {
+ border: 1px solid #3d3d3d;
+ border-radius: 8px;
+ background: #252525;
+ }
+ QTabBar::tab {
+ background: #2d2d2d;
+ color: #888;
+ padding: 10px 20px;
+ margin-right: 2px;
+ border-top-left-radius: 6px;
+ border-top-right-radius: 6px;
+ }
+ QTabBar::tab:selected {
+ background: #252525;
+ color: #fff;
+ }
+ QTabBar::tab:hover {
+ background: #353535;
+ color: #ccc;
+ }
+ """)
+
+ layout = QVBoxLayout(self)
+ layout.setSpacing(16)
+ layout.setContentsMargins(20, 20, 20, 20)
+
+ # 헤더
+ header = QLabel("🔑 AI API 키 설정")
+ header.setStyleSheet("""
+ font-size: 18px;
+ font-weight: bold;
+ color: #fff;
+ padding: 8px 0;
+ """)
+ layout.addWidget(header)
+
+ # 현재 프로바이더 선택
+ provider_layout = QHBoxLayout()
+ provider_label = QLabel("기본 프로바이더:")
+ provider_label.setStyleSheet("font-weight: bold;")
+
+ self.provider_combo = QComboBox()
+ for provider in self.PROVIDERS:
+ self.provider_combo.addItem(provider["name"], provider["type"])
+ self.provider_combo.setMinimumHeight(36)
+ self.provider_combo.setMinimumWidth(200)
+ self.provider_combo.setStyleSheet("""
+ QComboBox {
+ padding: 8px 12px;
+ border: 1px solid #555;
+ border-radius: 6px;
+ background: #2d2d2d;
+ color: #fff;
+ font-size: 13px;
+ }
+ QComboBox:hover {
+ border-color: #666;
+ }
+ QComboBox::drop-down {
+ border: none;
+ width: 30px;
+ }
+ QComboBox::down-arrow {
+ image: none;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 6px solid #888;
+ margin-right: 10px;
+ }
+ QComboBox QAbstractItemView {
+ background: #2d2d2d;
+ border: 1px solid #555;
+ selection-background-color: #0078d4;
+ }
+ """)
+
+ provider_layout.addWidget(provider_label)
+ provider_layout.addWidget(self.provider_combo)
+ provider_layout.addStretch()
+
+ layout.addLayout(provider_layout)
+
+ # 탭 위젯
+ self.tab_widget = QTabWidget()
+
+ for provider in self.PROVIDERS:
+ tab = ProviderTab(
+ provider_type=provider["type"],
+ provider_name=provider["name"],
+ available_models=provider["models"]
+ )
+ self.provider_tabs[provider["type"]] = tab
+ self.tab_widget.addTab(tab, provider["name"])
+
+ layout.addWidget(self.tab_widget)
+
+ # 버튼
+ button_layout = QHBoxLayout()
+ button_layout.addStretch()
+
+ cancel_btn = QPushButton("취소")
+ cancel_btn.setMinimumHeight(40)
+ cancel_btn.setMinimumWidth(100)
+ cancel_btn.setStyleSheet("""
+ QPushButton {
+ padding: 10px 24px;
+ border: 1px solid #555;
+ border-radius: 6px;
+ background: #2d2d2d;
+ color: #ddd;
+ font-size: 13px;
+ }
+ QPushButton:hover {
+ background: #3d3d3d;
+ border-color: #666;
+ }
+ """)
+ cancel_btn.clicked.connect(self.reject)
+
+ save_btn = QPushButton("저장")
+ save_btn.setMinimumHeight(40)
+ save_btn.setMinimumWidth(100)
+ save_btn.setStyleSheet("""
+ QPushButton {
+ padding: 10px 24px;
+ border: none;
+ border-radius: 6px;
+ background: #0078d4;
+ color: white;
+ font-size: 13px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background: #1084d8;
+ }
+ QPushButton:pressed {
+ background: #006cc1;
+ }
+ """)
+ save_btn.clicked.connect(self.save_and_close)
+
+ button_layout.addWidget(cancel_btn)
+ button_layout.addWidget(save_btn)
+
+ layout.addLayout(button_layout)
+
+ def load_current_provider(self):
+ """현재 설정된 프로바이더 로드"""
+ settings = get_settings_manager().ai_settings
+ current = settings.get_current_provider()
+
+ if current:
+ index = self.provider_combo.findData(current)
+ if index >= 0:
+ self.provider_combo.setCurrentIndex(index)
+
+ # 해당 탭으로 전환
+ for i, provider in enumerate(self.PROVIDERS):
+ if provider["type"] == current:
+ self.tab_widget.setCurrentIndex(i)
+ break
+
+ def save_and_close(self):
+ """설정 저장 후 닫기"""
+ settings_manager = get_settings_manager()
+
+ # 모든 탭의 설정 저장
+ for provider_type, tab in self.provider_tabs.items():
+ tab.save_settings()
+
+ # 기본 프로바이더 설정
+ current_provider = self.provider_combo.currentData()
+ settings_manager.ai_settings.set_current_provider(current_provider)
+
+ settings_manager.save()
+
+ # AI 클라이언트 업데이트
+ self._update_ai_client()
+
+ self.settings_changed.emit()
+ self.accept()
+
+ def _update_ai_client(self):
+ """저장된 설정으로 AI 클라이언트 업데이트"""
+ try:
+ from app.ai import get_ai_client, AIProviderType
+
+ settings = get_settings_manager().ai_settings
+ current_provider = settings.get_current_provider()
+
+ if current_provider:
+ api_key = settings.get_api_key(current_provider)
+ model = settings.get_model(current_provider)
+
+ if api_key:
+ client = get_ai_client()
+ provider_type = AIProviderType(current_provider)
+ client.set_provider(provider_type, api_key, model)
+ except Exception as e:
+ print(f"AI 클라이언트 업데이트 실패: {e}")
+
diff --git a/app/ui/main_window.py b/app/ui/main_window.py
new file mode 100644
index 0000000..968c4d0
--- /dev/null
+++ b/app/ui/main_window.py
@@ -0,0 +1,280 @@
+from PySide6.QtWidgets import (
+ QMainWindow,
+ QWidget,
+ QVBoxLayout,
+ QSplitter,
+ QToolBar,
+ QStatusBar,
+ QLabel,
+)
+from PySide6.QtCore import Qt, QRect
+from PySide6.QtGui import QAction
+
+from app.ui.analysis_panel import AnalysisPanel
+from app.core.sync_controller import sync_manager
+from app.ui.components.toggle_button import ToggleButton
+from app.ui.dialogs.api_key_dialog import APIKeyDialog
+from app.core.settings import get_settings_manager
+from app.ui.widgets.ai_floating_chat import AIFloatingButton, AIChatDialog, animate_open, animate_close
+
+
+class MainWindow(QMainWindow):
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle("SL200 AI Smart Analyzer")
+ self.resize(1800, 1000)
+
+ self.setup_ui()
+ self.setup_menubar()
+ self.setup_toolbar()
+
+ # 패널 데이터 로드 시 날짜 비교를 위한 시그널 연결
+ self.left_panel.data_loaded.connect(self.check_sync_availability)
+ self.right_panel.data_loaded.connect(self.check_sync_availability)
+
+ # AI 클라이언트 초기화
+ self._init_ai_client()
+
+ def setup_ui(self):
+ central = QWidget()
+ self.setCentralWidget(central)
+ layout = QVBoxLayout(central)
+
+ # 좌우 분할
+ splitter = QSplitter(Qt.Orientation.Horizontal)
+
+ # 왼쪽/오른쪽 패널 생성 (ID 부여)
+ self.left_panel = AnalysisPanel("left")
+ self.right_panel = AnalysisPanel("right")
+
+ splitter.addWidget(self.left_panel)
+ splitter.addWidget(self.right_panel)
+ splitter.setSizes([900, 900]) # 1:1 비율
+
+ layout.addWidget(splitter)
+
+ self.status_bar = QStatusBar()
+ self.setStatusBar(self.status_bar)
+
+ # 플로팅 AI 버튼
+ self._ai_button = AIFloatingButton(central)
+ self._ai_button.clicked.connect(self._open_ai_chat)
+ self._ai_button.show()
+ self._ai_chat: AIChatDialog | None = None
+ self._ai_button.move_to_bottom_right()
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ try:
+ if hasattr(self, "_ai_button") and self._ai_button:
+ self._ai_button.move_to_bottom_right()
+ except Exception:
+ pass
+
+ def setup_toolbar(self):
+ toolbar = QToolBar("Main Toolbar")
+ self.addToolBar(toolbar)
+
+ # 커스텀 토글 버튼
+ self.sync_toggle = ToggleButton(width=60, active_color="#00D084") # 녹색
+ self.sync_toggle.toggled.connect(self.on_sync_toggled)
+ self.sync_toggle.setEnabled(False) # 초기에는 비활성화
+
+ toolbar.addWidget(QLabel(" SYNC MODE: "))
+ toolbar.addWidget(self.sync_toggle)
+
+ # 상태 라벨
+ self.sync_status_label = QLabel(" (데이터 없음)")
+ self.sync_status_label.setStyleSheet("color: #888;")
+ toolbar.addWidget(self.sync_status_label)
+
+ def setup_menubar(self):
+ """메뉴바 설정"""
+ menubar = self.menuBar()
+
+ # 파일 메뉴
+ menubar.addMenu("파일(&F)")
+
+ # 설정 메뉴
+ settings_menu = menubar.addMenu("설정(&S)")
+
+ # AI API 키 설정
+ ai_settings_action = QAction("🔑 AI API 키 설정...", self)
+ ai_settings_action.setShortcut("Ctrl+Shift+A")
+ ai_settings_action.triggered.connect(self.show_api_key_dialog)
+ settings_menu.addAction(ai_settings_action)
+
+ # 도움말 메뉴
+ menubar.addMenu("도움말(&H)")
+
+ def show_api_key_dialog(self):
+ """API 키 설정 다이얼로그 표시"""
+ dialog = APIKeyDialog(self)
+ dialog.settings_changed.connect(self._on_ai_settings_changed)
+ dialog.exec()
+
+ def _on_ai_settings_changed(self):
+ """AI 설정이 변경되었을 때"""
+ self.status_bar.showMessage("AI 설정이 저장되었습니다.", 3000)
+ self._init_ai_client()
+
+ def check_sync_availability(self):
+ """두 패널의 데이터 날짜가 같은지 확인하여 Sync 버튼 활성화/비활성화"""
+ left_date = self.left_panel.get_data_date()
+ right_date = self.right_panel.get_data_date()
+
+ # 둘 다 데이터가 있는 경우에만 비교
+ if left_date and right_date:
+ if left_date == right_date:
+ # 날짜가 같으면 Sync 활성화
+ self.sync_toggle.setEnabled(True)
+ self.sync_status_label.setText(f" ({left_date})")
+ self.sync_status_label.setStyleSheet("color: #00D084;")
+ else:
+ # 날짜가 다르면 Sync 비활성화
+ self.sync_toggle.setEnabled(False)
+ self.sync_toggle.setChecked(False) # 자동으로 끔
+ self.sync_status_label.setText(f" (날짜 불일치: {left_date} ≠ {right_date})")
+ self.sync_status_label.setStyleSheet("color: #FF5252;")
+ elif left_date or right_date:
+ # 한쪽만 데이터가 있음
+ self.sync_toggle.setEnabled(False)
+ date_info = left_date or right_date
+ self.sync_status_label.setText(f" ({date_info} - 상대 패널 없음)")
+ self.sync_status_label.setStyleSheet("color: #888;")
+ else:
+ # 둘 다 데이터 없음
+ self.sync_toggle.setEnabled(False)
+ self.sync_status_label.setText(" (데이터 없음)")
+ self.sync_status_label.setStyleSheet("color: #888;")
+
+ def on_sync_toggled(self, checked):
+ """동기화 모드 변경 시 동작"""
+ sync_manager.is_sync_enabled = checked
+
+ if checked:
+ # [SYNC ON]
+ # 1. 오른쪽 패널의 파일 로드 버튼 숨김
+ self.right_panel.set_slave_mode(True)
+
+ # 2. 왼쪽 패널의 데이터가 있다면 오른쪽으로 복사
+ if self.left_panel.raw_data:
+ self.right_panel.receive_shared_data(self.left_panel.raw_data)
+
+ self.status_bar.showMessage("Sync ON: 오른쪽 패널이 왼쪽 데이터와 동기화됩니다.")
+ else:
+ # [SYNC OFF]
+ self.right_panel.set_slave_mode(False)
+ self.status_bar.showMessage("Sync OFF: 독립 분석 모드")
+
+ def _init_ai_client(self):
+ """저장된 설정으로 AI 클라이언트 초기화(가능할 때만)"""
+ try:
+ from app.ai import get_ai_client, AIProviderType
+
+ settings = get_settings_manager().ai_settings
+ current_provider = settings.get_current_provider()
+
+ if not current_provider:
+ return
+
+ api_key = settings.get_api_key(current_provider)
+ model = settings.get_model(current_provider)
+
+ if not api_key:
+ return
+
+ client = get_ai_client()
+ provider_type = AIProviderType(current_provider)
+ if client.set_provider(provider_type, api_key, model):
+ st = client.get_status()
+ self.status_bar.showMessage(f"AI 준비: {st.get('provider')} / {st.get('model')}", 3000)
+ except Exception as e:
+ print(f"AI 클라이언트 초기화 실패: {e}")
+
+ def _get_panel_ctx(self, panel_id: str, user_query: str | None = None):
+ panel = self.right_panel if panel_id == "right" else self.left_panel
+ ctx = panel.get_ai_context()
+
+ # 질의에 따라 추가 분석 결과를 포함(프롬프트 품질 개선)
+ if user_query:
+ q = user_query.lower()
+ if ("pg3" in q or "pg3-2" in q) and ("역" in user_query):
+ try:
+ ctx.setdefault("analysis", {})
+ ctx["analysis"]["pg3_2_per_station"] = panel.compute_pg_at_station("PG3-2")
+ except Exception:
+ pass
+
+ return ctx
+
+ def _is_qobject_alive(self, obj) -> bool:
+ """PySide6 객체가 이미 delete된 상태인지 안전하게 판별"""
+ if obj is None:
+ return False
+ try:
+ import shiboken6 # type: ignore
+ return bool(shiboken6.isValid(obj))
+ except Exception:
+ # shiboken6이 없거나, 일부 환경에서 import 실패 시 런타임 호출로 체크
+ try:
+ obj.objectName()
+ return True
+ except Exception:
+ return False
+
+ def _on_ai_chat_destroyed(self, *_args):
+ # 삭제된 객체 참조 제거(다음 클릭에서 RuntimeError 방지)
+ self._ai_chat = None
+ self._ai_button.set_open_state(False)
+
+ def _open_ai_chat(self):
+ # 열려 있으면: 웹처럼 버튼(X)로 닫기 동작
+ if self._ai_chat and self._is_qobject_alive(self._ai_chat) and self._ai_chat.isVisible():
+ btn = self._ai_button
+ start_rect = self._ai_chat.geometry()
+ # 닫힘 애니메이션은 버튼 위치로 축소
+ btn_pos = btn.mapTo(self, btn.rect().topLeft())
+ end_rect = QRect(btn_pos.x(), btn_pos.y(), btn.width(), btn.height())
+ animate_close(self._ai_chat, start_rect, end_rect, duration_ms=180)
+ return
+
+ # 레퍼런스는 남아있지만 이미 삭제된 경우 정리
+ if self._ai_chat and not self._is_qobject_alive(self._ai_chat):
+ self._ai_chat = None
+ self._ai_button.set_open_state(False)
+
+ self._ai_chat = AIChatDialog(self, self._get_panel_ctx)
+ self._ai_chat.setAttribute(Qt.WA_DeleteOnClose, True)
+ self._ai_chat.destroyed.connect(self._on_ai_chat_destroyed)
+ self._ai_button.set_open_state(True)
+
+ # 버튼 위치 기준으로 확장 애니메이션
+ btn = self._ai_button
+ # 메인윈도우 좌표로 변환
+ btn_top_left = btn.mapTo(self, btn.rect().topLeft())
+ start_rect = QRect(btn_top_left.x(), btn_top_left.y(), btn.width(), btn.height())
+
+ # 최종 크기: 버튼을 가리지 않도록 '버튼 위로' 배치(가능하면)
+ w, h = 560, 680
+ gap = 10
+ margin = 14
+
+ # 1) 우선: 버튼 위로(오른쪽 정렬)
+ end_x = btn_top_left.x() + btn.width() - w
+ end_y = btn_top_left.y() - h - gap
+
+ # 위로 배치 불가능하면: 버튼 왼쪽으로
+ if end_y < margin:
+ end_y = max(margin, btn_top_left.y() + btn.height() - h) # 세로는 버튼과 겹치지 않게 최대한 맞춤
+ end_x = btn_top_left.x() - w - gap
+
+ # 그래도 화면 밖이면: 화면 안으로 클램프(버튼 가림 최소화)
+ end_x = max(margin, min(end_x, self.width() - w - margin))
+ end_y = max(margin, min(end_y, self.height() - h - margin))
+
+ end_rect = QRect(end_x, end_y, w, h)
+
+ animate_open(self._ai_chat, start_rect, end_rect, duration_ms=280)
+
+
diff --git a/app/ui/views/__init__.py b/app/ui/views/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/ui/views/__pycache__/__init__.cpython-311.pyc b/app/ui/views/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..4bb06db
Binary files /dev/null and b/app/ui/views/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ui/views/__pycache__/ai_view.cpython-311.pyc b/app/ui/views/__pycache__/ai_view.cpython-311.pyc
new file mode 100644
index 0000000..3d8ae86
Binary files /dev/null and b/app/ui/views/__pycache__/ai_view.cpython-311.pyc differ
diff --git a/app/ui/views/__pycache__/dashboard_view.cpython-311.pyc b/app/ui/views/__pycache__/dashboard_view.cpython-311.pyc
new file mode 100644
index 0000000..6dfa2d0
Binary files /dev/null and b/app/ui/views/__pycache__/dashboard_view.cpython-311.pyc differ
diff --git a/app/ui/views/__pycache__/graph_view.cpython-311.pyc b/app/ui/views/__pycache__/graph_view.cpython-311.pyc
new file mode 100644
index 0000000..76ab6da
Binary files /dev/null and b/app/ui/views/__pycache__/graph_view.cpython-311.pyc differ
diff --git a/app/ui/views/__pycache__/graph_view2.cpython-311.pyc b/app/ui/views/__pycache__/graph_view2.cpython-311.pyc
new file mode 100644
index 0000000..5442ea3
Binary files /dev/null and b/app/ui/views/__pycache__/graph_view2.cpython-311.pyc differ
diff --git a/app/ui/views/ai_view.py b/app/ui/views/ai_view.py
new file mode 100644
index 0000000..2580598
--- /dev/null
+++ b/app/ui/views/ai_view.py
@@ -0,0 +1,649 @@
+"""
+AI Diagnosis View
+
+- 현재 로드된 로그 데이터를 요약/진단하고, 사용자의 질문을 LLM에 전달합니다.
+- API 키/모델은 app/core/settings.py 의 AI 설정(ai_settings.json)에서 로드합니다.
+"""
+
+from __future__ import annotations
+
+import json
+from dataclasses import asdict, is_dataclass
+from typing import Any, Dict, List, Optional, Tuple
+
+from PySide6.QtCore import QObject, QThread, Signal, Qt
+from PySide6.QtWidgets import (
+ QWidget,
+ QVBoxLayout,
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QTextEdit,
+ QComboBox,
+ QGroupBox,
+ QMessageBox,
+ QSpinBox,
+ QLineEdit,
+)
+
+from app.ai import get_ai_client, AIProviderType
+from app.core.settings import get_settings_manager
+from app.core.sync_controller import sync_manager
+from app.ui.dialogs.api_key_dialog import APIKeyDialog
+from app.ai.maintenance_kb import get_maintenance_kb
+
+
+def _safe_to_dict(obj: Any) -> Dict[str, Any]:
+ """데이터 객체를 JSON 직렬화 가능한 dict로 축약 변환"""
+ if obj is None:
+ return {}
+ if is_dataclass(obj):
+ d = asdict(obj)
+ elif hasattr(obj, "__dict__"):
+ d = dict(obj.__dict__)
+ else:
+ return {"value": str(obj)}
+
+ # 너무 긴 텍스트는 컷
+ for k, v in list(d.items()):
+ if isinstance(v, str) and len(v) > 400:
+ d[k] = v[:400] + "..."
+ return d
+
+
+def _compute_basic_stats(rows: List[Any]) -> Dict[str, Any]:
+ """로그 데이터(객체 리스트)에서 기본 통계를 계산"""
+ if not rows:
+ return {"count": 0}
+
+ def _get_float(name: str) -> List[float]:
+ vals = []
+ for r in rows:
+ v = getattr(r, name, None)
+ if v is None:
+ continue
+ try:
+ vals.append(float(v))
+ except Exception:
+ pass
+ return vals
+
+ def _get_bool_ratio(name: str) -> float:
+ total = len(rows)
+ if total == 0:
+ return 0.0
+ on = 0
+ for r in rows:
+ if bool(getattr(r, name, False)):
+ on += 1
+ return on / total
+
+ speeds = _get_float("trainspeed")
+ dtgs = _get_float("dtg")
+ pwms = _get_float("pwm_value")
+
+ stats = {
+ "count": len(rows),
+ "time_start": getattr(rows[0], "time", ""),
+ "time_end": getattr(rows[-1], "time", ""),
+ "speed": {
+ "min": min(speeds) if speeds else None,
+ "max": max(speeds) if speeds else None,
+ "avg": (sum(speeds) / len(speeds)) if speeds else None,
+ },
+ "dtg": {
+ "min": min(dtgs) if dtgs else None,
+ "max": max(dtgs) if dtgs else None,
+ "avg": (sum(dtgs) / len(dtgs)) if dtgs else None,
+ },
+ "pwm": {
+ "min": min(pwms) if pwms else None,
+ "max": max(pwms) if pwms else None,
+ "avg": (sum(pwms) / len(pwms)) if pwms else None,
+ },
+ # 자주 쓰는 플래그 비율(ON 비율)
+ "flags_ratio": {
+ "system_active": _get_bool_ratio("system_active"),
+ "over_spd_warning": _get_bool_ratio("over_spd_warning"),
+ "tasc": _get_bool_ratio("tasc"),
+ "twct_enable": _get_bool_ratio("twct_enable"),
+ "door_open": _get_bool_ratio("door_open"),
+ "door_close": _get_bool_ratio("door_close"),
+ "psd_open": _get_bool_ratio("psd_open"),
+ "psd_close": _get_bool_ratio("psd_close"),
+ },
+ }
+ return stats
+
+
+class _AIWorker(QObject):
+ finished = Signal(str)
+ failed = Signal(str)
+
+ def __init__(self, prompt: str):
+ super().__init__()
+ self.prompt = prompt
+
+ def run(self):
+ try:
+ client = get_ai_client()
+ if not client.is_ready():
+ raise RuntimeError("AI가 아직 준비되지 않았습니다. 설정에서 API 키를 입력하세요.")
+ resp = client.chat(self.prompt, temperature=0.2)
+ self.finished.emit(resp.content or "")
+ except Exception as e:
+ self.failed.emit(str(e))
+
+
+class AIDiagnosisView(QWidget):
+ """AnalysisPanel의 AI Diagnosis 탭 위젯"""
+
+ def __init__(self, panel_id: str, graph_view: Optional[QObject] = None, parent: Optional[QWidget] = None):
+ super().__init__(parent)
+ self.panel_id = panel_id
+ self.graph_view = graph_view
+ self.file_path: Optional[str] = None
+ self.data_list: List[Any] = []
+ self.current_index: Optional[int] = None
+ self.current_row: Optional[Any] = None
+
+ self._thread: Optional[QThread] = None
+ self._worker: Optional[_AIWorker] = None
+ self._busy: bool = False
+
+ self._build_ui()
+ self.refresh_status()
+
+ # 그래프 커서 이동(동기화 매니저 이벤트) 기반으로 현재 포인트 갱신
+ sync_manager.time_changed.connect(self._on_time_changed)
+
+ def _build_ui(self):
+ root = QVBoxLayout(self)
+ root.setContentsMargins(12, 12, 12, 12)
+ root.setSpacing(10)
+
+ # 상단 상태/컨트롤
+ top = QHBoxLayout()
+ self.lbl_status = QLabel("AI: (미설정)")
+ self.lbl_status.setStyleSheet("color: #ccc; font-weight: bold;")
+
+ self.provider_combo = QComboBox()
+ self.provider_combo.setMinimumWidth(220)
+ self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
+
+ btn_settings = QPushButton("🔑 설정")
+ btn_settings.clicked.connect(self._open_settings)
+
+ top.addWidget(self.lbl_status, stretch=1)
+ top.addWidget(QLabel("프로바이더:"))
+ top.addWidget(self.provider_combo)
+ top.addWidget(btn_settings)
+ root.addLayout(top)
+
+ # 데이터 범위 선택
+ range_box = QGroupBox("분석 범위")
+ range_layout = QHBoxLayout(range_box)
+ range_layout.addWidget(QLabel("커서 기준 ±(초):"))
+ self.spin_window_sec = QSpinBox()
+ self.spin_window_sec.setRange(10, 3600)
+ self.spin_window_sec.setValue(300)
+ range_layout.addWidget(self.spin_window_sec)
+ range_layout.addStretch()
+ root.addWidget(range_box)
+
+ # 사용자 입력(열번/호차/편성/증상)
+ user_box = QGroupBox("사용자 입력 (운행/편성/증상)")
+ user_layout = QVBoxLayout(user_box)
+ row1 = QHBoxLayout()
+ self.in_train_no = QLineEdit()
+ self.in_train_no.setPlaceholderText("열번 (예: 101)")
+ self.in_car_no = QLineEdit()
+ self.in_car_no.setPlaceholderText("호차 (예: 3)")
+ self.in_formation = QLineEdit()
+ self.in_formation.setPlaceholderText("편성 (예: 8량/4M4T 등)")
+ row1.addWidget(QLabel("열번:"))
+ row1.addWidget(self.in_train_no)
+ row1.addWidget(QLabel("호차:"))
+ row1.addWidget(self.in_car_no)
+ row1.addWidget(QLabel("편성:"))
+ row1.addWidget(self.in_formation)
+ user_layout.addLayout(row1)
+
+ self.in_symptoms = QTextEdit()
+ # '증상'을 자유 질의/요청까지 포함하는 단일 입력으로 정리(중복 제거)
+ self.in_symptoms.setPlaceholderText(
+ "증상/요청을 입력하세요.\n"
+ "예) 정위치 불가로 추정됨. 커서 시점 전후에서 문/PSD 불일치 여부와 원인 후보를 추론해줘.\n"
+ "예) 비상제동 개입 구간이 있다면 타임라인(사용자 표시) 기준으로 원인 후보를 분석해줘."
+ )
+ self.in_symptoms.setFixedHeight(90)
+ user_layout.addWidget(QLabel("증상/요청:"))
+ user_layout.addWidget(self.in_symptoms)
+
+ kb_row = QHBoxLayout()
+ self.lbl_kb = QLabel("정비지침서: 0건 로드")
+ btn_kb_reload = QPushButton("📚 지침서 새로고침")
+ btn_kb_reload.clicked.connect(self._reload_kb)
+ kb_row.addWidget(self.lbl_kb, stretch=1)
+ kb_row.addWidget(btn_kb_reload)
+ user_layout.addLayout(kb_row)
+
+ root.addWidget(user_box)
+
+ # 버튼들
+ btn_row = QHBoxLayout()
+ self.btn_diag_current = QPushButton("🧭 현재 포인트 진단")
+ self.btn_diag_current.clicked.connect(self.run_current_diagnosis)
+
+ self.btn_summarize = QPushButton("🧾 전체 요약")
+ self.btn_summarize.clicked.connect(self.run_overall_summary)
+
+ self.btn_infer = QPushButton("💬 증상/요청 추론")
+ self.btn_infer.clicked.connect(self.run_symptom_inference)
+
+ self.btn_clear = QPushButton("🧹 출력 지우기")
+ self.btn_clear.clicked.connect(lambda: self.txt_output.clear())
+
+ btn_row.addWidget(self.btn_diag_current)
+ btn_row.addWidget(self.btn_summarize)
+ btn_row.addWidget(self.btn_infer)
+ btn_row.addWidget(self.btn_clear)
+ btn_row.addStretch()
+ root.addLayout(btn_row)
+
+ # 출력
+ self.txt_output = QTextEdit()
+ self.txt_output.setReadOnly(True)
+ self.txt_output.setPlaceholderText("AI 응답이 여기에 표시됩니다.")
+
+ root.addWidget(QLabel("결과:"))
+ root.addWidget(self.txt_output, stretch=1)
+
+ self._reload_kb()
+
+ def set_file_context(self, file_path: str):
+ """현재 로드 중인 파일 경로 저장(자동 입력 추출용)"""
+ self.file_path = file_path
+ self._autofill_from_file_context(force=False)
+
+ def set_data(self, data_list: List[Any]):
+ self.data_list = data_list or []
+ # 커서를 아직 안 움직여도 AI가 동작하도록 기본값(첫 레코드)을 잡아둠
+ self.current_index = 0 if self.data_list else None
+ self.current_row = self.data_list[0] if self.data_list else None
+ self._autofill_from_data(force=False)
+ self.refresh_status()
+
+ def refresh_status(self):
+ """설정/클라이언트 상태를 UI에 반영"""
+ settings = get_settings_manager().ai_settings
+ current = settings.get_current_provider()
+
+ # 콤보 재구성(키가 설정된 프로바이더만)
+ self.provider_combo.blockSignals(True)
+ self.provider_combo.clear()
+ configured = []
+ for p in ["openai", "openrouter", "gemini", "xai"]:
+ if settings.is_provider_configured(p):
+ configured.append(p)
+
+ # 아무것도 없으면 전체 목록 보여주되 선택만 못 하게 안내
+ providers_to_show = configured if configured else ["openai", "openrouter", "gemini", "xai"]
+ for p in providers_to_show:
+ try:
+ name = {
+ "openai": "OpenAI",
+ "openrouter": "OpenRouter",
+ "gemini": "Google Gemini",
+ "xai": "xAI (Grok)",
+ }[p]
+ except Exception:
+ name = p
+ self.provider_combo.addItem(name, p)
+
+ if current:
+ idx = self.provider_combo.findData(current)
+ if idx >= 0:
+ self.provider_combo.setCurrentIndex(idx)
+ self.provider_combo.blockSignals(False)
+
+ client = get_ai_client()
+ st = client.get_status()
+ if st.get("ready"):
+ self.lbl_status.setText(f"AI: {st.get('provider')} / {st.get('model')} (키: {st.get('api_key')})")
+ self.lbl_status.setStyleSheet("color: #00D084; font-weight: bold;")
+ else:
+ self.lbl_status.setText("AI: 미준비 (설정에서 API 키 입력 필요)")
+ self.lbl_status.setStyleSheet("color: #FFAB00; font-weight: bold;")
+
+ def _reload_kb(self):
+ kb = get_maintenance_kb()
+ kb.reload()
+ self.lbl_kb.setText(f"정비지침서: {kb.count()}건 로드")
+
+ def _autofill_from_data(self, force: bool = False):
+ """데이터(레코드)에서 열번 등 자동 채움(사용자 수정 가능)"""
+ if not self.data_list:
+ return
+
+ first = self.data_list[0]
+ trainno = str(getattr(first, "trainno", "") or "").strip()
+ if trainno and (force or not (self.in_train_no.text() or "").strip()):
+ self.in_train_no.setText(trainno)
+
+ # 호차/편성은 데이터에 없을 수 있어 파일/문자열 기반 보완
+ self._autofill_from_file_context(force=force)
+
+ def _autofill_from_file_context(self, force: bool = False):
+ """파일명에서 호차/편성 힌트 추출(없으면 비움)"""
+ if not self.file_path:
+ return
+ name = str(self.file_path).replace("\\", "/").split("/")[-1]
+ up = name.upper()
+
+ # 예: ..._TC2_... -> 호차=2, 편성 힌트=TC2(+ HCR/TCR 등)
+ car_no = ""
+ formation = ""
+ import re
+
+ m = re.search(r"\bTC(\d+)\b", up)
+ if m:
+ car_no = m.group(1)
+ formation = f"TC{car_no}"
+
+ # HCR/TCR 등 운영 힌트를 편성에 함께 기록
+ hints = []
+ for h in ["HCR", "TCR"]:
+ if h in up:
+ hints.append(h)
+ if hints:
+ formation = (formation + " " + " ".join(hints)).strip()
+
+ if car_no and (force or not (self.in_car_no.text() or "").strip()):
+ self.in_car_no.setText(car_no)
+ if formation and (force or not (self.in_formation.text() or "").strip()):
+ self.in_formation.setText(formation)
+
+ def _open_settings(self):
+ dlg = APIKeyDialog(self)
+ dlg.settings_changed.connect(self._after_settings_changed)
+ dlg.exec()
+
+ def _after_settings_changed(self):
+ # MainWindow 쪽에서도 init을 하지만, 여기서도 즉시 반영
+ self.refresh_status()
+
+ def _on_provider_changed(self, _idx: int):
+ provider = self.provider_combo.currentData()
+ if not provider:
+ return
+
+ settings_mgr = get_settings_manager()
+ settings_mgr.ai_settings.set_current_provider(provider)
+ settings_mgr.save()
+
+ # 현재 프로바이더로 클라이언트 재초기화 시도
+ try:
+ from app.ai import AIProviderType
+ api_key = settings_mgr.ai_settings.get_api_key(provider)
+ model = settings_mgr.ai_settings.get_model(provider)
+ if api_key:
+ get_ai_client().set_provider(AIProviderType(provider), api_key, model)
+ except Exception as e:
+ self._append_output(f"[경고] 프로바이더 초기화 실패: {e}")
+
+ self.refresh_status()
+
+ def _on_time_changed(self, index: int, data: Any, source_id: str):
+ # sync 이벤트는 반대 패널에도 오므로, 내 panel_id와 무관하게 “현재 포인트”로만 사용
+ # 단, 현재 탭이 어느 패널에 붙었는지를 기준으로 source_id가 같은 경우에만 갱신
+ if source_id != self.panel_id:
+ return
+ self.current_index = index
+ self.current_row = data
+
+ def _append_output(self, text: str):
+ self.txt_output.append(text)
+ self.txt_output.verticalScrollBar().setValue(self.txt_output.verticalScrollBar().maximum())
+
+ def _build_prompt_common(self) -> str:
+ stats = _compute_basic_stats(self.data_list)
+ # 그래프 맥락(선택 신호/가시 구간/타임라인)을 같이 포함
+ graph_ctx = self._get_graph_context()
+ user_ctx = self._get_user_context()
+ kb_ctx = self._get_kb_context()
+ return (
+ "당신은 철도 차량 MMI 로그를 분석하는 진단 엔지니어 AI입니다.\n"
+ "출력은 한국어로, 근거를 데이터 필드명 기준으로 간단히 제시하세요.\n\n"
+ f"[데이터 요약]\n{json.dumps(stats, ensure_ascii=False, indent=2)}\n"
+ f"\n[그래프 컨텍스트(선택 신호/표시 구간/타임라인)]\n{json.dumps(graph_ctx, ensure_ascii=False, indent=2)}\n"
+ f"\n[사용자 입력(운행/편성/증상)]\n{json.dumps(user_ctx, ensure_ascii=False, indent=2)}\n"
+ + (f"\n[정비지침서 발췌]\n{kb_ctx}\n" if kb_ctx else "")
+ )
+
+ def _get_user_context(self) -> Dict[str, Any]:
+ return {
+ "train_no": (self.in_train_no.text() or "").strip(),
+ "car_no": (self.in_car_no.text() or "").strip(),
+ "formation": (self.in_formation.text() or "").strip(),
+ "symptoms": (self.in_symptoms.toPlainText() or "").strip(),
+ }
+
+ def _get_graph_context(self) -> Dict[str, Any]:
+ if not self.graph_view:
+ return {}
+ try:
+ # GraphView에 helper가 있으면 사용
+ if hasattr(self.graph_view, "get_ai_context"):
+ return self.graph_view.get_ai_context(self.current_index)
+
+ # fallback: 최소 정보만 수집
+ ctx: Dict[str, Any] = {}
+ if hasattr(self.graph_view, "axis_x"):
+ mn = self.graph_view.axis_x.min().toMSecsSinceEpoch()
+ mx = self.graph_view.axis_x.max().toMSecsSinceEpoch()
+ ctx["visible_range_ms"] = {"min": mn, "max": mx, "seconds": (mx - mn) / 1000.0}
+ if hasattr(self.graph_view, "signals_map"):
+ visible = []
+ for k, info in self.graph_view.signals_map.items():
+ try:
+ if info.get("series") and info["series"].isVisible():
+ visible.append({"key": k, "name": info.get("base", k)})
+ except Exception:
+ pass
+ ctx["visible_signals"] = visible
+ if hasattr(self.graph_view, "drawing_objects"):
+ ctx["drawing_objects"] = self._serialize_drawing_objects(self.graph_view.drawing_objects)
+ return ctx
+ except Exception:
+ return {}
+
+ def _serialize_drawing_objects(self, objs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ out = []
+ if not objs:
+ return out
+ for o in objs:
+ if not isinstance(o, dict):
+ continue
+ t = o.get("type")
+ if t not in ("timeline", "text", "rect", "circle", "line"):
+ continue
+ item = dict(o)
+ # QColor / Qt.PenStyle 등 직렬화 불가 요소 변환
+ col = item.get("color")
+ try:
+ if hasattr(col, "name"):
+ item["color"] = col.name()
+ except Exception:
+ pass
+ style = item.get("style")
+ try:
+ if hasattr(style, "value"):
+ item["style"] = style.value
+ except Exception:
+ pass
+ out.append(item)
+ return out
+
+ def _get_kb_context(self) -> str:
+ kb = get_maintenance_kb()
+ user = self._get_user_context()
+ query = " ".join([user.get("symptoms", "")]).strip()
+ if not query:
+ return ""
+ hits = kb.search(query, top_k=3)
+ if not hits:
+ return ""
+ # 프롬프트 길이 제한을 위해 짧게
+ lines = []
+ for h in hits:
+ title = h.get("title", "untitled")
+ snippet = h.get("snippet", "")
+ lines.append(f"- {title}: {snippet}")
+ return "\n".join(lines)[:2500]
+
+ def _slice_window(self) -> Tuple[List[Any], int, int]:
+ if not self.data_list:
+ return [], 0, 0
+ if self.current_index is None:
+ return self.data_list, 0, len(self.data_list) - 1
+
+ half = int(self.spin_window_sec.value())
+ start = max(0, self.current_index - half)
+ end = min(len(self.data_list) - 1, self.current_index + half)
+ return self.data_list[start : end + 1], start, end
+
+ def _run_prompt(self, prompt: str):
+ # 이미 실행 중이면 차단 (QThread deleteLater로 인한 래퍼 삭제 문제 회피)
+ if self._busy:
+ QMessageBox.information(self, "AI", "이미 요청이 실행 중입니다. 완료 후 다시 시도하세요.")
+ return
+
+ self.btn_diag_current.setEnabled(False)
+ self.btn_summarize.setEnabled(False)
+ self.btn_infer.setEnabled(False)
+ self._append_output("\n---\n[AI 요청] 실행 중...")
+
+ self._busy = True
+ self._thread = QThread(self)
+ self._worker = _AIWorker(prompt)
+ self._worker.moveToThread(self._thread)
+
+ self._thread.started.connect(self._worker.run)
+ self._worker.finished.connect(self._on_ai_finished)
+ self._worker.failed.connect(self._on_ai_failed)
+
+ # 정리
+ self._worker.finished.connect(self._thread.quit)
+ self._worker.failed.connect(self._thread.quit)
+ self._thread.finished.connect(self._on_thread_finished)
+
+ self._thread.start()
+
+ def _on_thread_finished(self):
+ # thread 객체가 deleteLater 등으로 사라져도 다음 실행에 영향 없도록 참조 정리
+ self._thread = None
+ self._worker = None
+ self._busy = False
+
+ def _on_ai_finished(self, text: str):
+ self._append_output(text.strip())
+ self.btn_diag_current.setEnabled(True)
+ self.btn_summarize.setEnabled(True)
+ self.btn_infer.setEnabled(True)
+ # 종료/정리는 thread.finished에서 처리
+
+ def _on_ai_failed(self, err: str):
+ self._append_output(f"[오류] {err}")
+ self.btn_diag_current.setEnabled(True)
+ self.btn_summarize.setEnabled(True)
+ self.btn_infer.setEnabled(True)
+ # 종료/정리는 thread.finished에서 처리
+
+ def run_overall_summary(self):
+ if not self.data_list:
+ QMessageBox.information(self, "AI", "먼저 로그 파일을 로드하세요.")
+ return
+
+ prompt = (
+ self._build_prompt_common()
+ + "\n[요청]\n"
+ "- 전체 운행 흐름(속도/DTG/문/ATC/경고) 관점에서 요약\n"
+ "- 이상 징후(과속경고, 문 상태 불일치, TASC/DTG 이상 등) 후보를 3~7개로 제시\n"
+ "- 각 후보에 대해 '가능 원인/추가 확인 포인트'를 짧게 제안\n"
+ )
+ self._run_prompt(prompt)
+
+ def run_current_diagnosis(self):
+ if not self.data_list:
+ QMessageBox.information(self, "AI", "먼저 로그 파일을 로드하세요.")
+ return
+
+ window_rows, s, e = self._slice_window()
+ current = _safe_to_dict(self.current_row)
+ window_stats = _compute_basic_stats(window_rows)
+
+ prompt = (
+ self._build_prompt_common()
+ + f"\n[커서 정보]\nindex={self.current_index}\n"
+ + f"\n[커서 레코드]\n{json.dumps(current, ensure_ascii=False, indent=2)}\n"
+ + f"\n[커서 주변({s}~{e}) 통계]\n{json.dumps(window_stats, ensure_ascii=False, indent=2)}\n"
+ + "\n[요청]\n"
+ "- 커서 시점의 상태를 설명하고, 주변 구간에서의 변화/이상 징후를 진단\n"
+ "- 문/PSD/ATC/TASC/DTG/과속경고 관련 이슈 가능성을 우선순위로 제시\n"
+ )
+ self._run_prompt(prompt)
+
+ def run_symptom_inference(self):
+ """증상/요청(단일 입력) 기반 추론 실행"""
+ if not self.data_list:
+ QMessageBox.information(self, "AI", "먼저 로그 파일을 로드하세요.")
+ return
+ stext = (self.in_symptoms.toPlainText() or "").strip()
+ if not stext:
+ QMessageBox.information(self, "AI", "증상/요청을 입력하세요.")
+ return
+
+ window_rows, s, e = self._slice_window()
+ window_stats = _compute_basic_stats(window_rows)
+ current = _safe_to_dict(self.current_row)
+
+ prompt = (
+ self._build_prompt_common()
+ + f"\n[커서 정보]\nindex={self.current_index}\n"
+ + f"\n[커서 레코드]\n{json.dumps(current, ensure_ascii=False, indent=2)}\n"
+ + f"\n[커서 주변({s}~{e}) 통계]\n{json.dumps(window_stats, ensure_ascii=False, indent=2)}\n"
+ + f"\n[사용자 증상/요청]\n{stext}\n"
+ + "\n[요청]\n- 입력된 증상/요청을 기준으로 원인 후보와 근거(필드명)를 제시하고, 추가 확인 포인트/정비 접근을 제안\n"
+ )
+ self._run_prompt(prompt)
+
+ def run_range_analysis(self, start_idx: int, end_idx: int, start_ms: int, end_ms: int):
+ """사용자 선택 구간 분석 요청"""
+ if not self.data_list:
+ QMessageBox.information(self, "AI", "먼저 로그 파일을 로드하세요.")
+ return
+ if start_idx < 0 or end_idx < 0 or start_idx >= len(self.data_list) or end_idx >= len(self.data_list):
+ QMessageBox.information(self, "AI", "선택 구간이 올바르지 않습니다.")
+ return
+ if start_idx > end_idx:
+ start_idx, end_idx = end_idx, start_idx
+
+ rows = self.data_list[start_idx:end_idx + 1]
+ window_stats = _compute_basic_stats(rows)
+ start_rec = _safe_to_dict(rows[0]) if rows else {}
+ end_rec = _safe_to_dict(rows[-1]) if rows else {}
+
+ prompt = (
+ self._build_prompt_common()
+ + f"\n[선택 구간]\nindex={start_idx}~{end_idx}\n"
+ + f"time_ms={start_ms}~{end_ms}\n"
+ + f"\n[시작 레코드]\n{json.dumps(start_rec, ensure_ascii=False, indent=2)}\n"
+ + f"\n[끝 레코드]\n{json.dumps(end_rec, ensure_ascii=False, indent=2)}\n"
+ + f"\n[구간 통계]\n{json.dumps(window_stats, ensure_ascii=False, indent=2)}\n"
+ + "\n[요청]\n"
+ + "- 선택 구간의 운행 흐름과 특이점(속도/DTG/문/ATC/경고)을 요약\n"
+ + "- 이상 징후 후보와 원인 추정, 추가 확인 포인트를 제시\n"
+ )
+ self._run_prompt(prompt)
+
diff --git a/app/ui/views/dashboard_view.py b/app/ui/views/dashboard_view.py
new file mode 100644
index 0000000..def8f73
--- /dev/null
+++ b/app/ui/views/dashboard_view.py
@@ -0,0 +1,1093 @@
+"""
+Legacy Viewer (Dashboard View)
+- 상단 20%: 속도계 그룹
+- 중간 30%: 신호 카드 그룹
+- 하단 50%: 그래프 그룹 (미니 그래프)
+"""
+
+from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
+ QGroupBox, QFrame, QLabel, QSplitter, QScrollArea,
+ QSizePolicy, QPushButton)
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor
+
+from app.ui.components.signal_components import (
+ OnOffSignalLabel, DataSignalLabel, SignalCard
+)
+from app.core.sync_controller import sync_manager
+from app.ui.views.graph_view import GraphView
+
+
+class DashboardView(QWidget):
+ def __init__(self, panel_id):
+ super().__init__()
+ self.panel_id = panel_id
+
+ # 노이즈 필터링 상태
+ self.last_valid_pwm = 0
+ self._signal_filter_state = {}
+ self._signal_valid_sources = {}
+ self._source_filtered_last = {}
+
+ # 신호 컴포넌트 참조 저장
+ self.signal_widgets = {}
+
+ self.setup_ui()
+ sync_manager.time_changed.connect(self.update_view)
+
+ def setup_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ # === 메인 Splitter (수직) ===
+ self.main_splitter = QSplitter(Qt.Vertical)
+ self.main_splitter.setChildrenCollapsible(False) # 완전히 접히지 않도록
+ self.main_splitter.setHandleWidth(5)
+ self.main_splitter.setStyleSheet("""
+ QSplitter::handle { background-color: #3E3E42; }
+ QSplitter::handle:hover { background-color: #007ACC; }
+ QSplitter::handle:pressed { background-color: #005A9E; }
+ """)
+
+ # === [1] 상단: 신호 카드 그룹 ===
+ signal_group = self._create_signal_group()
+ self.main_splitter.addWidget(signal_group)
+
+ # === [2] 하단: 미니 그래프 그룹 ===
+ graph_group = self._create_graph_group()
+ self.main_splitter.addWidget(graph_group)
+
+ # 초기 비율 설정 (60:40)
+ self.main_splitter.setSizes([300, 200])
+
+ main_layout.addWidget(self.main_splitter)
+
+ # =========================================================================
+ # [2] 신호 카드 그룹
+ # =========================================================================
+ def _create_signal_group(self):
+ # 전체 컨테이너 (스크롤 + VD 버튼)
+ wrapper = QWidget()
+ wrapper_layout = QVBoxLayout(wrapper)
+ wrapper_layout.setContentsMargins(0, 0, 0, 0)
+ wrapper_layout.setSpacing(0)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ scroll.setMinimumHeight(100)
+ scroll.setStyleSheet("""
+ QScrollArea { background-color: #1E1E1E; border: none; }
+ QScrollBar:vertical { background: #2D2D30; width: 8px; }
+ QScrollBar::handle:vertical { background: #555; border-radius: 4px; min-height: 20px; }
+ QScrollBar:horizontal { background: #2D2D30; height: 8px; }
+ QScrollBar::handle:horizontal { background: #555; border-radius: 4px; min-width: 20px; }
+ """)
+
+ container = QWidget()
+ container.setStyleSheet("background-color: #1E1E1E;")
+ grid = QGridLayout(container)
+ grid.setContentsMargins(3, 3, 3, 3)
+ grid.setSpacing(3)
+
+ # 카드 생성 - 3열 배치
+ # Row 0: ATC | ATO | TWC | FAIL
+ # Row 1: RLY(ATO) | DI(ATC) | DO(ATC)
+ # Row 2: ETC1 (DATA) | ETC2 (ON/OFF)
+
+ # Row 0: INFO, ATC, ATO, TWC (4열)
+ self.card_info = self._create_info_card()
+ self.card_atc = self._create_atc_card()
+ self.card_ato = self._create_ato_card()
+ self.card_twc = self._create_twc_card()
+ grid.addWidget(self.card_info, 0, 0)
+ grid.addWidget(self.card_atc, 0, 1)
+ grid.addWidget(self.card_ato, 0, 2)
+ grid.addWidget(self.card_twc, 0, 3)
+
+ # Row 1: FAIL, RLY(ATO), DI(ATC), DO(ATC)
+ self.card_fail = self._create_fail_card()
+ self.card_rly = self._create_rly_card()
+ self.card_di = self._create_di_card()
+ self.card_do = self._create_do_card()
+ grid.addWidget(self.card_fail, 1, 0)
+ grid.addWidget(self.card_rly, 1, 1)
+ grid.addWidget(self.card_di, 1, 2)
+ grid.addWidget(self.card_do, 1, 3)
+
+ # Row 2: ETC1 (DATA 전용), ETC2 (ON/OFF 전용)
+ self.card_etc1 = self._create_etc1_card() # DATA 컴포넌트
+ self.card_etc2 = self._create_etc2_card() # ON/OFF 컴포넌트
+ grid.addWidget(self.card_etc1, 2, 0, 1, 2) # 2열 차지
+ grid.addWidget(self.card_etc2, 2, 2, 1, 2) # 2열 차지
+
+ # 메인 카드 목록 (참조용)
+ self.main_cards = [
+ self.card_info, self.card_atc, self.card_ato, self.card_twc, self.card_fail,
+ self.card_rly, self.card_di, self.card_do,
+ self.card_etc1, self.card_etc2
+ ]
+
+ # VD 카드 (6개) - 토글 가능, 기본값 OFF (숨김)
+ self.vd_cards = [
+ self._create_vdi_a_card(),
+ self._create_vdi_b_card(),
+ self._create_vdi_c_card(),
+ self._create_vdi_d_card(),
+ self._create_vdo_a_card(),
+ self._create_vdo_b_card(),
+ ]
+
+ # VD 카드 배치 (4열, Row 3~4)
+ for i, card in enumerate(self.vd_cards):
+ card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ row = 3 + (i // 4)
+ col = i % 4
+ grid.addWidget(card, row, col)
+ card.setVisible(False) # 기본값: 숨김 (OFF)
+
+ scroll.setWidget(container)
+ wrapper_layout.addWidget(scroll, 1)
+
+ # VD 토글 버튼 (오른쪽 하단)
+ btn_container = QWidget()
+ btn_container.setStyleSheet("background-color: #1E1E1E;")
+ btn_layout = QHBoxLayout(btn_container)
+ btn_layout.setContentsMargins(5, 2, 5, 2)
+ btn_layout.addStretch()
+
+ self.vd_visible = False # VD 카드 표시 상태 - 기본값 OFF
+ self.btn_vd_toggle = QPushButton("VD ▲") # 기본값: 숨김 상태
+ self.btn_vd_toggle.setFixedSize(50, 22)
+ self.btn_vd_toggle.setStyleSheet("""
+ QPushButton {
+ background-color: #4C4C4C;
+ color: #888888;
+ border: 1px solid #666;
+ border-radius: 3px;
+ font-size: 10px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #5C5C5C;
+ }
+ QPushButton:pressed {
+ background-color: #3C3C3C;
+ }
+ """)
+ self.btn_vd_toggle.clicked.connect(self._toggle_vd_cards)
+ btn_layout.addWidget(self.btn_vd_toggle)
+
+ wrapper_layout.addWidget(btn_container)
+
+ return wrapper
+
+ def _toggle_vd_cards(self):
+ """VD 카드들(VDI A/B/C/D, VDO A/B) 표시/숨김 토글"""
+ self.vd_visible = not self.vd_visible
+ for card in self.vd_cards:
+ card.setVisible(self.vd_visible)
+
+ if self.vd_visible:
+ self.btn_vd_toggle.setText("VD ▼")
+ self.btn_vd_toggle.setStyleSheet("""
+ QPushButton {
+ background-color: #4C4C4C;
+ color: #00FF00;
+ border: 1px solid #666;
+ border-radius: 3px;
+ font-size: 10px;
+ font-weight: bold;
+ }
+ QPushButton:hover { background-color: #5C5C5C; }
+ """)
+ else:
+ self.btn_vd_toggle.setText("VD ▲")
+ self.btn_vd_toggle.setStyleSheet("""
+ QPushButton {
+ background-color: #4C4C4C;
+ color: #888888;
+ border: 1px solid #666;
+ border-radius: 3px;
+ font-size: 10px;
+ font-weight: bold;
+ }
+ QPushButton:hover { background-color: #5C5C5C; }
+ """)
+
+ # -------------------------------------------------------------------------
+ # INFO 카드 (상단 상태 요약)
+ # -------------------------------------------------------------------------
+ # -------------------------------------------------------------------------
+ # INFO 카드 (상단 상태 요약)
+ # -------------------------------------------------------------------------
+ # -------------------------------------------------------------------------
+ # INFO 카드 (상단 상태 요약)
+ # -------------------------------------------------------------------------
+ def _create_info_card(self):
+ card = SignalCard("INFO")
+ # 커스텀 레이아웃을 위해 add_signal 대신 signal_grid 직접 사용
+ grid = card.signal_grid
+
+ # 1. TIME (상단, 전체 너비 사용 및 중앙 정렬)
+ time_sig = DataSignalLabel("time", "TIME", "")
+ time_sig.set_value_style("font-size: 16px; font-weight: bold; color: #00D084;") # 강조
+ # DataSignalLabel은 내부적으로 QHBoxLayout을 사용하므로,
+ # 중앙 정렬을 위해 컨테이너나 스타일 조정이 필요할 수 있으나,
+ # 여기서는 Grid의 0행 전체를 차지하게 함.
+ grid.addWidget(time_sig, 0, 0, 1, 4, Qt.AlignCenter)
+
+ card.signals["time"] = time_sig
+ self.signal_widgets["info_time"] = time_sig
+
+ # 2. SPEED (속도계) - 1행, 전체 너비
+ from app.ui.components.speedometer import SpeedometerWidget
+ self.speedometer = SpeedometerWidget(title="SPEED", unit="km/h", max_value=120)
+ grid.addWidget(self.speedometer, 1, 0, 1, 4, Qt.AlignCenter)
+
+ card.signals["speed"] = self.speedometer # 필요 시 참조용
+ self.signal_widgets["info_speed"] = self.speedometer
+
+ # 3. 나머지 데이터 (LIMIT, DTG, PWM, ATC CODE) - 2행에 2열씩 배치
+ # Row 2: LIMIT, DTG
+ # Row 3: PWM, ATC CODE
+
+ limit_sig = DataSignalLabel("limit", "LIMIT", "km/h")
+ dtg_sig = DataSignalLabel("dtg", "DTG", "m")
+ pwm_sig = DataSignalLabel("pwm", "PWM", "%")
+ atc_sig = DataSignalLabel("atc_code", "ATC CODE", "")
+
+ # (위젯, 행, 열, 행스팬, 열스팬)
+ grid.addWidget(limit_sig, 2, 0, 1, 2)
+ grid.addWidget(dtg_sig, 2, 2, 1, 2)
+ grid.addWidget(pwm_sig, 3, 0, 1, 2)
+ grid.addWidget(atc_sig, 3, 2, 1, 2)
+
+ # signals 등록
+ card.signals["limit"] = limit_sig
+ card.signals["dtg"] = dtg_sig
+ card.signals["pwm"] = pwm_sig
+ card.signals["atc_code"] = atc_sig
+
+ self.signal_widgets["info_limit"] = limit_sig
+ self.signal_widgets["info_dtg"] = dtg_sig
+ self.signal_widgets["info_pwm"] = pwm_sig
+ self.signal_widgets["info_atc_code"] = atc_sig
+
+ # 레이아웃 마무리 (SignalCard 내부 메서드 호출 대신 수동으로 stretch 설정)
+ grid.setRowStretch(4, 1)
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # ATC 카드
+ # -------------------------------------------------------------------------
+ def _create_atc_card(self):
+ card = SignalCard("ATC")
+
+ # DATA: 운전실 상태
+ cab = DataSignalLabel("cab", "운전실", description="운전실 선택 상태 (TC1/TC2/OFF)")
+ card.add_signal(cab, "cab")
+ self.signal_widgets["atc_cab"] = cab
+
+ # DATA: ATC ACTIVE, CARR, CODE, CODE FREQ, LIMIT
+ for name, desc in [
+ ("ATC ACT", "ATC 활성화 상태"),
+ ("CARR", "ATC 캐리어 주파수"),
+ ("CODE", "ATC 속도 코드"),
+ ("FREQ", "코드 주파수"),
+ ("LIMIT", "ATC 제한속도"),
+ ]:
+ sig = DataSignalLabel(name.replace(" ", "_").lower(), name, description=desc)
+ card.add_signal(sig, name.replace(" ", "_").lower())
+ self.signal_widgets[f"atc_{name.replace(' ', '_').lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # TWC 카드
+ # -------------------------------------------------------------------------
+ def _create_twc_card(self):
+ card = SignalCard("TWC")
+
+ # ON/OFF
+ for name, desc in [
+ ("DCW", "Door Close Warning"),
+ ("TWX Tx", "TWC 송신 활성화"),
+ ("BERTH", "Train Berth - 정위치 정차"),
+ ("W.DOOR", "Wrong Door - 잘못된 도어"),
+ ]:
+ sig = OnOffSignalLabel(name.replace(" ", "_").lower(), name, desc)
+ card.add_signal(sig, name.replace(" ", "_").lower())
+ self.signal_widgets[f"twc_{name.replace(' ', '_').lower()}"] = sig
+
+ # DATA
+ for name, desc in [
+ ("열번", "열차 번호"),
+ ("현재역", "현재 역 코드"),
+ ("다음역", "다음 역 코드"),
+ ("종착역", "종착역 코드"),
+ ("DOOR", "다음 도어 방향"),
+ ]:
+ sig = DataSignalLabel(name, name, description=desc)
+ card.add_signal(sig, name)
+ self.signal_widgets[f"twc_{name}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # ATO 카드
+ # -------------------------------------------------------------------------
+ def _create_ato_card(self):
+ card = SignalCard("ATO")
+
+ # ON/OFF
+ for name, full_name, desc in [
+ ("TASC", "TASC", "Train Automatic Stop Control - 자동정차제어"),
+ ("TASC DB", "TASC DB", "TASC Dynamic Brake"),
+ ("ATO EB", "ATO EB REQ", "ATO Emergency Brake Request - ATO 비상제동 요청"),
+ ]:
+ sig = OnOffSignalLabel(name, full_name, desc)
+ card.add_signal(sig, name.lower().replace(" ", "_"))
+ self.signal_widgets[f"ato_{name.lower().replace(' ', '_')}"] = sig
+
+ # DATA
+ for name, desc in [
+ ("MARKER", "ATO 마커 (PG1/2/3/X)"),
+ ("OSC", "OSC 주파수"),
+ ("PWM", "PWM 값"),
+ ("DTG", "남은거리(m)"),
+ ]:
+ sig = DataSignalLabel(name.lower(), name, description=desc)
+ card.add_signal(sig, name.lower())
+ self.signal_widgets[f"ato_{name.lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # RLY (ATO) 카드
+ # -------------------------------------------------------------------------
+ def _create_rly_card(self):
+ card = SignalCard("RLY (ATO)")
+
+ for name in ["DR", "BR", "ADC", "ADOL", "ADOR", "OSC", "KUR", "SEL"]:
+ sig = OnOffSignalLabel(name, name, f"RLY {name} 릴레이 출력")
+ card.add_signal(sig, name.lower())
+ self.signal_widgets[f"rly_{name.lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # FAIL 카드
+ # -------------------------------------------------------------------------
+ def _create_fail_card(self):
+ card = SignalCard("FAIL")
+
+ for name, desc in [
+ ("ATO.R", "ATO Receiver Fail"),
+ ("ATO.C", "ATO Controller Fail"),
+ ("TCMS", "TCMS Communication Fail"),
+ ("TACHO#1", "Tachometer #1 Fail"),
+ ("TACHO#2", "Tachometer #2 Fail"),
+ ]:
+ sig = OnOffSignalLabel(name.replace(".", "_").replace("#", ""), name, desc)
+ sig.set_style_override(
+ on_style="background-color: #E53935; color: white; border-radius: 3px; padding: 2px 6px; font-weight: bold;",
+ )
+ card.add_signal(sig, name.replace(".", "_").replace("#", "").lower())
+ self.signal_widgets[f"fail_{name.replace('.', '_').replace('#', '').lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # DI (ATC) 카드
+ # -------------------------------------------------------------------------
+ def _create_di_card(self):
+ card = SignalCard("DI (ATC)")
+
+ # ON/OFF
+ for name in ["HCR", "TCR", "START"]:
+ sig = OnOffSignalLabel(name, name, f"DI {name} 입력 신호")
+ card.add_signal(sig, name.lower())
+ self.signal_widgets[f"di_{name.lower()}"] = sig
+
+ # DATA 형태로 표시 (운전모드/마스콘/역전기/PSD/DOOR)
+ for name, desc in [
+ ("운전모드", "운전 모드"),
+ ("마스콘", "마스콘 위치"),
+ ("역전기", "역전기 위치"),
+ ("PSD", "PSD 상태"),
+ ("DOOR", "도어 상태"),
+ ]:
+ sig = DataSignalLabel(name.lower(), name, description=desc)
+ card.add_signal(sig, name.lower())
+ self.signal_widgets[f"di_{name.lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # DO (ATC) 카드
+ # -------------------------------------------------------------------------
+ def _create_do_card(self):
+ card = SignalCard("DO (ATC)")
+
+ for name in ["EB+", "EB-", "FSB", "ZVR", "EDR", "EDL"]:
+ sig = OnOffSignalLabel(name.replace("+", "p").replace("-", "m"), name, f"DO {name} 출력 신호")
+ card.add_signal(sig, name.replace("+", "p").replace("-", "m").lower())
+ self.signal_widgets[f"do_{name.replace('+', 'p').replace('-', 'm').lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # ETC1 카드 (DATA 컴포넌트만) - 가로 배치
+ # -------------------------------------------------------------------------
+ def _create_etc1_card(self):
+ card = SignalCard("ETC1")
+ card.set_horizontal_data_mode(True) # DATA를 가로로 배치
+
+ # DATA
+ for name in ["INIT PDT", "MNUL PDT", "TCMS DR", "DIA1", "DIA2", "VER", "DOOR MD"]:
+ sig = DataSignalLabel(name.replace(" ", "_").lower(), name, description=f"ETC {name}")
+ card.add_signal(sig, name.replace(" ", "_").lower())
+ self.signal_widgets[f"etc_{name.replace(' ', '_').lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # ETC2 카드 (ON/OFF 컴포넌트만)
+ # -------------------------------------------------------------------------
+ def _create_etc2_card(self):
+ card = SignalCard("ETC2")
+
+ # ON/OFF
+ for name in ["SYS ACT", "INCH", "CS", "평상", "회복", "O.SPD", "사전제동", "가속제한",
+ "-70↑", "+70↑", "TDIR A", "TDIR B"]:
+ sig = OnOffSignalLabel(name.replace(" ", "_"), name, f"ETC {name} 신호")
+ card.add_signal(sig, name.replace(" ", "_").lower())
+ self.signal_widgets[f"etc_{name.replace(' ', '_').lower()}"] = sig
+
+ return card
+
+ # -------------------------------------------------------------------------
+ # VDI A/B/C/D 카드
+ # -------------------------------------------------------------------------
+ def _create_vdi_a_card(self):
+ return self._create_vdi_ab_card("VDI A", "vdi_a")
+
+ def _create_vdi_b_card(self):
+ return self._create_vdi_ab_card("VDI B", "vdi_b")
+
+ def _create_vdi_ab_card(self, title, prefix):
+ card = SignalCard(title)
+ for name in ["HCR", "TCR", "FA", "AUTO", "MCS", "YARD", "FMC",
+ "D.OPEN", "D.CLOSE", "MC EB", "MC BR", "MC DR", "FWD", "NEU", "RVS"]:
+ sig = OnOffSignalLabel(name.replace(".", "_"), name, f"{title} {name}")
+ card.add_signal(sig, name.replace(".", "_").lower())
+ self.signal_widgets[f"{prefix}_{name.replace('.', '_').lower()}"] = sig
+ return card
+
+ def _create_vdi_c_card(self):
+ return self._create_vdi_cd_card("VDI C", "vdi_c")
+
+ def _create_vdi_d_card(self):
+ return self._create_vdi_cd_card("VDI D", "vdi_d")
+
+ def _create_vdi_cd_card(self, title, prefix):
+ card = SignalCard(title)
+ for name in ["PSD OP", "PSD CL", "START", "UNIT1", "EB+FB", "EB-FB",
+ "FSB FB", "ZVR FB", "EDR FB", "EDL FB", "TC1", "TC2"]:
+ sig = OnOffSignalLabel(name.replace("+", "p").replace("-", "m").replace(" ", "_"),
+ name, f"{title} {name}")
+ card.add_signal(sig, name.replace("+", "p").replace("-", "m").replace(" ", "_").lower())
+ self.signal_widgets[f"{prefix}_{name.replace('+', 'p').replace('-', 'm').replace(' ', '_').lower()}"] = sig
+ return card
+
+ # -------------------------------------------------------------------------
+ # VDO A/B 카드
+ # -------------------------------------------------------------------------
+ def _create_vdo_a_card(self):
+ return self._create_vdo_card("VDO A", "vdo_a")
+
+ def _create_vdo_b_card(self):
+ return self._create_vdo_card("VDO B", "vdo_b")
+
+ def _create_vdo_card(self, title, prefix):
+ card = SignalCard(title)
+ for name in ["EB+", "EB-", "FSB", "ZVR", "EDL", "EDR"]:
+ sig = OnOffSignalLabel(name.replace("+", "p").replace("-", "m"), name, f"{title} {name}")
+ card.add_signal(sig, name.replace("+", "p").replace("-", "m").lower())
+ self.signal_widgets[f"{prefix}_{name.replace('+', 'p').replace('-', 'm').lower()}"] = sig
+ return card
+
+ # =========================================================================
+ # [3] 그래프 그룹 (GraphView 전체 임베드)
+ # =========================================================================
+ def _create_graph_group(self):
+ # GraphView를 그대로 임베드
+ self.embedded_graph = GraphView(self.panel_id)
+ self.embedded_graph.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self.embedded_graph.setMinimumHeight(150)
+ return self.embedded_graph
+
+ def set_data(self, data_list):
+ """외부에서 데이터 로드 시 그래프에도 전달"""
+ if hasattr(self, 'embedded_graph'):
+ self.embedded_graph.set_data(data_list)
+
+ # source 기반 신뢰도 추정 (가짜 0 제거용)
+ signal_attr_map = {
+ "trainspeed": "trainspeed",
+ "pwm_value": "pwm_value",
+ "dtg": "dtg",
+ "osc_f": "osc_f",
+ }
+ self._signal_valid_sources = self._infer_valid_sources(data_list, signal_attr_map)
+ self._source_filtered_last = {}
+
+ # =========================================================================
+ # 노이즈 필터링 함수들 (1초 단위 깜박임 필터링)
+ # =========================================================================
+ def apply_pwm_filter(self, raw_pwm):
+ if 15 <= raw_pwm <= 90:
+ self.last_valid_pwm = raw_pwm
+ return raw_pwm
+ if raw_pwm == 0:
+ return self.last_valid_pwm
+ return 0
+
+ def filter_majority_signal(self, signal_name: str, current_value: bool,
+ window_size: int = 5) -> bool:
+ """
+ 과반수 기반 디지털 신호 필터 (매 프레임 깜박이는 신호용)
+
+ 최근 window_size개 값 중 과반수가 ON이면 ON 반환
+ SYSTEM ACTIVE처럼 매 초 깜박이는 신호에 적용
+ """
+ state_key = f"maj_{signal_name}"
+ if state_key not in self._signal_filter_state:
+ self._signal_filter_state[state_key] = {
+ 'history': [],
+ }
+
+ state = self._signal_filter_state[state_key]
+ state['history'].append(current_value)
+
+ # window_size 유지
+ if len(state['history']) > window_size:
+ state['history'].pop(0)
+
+ # 과반수 계산
+ on_count = sum(state['history'])
+ return on_count > len(state['history']) // 2
+
+ def filter_digital_signal(self, signal_name: str, current_value: bool,
+ stable_frames: int = 3) -> bool:
+ """
+ 디지털 신호 노이즈 필터링 (1초 깜박임 방지) - 완화된 버전
+
+ Args:
+ signal_name: 신호 이름 (상태 추적용)
+ current_value: 현재 값
+ stable_frames: 값이 변경되기 위해 필요한 연속 프레임 수 (기본 3프레임)
+
+ Logic:
+ - 첫 번째 값은 그대로 반영 (초기화)
+ - 이후 값이 변경되려면 stable_frames 연속 같은 값이어야 함
+ - 깜박임(1-2프레임 변동)은 무시됨
+ """
+ if signal_name not in self._signal_filter_state:
+ # 초기값은 현재 값으로 설정 (첫 프레임부터 정상 표시)
+ self._signal_filter_state[signal_name] = {
+ 'stable_value': current_value, # 현재 값으로 초기화
+ 'pending_value': current_value,
+ 'same_count': 0,
+ }
+ return current_value
+
+ state = self._signal_filter_state[signal_name]
+
+ if current_value == state['stable_value']:
+ # 안정값과 같으면 유지
+ state['pending_value'] = current_value
+ state['same_count'] = 0
+ return state['stable_value']
+ elif current_value == state['pending_value']:
+ # 대기값과 같으면 카운트 증가
+ state['same_count'] += 1
+ if state['same_count'] >= stable_frames:
+ state['stable_value'] = current_value
+ state['same_count'] = 0
+ return state['stable_value']
+ else:
+ # 새로운 값이면 대기값 갱신
+ state['pending_value'] = current_value
+ state['same_count'] = 1
+ return state['stable_value']
+
+ def filter_numeric_signal(self, signal_name: str, current_value: float,
+ min_valid: float = 0, stable_frames: int = 3) -> float:
+ """
+ 숫자 신호 노이즈 필터링 (1초 깜박임 방지) - 완화된 버전
+
+ Args:
+ signal_name: 신호 이름
+ current_value: 현재 값
+ min_valid: 유효 최소값 (기본 0 = 모든 값 허용)
+ stable_frames: 값이 변경되기 위해 필요한 연속 프레임 수
+
+ Logic:
+ - 첫 번째 값은 그대로 반영
+ - 이후 값 변경은 stable_frames 연속 같은 값이어야 함
+ """
+ state_key = f"num_{signal_name}"
+ if state_key not in self._signal_filter_state:
+ self._signal_filter_state[state_key] = {
+ 'stable_value': current_value, # 현재 값으로 초기화
+ 'pending_value': current_value,
+ 'same_count': 0,
+ }
+ return current_value
+
+ state = self._signal_filter_state[state_key]
+
+ # 같은 값이면 유지
+ if current_value == state['stable_value']:
+ state['pending_value'] = current_value
+ state['same_count'] = 0
+ return state['stable_value']
+
+ # 다른 값이면 카운트
+ if current_value == state['pending_value']:
+ state['same_count'] += 1
+ if state['same_count'] >= stable_frames:
+ state['stable_value'] = current_value
+ state['same_count'] = 0
+ else:
+ state['pending_value'] = current_value
+ state['same_count'] = 1
+
+ return state['stable_value']
+
+ def filter_string_signal(self, signal_name: str, current_value: str,
+ stable_frames: int = 3) -> str:
+ """
+ 문자열 신호 노이즈 필터링 (1초 깜박임 방지)
+
+ Args:
+ signal_name: 신호 이름
+ current_value: 현재 값
+ stable_frames: 안정된 것으로 인정할 연속 프레임 수
+
+ Logic:
+ - 값이 변경되면 stable_frames 동안 이전 값 유지
+ - 새 값이 stable_frames 연속되면 변경 인정
+ """
+ state_key = f"str_{signal_name}"
+ if state_key not in self._signal_filter_state:
+ self._signal_filter_state[state_key] = {
+ 'stable_value': current_value,
+ 'pending_value': current_value,
+ 'same_count': 0,
+ }
+
+ state = self._signal_filter_state[state_key]
+
+ if current_value == state['stable_value']:
+ # 안정값과 같으면 유지
+ state['pending_value'] = current_value
+ state['same_count'] = 0
+ return state['stable_value']
+ elif current_value == state['pending_value']:
+ # 대기값과 같으면 카운트 증가
+ state['same_count'] += 1
+ if state['same_count'] >= stable_frames:
+ state['stable_value'] = current_value
+ state['same_count'] = 0
+ return state['stable_value']
+ else:
+ # 새로운 값이면 대기값 갱신
+ state['pending_value'] = current_value
+ state['same_count'] = 1
+ return state['stable_value']
+
+ def filter_mode_signal(self, signal_name: str, current_mode: str,
+ stable_frames: int = 3) -> str:
+ """
+ 모드 신호 노이즈 필터링 (FA/AUTO/MCS 등 모드 전환 시)
+
+ Logic:
+ - None이 아닌 모드가 들어오면 즉시 표시
+ - None이 들어오면 stable_frames 동안 이전 모드 유지
+ - None이 stable_frames 연속되면 None으로 인정
+ """
+ state_key = f"mode_{signal_name}"
+ if state_key not in self._signal_filter_state:
+ self._signal_filter_state[state_key] = {
+ 'stable_mode': None,
+ 'none_count': 0,
+ }
+
+ state = self._signal_filter_state[state_key]
+
+ if current_mode is not None:
+ state['none_count'] = 0
+ state['stable_mode'] = current_mode
+ return current_mode
+ else:
+ state['none_count'] += 1
+ if state['none_count'] >= stable_frames:
+ state['stable_mode'] = None
+ return state['stable_mode']
+
+ def _infer_valid_sources(self, data_list, signal_attr_map, zero_ratio_threshold=0.98, min_samples=50):
+ """
+ 소스별로 '거의 항상 0'인 신호를 찾아 유효 소스를 추정한다.
+ - 특정 소스가 해당 신호에서 0 비율이 매우 높고, 다른 소스는 그렇지 않으면 해당 소스를 제외.
+ """
+ stats = {k: {} for k in signal_attr_map.keys()}
+ for d in data_list or []:
+ src = getattr(d, "source", None)
+ if src is None:
+ continue
+ for key, attr in signal_attr_map.items():
+ val = getattr(d, attr, None)
+ if val is None:
+ continue
+ s = stats[key].setdefault(src, {"n": 0, "zero": 0})
+ s["n"] += 1
+ if val == 0:
+ s["zero"] += 1
+
+ valid_sources_map = {}
+ for key, by_src in stats.items():
+ if not by_src or len(by_src) <= 1:
+ valid_sources_map[key] = None
+ continue
+ ratios = {}
+ for src, s in by_src.items():
+ if s["n"] < min_samples:
+ continue
+ ratios[src] = (s["zero"] / s["n"]) if s["n"] else 0
+ if not ratios:
+ valid_sources_map[key] = None
+ continue
+ low_zero = [src for src, r in ratios.items() if r < zero_ratio_threshold]
+ high_zero = [src for src, r in ratios.items() if r >= zero_ratio_threshold]
+ if low_zero and high_zero:
+ valid_sources_map[key] = set(low_zero)
+ else:
+ valid_sources_map[key] = None
+ return valid_sources_map
+
+ def _get_source_filtered_value(self, data, key, attr, default=0):
+ """
+ source 기반으로 유효 값만 사용. 유효하지 않은 소스면 직전 유효값 유지.
+ """
+ val = getattr(data, attr, default)
+ src = getattr(data, "source", None)
+ valid_sources = self._signal_valid_sources.get(key)
+ is_valid = (valid_sources is None) or (src in valid_sources)
+ if is_valid:
+ self._source_filtered_last[key] = val
+ return val
+ if key in self._source_filtered_last:
+ return self._source_filtered_last[key]
+ return val if val is not None else default
+
+ # =========================================================================
+ # 데이터 업데이트
+ # =========================================================================
+ def update_view(self, index, data, source_id):
+ if not data:
+ return
+
+ # === INFO 카드 업데이트 ===
+ speed = self._get_source_filtered_value(data, "trainspeed", "trainspeed", default=0)
+ raw_pwm = self._get_source_filtered_value(data, "pwm_value", "pwm_value", default=0)
+ filtered_pwm = self.apply_pwm_filter(raw_pwm)
+ limit_spd = self.filter_numeric_signal("limitspeed", getattr(data, 'limitspeed', 0))
+ raw_dtg = self._get_source_filtered_value(data, "dtg", "dtg", default=0)
+ dtg = self.filter_numeric_signal("dtg", raw_dtg, min_valid=1)
+ atc_code = getattr(data, 'atc_code', '-')
+ time_str = getattr(data, 'time', '-')[-8:]
+
+ if w := self.signal_widgets.get("info_time"):
+ w.set_value(time_str)
+ if w := self.signal_widgets.get("info_speed"):
+ w.set_value(f"{speed:.1f}")
+ if hasattr(w, "set_limit"):
+ w.set_limit(limit_spd)
+ if hasattr(w, "set_atc_code"):
+ w.set_atc_code(atc_code)
+ if w := self.signal_widgets.get("info_limit"):
+ w.set_value(int(limit_spd))
+ if w := self.signal_widgets.get("info_dtg"):
+ w.set_value(f"{dtg:.0f}")
+ if w := self.signal_widgets.get("info_pwm"):
+ w.set_value(filtered_pwm)
+ if w := self.signal_widgets.get("info_atc_code"):
+ w.set_value(str(atc_code))
+
+ # === 신호 카드 업데이트 ===
+ self._update_signals(data)
+
+ def _update_signals(self, data):
+ """신호 카드 내 모든 신호 업데이트 (노이즈 필터링 적용)"""
+
+ # ===================================================================
+ # ATC 카드
+ # ===================================================================
+ tc1 = self.filter_digital_signal("tc1", getattr(data, 'tc1', False))
+ tc2 = self.filter_digital_signal("tc2", getattr(data, 'tc2', False))
+ cab_text = "TC1" if tc1 else ("TC2" if tc2 else "OFF")
+ if w := self.signal_widgets.get("atc_cab"):
+ w.set_value(cab_text)
+
+ # ===================================================================
+ # DO 카드 (모든 출력 신호에 필터 적용)
+ # ===================================================================
+ do_signals = [
+ ("ebp", "do_ebp"), ("ebm", "do_ebm"), ("fsb", "do_fsb"),
+ ("zvr", "do_zvr"), ("edr", "do_edr"), ("edl", "do_edl")
+ ]
+ for name, attr in do_signals:
+ val = self.filter_digital_signal(attr, getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"do_{name}"):
+ w.set_status(val)
+
+ # ===================================================================
+ # DI 카드 (모든 입력 신호에 필터 적용)
+ # ===================================================================
+ # HCR, TCR, START
+ if w := self.signal_widgets.get("di_hcr"):
+ w.set_status(self.filter_digital_signal("di_hcr", getattr(data, 'hcr', False)))
+ if w := self.signal_widgets.get("di_tcr"):
+ w.set_status(self.filter_digital_signal("di_tcr", getattr(data, 'tcr', False)))
+ if w := self.signal_widgets.get("di_start"):
+ w.set_status(self.filter_digital_signal("di_start", getattr(data, 'ato_start_btn', False)))
+
+ # 운전모드 (DATA)
+ raw_mode = None
+ for m in ["fa", "auto", "mcs", "yard", "fmc"]:
+ if self.filter_digital_signal(f"di_{m}", getattr(data, m, False)):
+ raw_mode = m.upper()
+ break
+ mode_text = raw_mode or "없음"
+ if w := self.signal_widgets.get("di_운전모드"):
+ w.set_value(mode_text)
+
+ # PSD
+ psd_open = self.filter_digital_signal("psd_open", getattr(data, 'psd_open', False))
+ psd_close = self.filter_digital_signal("psd_close", getattr(data, 'psd_close', False))
+ psd_text = "열림" if psd_open else ("닫힘" if psd_close else "없음")
+ if w := self.signal_widgets.get("di_psd"):
+ w.set_value(psd_text)
+
+ # DOOR
+ door_open = self.filter_digital_signal("door_open", getattr(data, 'door_open', False))
+ door_close = self.filter_digital_signal("door_close", getattr(data, 'door_close', False))
+ door_text = "열림" if door_open else ("닫힘" if door_close else "없음")
+ if w := self.signal_widgets.get("di_door"):
+ w.set_value(door_text)
+
+ # 마스콘
+ mc_eb = self.filter_digital_signal("mascon_eb", getattr(data, 'mascon_eb', False))
+ mc_br = self.filter_digital_signal("mascon_br", getattr(data, 'mascon_br', False))
+ mc_dr = self.filter_digital_signal("mascon_dr", getattr(data, 'mascon_dr', False))
+ raw_mc_mode = "EB" if mc_eb else ("BR" if mc_br else ("DR" if mc_dr else None))
+ mc_text = raw_mc_mode or "없음"
+ if w := self.signal_widgets.get("di_마스콘"):
+ w.set_value(mc_text)
+
+ # 역전기
+ fwd = self.filter_digital_signal("reversingrod_fwd", getattr(data, 'reversingrod_fwd', False))
+ neu = self.filter_digital_signal("reversingrod_neu", getattr(data, 'reversingrod_neu', False))
+ rvs = self.filter_digital_signal("reversingrod_rvs", getattr(data, 'reversingrod_rvs', False))
+ rev_text = "전진" if fwd else ("중립" if neu else ("후진" if rvs else "없음"))
+ if w := self.signal_widgets.get("di_역전기"):
+ w.set_value(rev_text)
+
+ # ===================================================================
+ # ATO 카드
+ # ===================================================================
+ if w := self.signal_widgets.get("ato_tasc"):
+ w.set_status(self.filter_digital_signal("tasc", getattr(data, 'tasc', False)))
+ if w := self.signal_widgets.get("ato_tasc_db"):
+ w.set_status(self.filter_digital_signal("tascdb", getattr(data, 'tascdb', False)))
+ if w := self.signal_widgets.get("ato_ato_eb"):
+ w.set_status(self.filter_digital_signal("ato_eb_req", getattr(data, 'ato_eb_req', False)))
+
+ # ATO DATA
+ if w := self.signal_widgets.get("ato_marker"):
+ marker_val = self.filter_string_signal("ato_marker", str(getattr(data, 'marker', '-')))
+ w.set_value(marker_val)
+ if w := self.signal_widgets.get("ato_osc"):
+ raw_osc = self._get_source_filtered_value(data, "osc_f", "osc_f", default=0)
+ osc = self.filter_numeric_signal("osc_f", raw_osc, min_valid=0)
+ w.set_value(f"{osc:.1f}" if osc else "-")
+ if w := self.signal_widgets.get("ato_pwm"):
+ pwm_val = self.apply_pwm_filter(self._get_source_filtered_value(data, "pwm_value", "pwm_value", default=0))
+ w.set_value(pwm_val)
+ if w := self.signal_widgets.get("ato_dtg"):
+ dtg_val = self.filter_numeric_signal("dtg_ato", self._get_source_filtered_value(data, "dtg", "dtg", default=0), min_valid=1)
+ w.set_value(f"{dtg_val:.0f}")
+
+ # ===================================================================
+ # RLY (ATO) 카드
+ # ===================================================================
+ rly_signals = [
+ ("dr", "trac_dr"), ("br", "trac_br"), ("adc", "adc"),
+ ("adol", "adol"), ("ador", "ador"), ("osc", "osc"),
+ ("kur", "kur"), ("sel", "start_enable")
+ ]
+ for name, attr in rly_signals:
+ val = self.filter_digital_signal(f"rly_{attr}", getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"rly_{name}"):
+ w.set_status(val)
+
+ # ===================================================================
+ # FAIL 카드
+ # ===================================================================
+ fail_signals = [
+ ("ato_r", "fail_atcr"), ("ato_c", "fail_atoc"), ("tcms", "fail_tcms"),
+ ("tacho1", "fail_tacho1"), ("tacho2", "fail_tacho2")
+ ]
+ for name, attr in fail_signals:
+ val = self.filter_digital_signal(f"fail_{attr}", getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"fail_{name}"):
+ w.set_status(val)
+
+ # ===================================================================
+ # TWC 카드
+ # ===================================================================
+ if w := self.signal_widgets.get("twc_dcw"):
+ w.set_status(self.filter_digital_signal("door_close_warning", getattr(data, 'door_close_warning', False)))
+ if w := self.signal_widgets.get("twc_twx_tx"):
+ w.set_status(self.filter_digital_signal("twct_enable", getattr(data, 'twct_enable', False)))
+ if w := self.signal_widgets.get("twc_berth"):
+ w.set_status(self.filter_digital_signal("trainberth", getattr(data, 'trainberth', False)))
+ if w := self.signal_widgets.get("twc_w.door"):
+ w.set_status(self.filter_digital_signal("wrongdoor", getattr(data, 'wrongdoor', False)))
+
+ # TWC DATA
+ if w := self.signal_widgets.get("twc_열번"):
+ w.set_value(getattr(data, 'trainno', '-'))
+ if w := self.signal_widgets.get("twc_현재역"):
+ w.set_value(getattr(data, 'pstn', '-'))
+ if w := self.signal_widgets.get("twc_다음역"):
+ w.set_value(getattr(data, 'nstn', '-'))
+ if w := self.signal_widgets.get("twc_종착역"):
+ w.set_value(getattr(data, 'dstn', '-'))
+ if w := self.signal_widgets.get("twc_DOOR"):
+ w.set_value(getattr(data, 'nextdoor', '-'))
+
+ # ===================================================================
+ # ETC 카드
+ # ===================================================================
+ # SYSTEM ACTIVE는 매 프레임 깜박이므로 과반수 필터 사용
+ if w := self.signal_widgets.get("etc_sys_act"):
+ val = self.filter_majority_signal("system_active", getattr(data, 'system_active', False))
+ w.set_status(val)
+
+ # 나머지 ETC 신호는 일반 필터 사용
+ etc_signals = [
+ ("inch", "inching"), ("cs", "trac_cs"),
+ ("평상", "nomal"), ("회복", "recovery"), ("o.spd", "over_spd_warning"),
+ ("사전제동", "pre_brake"), ("가속제한", "limit_drive"),
+ ("-70↑", "sh_stop1"), ("+70↑", "ov_stop1"),
+ ("tdir_a", "tacho_dir_a"), ("tdir_b", "tacho_dir_b")
+ ]
+ for name, attr in etc_signals:
+ val = self.filter_digital_signal(f"etc_{attr}", getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"etc_{name}"):
+ w.set_status(val)
+
+ # ETC DATA (문자열 필터 적용)
+ if w := self.signal_widgets.get("etc_init_pdt"):
+ val = self.filter_string_signal("etc_ipdt", str(getattr(data, 'ipdt', '-')))
+ w.set_value(val)
+ if w := self.signal_widgets.get("etc_mnul_pdt"):
+ val = self.filter_string_signal("etc_mpdt", str(getattr(data, 'mpdt', '-')))
+ w.set_value(val)
+ if w := self.signal_widgets.get("etc_tcms_dr"):
+ val = self.filter_string_signal("etc_tcmsdoor", str(getattr(data, 'tcmsdoor', '-')))
+ w.set_value(val)
+ if w := self.signal_widgets.get("etc_door_md"):
+ val = self.filter_string_signal("etc_doormod", str(getattr(data, 'doormod', '-')))
+ w.set_value(val)
+
+ # ===================================================================
+ # VDI A/B/C/D, VDO A/B - 모든 신호에 필터 적용
+ # ===================================================================
+ # VDI A
+ vdi_a_map = {
+ "hcr": "vdia_hcr", "tcr": "vdia_tcr", "fa": "vdia_fa",
+ "auto": "vdia_auto", "mcs": "vdia_mcs", "yard": "vdia_yard",
+ "fmc": "vdia_fmc", "d_open": "vdia_dooropen", "d_close": "vdia_doorclose",
+ "mc_eb": "vdia_masconeb", "mc_br": "vdia_masconbr", "mc_dr": "vdia_mascondr",
+ "fwd": "vdia_fwd", "neu": "vdia_neu", "rvs": "vdia_rvs"
+ }
+ for widget_name, attr in vdi_a_map.items():
+ val = self.filter_digital_signal(attr, getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"vdi_a_{widget_name}"):
+ w.set_status(val)
+
+ # VDI B
+ vdi_b_map = {
+ "hcr": "vdib_hcr", "tcr": "vdib_tcr", "fa": "vdib_fa",
+ "auto": "vdib_auto", "mcs": "vdib_mcs", "yard": "vdib_yard",
+ "fmc": "vdib_fmc", "d_open": "vdib_dooropen", "d_close": "vdib_doorclose",
+ "mc_eb": "vdib_masconeb", "mc_br": "vdib_masconbr", "mc_dr": "vdib_mascondr",
+ "fwd": "vdib_fwd", "neu": "vdib_neu", "rvs": "vdib_rvs"
+ }
+ for widget_name, attr in vdi_b_map.items():
+ val = self.filter_digital_signal(attr, getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"vdi_b_{widget_name}"):
+ w.set_status(val)
+
+ # VDI C
+ vdi_c_map = {
+ "psd_op": "vdic_psdopen", "psd_cl": "vdic_psdclose", "start": "vdic_startbtn",
+ "unit1": "vdic_unit1", "ebpfb": "vdic_ebpfb", "ebmfb": "vdic_ebmfb",
+ "fsb_fb": "vdic_fsbfb", "zvr_fb": "vdic_zvrfb", "edr_fb": "vdic_edrfb",
+ "edl_fb": "vdic_edlfb", "tc1": "vdic_tc1", "tc2": "vdic_tc2"
+ }
+ for widget_name, attr in vdi_c_map.items():
+ val = self.filter_digital_signal(attr, getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"vdi_c_{widget_name}"):
+ w.set_status(val)
+
+ # VDI D
+ vdi_d_map = {
+ "psd_op": "vdid_psdopen", "psd_cl": "vdid_psdclose", "start": "vdid_startbtn",
+ "unit1": "vdid_unit1", "ebpfb": "vdid_ebpfb", "ebmfb": "vdid_ebmfb",
+ "fsb_fb": "vdid_fsbfb", "zvr_fb": "vdid_zvrfb", "edr_fb": "vdid_edrfb",
+ "edl_fb": "vdid_edlfb", "tc1": "vdid_tc1", "tc2": "vdid_tc2"
+ }
+ for widget_name, attr in vdi_d_map.items():
+ val = self.filter_digital_signal(attr, getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"vdi_d_{widget_name}"):
+ w.set_status(val)
+
+ # VDO A
+ vdo_a_map = {
+ "ebp": "vdoa_ebp", "ebm": "vdoa_ebm", "fsb": "vdoa_fsb",
+ "zvr": "vdoa_zvr", "edl": "vdoa_edl", "edr": "vdoa_edr"
+ }
+ for widget_name, attr in vdo_a_map.items():
+ val = self.filter_digital_signal(attr, getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"vdo_a_{widget_name}"):
+ w.set_status(val)
+
+ # VDO B
+ vdo_b_map = {
+ "ebp": "vdob_ebp", "ebm": "vdob_ebm", "fsb": "vdob_fsb",
+ "zvr": "vdob_zvr", "edl": "vdob_edl", "edr": "vdob_edr"
+ }
+ for widget_name, attr in vdo_b_map.items():
+ val = self.filter_digital_signal(attr, getattr(data, attr, False))
+ if w := self.signal_widgets.get(f"vdo_b_{widget_name}"):
+ w.set_status(val)
+
+
diff --git a/app/ui/views/graph_view.py b/app/ui/views/graph_view.py
new file mode 100644
index 0000000..791eb95
--- /dev/null
+++ b/app/ui/views/graph_view.py
@@ -0,0 +1,3488 @@
+import sys
+import os
+import time
+from functools import partial
+from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QCheckBox,
+ QGroupBox, QMenu, QLabel, QFrame, QPushButton,
+ QGraphicsLineItem, QGraphicsRectItem, QGraphicsEllipseItem, QGraphicsTextItem, QGraphicsItem,
+ QDialog, QFormLayout, QSpinBox, QDialogButtonBox, QColorDialog, QApplication, QComboBox, QTextEdit, QMessageBox, QLineEdit,
+ QDateEdit)
+from PySide6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis, QDateTimeAxis
+from PySide6.QtCore import Qt, Signal, QPointF, QDateTime, QMargins, QRectF, QDate
+from PySide6.QtGui import QPainter, QPen, QColor, QFont, QBrush, QPolygonF, QCursor, QAction, QFontMetrics, QTextCharFormat
+
+# 동기화 모듈 (없으면 더미 처리)
+try:
+ from app.core.sync_controller import sync_manager
+except ImportError:
+ class MockSyncManager:
+ def request_sync(self, index, data, panel_id): pass
+ def request_x_range_sync(self, min_ms, max_ms, panel_id): pass
+ class _DummySignal:
+ def connect(self, *_args, **_kwargs): pass
+ x_range_changed = _DummySignal()
+ sync_manager = MockSyncManager()
+
+# --- [1. 역정보 매핑 테이블 (부산 1호선 - stationcode.cs 기준)] ---
+STATION_CODE_MAP = {
+ # 특수 코드
+ 89: "신평(TC2)", 90: "신평(TC1)",
+ # 다대포 방면 (95~101)
+ 95: "다대포해수욕장", 96: "다대포항", 97: "낫개", 98: "신장림", 99: "장림",
+ 100: "동매", 101: "신평",
+ # 본선 (102~135)
+ 102: "하단", 103: "당리", 104: "사하", 105: "괴정", 106: "대티",
+ 107: "서대신", 108: "동대신", 109: "토성", 110: "자갈치", 111: "남포",
+ 112: "중앙", 113: "부산역", 114: "초량", 115: "부산진", 116: "좌천",
+ 117: "범일", 118: "범내골", 119: "서면", 120: "부전", 121: "양정",
+ 122: "시청", 123: "연산", 124: "교대", 125: "동래", 126: "명륜",
+ 127: "온천장", 128: "부산대", 129: "장전", 130: "구서", 131: "두실",
+ 132: "남산", 133: "범어사", 134: "노포",
+}
+
+# --- [2. 커스텀 그리기 도구 아이템들] ---
+class DraggableItemMixin:
+ """드래그 및 우클릭 메뉴 공통 기능"""
+ def __init__(self):
+ self.current_color = QColor(255, 0, 0, 100) # 빨간색 75% 투명도
+ self.current_width = 2
+ self.fill_color = QColor(255, 0, 0, 100) # 채우기 색상 (기본값)
+ self.fill_opacity = 100 # 채우기 투명도 (75%)
+ self.timestamp = None # 시간 고정용 타임스탬프
+ self.y_value = None # Y축 값 고정용
+
+ def contextMenuEvent(self, event):
+ menu = QMenu()
+ action_color = menu.addAction("색상 변경")
+ action_width = menu.addAction("선 굵기 변경")
+ menu.addSeparator()
+ action_delete = menu.addAction("삭제")
+
+ selected = menu.exec(event.screenPos())
+
+ if selected == action_color:
+ c = QColorDialog.getColor(self.current_color, None, "색상 선택")
+ if c.isValid():
+ self.current_color = c
+ self.update_appearance()
+ elif selected == action_width:
+ # 간단한 입력 다이얼로그 대용 (실제로는 커스텀 다이얼로그 권장)
+ self.current_width = (self.current_width % 5) + 1 # 1~5 순환
+ self.update_appearance()
+ elif selected == action_delete:
+ self.scene().removeItem(self)
+
+ def update_appearance(self):
+ pass
+
+ def mouseDoubleClickEvent(self, event):
+ """더블클릭 시 속성 다이얼로그 표시"""
+ dialog = ShapePropertiesDialog(self)
+ if dialog.exec():
+ self.update_appearance()
+ super().mouseDoubleClickEvent(event)
+
+ def itemChange(self, change, value):
+ """아이템 변경 이벤트 - X축은 시간에 고정, Y축은 값에 고정"""
+ if change == QGraphicsItem.ItemPositionChange and hasattr(self, 'timestamp') and self.timestamp is not None:
+ # X축은 시간에 고정
+ if hasattr(self.scene(), 'parent_view') and self.scene().parent_view:
+ chart = self.scene().parent_view.chart
+ # 시간 값을 픽셀 좌표로 변환
+ pixel_x = chart.mapToPosition(QPointF(self.timestamp, 0)).x()
+
+ # 타임라인의 경우 Y축은 자유롭게 움직임
+ if isinstance(self, CustomTimelineItem):
+ return QPointF(pixel_x, value.y())
+ # 다른 객체들은 Y축도 값에 고정
+ elif hasattr(self, 'y_value') and self.y_value is not None:
+ pixel_pos = chart.mapToPosition(QPointF(self.timestamp, self.y_value))
+ return QPointF(pixel_x, value.y())
+ else:
+ return QPointF(pixel_x, value.y())
+ return super().itemChange(change, value)
+
+class CustomLineItem(QGraphicsLineItem, DraggableItemMixin):
+ def __init__(self, x, y, height, parent=None):
+ QGraphicsLineItem.__init__(self, parent)
+ DraggableItemMixin.__init__(self)
+ self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemSendsGeometryChanges)
+ self.setLine(x, y, x, y + height)
+ self.update_appearance()
+
+ def update_appearance(self):
+ self.setPen(QPen(self.current_color, self.current_width, Qt.DashLine))
+
+class CustomTimelineItem(QGraphicsLineItem, DraggableItemMixin):
+ def __init__(self, x, y, height, timestamp, parent=None):
+ QGraphicsLineItem.__init__(self, parent)
+ DraggableItemMixin.__init__(self)
+ self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemSendsGeometryChanges)
+
+ self.timestamp = timestamp
+ self.y_value = 0 # 타임라인은 Y축 고정 필요 없음
+
+ # 시간 라벨 생성
+ self.time_label = QGraphicsTextItem(self)
+
+ self.setLine(x, y, x, y + height)
+ self.update_appearance()
+
+ def update_appearance(self):
+ # 설정된 색상과 굵기로 선 그리기
+ self.setPen(QPen(self.current_color, self.current_width))
+
+ def update_time_label(self):
+ """시간 라벨 업데이트"""
+ # 타임스탬프를 시간으로 변환
+ dt = QDateTime.fromMSecsSinceEpoch(int(self.timestamp))
+ time_str = dt.toString("HH:mm:ss")
+
+ # 라벨 설정
+ self.time_label.setPlainText(time_str)
+ self.time_label.setDefaultTextColor(QColor("#2196F3"))
+ self.time_label.setFont(QFont("Malgun Gothic", 8, QFont.Bold))
+
+ # 라벨 위치 설정 (선 상단 중앙)
+ label_rect = self.time_label.boundingRect()
+ label_x = self.line().x1() - label_rect.width() / 2
+ label_y = self.line().y1() - label_rect.height() - 2
+ self.time_label.setPos(label_x, label_y)
+
+ def setLine(self, x1, y1, x2, y2):
+ """선 설정 및 라벨 위치 업데이트"""
+ super().setLine(x1, y1, x2, y2)
+ self.update_time_label()
+
+ def mouseDoubleClickEvent(self, event):
+ """더블클릭 시 타임라인 속성 다이얼로그"""
+ dialog = TimelinePropertiesDialog(self)
+ if dialog.exec():
+ self.update_appearance()
+ super().mouseDoubleClickEvent(event)
+
+class CustomRectItem(QGraphicsRectItem, DraggableItemMixin):
+ def __init__(self, x, y, w, h, parent=None):
+ QGraphicsRectItem.__init__(self, parent)
+ DraggableItemMixin.__init__(self)
+ self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemSendsGeometryChanges)
+ self.setRect(x, y, w, h)
+ self.current_color = QColor(255, 0, 0, 255) # 빨간 테두리
+ self.fill_color = QColor(255, 0, 0, 100) # 빨간 채우기 (75% 투명도)
+
+ self.update_appearance()
+
+ def update_appearance(self):
+ self.setPen(QPen(self.current_color, self.current_width))
+ self.setBrush(QBrush(self.fill_color))
+
+class CustomEllipseItem(QGraphicsEllipseItem, DraggableItemMixin):
+ def __init__(self, x, y, w, h, parent=None):
+ QGraphicsEllipseItem.__init__(self, parent)
+ DraggableItemMixin.__init__(self)
+ self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemSendsGeometryChanges)
+ self.setRect(x, y, w, h)
+ self.current_color = QColor(255, 0, 0, 255) # 빨간 테두리
+ self.fill_color = QColor(255, 0, 0, 100) # 빨간 채우기 (75% 투명도)
+
+ self.update_appearance()
+
+ def update_appearance(self):
+ self.setPen(QPen(self.current_color, self.current_width))
+ self.setBrush(QBrush(self.fill_color))
+
+
+class CustomTextItem(QGraphicsTextItem, DraggableItemMixin):
+ def __init__(self, x, y, text="텍스트", parent=None):
+ QGraphicsTextItem.__init__(self, parent)
+ DraggableItemMixin.__init__(self)
+ self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemSendsGeometryChanges)
+ self.setPos(x, y)
+ self.setPlainText(text)
+
+ # 텍스트 속성 초기화
+ self.text_color = QColor("white")
+ self.font_family = "Malgun Gothic"
+ self.font_size = 12
+ self.font_bold = False
+ self.font_italic = False
+ self.text_shadow = False
+ self.shadow_type = "black_white" # "black_white" 또는 "white_black"
+
+ self.update_appearance()
+
+ def update_appearance(self):
+ # 폰트 설정
+ font = QFont(self.font_family, self.font_size)
+ font.setBold(self.font_bold)
+ font.setItalic(self.font_italic)
+ self.setFont(font)
+
+ # 색상 설정
+ self.setDefaultTextColor(self.text_color)
+
+ def mouseDoubleClickEvent(self, event):
+ """더블클릭 시 속성 다이얼로그 표시"""
+ dialog = TextPropertiesDialog(self)
+ if dialog.exec():
+ self.update_appearance()
+ super().mouseDoubleClickEvent(event)
+
+ def paint(self, painter, option, widget):
+ """그림자 효과가 있는 텍스트 렌더링"""
+ if self.text_shadow:
+ self.draw_shadow_text(painter)
+ else:
+ super().paint(painter, option, widget)
+
+ def draw_shadow_text(self, painter):
+ """그림자 효과 적용한 텍스트 그리기"""
+ # 현재 텍스트와 폰트 정보
+ text = self.toPlainText()
+ font = self.font()
+ color = self.defaultTextColor()
+
+ # 그림자 오프셋 계산 (폰트 크기에 비례)
+ shadow_offset = max(1, font.pointSize() // 8)
+
+ # 그림자 색상 결정
+ if self.shadow_type == "black_white":
+ shadow_color = QColor("black") if color.lightness() > 128 else QColor("white")
+ else: # "white_black"
+ shadow_color = QColor("white") if color.lightness() > 128 else QColor("black")
+
+ # 8방향 그림자 그리기
+ painter.setFont(font)
+ painter.setPen(shadow_color)
+
+ directions = [
+ (-shadow_offset, -shadow_offset), (0, -shadow_offset), (shadow_offset, -shadow_offset),
+ (-shadow_offset, 0), (shadow_offset, 0),
+ (-shadow_offset, shadow_offset), (0, shadow_offset), (shadow_offset, shadow_offset)
+ ]
+
+ for dx, dy in directions:
+ painter.drawText(self.boundingRect().translated(dx, dy), Qt.AlignLeft, text)
+
+ # 본문 텍스트 그리기
+ painter.setPen(color)
+ painter.drawText(self.boundingRect(), Qt.AlignLeft, text)
+
+
+# --- [3. 라인 속성 변경 팝업] ---
+class SeriesPropDialog(QDialog):
+ def __init__(self, series, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle(f"{series.name()} - 속성 설정")
+ self.series = series
+ self.resize(350, 280)
+
+ # 배경색 설정 (다크모드)
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #2D2D30;
+ color: white;
+ }
+ QLabel {
+ color: white;
+ }
+ QGroupBox {
+ color: white;
+ font-weight: bold;
+ border: 1px solid #3E3E42;
+ border-radius: 4px;
+ margin-top: 10px;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 3px;
+ color: white;
+ }
+ QComboBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #2D2D30;
+ color: white;
+ selection-background-color: #0078D7;
+ }
+ QSpinBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QCheckBox {
+ color: white;
+ }
+ """)
+
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(15, 15, 15, 15)
+ layout.setSpacing(10)
+
+ # 라인 정보 섹션
+ info_group = QGroupBox("라인 정보")
+ info_layout = QFormLayout(info_group)
+
+ self.lb_name = QLabel(series.name())
+ self.lb_name.setStyleSheet("font-weight: bold; font-size: 11px;")
+ info_layout.addRow("신호 이름:", self.lb_name)
+
+ # 신호 설명 추가
+ descriptions = {
+ "SPEED (속도)": "열차의 현재 속도 (km/h)",
+ "PWM": "모터 제어 신호 (0-100%)",
+ "TASC (목표)": "자동 속도 제어 목표값",
+ "ATC Code": "ATC 안전 코드 값",
+ "DTG (잔여거리)": "목적지까지 남은 거리 (m)"
+ }
+ desc_text = descriptions.get(series.name(), "데이터 시각화")
+ self.lb_desc = QLabel(desc_text)
+ self.lb_desc.setStyleSheet("color: #666666; font-size: 10px;")
+ info_layout.addRow("설명:", self.lb_desc)
+
+ layout.addWidget(info_group)
+
+ # 속성 설정 섹션
+ prop_group = QGroupBox("속성 설정")
+ prop_layout = QFormLayout(prop_group)
+
+ # 색상
+ self.btn_color = QPushButton()
+ self.current_color = series.pen().color()
+ self.btn_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {self.current_color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+ self.btn_color.clicked.connect(self.choose_color)
+ prop_layout.addRow("색상:", self.btn_color)
+
+ # 선 굵기
+ self.sb_width = QSpinBox()
+ self.sb_width.setRange(1, 10)
+ self.sb_width.setValue(series.pen().width())
+ self.sb_width.setFixedWidth(80)
+ prop_layout.addRow("선 굵기:", self.sb_width)
+
+ # 선 스타일
+ self.cb_style = QComboBox()
+ self.cb_style.addItems(["실선", "점선", "대시선", "점대시선"])
+ current_style = series.pen().style()
+ style_map = {
+ Qt.SolidLine: 0,
+ Qt.DotLine: 1,
+ Qt.DashLine: 2,
+ Qt.DashDotLine: 3
+ }
+ self.cb_style.setCurrentIndex(style_map.get(current_style, 0))
+ prop_layout.addRow("선 스타일:", self.cb_style)
+
+ # 라벨 표시
+ self.chk_show_label = QCheckBox("차트에 라벨 표시")
+ self.chk_show_label.setChecked(False) # 기본적으로 라벨 숨김
+ prop_layout.addRow("", self.chk_show_label)
+
+ layout.addWidget(prop_group)
+
+ # 버튼들
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ def choose_color(self):
+ c = QColorDialog.getColor(self.current_color, self, "색상 선택")
+ if c.isValid():
+ self.current_color = c
+ self.btn_color.setStyleSheet(f"background-color: {c.name()}; border: 1px solid gray;")
+
+ def apply_settings(self):
+ pen = self.series.pen()
+ pen.setColor(self.current_color)
+ pen.setWidth(self.sb_width.value())
+
+ # 선 스타일 설정
+ style_map = [Qt.SolidLine, Qt.DotLine, Qt.DashLine, Qt.DashDotLine]
+ pen.setStyle(style_map[self.cb_style.currentIndex()])
+
+ self.series.setPen(pen)
+
+ # 라벨 표시 설정 (시리즈 이름 변경으로 구현)
+ if self.chk_show_label.isChecked():
+ self.series.setName(f"{self.series.name()} ✓")
+ else:
+ # 체크 표시 제거 (원래 이름 복원)
+ name = self.series.name().replace(" ✓", "")
+ self.series.setName(name)
+
+
+# --- [3.5. 도형 속성 변경 팝업] ---
+class ShapePropertiesDialog(QDialog):
+ def __init__(self, shape_obj, parent=None):
+ super().__init__(parent)
+ self.shape_obj = shape_obj
+ self._deleted = False # 삭제 여부
+ self.setWindowTitle("도형 속성 설정")
+ self.resize(350, 300)
+
+ # 배경색 설정 (다크모드)
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #2D2D30;
+ color: white;
+ }
+ QLabel {
+ color: white;
+ }
+ QGroupBox {
+ color: white;
+ font-weight: bold;
+ border: 1px solid #3E3E42;
+ border-radius: 4px;
+ margin-top: 10px;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 3px;
+ color: white;
+ }
+ QComboBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #2D2D30;
+ color: white;
+ selection-background-color: #0078D7;
+ }
+ QSpinBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QCheckBox {
+ color: white;
+ }
+ """)
+
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(15, 15, 15, 15)
+ layout.setSpacing(10)
+
+ # 속성 설정 그룹
+ prop_group = QGroupBox("속성 설정")
+ prop_layout = QFormLayout(prop_group)
+
+ # 테두리 색상
+ self.btn_border_color = QPushButton()
+ self.border_color = shape_obj['border_color']
+ self.btn_border_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {self.border_color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+ self.btn_border_color.clicked.connect(self.choose_border_color)
+ prop_layout.addRow("테두리 색상:", self.btn_border_color)
+
+ # 테두리 굵기
+ self.sb_border_width = QSpinBox()
+ self.sb_border_width.setRange(1, 10)
+ self.sb_border_width.setValue(shape_obj['border_width'])
+ self.sb_border_width.setFixedWidth(80)
+ prop_layout.addRow("테두리 굵기:", self.sb_border_width)
+
+ # 채우기 색상
+ self.btn_fill_color = QPushButton()
+ self.fill_color = shape_obj['fill_color']
+ self.btn_fill_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {self.fill_color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+ self.btn_fill_color.clicked.connect(self.choose_fill_color)
+ prop_layout.addRow("채우기 색상:", self.btn_fill_color)
+
+ # 채우기 투명도
+ self.sb_fill_opacity = QSpinBox()
+ self.sb_fill_opacity.setRange(0, 255)
+ self.sb_fill_opacity.setValue(shape_obj['fill_opacity'])
+ self.sb_fill_opacity.setFixedWidth(80)
+ prop_layout.addRow("채우기 투명도:", self.sb_fill_opacity)
+
+ layout.addWidget(prop_group)
+
+ # 삭제 버튼
+ btn_delete = QPushButton("🗑️ 삭제")
+ btn_delete.setStyleSheet("""
+ QPushButton {
+ background-color: #E53935;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #F44336;
+ }
+ QPushButton:pressed {
+ background-color: #C62828;
+ }
+ """)
+ btn_delete.clicked.connect(self.delete_shape)
+ layout.addWidget(btn_delete)
+
+ # 버튼들
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ def delete_shape(self):
+ """도형 삭제"""
+ self._deleted = True
+ self.reject()
+
+ def choose_border_color(self):
+ color = QColorDialog.getColor(self.border_color, self, "테두리 색상 선택")
+ if color.isValid():
+ self.border_color = color
+ self.btn_border_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+
+ def choose_fill_color(self):
+ color = QColorDialog.getColor(self.fill_color, self, "채우기 색상 선택")
+ if color.isValid():
+ # 투명도 적용
+ color.setAlpha(self.sb_fill_opacity.value())
+ self.fill_color = color
+ self.btn_fill_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+
+ def accept(self):
+ # 설정 적용
+ self.shape_obj['border_color'] = self.border_color
+ self.shape_obj['border_width'] = self.sb_border_width.value()
+
+ fill_color = QColor(self.fill_color)
+ fill_color.setAlpha(self.sb_fill_opacity.value())
+ self.shape_obj['fill_color'] = fill_color
+ self.shape_obj['fill_opacity'] = self.sb_fill_opacity.value()
+
+ super().accept()
+
+
+# --- [3.7. 타임라인 속성 변경 팝업] ---
+class TimelinePropertiesDialog(QDialog):
+ deleted = Signal() # 삭제 시그널
+
+ def __init__(self, timeline_obj, parent=None, graph_view=None):
+ super().__init__(parent)
+ self.timeline_obj = timeline_obj
+ self.graph_view = graph_view
+ self.setWindowTitle("타임라인 속성 설정")
+ self.resize(400, 400)
+ self._deleted = False
+
+ # 배경색 설정 (다크모드)
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #2D2D30;
+ color: white;
+ }
+ QLabel {
+ color: white;
+ }
+ QGroupBox {
+ color: white;
+ font-weight: bold;
+ border: 1px solid #3E3E42;
+ border-radius: 4px;
+ margin-top: 10px;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 3px;
+ color: white;
+ }
+ QComboBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #2D2D30;
+ color: white;
+ selection-background-color: #0078D7;
+ }
+ QSpinBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QLineEdit {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ """)
+
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(15, 15, 15, 15)
+ layout.setSpacing(10)
+
+ # 시간 정보 표시
+ time_label = QLabel(f"⏱️ 시간: {timeline_obj['label']}")
+ time_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2196F3;")
+ layout.addWidget(time_label)
+
+ # 활성화된 데이터 값 표시
+ if graph_view:
+ data_group = QGroupBox("현재 위치 데이터 값")
+ data_layout = QFormLayout(data_group)
+
+ # 타임라인 위치에서 데이터 값 조회
+ timestamp = timeline_obj['timestamp']
+ data_values = self._get_data_at_timestamp(timestamp)
+
+ for key, (name, value, visible) in data_values.items():
+ if visible:
+ value_label = QLabel(f"{value}")
+ value_label.setStyleSheet("color: #00FF00; font-weight: bold;")
+ data_layout.addRow(f"{name}:", value_label)
+
+ layout.addWidget(data_group)
+
+ # 속성 설정 그룹
+ prop_group = QGroupBox("속성 설정")
+ prop_layout = QFormLayout(prop_group)
+
+ # 선 색상
+ self.btn_color = QPushButton()
+ self.line_color = timeline_obj['color']
+ self.btn_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {self.line_color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+ self.btn_color.clicked.connect(self.choose_color)
+ prop_layout.addRow("선 색상:", self.btn_color)
+
+ # 선 굵기
+ self.sb_width = QSpinBox()
+ self.sb_width.setRange(1, 10)
+ self.sb_width.setValue(timeline_obj['width'])
+ self.sb_width.setFixedWidth(80)
+ prop_layout.addRow("선 굵기:", self.sb_width)
+
+ # 선 스타일
+ self.cb_style = QComboBox()
+ self.cb_style.addItems(["실선", "점선", "대시선", "점대시선"])
+ style_map = {
+ Qt.SolidLine: 0,
+ Qt.DotLine: 1,
+ Qt.DashLine: 2,
+ Qt.DashDotLine: 3
+ }
+ self.cb_style.setCurrentIndex(style_map.get(timeline_obj['style'], 0))
+ prop_layout.addRow("선 스타일:", self.cb_style)
+
+ # 라벨 텍스트
+ self.le_label = QLineEdit()
+ self.le_label.setText(timeline_obj['label'])
+ prop_layout.addRow("라벨 텍스트:", self.le_label)
+
+ layout.addWidget(prop_group)
+
+ # 삭제 버튼
+ btn_delete = QPushButton("🗑️ 삭제")
+ btn_delete.setStyleSheet("""
+ QPushButton {
+ background-color: #E53935;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #F44336;
+ }
+ QPushButton:pressed {
+ background-color: #C62828;
+ }
+ """)
+ btn_delete.clicked.connect(self.delete_timeline)
+ layout.addWidget(btn_delete)
+
+ # 버튼들
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ def _get_data_at_timestamp(self, timestamp):
+ """해당 타임스탬프에서의 데이터 값 조회"""
+ result = {}
+ if not self.graph_view or not self.graph_view.data_list:
+ return result
+
+ # 인덱스 계산
+ idx = int((timestamp - self.graph_view.start_timestamp) / 1000)
+ if idx < 0 or idx >= len(self.graph_view.data_list):
+ return result
+
+ # 각 시리즈의 값과 가시성 상태 조회
+ for key, info in self.graph_view.signals_map.items():
+ is_visible = info['chk'].isChecked()
+ if idx < len(info['data']):
+ val = info['data'][idx]
+ if key in ['speed', 'dtg']:
+ result[key] = (info['base'], f"{val:.1f}", is_visible)
+ else:
+ result[key] = (info['base'], f"{int(val)}", is_visible)
+
+ return result
+
+ def choose_color(self):
+ color = QColorDialog.getColor(self.line_color, self, "선 색상 선택")
+ if color.isValid():
+ self.line_color = color
+ self.btn_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+
+ def delete_timeline(self):
+ """타임라인 삭제"""
+ self._deleted = True
+ self.reject() # 다이얼로그 닫기
+
+ def accept(self):
+ # 설정 적용
+ self.timeline_obj['color'] = self.line_color
+ self.timeline_obj['width'] = self.sb_width.value()
+
+ # 선 스타일 설정
+ style_map = [Qt.SolidLine, Qt.DotLine, Qt.DashLine, Qt.DashDotLine]
+ self.timeline_obj['style'] = style_map[self.cb_style.currentIndex()]
+
+ # 라벨 업데이트
+ self.timeline_obj['label'] = self.le_label.text()
+
+ super().accept()
+
+
+# --- [3.8. 마커 속성 변경 팝업] ---
+class MarkerPropertiesDialog(QDialog):
+ def __init__(self, marker_type, marker_data, parent=None, graph_view=None):
+ """
+ marker_type: 'PG', 'TWC', 'STATION'
+ marker_data: (timestamp, label) 또는 {'start': (t, lbl), 'end': (t, lbl)} for TWC
+ """
+ super().__init__(parent)
+ self.marker_type = marker_type
+ self.marker_data = marker_data
+ self.graph_view = graph_view
+ self.setWindowTitle(f"{marker_type} 마커 속성")
+ self.resize(380, 320)
+
+ # 배경색 설정 (다크모드)
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #2D2D30;
+ color: white;
+ }
+ QLabel {
+ color: white;
+ }
+ QGroupBox {
+ color: white;
+ font-weight: bold;
+ border: 1px solid #3E3E42;
+ border-radius: 4px;
+ margin-top: 10px;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 3px;
+ color: white;
+ }
+ QCheckBox {
+ color: white;
+ }
+ """)
+
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(15, 15, 15, 15)
+ layout.setSpacing(10)
+
+ # 마커 정보 표시
+ if isinstance(marker_data, dict): # TWC 쌍 (start/end)
+ info_text = f"📍 {marker_type}: {marker_data['start'][1]} ~ {marker_data['end'][1]}"
+ timestamp = marker_data['start'][0]
+ else:
+ timestamp, label = marker_data
+ info_text = f"📍 {marker_type}: {label}"
+
+ info_label = QLabel(info_text)
+ info_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #4CAF50;")
+ layout.addWidget(info_label)
+
+ # 시간 표시
+ dt = QDateTime.fromMSecsSinceEpoch(int(timestamp))
+ time_str = dt.toString("HH:mm:ss")
+ time_label = QLabel(f"⏱️ 시간: {time_str}")
+ time_label.setStyleSheet("color: #2196F3;")
+ layout.addWidget(time_label)
+
+ # TWC 영역 표시 옵션 (TWC 마커일 경우만)
+ if marker_type == "TWC" and isinstance(marker_data, dict):
+ region_group = QGroupBox("영역 표시 설정")
+ region_layout = QFormLayout(region_group)
+
+ self.chk_show_region = QCheckBox("TWC 영역 표시 (Rx ~ End)")
+ self.chk_show_region.setChecked(marker_data.get('show_region', False))
+ region_layout.addRow(self.chk_show_region)
+
+ # 영역 색상
+ self.btn_region_color = QPushButton()
+ self.region_color = marker_data.get('region_color', QColor("#00FF00"))
+ self.btn_region_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {self.region_color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+ self.btn_region_color.clicked.connect(self.choose_region_color)
+ region_layout.addRow("영역 색상:", self.btn_region_color)
+
+ # 영역 투명도
+ self.sb_region_opacity = QSpinBox()
+ self.sb_region_opacity.setRange(10, 150)
+ self.sb_region_opacity.setValue(marker_data.get('region_opacity', 50))
+ self.sb_region_opacity.setStyleSheet("color: white; background-color: #3C3C3C;")
+ region_layout.addRow("투명도:", self.sb_region_opacity)
+
+ layout.addWidget(region_group)
+
+ # 버튼들
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ def choose_region_color(self):
+ color = QColorDialog.getColor(self.region_color, self, "영역 색상 선택")
+ if color.isValid():
+ self.region_color = color
+ self.btn_region_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+
+ def accept(self):
+ # TWC 영역 설정 저장
+ if self.marker_type == "TWC" and isinstance(self.marker_data, dict):
+ self.marker_data['show_region'] = self.chk_show_region.isChecked()
+ self.marker_data['region_color'] = self.region_color
+ self.marker_data['region_opacity'] = self.sb_region_opacity.value()
+ super().accept()
+
+
+# --- [3.6. 텍스트 속성 변경 팝업] ---
+class TextPropertiesDialog(QDialog):
+ def __init__(self, text_obj, parent=None):
+ super().__init__(parent)
+ self.text_obj = text_obj
+ self._deleted = False # 삭제 여부
+ self.setWindowTitle("텍스트 속성 설정")
+ self.resize(400, 400)
+
+ # 배경색 설정 (다크모드)
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #2D2D30;
+ color: white;
+ }
+ QLabel {
+ color: white;
+ }
+ QGroupBox {
+ color: white;
+ font-weight: bold;
+ border: 1px solid #3E3E42;
+ border-radius: 4px;
+ margin-top: 10px;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 3px;
+ color: white;
+ }
+ QComboBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #2D2D30;
+ color: white;
+ selection-background-color: #0078D7;
+ }
+ QSpinBox {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ QCheckBox {
+ color: white;
+ }
+ QTextEdit {
+ color: white;
+ background-color: #3C3C3C;
+ border: 1px solid #555555;
+ }
+ """)
+
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(15, 15, 15, 15)
+ layout.setSpacing(10)
+
+ # 텍스트 편집 그룹
+ text_group = QGroupBox("텍스트 편집")
+ text_layout = QVBoxLayout(text_group)
+
+ self.text_edit = QTextEdit()
+ self.text_edit.setPlainText(text_obj['text'])
+ self.text_edit.setMaximumHeight(60)
+ text_layout.addWidget(self.text_edit)
+
+ layout.addWidget(text_group)
+
+ # 폰트 속성 그룹
+ font_group = QGroupBox("폰트 속성")
+ font_layout = QFormLayout(font_group)
+
+ # 폰트 패밀리
+ self.cb_font_family = QComboBox()
+ self.cb_font_family.addItems(["Malgun Gothic", "Arial", "Times New Roman", "Courier New", "Verdana"])
+ current_font = text_obj['font_family']
+ if current_font in ["Malgun Gothic", "Arial", "Times New Roman", "Courier New", "Verdana"]:
+ self.cb_font_family.setCurrentText(current_font)
+ font_layout.addRow("폰트:", self.cb_font_family)
+
+ # 폰트 크기
+ self.sb_font_size = QSpinBox()
+ self.sb_font_size.setRange(8, 72)
+ self.sb_font_size.setValue(text_obj['font_size'])
+ font_layout.addRow("크기:", self.sb_font_size)
+
+ # 텍스트 색상
+ self.btn_text_color = QPushButton()
+ self.text_color = text_obj['text_color']
+ self.btn_text_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {self.text_color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+ self.btn_text_color.clicked.connect(self.choose_text_color)
+ font_layout.addRow("색상:", self.btn_text_color)
+
+ # 굵게, 기울임
+ self.chk_bold = QCheckBox("굵게")
+ self.chk_bold.setChecked(text_obj['font_bold'])
+ font_layout.addRow("", self.chk_bold)
+
+ self.chk_italic = QCheckBox("기울임")
+ self.chk_italic.setChecked(text_obj['font_italic'])
+ font_layout.addRow("", self.chk_italic)
+
+ layout.addWidget(font_group)
+
+ # 효과 그룹
+ effect_group = QGroupBox("효과")
+ effect_layout = QFormLayout(effect_group)
+
+ self.chk_shadow = QCheckBox("그림자 효과")
+ self.chk_shadow.setChecked(text_obj['text_shadow'])
+ effect_layout.addRow("", self.chk_shadow)
+
+ self.cb_shadow_type = QComboBox()
+ self.cb_shadow_type.addItems(["흰색 배경 검은 글자", "검은 배경 흰 글자"])
+ if text_obj['shadow_type'] == "white_black":
+ self.cb_shadow_type.setCurrentIndex(0)
+ else:
+ self.cb_shadow_type.setCurrentIndex(1)
+ effect_layout.addRow("그림자 타입:", self.cb_shadow_type)
+
+ layout.addWidget(effect_group)
+
+ # 삭제 버튼
+ btn_delete = QPushButton("🗑️ 삭제")
+ btn_delete.setStyleSheet("""
+ QPushButton {
+ background-color: #E53935;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #F44336;
+ }
+ QPushButton:pressed {
+ background-color: #C62828;
+ }
+ """)
+ btn_delete.clicked.connect(self.delete_text)
+ layout.addWidget(btn_delete)
+
+ # 버튼들
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ def delete_text(self):
+ """텍스트 삭제"""
+ self._deleted = True
+ self.reject()
+
+ def choose_text_color(self):
+ color = QColorDialog.getColor(self.text_color, self, "텍스트 색상 선택")
+ if color.isValid():
+ self.text_color = color
+ self.btn_text_color.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {color.name()};
+ border: 1px solid gray;
+ border-radius: 4px;
+ min-width: 60px;
+ min-height: 25px;
+ }}
+ """)
+
+ def accept(self):
+ # 설정 적용
+ self.text_obj['text'] = self.text_edit.toPlainText()
+ self.text_obj['font_family'] = self.cb_font_family.currentText()
+ self.text_obj['font_size'] = self.sb_font_size.value()
+ self.text_obj['text_color'] = self.text_color
+ self.text_obj['font_bold'] = self.chk_bold.isChecked()
+ self.text_obj['font_italic'] = self.chk_italic.isChecked()
+ self.text_obj['text_shadow'] = self.chk_shadow.isChecked()
+
+ if self.cb_shadow_type.currentIndex() == 0:
+ self.text_obj['shadow_type'] = "white_black"
+ else:
+ self.text_obj['shadow_type'] = "black_white"
+
+ super().accept()
+
+
+# --- [4. 인터랙티브 차트 뷰] ---
+class InteractiveChartView(QChartView):
+ cursorMoved = Signal(float, object)
+
+ def __init__(self, chart, parent=None):
+ super().__init__(chart)
+ self.parent_view = parent # GraphView 참조
+ self.setRenderHint(QPainter.Antialiasing)
+ self.setMouseTracking(True)
+ self.setDragMode(QChartView.NoDrag)
+ self.setFocusPolicy(Qt.StrongFocus) # 키보드 포커스 받기
+
+ self.is_panning = False
+ self.last_mouse_pos = None
+ self.cursor_pos_x = None
+ self.cursor_pen = QPen(QColor("red"), 1, Qt.SolidLine)
+ self.selected_object = None # 선택된 드로잉 객체
+
+ # 드래그 관련 상태
+ self.is_dragging_object = False
+ self.drag_start_pos = None
+ self.drag_start_chart_pos = None
+
+ # 구간 선택(드래그) 상태
+ self.is_selecting_range = False
+ self.range_start_pos = None
+ self.range_rect = None
+ self._range_min_ms = None
+ self._range_max_ms = None
+ self.range_brush = QBrush(QColor(0, 120, 215, 50))
+ self.range_pen = QPen(QColor(0, 120, 215, 180), 1, Qt.SolidLine)
+
+ self.event_markers = {"PG": [], "TWC": [], "STATION": []}
+ self.twc_regions = [] # TWC 영역 표시용 데이터
+ self.pg_regions = [] # PG 영역 표시용 데이터
+
+ def wheelEvent(self, event):
+ # 줌 기능 (X축 중심)
+ chart = self.chart()
+ axis_x = chart.axes(Qt.Horizontal)[0]
+
+ zoom_factor = 1.2 if event.angleDelta().y() > 0 else 0.8
+
+ min_val = axis_x.min().toMSecsSinceEpoch()
+ max_val = axis_x.max().toMSecsSinceEpoch()
+ current_range = max_val - min_val
+
+ # 줌 한계 설정 (최소 1초 ~ 최대 24시간)
+ mouse_ratio = (event.position().x() - chart.plotArea().left()) / chart.plotArea().width()
+ new_range = current_range * (1 / zoom_factor)
+
+ if new_range < 1000 or new_range > 3600 * 1000 * 24: return
+
+ center = min_val + (current_range * mouse_ratio)
+ new_min = center - (new_range * mouse_ratio)
+ new_max = new_min + new_range
+
+ axis_x.setRange(QDateTime.fromMSecsSinceEpoch(int(new_min)),
+ QDateTime.fromMSecsSinceEpoch(int(new_max)))
+
+ # SpinBox 값 업데이트
+ if self.parent_view:
+ self.parent_view.update_x_scale_spinbox()
+
+ event.accept()
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.MiddleButton:
+ self.is_panning = True
+ self.last_mouse_pos = event.pos()
+ self.setCursor(Qt.ClosedHandCursor)
+ event.accept()
+ elif event.button() == Qt.LeftButton and self.parent_view.drawing_mode:
+ # 그리기 모드일 때 시작점 설정
+ if self.parent_view.drawing_mode == "text":
+ # 텍스트 모드: 클릭한 위치에 텍스트 추가
+ pos = event.pos()
+ plot_area = self.chart().plotArea()
+ if plot_area.contains(pos.x(), pos.y()):
+ chart_pos = self.chart().mapToValue(pos)
+ self.parent_view.add_text_at_position(chart_pos.x(), chart_pos.y())
+ self.parent_view.drawing_mode = None # 그리기 모드 해제
+ self.setCursor(Qt.ArrowCursor)
+ elif self.parent_view.drawing_mode in ["rect", "circle"]:
+ # 도형 모드: 드래그 시작
+ self.parent_view.drawing_start_pos = event.pos()
+ self.parent_view.temp_shape = None
+ event.accept()
+ elif event.button() == Qt.LeftButton:
+ # 드로잉 객체 클릭 체크
+ clicked_obj = self.get_object_at_position(event.pos())
+ if clicked_obj:
+ self.selected_object = clicked_obj
+ # 드래그 시작 준비
+ self.is_dragging_object = True
+ self.drag_start_pos = event.pos()
+ self.drag_start_chart_pos = self.chart().mapToValue(event.pos())
+ self.setCursor(Qt.SizeAllCursor)
+ self.setFocus() # 키보드 포커스 가져오기
+ self.update() # 선택 표시 업데이트
+ event.accept()
+ return
+ else:
+ # 빈 곳 클릭 시 선택 해제
+ if self.selected_object:
+ self.selected_object = None
+ self.update()
+ # 구간 선택 시작
+ self.is_selecting_range = True
+ self.range_start_pos = event.pos()
+ self.range_rect = QRectF(self.range_start_pos, self.range_start_pos)
+ self._range_min_ms = None
+ self._range_max_ms = None
+ self.update()
+
+ super().mousePressEvent(event)
+
+ def get_object_at_position(self, pos):
+ """특정 위치의 드로잉 객체 반환"""
+ if not hasattr(self, 'parent_view') or not self.parent_view:
+ return None
+
+ chart = self.chart()
+ plot_area = chart.plotArea()
+ if not plot_area.contains(pos.x(), pos.y()):
+ return None
+
+ chart_pos = chart.mapToValue(pos)
+
+ # 객체들과의 거리 계산 (역순으로 - 나중에 추가된 객체가 위에 있음)
+ for obj in reversed(self.parent_view.drawing_objects):
+ if obj['type'] in ['rect', 'circle', 'text']:
+ # 사각형 기반 객체 (도형, 텍스트 모두 동일하게 처리)
+ t = chart_pos.x()
+ y = chart_pos.y()
+ if (obj['timestamp_left'] <= t <= obj['timestamp_right'] and
+ obj['y_bottom'] <= y <= obj['y_top']):
+ return obj
+ elif obj['type'] == 'timeline':
+ # 타임라인과의 거리 계산
+ px = chart.mapToPosition(QPointF(obj['timestamp'], 0)).x()
+ if abs(pos.x() - px) < 10: # 선 근처 10픽셀
+ return obj
+
+ return None
+
+ def get_marker_at_position(self, pos):
+ """특정 위치의 마커 반환 (타입, 데이터)"""
+ chart = self.chart()
+ plot_area = chart.plotArea()
+
+ # 상단 마커 영역 체크 (plot_area 상단 50px)
+ if not (plot_area.left() <= pos.x() <= plot_area.right() and
+ pos.y() >= plot_area.top() - 10 and pos.y() <= plot_area.top() + 50):
+ return None
+
+ # 마커 순회하며 위치 확인
+ for kind, points in self.event_markers.items():
+ for i, (timestamp, label) in enumerate(points):
+ px = chart.mapToPosition(QPointF(timestamp, 0)).x()
+ if abs(pos.x() - px) < 15: # 마커 근처 15픽셀
+ if kind == "TWC":
+ # TWC는 Rx~End 쌍으로 처리
+ return self._get_twc_pair(i, timestamp, label)
+ return (kind, (timestamp, label))
+
+ return None
+
+ def _get_twc_pair(self, index, timestamp, label):
+ """TWC 마커의 Rx~End 쌍 찾기"""
+ twc_markers = self.event_markers.get("TWC", [])
+
+ if "Rx" in label:
+ # Rx 마커면 다음 End 마커 찾기
+ for j in range(index + 1, len(twc_markers)):
+ end_ts, end_label = twc_markers[j]
+ if "End" in end_label:
+ return ("TWC", {
+ 'start': (timestamp, label),
+ 'end': (end_ts, end_label),
+ 'show_region': False,
+ 'region_color': QColor("#00FF00"),
+ 'region_opacity': 50
+ })
+ elif "End" in label:
+ # End 마커면 이전 Rx 마커 찾기
+ for j in range(index - 1, -1, -1):
+ rx_ts, rx_label = twc_markers[j]
+ if "Rx" in rx_label:
+ return ("TWC", {
+ 'start': (rx_ts, rx_label),
+ 'end': (timestamp, label),
+ 'show_region': False,
+ 'region_color': QColor("#00FF00"),
+ 'region_opacity': 50
+ })
+
+ # 단독 마커
+ return ("TWC", (timestamp, label))
+
+ def mouseDoubleClickEvent(self, event):
+ """더블클릭으로 객체 속성 편집 - 우선순위: 마커 < 데이터라인 < 드로잉객체"""
+
+ # 1. 드로잉 객체 더블클릭 체크 (최우선)
+ # 매번 현재 위치에서 객체를 새로 검색 (삭제된 객체 잔재 방지)
+ obj = self.get_object_at_position(event.pos())
+
+ if obj:
+ if obj['type'] in ['rect', 'circle']:
+ dialog = ShapePropertiesDialog(obj)
+ if dialog.exec():
+ self.update()
+ elif dialog._deleted:
+ # 도형 삭제
+ if obj in self.parent_view.drawing_objects:
+ self.parent_view.drawing_objects.remove(obj)
+ self.selected_object = None # 선택 초기화
+ self.update()
+ return
+ elif obj['type'] == 'text':
+ dialog = TextPropertiesDialog(obj)
+ if dialog.exec():
+ self.update()
+ elif dialog._deleted:
+ # 텍스트 삭제
+ if obj in self.parent_view.drawing_objects:
+ self.parent_view.drawing_objects.remove(obj)
+ self.selected_object = None
+ self.update()
+ return
+ elif obj['type'] == 'timeline':
+ dialog = TimelinePropertiesDialog(obj, None, self.parent_view)
+ if dialog.exec():
+ self.update()
+ elif dialog._deleted:
+ # 타임라인 삭제
+ if obj in self.parent_view.drawing_objects:
+ self.parent_view.drawing_objects.remove(obj)
+ self.selected_object = None
+ self.update()
+ return
+
+ # 2. 마커 더블클릭 체크 (드로잉 객체가 없을 때만)
+ marker_info = self.get_marker_at_position(event.pos())
+ if marker_info:
+ marker_type, marker_data = marker_info
+ dialog = MarkerPropertiesDialog(marker_type, marker_data, None, self.parent_view)
+ if dialog.exec():
+ self.update()
+ return
+
+ # 3. 기본 동작 (데이터 라인 등)
+ super().mouseDoubleClickEvent(event)
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == Qt.MiddleButton:
+ self.is_panning = False
+ self.setCursor(Qt.ArrowCursor)
+ event.accept()
+ elif event.button() == Qt.LeftButton and self.is_dragging_object:
+ # 드래그 종료
+ self.is_dragging_object = False
+ self.drag_start_pos = None
+ self.drag_start_chart_pos = None
+ self.setCursor(Qt.ArrowCursor)
+ event.accept()
+ elif event.button() == Qt.LeftButton and self.is_selecting_range:
+ # 구간 선택 종료
+ self.is_selecting_range = False
+ if self.range_rect and self.range_start_pos:
+ drag_distance = (event.pos() - self.range_start_pos).manhattanLength()
+ if drag_distance >= 10 and self._range_min_ms is not None and self._range_max_ms is not None:
+ # 컨텍스트 메뉴 표시
+ if hasattr(self, "parent_view") and self.parent_view:
+ self.parent_view.show_range_context_menu(event.globalPos(), self._range_min_ms, self._range_max_ms)
+ self.range_rect = None
+ self.range_start_pos = None
+ self.update()
+ elif event.button() == Qt.LeftButton and self.parent_view.drawing_start_pos and self.parent_view.drawing_mode in ["rect", "circle"]:
+ # 드래그 종료: 실제 도형 생성 (충분한 드래그 거리 확인)
+ start_pos = self.parent_view.drawing_start_pos
+ end_pos = event.pos()
+
+ # 드래그 거리 확인 (최소 10픽셀 이상)
+ drag_distance = (end_pos - start_pos).manhattanLength()
+ if drag_distance >= 10:
+ # 임시 도형 제거
+ if self.parent_view.temp_shape:
+ self.scene().removeItem(self.parent_view.temp_shape)
+ self.parent_view.temp_shape = None
+
+ # 실제 도형 생성
+ self.parent_view.create_final_shape(start_pos, end_pos)
+
+ # 그리기 모드 해제
+ self.parent_view.drawing_mode = None
+ self.parent_view.drawing_start_pos = None
+ if self.parent_view.temp_shape:
+ self.scene().removeItem(self.parent_view.temp_shape)
+ self.parent_view.temp_shape = None
+ self.setCursor(Qt.ArrowCursor)
+ event.accept()
+ else:
+ super().mouseReleaseEvent(event)
+
+ def mouseMoveEvent(self, event):
+ chart = self.chart()
+ # 패닝 (Middle Button)
+ if self.is_panning and self.last_mouse_pos:
+ delta = event.pos() - self.last_mouse_pos
+ self.last_mouse_pos = event.pos()
+ chart.scroll(-delta.x(), 0)
+ return
+
+ # 드로잉 객체 드래그 이동
+ if self.is_dragging_object and self.selected_object and self.drag_start_chart_pos:
+ current_chart_pos = chart.mapToValue(event.pos())
+ delta_x = current_chart_pos.x() - self.drag_start_chart_pos.x()
+ delta_y = current_chart_pos.y() - self.drag_start_chart_pos.y()
+
+ obj = self.selected_object
+ if obj['type'] in ['rect', 'circle', 'text']:
+ # 모든 사각형 기반 객체는 동일하게 처리
+ obj['timestamp_left'] += delta_x
+ obj['timestamp_right'] += delta_x
+ obj['y_top'] += delta_y
+ obj['y_bottom'] += delta_y
+ elif obj['type'] == 'timeline':
+ obj['timestamp'] += delta_x
+ # 라벨 업데이트
+ obj['label'] = QDateTime.fromMSecsSinceEpoch(int(obj['timestamp'])).toString("HH:mm:ss")
+
+ self.drag_start_chart_pos = current_chart_pos
+ self.update()
+ return
+
+ # 구간 선택 드래그
+ if self.is_selecting_range and self.range_start_pos:
+ self.range_rect = QRectF(self.range_start_pos, event.pos()).normalized()
+ # 시간 범위 계산 (plot 영역 안에서만)
+ chart = self.chart()
+ plot_area = chart.plotArea()
+ rect = self.range_rect
+ if rect.intersects(plot_area):
+ left = max(rect.left(), plot_area.left())
+ right = min(rect.right(), plot_area.right())
+ if right > left:
+ min_ms = chart.mapToValue(QPointF(left, plot_area.top())).x()
+ max_ms = chart.mapToValue(QPointF(right, plot_area.top())).x()
+ self._range_min_ms = min(min_ms, max_ms)
+ self._range_max_ms = max(min_ms, max_ms)
+ self.update()
+ return
+
+ # 그리기 모드 드래그 중
+ if self.parent_view.drawing_start_pos and self.parent_view.drawing_mode in ["rect", "circle"]:
+ # 임시 도형 업데이트
+ start_pos = self.parent_view.drawing_start_pos
+ current_pos = event.pos()
+
+ # 기존 임시 도형 제거
+ if self.parent_view.temp_shape:
+ self.scene().removeItem(self.parent_view.temp_shape)
+
+ # 새로운 임시 도형 생성
+ self.parent_view.temp_shape = self.parent_view.create_temp_shape(start_pos, current_pos)
+ if self.parent_view.temp_shape:
+ self.scene().addItem(self.parent_view.temp_shape)
+ return
+
+ # 커서 이동 (Red Line)
+ pos = event.pos()
+ plot_area = chart.plotArea()
+ if plot_area.contains(pos.x(), pos.y()):
+ self.cursor_pos_x = pos.x()
+ val_x_dt = chart.mapToValue(pos).x()
+ self.cursorMoved.emit(val_x_dt, None)
+ self.scene().update()
+ super().mouseMoveEvent(event)
+
+ def keyPressEvent(self, event):
+ """키보드 단축키 처리"""
+ if not self.selected_object:
+ super().keyPressEvent(event)
+ return
+
+ obj = self.selected_object
+ chart = self.chart()
+
+ # 이동 단위 계산 (화면 기준 5픽셀)
+ axis_x = chart.axes(Qt.Horizontal)[0]
+ axis_y = chart.axes(Qt.Vertical)[0]
+ x_range = axis_x.max().toMSecsSinceEpoch() - axis_x.min().toMSecsSinceEpoch()
+ y_range = axis_y.max() - axis_y.min()
+ plot_area = chart.plotArea()
+
+ move_x = (5 / plot_area.width()) * x_range # 5픽셀에 해당하는 X 이동량
+ move_y = (5 / plot_area.height()) * y_range # 5픽셀에 해당하는 Y 이동량
+
+ key = event.key()
+
+ # 방향키로 미세 이동
+ if key == Qt.Key_Left:
+ if obj['type'] in ['rect', 'circle', 'text']:
+ obj['timestamp_left'] -= move_x
+ obj['timestamp_right'] -= move_x
+ elif obj['type'] == 'timeline':
+ obj['timestamp'] -= move_x
+ obj['label'] = QDateTime.fromMSecsSinceEpoch(int(obj['timestamp'])).toString("HH:mm:ss")
+ self.update()
+ event.accept()
+
+ elif key == Qt.Key_Right:
+ if obj['type'] in ['rect', 'circle', 'text']:
+ obj['timestamp_left'] += move_x
+ obj['timestamp_right'] += move_x
+ elif obj['type'] == 'timeline':
+ obj['timestamp'] += move_x
+ obj['label'] = QDateTime.fromMSecsSinceEpoch(int(obj['timestamp'])).toString("HH:mm:ss")
+ self.update()
+ event.accept()
+
+ elif key == Qt.Key_Up:
+ if obj['type'] in ['rect', 'circle', 'text']:
+ obj['y_top'] += move_y
+ obj['y_bottom'] += move_y
+ self.update()
+ event.accept()
+
+ elif key == Qt.Key_Down:
+ if obj['type'] in ['rect', 'circle', 'text']:
+ obj['y_top'] -= move_y
+ obj['y_bottom'] -= move_y
+ self.update()
+ event.accept()
+
+ # Delete 키로 삭제
+ elif key == Qt.Key_Delete:
+ if obj in self.parent_view.drawing_objects:
+ self.parent_view.drawing_objects.remove(obj)
+ self.selected_object = None
+ self.update()
+ event.accept()
+
+ # Enter 키로 속성 팝업 열기
+ elif key == Qt.Key_Return or key == Qt.Key_Enter:
+ if obj['type'] in ['rect', 'circle']:
+ dialog = ShapePropertiesDialog(obj)
+ if dialog.exec():
+ self.update()
+ elif dialog._deleted:
+ if obj in self.parent_view.drawing_objects:
+ self.parent_view.drawing_objects.remove(obj)
+ self.selected_object = None
+ self.update()
+ elif obj['type'] == 'text':
+ dialog = TextPropertiesDialog(obj)
+ if dialog.exec():
+ self.update()
+ elif dialog._deleted:
+ if obj in self.parent_view.drawing_objects:
+ self.parent_view.drawing_objects.remove(obj)
+ self.selected_object = None
+ self.update()
+ elif obj['type'] == 'timeline':
+ dialog = TimelinePropertiesDialog(obj, None, self.parent_view)
+ if dialog.exec():
+ self.update()
+ elif dialog._deleted:
+ if obj in self.parent_view.drawing_objects:
+ self.parent_view.drawing_objects.remove(obj)
+ self.selected_object = None
+ self.update()
+ event.accept()
+
+ # Escape 키로 선택 해제
+ elif key == Qt.Key_Escape:
+ self.selected_object = None
+ self.update()
+ event.accept()
+
+ else:
+ super().keyPressEvent(event)
+
+ def drawForeground(self, painter, rect):
+ chart = self.chart()
+ plot_area = chart.plotArea()
+
+ # 0. PG 영역 표시 (배경으로 먼저 그리기)
+ for region in self.pg_regions:
+ if region.get('show_region', False):
+ start_ts = region['start'][0]
+ end_ts = region['end'][0]
+
+ start_px = chart.mapToPosition(QPointF(start_ts, 0)).x()
+ end_px = chart.mapToPosition(QPointF(end_ts, 0)).x()
+
+ if end_px > plot_area.left() and start_px < plot_area.right():
+ # 영역이 화면에 보이면 그리기
+ left = max(start_px, plot_area.left())
+ right = min(end_px, plot_area.right())
+
+ region_color = QColor(region.get('region_color', QColor("#FF6600")))
+ region_color.setAlpha(region.get('region_opacity', 40))
+
+ painter.fillRect(
+ QRectF(left, plot_area.top(), right - left, plot_area.height()),
+ QBrush(region_color)
+ )
+
+ # 0.5. TWC 영역 표시
+ for region in self.twc_regions:
+ if region.get('show_region', False):
+ start_ts = region['start'][0]
+ end_ts = region['end'][0]
+
+ start_px = chart.mapToPosition(QPointF(start_ts, 0)).x()
+ end_px = chart.mapToPosition(QPointF(end_ts, 0)).x()
+
+ if end_px > plot_area.left() and start_px < plot_area.right():
+ # 영역이 화면에 보이면 그리기
+ left = max(start_px, plot_area.left())
+ right = min(end_px, plot_area.right())
+
+ region_color = QColor(region.get('region_color', QColor("#00FF00")))
+ region_color.setAlpha(region.get('region_opacity', 50))
+
+ painter.fillRect(
+ QRectF(left, plot_area.top(), right - left, plot_area.height()),
+ QBrush(region_color)
+ )
+
+ # 1. 커서 라인 (Red Vertical Line)
+ if self.cursor_pos_x and plot_area.left() <= self.cursor_pos_x <= plot_area.right():
+ painter.setPen(self.cursor_pen)
+ painter.drawLine(int(self.cursor_pos_x), int(plot_area.top()),
+ int(self.cursor_pos_x), int(plot_area.bottom()))
+
+ # 1.5. 구간 선택 표시
+ if self.range_rect:
+ painter.setPen(self.range_pen)
+ painter.setBrush(self.range_brush)
+ painter.drawRect(self.range_rect)
+
+ # 2. 이벤트 마커 (PG, TWC, Station)
+ font = QFont("Malgun Gothic", 9, QFont.Bold)
+ painter.setFont(font)
+
+ # 스타일 정의
+ styles = {
+ "PG": {"color": QColor("#FF6600"), "offset": 10, "line_style": Qt.DashLine}, # 오렌지
+ "TWC": {"color": QColor(0, 180, 0), "offset": 25, "line_style": Qt.DotLine}, # 밝은 녹색
+ "STATION": {"color": QColor("#FFEB3B"), "offset": 40, "line_style": Qt.SolidLine} # 노란색
+ }
+
+ for kind, points in self.event_markers.items():
+ style = styles.get(kind, styles["PG"])
+
+ for timestamp, label in points:
+ # 좌표 변환 (Time -> Pixel)
+ px = chart.mapToPosition(QPointF(timestamp, 0)).x()
+
+ # 화면 영역 내에 있을 때만 그리기
+ if plot_area.left() <= px <= plot_area.right():
+ # 세로선
+ painter.setPen(QPen(style["color"], 1, style["line_style"]))
+ painter.drawLine(int(px), int(plot_area.top()),
+ int(px), int(plot_area.top() + style["offset"] + 10))
+
+ # 라벨 (역삼각형 + 텍스트)
+ painter.setBrush(style["color"])
+ painter.setPen(Qt.NoPen)
+
+ # 역삼각형 좌표
+ tip_y = plot_area.top() + 5
+ arrow = [
+ QPointF(px, tip_y),
+ QPointF(px - 4, tip_y - 6),
+ QPointF(px + 4, tip_y - 6)
+ ]
+ painter.drawPolygon(QPolygonF(arrow))
+
+ # 텍스트
+ painter.setPen(style["color"])
+ painter.drawText(int(px + 6), int(plot_area.top() + style["offset"]), label)
+
+ # 3. 드로잉 객체들 (도형, 텍스트, 타임라인)
+ if hasattr(self, 'parent_view') and self.parent_view:
+ for obj in self.parent_view.drawing_objects:
+ if obj['type'] == 'rect':
+ # 사각형 렌더링 - 4개 꼭짓점 좌표로 렌더링
+ left_px = chart.mapToPosition(QPointF(obj['timestamp_left'], 0)).x()
+ right_px = chart.mapToPosition(QPointF(obj['timestamp_right'], 0)).x()
+ top_py = chart.mapToPosition(QPointF(0, obj['y_top'])).y()
+ bottom_py = chart.mapToPosition(QPointF(0, obj['y_bottom'])).y()
+
+ # 사각형 영역 계산
+ rect = QRectF(left_px, top_py, right_px - left_px, bottom_py - top_py)
+
+ # 채우기
+ fill_color = QColor(obj['fill_color'])
+ fill_color.setAlpha(obj['fill_opacity'])
+ painter.setBrush(QBrush(fill_color))
+ painter.setPen(QPen(obj['border_color'], obj['border_width']))
+ painter.drawRect(rect)
+
+ elif obj['type'] == 'circle':
+ # 원 렌더링 - 4개 꼭짓점 좌표로 렌더링
+ left_px = chart.mapToPosition(QPointF(obj['timestamp_left'], 0)).x()
+ right_px = chart.mapToPosition(QPointF(obj['timestamp_right'], 0)).x()
+ top_py = chart.mapToPosition(QPointF(0, obj['y_top'])).y()
+ bottom_py = chart.mapToPosition(QPointF(0, obj['y_bottom'])).y()
+
+ # 타원 영역 계산
+ rect = QRectF(left_px, top_py, right_px - left_px, bottom_py - top_py)
+
+ # 채우기
+ fill_color = QColor(obj['fill_color'])
+ fill_color.setAlpha(obj['fill_opacity'])
+ painter.setBrush(QBrush(fill_color))
+ painter.setPen(QPen(obj['border_color'], obj['border_width']))
+ painter.drawEllipse(rect)
+
+ elif obj['type'] == 'timeline':
+ # 타임라인 렌더링
+ px = chart.mapToPosition(QPointF(obj['timestamp'], 0)).x()
+ if plot_area.left() <= px <= plot_area.right():
+ # 세로선
+ painter.setPen(QPen(obj['color'], obj['width'], obj['style']))
+ painter.drawLine(int(px), int(plot_area.top()),
+ int(px), int(plot_area.bottom()))
+
+ # 시간 라벨
+ font = QFont("Malgun Gothic", 8, QFont.Bold)
+ painter.setFont(font)
+ painter.setPen(obj['color'])
+ label_rect = painter.fontMetrics().boundingRect(obj['label'])
+ label_x = px - label_rect.width() / 2
+ label_y = plot_area.top() - label_rect.height() - 2
+ painter.drawText(int(label_x), int(label_y + label_rect.height()), obj['label'])
+
+ elif obj['type'] == 'text':
+ # 텍스트 렌더링 (사각형 기반 좌표 사용)
+ left_px = chart.mapToPosition(QPointF(obj['timestamp_left'], 0)).x()
+ right_px = chart.mapToPosition(QPointF(obj['timestamp_right'], 0)).x()
+ top_py = chart.mapToPosition(QPointF(0, obj['y_top'])).y()
+ bottom_py = chart.mapToPosition(QPointF(0, obj['y_bottom'])).y()
+
+ # 텍스트 영역 사각형
+ text_rect = QRectF(left_px, top_py, right_px - left_px, bottom_py - top_py)
+
+ if plot_area.intersects(text_rect):
+ # 폰트 설정
+ font = QFont(obj['font_family'], obj['font_size'])
+ font.setBold(obj['font_bold'])
+ font.setItalic(obj['font_italic'])
+ painter.setFont(font)
+ painter.setPen(obj['text_color'])
+
+ # 텍스트 위치 (사각형 중앙 기준)
+ text_x = left_px
+ text_y = (top_py + bottom_py) / 2 + obj['font_size'] / 3
+
+ # 그림자 효과
+ if obj['text_shadow']:
+ shadow_offset = max(1, font.pointSize() // 8)
+ shadow_color = QColor("black") if obj['text_color'].lightness() > 128 else QColor("white")
+ painter.setPen(shadow_color)
+
+ # 8방향 그림자
+ for dx, dy in [(-shadow_offset, -shadow_offset), (0, -shadow_offset), (shadow_offset, -shadow_offset),
+ (-shadow_offset, 0), (shadow_offset, 0),
+ (-shadow_offset, shadow_offset), (0, shadow_offset), (shadow_offset, shadow_offset)]:
+ painter.drawText(int(text_x + dx), int(text_y + dy), obj['text'])
+
+ # 본문 텍스트
+ painter.setPen(obj['text_color'])
+ painter.drawText(int(text_x), int(text_y), obj['text'])
+
+ # 4. 선택된 객체 표시 (점선 사각형)
+ if self.selected_object:
+ obj = self.selected_object
+ selection_pen = QPen(QColor("#00BFFF"), 2, Qt.DashLine)
+ painter.setPen(selection_pen)
+ painter.setBrush(Qt.NoBrush)
+
+ if obj['type'] in ['rect', 'circle', 'text']:
+ # 모든 사각형 기반 객체 동일하게 처리
+ left_px = chart.mapToPosition(QPointF(obj['timestamp_left'], 0)).x()
+ right_px = chart.mapToPosition(QPointF(obj['timestamp_right'], 0)).x()
+ top_py = chart.mapToPosition(QPointF(0, obj['y_top'])).y()
+ bottom_py = chart.mapToPosition(QPointF(0, obj['y_bottom'])).y()
+
+ # 선택 표시 사각형 (약간 확장)
+ margin = 4
+ sel_rect = QRectF(left_px - margin, top_py - margin,
+ right_px - left_px + margin*2, bottom_py - top_py + margin*2)
+ painter.drawRect(sel_rect)
+
+ elif obj['type'] == 'text_old_unused':
+ # 이전 방식 (사용하지 않음)
+ px = chart.mapToPosition(QPointF(obj.get('timestamp', 0), obj.get('y_value', 0))).x()
+ py = chart.mapToPosition(QPointF(0, obj.get('y_value', 0))).y()
+
+ # 텍스트 바운딩 박스 계산
+ font = QFont(obj['font_family'], obj['font_size'])
+ font.setBold(obj['font_bold'])
+ fm = QFontMetrics(font)
+ text_rect = fm.boundingRect(obj['text'])
+
+ margin = 4
+ sel_rect = QRectF(px - margin, py - text_rect.height() - margin,
+ text_rect.width() + margin*2, text_rect.height() + margin*2)
+ painter.drawRect(sel_rect)
+
+ elif obj['type'] == 'timeline':
+ px = chart.mapToPosition(QPointF(obj['timestamp'], 0)).x()
+
+ # 타임라인 상하단에 작은 사각형 표시
+ handle_size = 8
+ painter.setBrush(QBrush(QColor("#00BFFF")))
+ painter.drawRect(int(px - handle_size/2), int(plot_area.top() - handle_size/2),
+ handle_size, handle_size)
+ painter.drawRect(int(px - handle_size/2), int(plot_area.bottom() - handle_size/2),
+ handle_size, handle_size)
+
+
+# --- [5. 그래프 뷰 메인 위젯] ---
+class GraphView(QWidget):
+ range_ai_requested = Signal(int, int, int, int)
+ def __init__(self, panel_id):
+ super().__init__()
+ self.panel_id = panel_id
+ self.data_list = []
+ self.start_timestamp = 0
+
+ # 날짜/열번 선택용 상태
+ self._available_qdates = set() # Set[QDate]
+ self._date_to_first_idx = {} # Dict[QDate, int]
+ self._trainno_to_first_idx = {} # Dict[str, int]
+ self._last_valid_qdate = None # Optional[QDate]
+ self._suppress_x_range_sync = False
+
+ # 성능 디버그 (환경변수로 제어: MMI_GRAPH_PERF=1)
+ self.perf_debug = os.environ.get("MMI_GRAPH_PERF", "0") == "1"
+
+ self.layout = QHBoxLayout(self)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.layout.setSpacing(0)
+ self.setStyleSheet("background-color: #1E1E1E;") # 다크모드 배경
+
+ # === 차트 구성 ===
+ self.chart = QChart()
+ self.chart.legend().hide()
+ self.chart.setMargins(QMargins(5, 5, 5, 5))
+ self.chart.setBackgroundBrush(QColor("#2D2D2D")) # 다크모드 차트 배경
+
+ self.axis_x = QDateTimeAxis()
+ self.axis_x.setFormat("hh:mm:ss")
+ self.axis_x.setTickCount(9)
+ self.axis_x.setGridLineVisible(True)
+ self.axis_x.setGridLineColor(QColor("#555555")) # 다크모드 그리드
+ self.axis_x.setLabelsColor(QColor("#CCCCCC")) # 다크모드 라벨 색상
+ self.chart.addAxis(self.axis_x, Qt.AlignBottom)
+
+ # 왼쪽 Y축 (속도, PWM, TASC, ATC)
+ self.axis_y_left = QValueAxis()
+ self.axis_y_left.setRange(0, 110) # 일반 신호 범위 0~110
+ self.axis_y_left.setGridLineVisible(True)
+ self.axis_y_left.setGridLineColor(QColor("#555555")) # 다크모드 그리드
+ self.axis_y_left.setLabelFormat("%d")
+ self.axis_y_left.setTitleText("속도/PWM/ATC/TASC")
+ self.axis_y_left.setLabelsColor(QColor("#CCCCCC")) # 다크모드 라벨 색상
+ self.axis_y_left.setTitleBrush(QColor("#CCCCCC")) # 타이틀 색상
+ self.chart.addAxis(self.axis_y_left, Qt.AlignLeft)
+
+ # 오른쪽 Y축 (DTG 전용)
+ self.axis_y_right = QValueAxis()
+ self.axis_y_right.setRange(0, 2000) # DTG 범위 0~2000m
+ self.axis_y_right.setGridLineVisible(False) # 그리드 라인 숨김
+ self.axis_y_right.setLabelFormat("%d")
+ self.axis_y_right.setTitleText("DTG (m)")
+ self.axis_y_right.setLabelsColor(QColor("#FFAB00")) # DTG 색상과 맞춤
+ self.axis_y_right.setTitleBrush(QColor("#FFAB00")) # 타이틀 색상
+ self.chart.addAxis(self.axis_y_right, Qt.AlignRight)
+
+ # 기본 axis_y는 왼쪽 축으로 설정 (호환성 유지)
+ self.axis_y = self.axis_y_left
+
+ self.chart_view = InteractiveChartView(self.chart, self)
+ self.chart_view.cursorMoved.connect(self.on_cursor_moved)
+
+ # 차트 축 변경 시 객체 위치 업데이트
+ self.axis_x.rangeChanged.connect(self.update_drawing_objects_position)
+ self.axis_x.rangeChanged.connect(self._on_axis_x_range_changed)
+ self.axis_y_left.rangeChanged.connect(self.update_drawing_objects_position)
+ self.axis_y_right.rangeChanged.connect(self.update_drawing_objects_position)
+ self.layout.addWidget(self.chart_view, stretch=85)
+
+ # 우클릭 메뉴 연결
+ self.chart_view.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.chart_view.customContextMenuRequested.connect(self.show_context_menu)
+
+ # === 우측 제어 패널 ===
+ self.right_panel = QFrame()
+ self.right_panel.setStyleSheet("background-color: #252526; border-left: 1px solid #3E3E42;")
+ self.right_panel.setMaximumWidth(260)
+ self.panel_layout = QVBoxLayout(self.right_panel)
+ self.panel_layout.setContentsMargins(10, 10, 10, 10)
+ self.panel_layout.setSpacing(10)
+
+ # 1. 파일 정보 박스
+ self.info_box = QComboBox()
+ self.info_box.addItem("[No File]")
+ self.info_box.setStyleSheet("""
+ QComboBox { border: 1px solid #0078D7; border-radius: 4px; padding: 5px; color: white; background-color: #0078D7; font-weight: bold;}
+ QComboBox::drop-down { border: 0px; }
+ QComboBox QAbstractItemView { background-color: #2D2D30; color: white; selection-background-color: #0078D7; }
+ """)
+ self.panel_layout.addWidget(self.info_box)
+
+ # 1-1. 시간 표시 (HH:mm:ss만)
+ self.lbl_time_top = QLabel("⏱️ --:--:--")
+ self.lbl_time_top.setStyleSheet("font-weight: bold; color: #FFFFFF; font-size: 14px;")
+ self.panel_layout.addWidget(self.lbl_time_top)
+
+ # 1-2. 날짜 선택 (데이터 있는 날짜만 강조/선택 가능)
+ date_title = QLabel("📅 날짜")
+ date_title.setStyleSheet("font-weight: bold; margin-top: 6px; color: #CCCCCC;")
+ self.panel_layout.addWidget(date_title)
+
+ self.date_picker = QDateEdit()
+ self.date_picker.setCalendarPopup(True)
+ self.date_picker.setDisplayFormat("yyyy-MM-dd")
+ self.date_picker.setEnabled(False)
+ self.date_picker.setStyleSheet("""
+ QDateEdit { padding: 5px; color: white; background-color: #3C3C3C; border: 1px solid #555555; border-radius: 3px; }
+ QDateEdit::drop-down { border: 0px; }
+ """)
+ self.date_picker.dateChanged.connect(self.on_date_changed)
+ self.panel_layout.addWidget(self.date_picker)
+
+ # 1-3. 열번(열차 번호) 선택
+ train_title = QLabel("🚆 열번")
+ train_title.setStyleSheet("font-weight: bold; margin-top: 6px; color: #CCCCCC;")
+ self.panel_layout.addWidget(train_title)
+
+ self.cb_trainno = QComboBox()
+ self.cb_trainno.setEnabled(False)
+ self.cb_trainno.setStyleSheet("""
+ QComboBox { padding: 5px; color: white; background-color: #3C3C3C; border: 1px solid #555555; border-radius: 3px; }
+ QComboBox::drop-down { border: 0px; }
+ QComboBox QAbstractItemView { background-color: #2D2D30; color: white; selection-background-color: #0078D7; }
+ """)
+ self.cb_trainno.currentIndexChanged.connect(self.on_trainno_selected)
+ self.panel_layout.addWidget(self.cb_trainno)
+
+ # 2. 범례 및 신호 토글 (Legend)
+ grp = QGroupBox("Signal Selection")
+ grp.setStyleSheet("""
+ QGroupBox { border: 1px solid #3E3E42; border-radius: 4px; margin-top: 10px; font-weight: bold; color: #CCCCCC; }
+ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; color: #CCCCCC; }
+ """)
+ self.legend_layout = QVBoxLayout(grp)
+ self.legend_layout.setSpacing(5)
+ self.panel_layout.addWidget(grp)
+
+ self.signals_map = {}
+ self.init_signals()
+
+ # 3. 역 이동 (Station Jump)
+ stn_label = QLabel("🚩 역 이동 (Station Jump)")
+ stn_label.setStyleSheet("font-weight: bold; margin-top: 10px; color: #CCCCCC;")
+ self.panel_layout.addWidget(stn_label)
+
+ self.cb_stations = QComboBox()
+ self.cb_stations.setStyleSheet("""
+ QComboBox { padding: 5px; color: white; background-color: #3C3C3C; border: 1px solid #555555; border-radius: 3px; }
+ QComboBox::drop-down { border: 0px; }
+ QComboBox QAbstractItemView { background-color: #2D2D30; color: white; selection-background-color: #0078D7; }
+ """)
+ self.cb_stations.currentIndexChanged.connect(self.on_station_selected)
+ self.panel_layout.addWidget(self.cb_stations)
+
+ # 4. 그리기 툴박스
+ toolbox_group = QGroupBox("그리기 도구")
+ toolbox_group.setStyleSheet("""
+ QGroupBox { border: 1px solid #3E3E42; border-radius: 4px; margin-top: 10px; font-weight: bold; color: #CCCCCC; }
+ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; color: #CCCCCC; }
+ """)
+ toolbox_layout = QVBoxLayout(toolbox_group)
+ toolbox_layout.setSpacing(5)
+
+ # 그리기 도구 버튼들
+ tool_buttons = [
+ ("📝 텍스트", self.set_text_drawing_mode),
+ ("🔲 사각형", self.set_rect_drawing_mode),
+ ("🔴 원", self.set_circle_drawing_mode),
+ ("📏 세로선", lambda: self.add_drawing_tool("line")),
+ ]
+
+ for text, callback in tool_buttons:
+ btn = QPushButton(text)
+ btn.setStyleSheet("""
+ QPushButton {
+ background-color: #3C3C3C;
+ color: white;
+ border: 1px solid #555555;
+ border-radius: 4px;
+ padding: 8px;
+ text-align: left;
+ }
+ QPushButton:hover {
+ background-color: #4C4C4C;
+ border: 1px solid #666666;
+ }
+ QPushButton:pressed {
+ background-color: #2C2C2C;
+ }
+ """)
+ btn.clicked.connect(callback)
+ toolbox_layout.addWidget(btn)
+
+ self.panel_layout.addWidget(toolbox_group)
+
+ self.panel_layout.addStretch(1)
+
+ # 4. X축 스케일 조정 SpinBox
+ scale_group = QGroupBox("X축 범위 (초)")
+ scale_group.setStyleSheet("""
+ QGroupBox { border: 1px solid #3E3E42; border-radius: 4px; margin-top: 10px; font-weight: bold; color: #CCCCCC; }
+ QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; color: #CCCCCC; }
+ """)
+ scale_layout = QHBoxLayout(scale_group)
+ scale_layout.setContentsMargins(8, 15, 8, 8)
+
+ self.spin_x_scale = QSpinBox()
+ self.spin_x_scale.setRange(10, 7200) # 10초 ~ 2시간
+ self.spin_x_scale.setValue(600) # 기본 600초 (10분)
+ self.spin_x_scale.setSingleStep(10)
+ self.spin_x_scale.setSuffix(" 초")
+ self.spin_x_scale.setStyleSheet("""
+ QSpinBox {
+ background-color: #3C3C3C;
+ color: white;
+ border: 1px solid #555555;
+ border-radius: 4px;
+ padding: 5px;
+ font-size: 12px;
+ }
+ QSpinBox::up-button, QSpinBox::down-button {
+ background-color: #4C4C4C;
+ border: none;
+ width: 20px;
+ }
+ QSpinBox::up-button:hover, QSpinBox::down-button:hover {
+ background-color: #5C5C5C;
+ }
+ """)
+ self.spin_x_scale.valueChanged.connect(self.on_x_scale_changed)
+ scale_layout.addWidget(self.spin_x_scale)
+
+ self.panel_layout.addWidget(scale_group)
+
+ self.layout.addWidget(self.right_panel, stretch=15)
+ self.clicked_series = None
+ self.last_click_time = 0
+
+ self.station_timestamp_map = {}
+
+ # 다른 패널에서 온 X축 스케일 동기화 수신
+ try:
+ sync_manager.x_range_changed.connect(self._apply_synced_x_range)
+ except Exception:
+ pass
+
+ # 그리기 모드 상태
+ self.drawing_mode = None # None, "rect", "circle", "text"
+ self.drawing_start_pos = None
+ self.temp_shape = None
+
+ # 차트에 통합된 드로잉 객체들 (차트와 한몸)
+ self.drawing_objects = []
+
+ def init_signals(self):
+ # 신호 정의: (key, DisplayName, Color)
+ config = [
+ ("speed", "SPEED (속도)", "#2979FF"),
+ ("pwm", "PWM", "#9370DB"),
+ ("tasc", "TASC (목표)", "#00C853"),
+ ("atc", "ATC Code", "#FF3D00"),
+ ("dtg", "DTG (잔여거리)", "#FFAB00")
+ ]
+
+ for key, name, color in config:
+ series = QLineSeries()
+ series.setName(name)
+ pen = QPen(QColor(color))
+ pen.setWidth(2)
+ series.setPen(pen)
+
+ # 시리즈 클릭 이벤트 연결
+ series.clicked.connect(lambda p, s=series: self.on_series_clicked(s))
+
+ self.chart.addSeries(series)
+ series.attachAxis(self.axis_x)
+
+ # DTG는 오른쪽 Y축, 나머지는 왼쪽 Y축 사용
+ if key == "dtg":
+ series.attachAxis(self.axis_y_right)
+ else:
+ series.attachAxis(self.axis_y_left)
+
+ # 우측 패널에 체크박스 및 라벨 추가
+ row = QHBoxLayout()
+ chk = QCheckBox()
+ chk.setChecked(True) # 기본값: 모두 켜기
+ series.setVisible(True)
+
+ chk.setFixedSize(20, 20)
+ chk.setStyleSheet(f"""
+ QCheckBox::indicator {{ width: 16px; height: 16px; background-color: {color}; border: 1px solid gray; border-radius: 3px; }}
+ QCheckBox::indicator:checked {{ background-color: {color}; border: 2px solid white; }}
+ QCheckBox::indicator:unchecked {{ background-color: #3C3C3C; border: 1px solid {color}; }}
+ """)
+
+ lbl = QLabel(f"{name} (-)")
+ lbl.setStyleSheet("font-family: 'Malgun Gothic'; font-size: 11px; color: #CCCCCC;")
+
+ row.addWidget(chk)
+ row.addWidget(lbl)
+ row.addStretch(1)
+ self.legend_layout.addLayout(row)
+
+ # functools.partial을 사용하여 클로저 문제 방지
+ chk.stateChanged.connect(partial(self.toggle_series, key))
+
+ self.signals_map[key] = {
+ "series": series, "label": lbl, "chk": chk, "base": name, "data": []
+ }
+
+ def show_range_context_menu(self, global_pos, min_ms, max_ms):
+ """드래그 선택 구간 컨텍스트 메뉴"""
+ if min_ms is None or max_ms is None:
+ return
+ menu = QMenu(self)
+ menu.setStyleSheet("""
+ QMenu {
+ background-color: #2D2D30;
+ color: white;
+ border: 1px solid #3E3E42;
+ padding: 5px;
+ }
+ QMenu::item:selected {
+ background-color: #0078D7;
+ color: white;
+ }
+ """)
+ action_ai = QAction("이 구간에 대해 AI에게 질문하기", self)
+ action_ai.triggered.connect(lambda: self._request_ai_for_range(min_ms, max_ms))
+ menu.addAction(action_ai)
+ menu.exec(global_pos)
+
+ def _request_ai_for_range(self, min_ms, max_ms):
+ """AI 요청 시그널 발생"""
+ if not self.data_list or self.start_timestamp == 0:
+ return
+ start_ms = min(min_ms, max_ms)
+ end_ms = max(min_ms, max_ms)
+ start_idx = int((start_ms - self.start_timestamp) / 1000)
+ end_idx = int((end_ms - self.start_timestamp) / 1000)
+ start_idx = max(0, min(len(self.data_list) - 1, start_idx))
+ end_idx = max(0, min(len(self.data_list) - 1, end_idx))
+ if start_idx > end_idx:
+ start_idx, end_idx = end_idx, start_idx
+ self.range_ai_requested.emit(int(start_ms), int(end_ms), int(start_idx), int(end_idx))
+
+ def toggle_series(self, key, state):
+ if key not in self.signals_map:
+ return
+ series = self.signals_map[key]['series']
+ is_visible = (state == Qt.Checked or state == 2) # Qt.Checked = 2
+ series.setVisible(is_visible)
+ # 차트 갱신
+ self.chart_view.viewport().update()
+ self.chart.update()
+
+ def on_series_clicked(self, series):
+ """더블클릭(빠른 클릭) 시 속성창 띄우기"""
+ import time
+ now = time.time()
+ # 0.3초 내에 같은 시리즈 다시 클릭 시
+ if series == self.clicked_series and (now - self.last_click_time) < 0.3:
+ dlg = SeriesPropDialog(series, self)
+ if dlg.exec_():
+ dlg.apply_settings()
+ # 체크박스 색상도 동기화
+ key = [k for k, v in self.signals_map.items() if v['series'] == series][0]
+ new_col = series.pen().color().name()
+ self.signals_map[key]['chk'].setStyleSheet(
+ f"QCheckBox::indicator {{ width: 16px; height: 16px; background-color: {new_col}; border: 1px solid gray; }}"
+ f"QCheckBox::indicator:checked {{ background-color: {new_col}; border: 2px solid black; }}")
+ self.clicked_series = series
+ self.last_click_time = now
+
+ def show_context_menu(self, pos):
+ """우클릭 컨텍스트 메뉴"""
+ # 컨텍스트 메뉴 위치 저장
+ self.context_menu_pos = pos
+
+ menu = QMenu(self)
+ menu.setStyleSheet("""
+ QMenu {
+ background-color: #2D2D30;
+ color: white;
+ border: 1px solid #3E3E42;
+ padding: 5px;
+ }
+ QMenu::item {
+ background-color: transparent;
+ color: white;
+ padding: 5px 20px;
+ margin: 2px 0px;
+ }
+ QMenu::item:selected {
+ background-color: #0078D7;
+ color: white;
+ }
+ QMenu::separator {
+ height: 1px;
+ background-color: #3E3E42;
+ margin: 5px 0px;
+ }
+ """)
+
+ # 타임라인 추가
+ action_timeline = QAction("⏱️ 타임라인 추가 (세로선)", self)
+ action_timeline.setShortcut("Ctrl+T")
+ action_timeline.triggered.connect(lambda: self.add_timeline_at_menu_pos())
+ menu.addAction(action_timeline)
+
+ menu.addSeparator()
+
+ # 화면 초기화
+ action_reset = QAction("🔄 화면 초기화", self)
+ action_reset.setShortcut("Ctrl+R")
+ action_reset.triggered.connect(self.reset_view)
+ menu.addAction(action_reset)
+
+ menu.exec(self.chart_view.mapToGlobal(pos))
+
+ def set_drag_mode(self):
+ """드래그 모드 활성화 (손모양 커서)"""
+ self.chart_view.setDragMode(QChartView.ScrollHandDrag)
+ self.chart_view.setCursor(Qt.OpenHandCursor)
+
+ def set_view_mode(self):
+ """뷰잉 모드 활성화 (눈모양 커서)"""
+ self.chart_view.setDragMode(QChartView.NoDrag)
+ self.chart_view.setCursor(Qt.CrossCursor)
+ self.drawing_mode = None # 그리기 모드 해제
+
+ def set_rect_drawing_mode(self):
+ """사각형 그리기 모드 활성화"""
+ self.drawing_mode = "rect"
+ self.chart_view.setDragMode(QChartView.NoDrag)
+ self.chart_view.setCursor(Qt.CrossCursor)
+
+ def set_circle_drawing_mode(self):
+ """원 그리기 모드 활성화"""
+ self.drawing_mode = "circle"
+ self.chart_view.setDragMode(QChartView.NoDrag)
+ self.chart_view.setCursor(Qt.CrossCursor)
+
+ def set_text_drawing_mode(self):
+ """텍스트 그리기 모드 활성화"""
+ self.drawing_mode = "text"
+ self.chart_view.setDragMode(QChartView.NoDrag)
+ self.chart_view.setCursor(Qt.IBeamCursor)
+
+ def reset_view(self):
+ self.chart.zoomReset()
+ if self.data_list:
+ start_dt = QDateTime.fromMSecsSinceEpoch(self.start_timestamp)
+ self.axis_x.setRange(start_dt, start_dt.addSecs(600))
+ self.spin_x_scale.blockSignals(True)
+ self.spin_x_scale.setValue(600)
+ self.spin_x_scale.blockSignals(False)
+
+ def on_x_scale_changed(self, value):
+ """X축 스케일 SpinBox 값 변경 시"""
+ # 현재 X축 중앙 시간 계산
+ curr_min = self.axis_x.min()
+ curr_max = self.axis_x.max()
+ center_ms = (curr_min.toMSecsSinceEpoch() + curr_max.toMSecsSinceEpoch()) / 2
+ center_dt = QDateTime.fromMSecsSinceEpoch(int(center_ms))
+
+ # 새 범위 설정 (중앙 기준)
+ half_range = value // 2
+ new_min = center_dt.addSecs(-half_range)
+ new_max = center_dt.addSecs(value - half_range)
+ self.axis_x.setRange(new_min, new_max)
+
+ def _on_axis_x_range_changed(self, min_dt: QDateTime, max_dt: QDateTime):
+ """현재 패널에서 X축 범위가 바뀌면 다른 패널로 전파"""
+ if self._suppress_x_range_sync:
+ return
+ try:
+ min_ms = min_dt.toMSecsSinceEpoch()
+ max_ms = max_dt.toMSecsSinceEpoch()
+ sync_manager.request_x_range_sync(int(min_ms), int(max_ms), self.panel_id)
+ except Exception:
+ pass
+
+ def _apply_synced_x_range(self, min_ms: int, max_ms: int, source_id):
+ """다른 패널에서 보낸 X축 범위를 적용"""
+ # 자기 자신이 보낸 이벤트면 무시
+ if str(source_id) == str(self.panel_id):
+ return
+ if not hasattr(self, "axis_x"):
+ return
+ try:
+ self._suppress_x_range_sync = True
+ self.axis_x.setRange(
+ QDateTime.fromMSecsSinceEpoch(int(min_ms)),
+ QDateTime.fromMSecsSinceEpoch(int(max_ms)),
+ )
+ # SpinBox도 갱신
+ self.update_x_scale_spinbox()
+ finally:
+ self._suppress_x_range_sync = False
+
+ def update_x_scale_spinbox(self):
+ """현재 X축 범위를 SpinBox에 반영"""
+ if not hasattr(self, 'spin_x_scale'):
+ return
+ curr_min = self.axis_x.min()
+ curr_max = self.axis_x.max()
+ current_range_secs = curr_min.secsTo(curr_max)
+ self.spin_x_scale.blockSignals(True)
+ self.spin_x_scale.setValue(max(10, min(7200, current_range_secs)))
+ self.spin_x_scale.blockSignals(False)
+
+ def add_drawing_tool(self, tool_type):
+ """도형 추가 로직"""
+ # 현재 보고 있는 뷰의 중앙 좌표 계산
+ plot_area = self.chart.plotArea()
+ center_x = plot_area.center().x()
+ center_y = plot_area.center().y()
+
+ scene = self.chart_view.scene()
+
+ if tool_type == "line":
+ # 차트 높이만큼 세로선 생성
+ item = CustomLineItem(center_x, plot_area.top(), plot_area.height())
+ elif tool_type == "rect":
+ item = CustomRectItem(center_x - 30, center_y - 20, 60, 40)
+ elif tool_type == "circle":
+ item = CustomEllipseItem(center_x - 30, center_y - 30, 60, 60)
+ else:
+ return
+
+ scene.addItem(item)
+
+ def add_timeline_at_menu_pos(self):
+ """컨텍스트 메뉴 위치에 타임라인(세로선) 추가"""
+ if not hasattr(self, 'context_menu_pos'):
+ return
+
+ # 차트 좌표로 변환
+ chart = self.chart
+ chart_pos = chart.mapToValue(self.context_menu_pos)
+
+ # 타임라인 객체 생성 (차트에 통합)
+ timeline_obj = {
+ 'type': 'timeline',
+ 'timestamp': chart_pos.x(),
+ 'color': QColor("#2196F3"),
+ 'width': 2,
+ 'style': Qt.SolidLine,
+ 'label': QDateTime.fromMSecsSinceEpoch(int(chart_pos.x())).toString("HH:mm:ss")
+ }
+
+ self.drawing_objects.append(timeline_obj)
+ self.chart_view.update()
+
+ def create_temp_shape(self, start_pos, current_pos):
+ """임시 도형 생성 (드래그 중 표시용) - 픽셀 좌표 사용"""
+ if not self.drawing_mode or self.drawing_mode not in ["rect", "circle"]:
+ return None
+
+ # 픽셀 좌표 직접 사용 (차트 좌표 변환 없이)
+ x1, y1 = start_pos.x(), start_pos.y()
+ x2, y2 = current_pos.x(), current_pos.y()
+
+ # 좌상단과 우하단 좌표 계산
+ left = min(x1, x2)
+ top = min(y1, y2)
+ width = abs(x2 - x1)
+ height = abs(y2 - y1)
+
+ if self.drawing_mode == "rect":
+ temp_item = QGraphicsRectItem(left, top, width, height)
+ temp_item.setPen(QPen(QColor(255, 0, 0, 150), 2, Qt.DashLine))
+ temp_item.setBrush(QBrush(QColor(255, 0, 0, 50)))
+ elif self.drawing_mode == "circle":
+ temp_item = QGraphicsEllipseItem(left, top, width, height)
+ temp_item.setPen(QPen(QColor(255, 0, 0, 150), 2, Qt.DashLine))
+ temp_item.setBrush(QBrush(QColor(255, 0, 0, 50)))
+
+ return temp_item
+
+ def create_final_shape(self, start_pos, end_pos):
+ """최종 도형 생성"""
+ if not self.drawing_mode or self.drawing_mode not in ["rect", "circle"]:
+ return
+
+ # 차트 좌표로 변환
+ chart = self.chart
+ start_chart_pos = chart.mapToValue(start_pos)
+ end_chart_pos = chart.mapToValue(end_pos)
+
+ x1, y1 = start_chart_pos.x(), start_chart_pos.y()
+ x2, y2 = end_chart_pos.x(), end_chart_pos.y()
+
+ # 최소 크기 확인 (픽셀 거리로 확인)
+ pixel_width = abs(end_pos.x() - start_pos.x())
+ pixel_height = abs(end_pos.y() - start_pos.y())
+ if pixel_width < 10 or pixel_height < 10:
+ return # 너무 작으면 생성하지 않음
+
+ # 도형 객체 생성 (차트에 통합) - 4개 꼭짓점의 시간/값 좌표 저장
+ if self.drawing_mode == "rect":
+ shape_obj = {
+ 'type': 'rect',
+ 'timestamp_left': min(x1, x2), # 좌측 시간
+ 'timestamp_right': max(x1, x2), # 우측 시간
+ 'y_top': max(y1, y2), # 상단 Y값 (Y축 반전)
+ 'y_bottom': min(y1, y2), # 하단 Y값
+ 'border_color': QColor(255, 0, 0, 255),
+ 'border_width': 2,
+ 'fill_color': QColor(255, 0, 0, 100),
+ 'fill_opacity': 100
+ }
+ elif self.drawing_mode == "circle":
+ shape_obj = {
+ 'type': 'circle',
+ 'timestamp_left': min(x1, x2), # 좌측 시간
+ 'timestamp_right': max(x1, x2), # 우측 시간
+ 'y_top': max(y1, y2), # 상단 Y값 (Y축 반전)
+ 'y_bottom': min(y1, y2), # 하단 Y값
+ 'border_color': QColor(255, 0, 0, 255),
+ 'border_width': 2,
+ 'fill_color': QColor(255, 0, 0, 100),
+ 'fill_opacity': 100
+ }
+
+ self.drawing_objects.append(shape_obj)
+ self.chart_view.update()
+
+ def add_text_at_position(self, x, y):
+ """지정된 위치에 텍스트 추가 (팝업으로 속성 입력)"""
+ # 텍스트 객체를 사각형 기반으로 생성 (4꼭짓점 좌표)
+ # 기본 크기 설정 (차트 좌표 기준)
+ axis_x = self.axis_x
+ axis_y = self.axis_y_left
+
+ # 기본 너비/높이 계산 (대략적인 픽셀 크기를 차트 좌표로 변환)
+ x_range = axis_x.max().toMSecsSinceEpoch() - axis_x.min().toMSecsSinceEpoch()
+ y_range = axis_y.max() - axis_y.min()
+ plot_area = self.chart.plotArea()
+
+ # 100x30 픽셀 크기에 해당하는 차트 좌표
+ default_width = (100 / plot_area.width()) * x_range
+ default_height = (30 / plot_area.height()) * y_range
+
+ text_obj = {
+ 'type': 'text',
+ # 사각형 기반 좌표 (도형과 동일)
+ 'timestamp_left': x,
+ 'timestamp_right': x + default_width,
+ 'y_top': y + default_height / 2,
+ 'y_bottom': y - default_height / 2,
+ # 텍스트 속성
+ 'text': "",
+ 'font_family': "Malgun Gothic",
+ 'font_size': 12,
+ 'text_color': QColor("white"),
+ 'font_bold': False,
+ 'font_italic': False,
+ 'text_shadow': False,
+ 'shadow_type': "black_white"
+ }
+
+ # 팝업으로 텍스트 속성 입력받기
+ dialog = TextPropertiesDialog(text_obj, self)
+ if dialog.exec():
+ # 확인 버튼 누르면 객체 추가
+ if text_obj['text'].strip(): # 텍스트가 있으면
+ self.drawing_objects.append(text_obj)
+ self.chart_view.update()
+
+ def update_drawing_objects_position(self):
+ """차트 줌/패닝 시 객체들의 위치 재계산"""
+ for item in self.chart_view.scene().items():
+ if hasattr(item, 'timestamp') and item.timestamp is not None and hasattr(item, 'y_value') and item.y_value is not None:
+ # 시간과 Y값을 픽셀 좌표로 변환
+ pixel_pos = self.chart.mapToPosition(QPointF(item.timestamp, item.y_value))
+ # Y축만 현재 위치 유지, X축은 시간에 고정
+ current_pos = item.pos()
+ item.setPos(pixel_pos.x(), current_pos.y())
+
+ def set_index(self, index):
+ """외부에서 인덱스를 받아 커서만 이동 (싱크 재요청 금지)"""
+ if not self.data_list or self.start_timestamp == 0:
+ return
+
+ timestamp_ms = self.start_timestamp + (index * 1000)
+
+ # 1. 커서 위치 시각적 업데이트 (Map Value -> Position)
+ self.chart_view.cursor_pos_x = self.chart.mapToPosition(QPointF(timestamp_ms, 0)).x()
+ self.chart_view.scene().update()
+
+ # 2. 텍스트 라벨 업데이트
+ if 0 <= index < len(self.data_list):
+ for key, info in self.signals_map.items():
+ if index < len(info['data']):
+ val = info['data'][index]
+ # 소수점 표시 구분
+ if key in ['speed', 'dtg']:
+ info['label'].setText(f"{info['base']} ({val:.1f})")
+ else:
+ info['label'].setText(f"{info['base']} ({int(val)})")
+
+ # 3. 뷰포트 자동 이동 (현재 커서가 화면 밖이면)
+ dt = QDateTime.fromMSecsSinceEpoch(int(timestamp_ms))
+ curr_min = self.axis_x.min()
+ curr_max = self.axis_x.max()
+ if dt < curr_min or dt > curr_max:
+ self.axis_x.setRange(dt.addSecs(-30), dt.addSecs(30))
+
+ def _refresh_date_picker(self, old_dates=None):
+ """날짜 picker에 '데이터가 있는 날짜'만 강조하고, 그 날짜만 선택되도록 제어"""
+ if old_dates is None:
+ old_dates = set()
+
+ # 날짜 목록이 없으면 비활성화
+ if not self._available_qdates:
+ self.date_picker.blockSignals(True)
+ self.date_picker.setEnabled(False)
+ self.date_picker.blockSignals(False)
+ self._last_valid_qdate = None
+ return
+
+ dates_sorted = sorted(self._available_qdates)
+ min_d, max_d = dates_sorted[0], dates_sorted[-1]
+
+ self.date_picker.blockSignals(True)
+ self.date_picker.setEnabled(True)
+ self.date_picker.setDateRange(min_d, max_d)
+ # 현재 선택이 유효하지 않으면 첫 날짜로 세팅
+ if self._last_valid_qdate not in self._available_qdates:
+ self._last_valid_qdate = min_d
+ self.date_picker.setDate(self._last_valid_qdate)
+ self.date_picker.blockSignals(False)
+
+ # 캘린더에서 데이터 날짜만 색상 강조
+ cal = self.date_picker.calendarWidget()
+ if cal is None:
+ return
+
+ # 이전 하이라이트 제거
+ clear_fmt = QTextCharFormat()
+ for d in old_dates:
+ try:
+ cal.setDateTextFormat(d, clear_fmt)
+ except Exception:
+ pass
+
+ highlight_fmt = QTextCharFormat()
+ highlight_fmt.setBackground(QBrush(QColor("#0078D7")))
+ highlight_fmt.setForeground(QBrush(QColor("#FFFFFF")))
+ highlight_fmt.setFontWeight(QFont.Bold)
+ for d in self._available_qdates:
+ try:
+ cal.setDateTextFormat(d, highlight_fmt)
+ except Exception:
+ pass
+
+ def _refresh_trainno_combo(self):
+ """열번 드롭다운을 데이터 기반으로 갱신"""
+ self.cb_trainno.blockSignals(True)
+ self.cb_trainno.clear()
+
+ if not self._trainno_to_first_idx:
+ self.cb_trainno.addItem("[열번 없음]")
+ self.cb_trainno.setEnabled(False)
+ self.cb_trainno.blockSignals(False)
+ return
+
+ # 표시 순서: 값 기준 정렬 (기본은 문자열 정렬, '0000'은 제외됨)
+ for trainno in sorted(self._trainno_to_first_idx.keys()):
+ self.cb_trainno.addItem(trainno, trainno)
+ self.cb_trainno.setEnabled(True)
+ self.cb_trainno.setCurrentIndex(0)
+ self.cb_trainno.blockSignals(False)
+
+ def on_date_changed(self, qdate: QDate):
+ """사용자가 날짜를 바꾸면 해당 날짜(첫 등장 시점)로 이동"""
+ if not self._available_qdates:
+ return
+
+ if qdate not in self._available_qdates:
+ # 허용되지 않은 날짜 선택 방지: 마지막 유효 날짜로 되돌림
+ if self._last_valid_qdate is not None:
+ self.date_picker.blockSignals(True)
+ self.date_picker.setDate(self._last_valid_qdate)
+ self.date_picker.blockSignals(False)
+ return
+
+ self._last_valid_qdate = qdate
+ idx = self._date_to_first_idx.get(qdate, None)
+ if idx is not None:
+ self._jump_to_index(idx)
+
+ def on_trainno_selected(self, combo_index: int):
+ """열번 변경 시 해당 열번의 첫 등장 위치(시간대)로 이동"""
+ if combo_index < 0:
+ return
+ trainno = self.cb_trainno.currentData()
+ if not trainno:
+ trainno = self.cb_trainno.currentText()
+ idx = self._trainno_to_first_idx.get(str(trainno), None)
+ if idx is not None:
+ self._jump_to_index(idx)
+
+ def _jump_to_index(self, index: int):
+ """인덱스를 시간(ms)로 변환해 현재 스케일 유지하며 이동"""
+ if not self.data_list or self.start_timestamp == 0:
+ return
+ t_ms = self.start_timestamp + (int(index) * 1000)
+ self._jump_to_ms(t_ms)
+
+ def _jump_to_ms(self, t_ms: int):
+ """지정한 시간(ms)을 중심으로 현재 X축 스케일 유지하며 이동 + 커서/패널 갱신"""
+ dt = QDateTime.fromMSecsSinceEpoch(int(t_ms))
+
+ curr_min = self.axis_x.min()
+ curr_max = self.axis_x.max()
+ current_range_secs = curr_min.secsTo(curr_max)
+ if current_range_secs <= 0 and hasattr(self, "spin_x_scale"):
+ current_range_secs = int(self.spin_x_scale.value())
+ if current_range_secs <= 0:
+ current_range_secs = 600
+
+ half_range = current_range_secs // 2
+ new_min = dt.addSecs(-half_range)
+ new_max = dt.addSecs(current_range_secs - half_range)
+ self.axis_x.setRange(new_min, new_max)
+
+ self.chart_view.cursor_pos_x = self.chart.mapToPosition(QPointF(t_ms, 0)).x()
+ self.chart_view.scene().update()
+ self.on_cursor_moved(t_ms, None)
+
+ def on_station_selected(self, index):
+ """콤보박스에서 역 선택 시 이동 (현재 확대/축소 스케일 유지)"""
+ if index in self.station_timestamp_map:
+ t_ms = self.station_timestamp_map[index]
+ dt = QDateTime.fromMSecsSinceEpoch(int(t_ms))
+
+ # 현재 X축 범위(스케일) 유지
+ curr_min = self.axis_x.min()
+ curr_max = self.axis_x.max()
+ current_range_secs = curr_min.secsTo(curr_max) # 현재 표시 범위(초)
+
+ # 선택한 역을 중심으로 현재 스케일 유지하며 이동
+ half_range = current_range_secs // 2
+ new_min = dt.addSecs(-half_range)
+ new_max = dt.addSecs(current_range_secs - half_range)
+ self.axis_x.setRange(new_min, new_max)
+
+ # 커서 이동 및 싱크 요청
+ self.chart_view.cursor_pos_x = self.chart.mapToPosition(QPointF(t_ms, 0)).x()
+ self.chart_view.scene().update()
+ self.on_cursor_moved(t_ms, None)
+
+ def filter_noise(self, raw_data, min_valid=0, threshold=3, speed_data=None, all_zero_mask=None, preserve_valid_zero=False):
+ """
+ 범용 노이즈 필터링 (튀는 값 제거)
+
+ Args:
+ raw_data: 원본 데이터 리스트
+ min_valid: 유효 최소값 (이 값 미만은 노이즈로 간주)
+ threshold: 연속 노이즈 허용 횟수 (이 횟수 초과하면 실제 0으로 인정)
+ speed_data: 속도 데이터 (정차 시 필터링 해제용)
+ all_zero_mask: 모든 신호가 0인 구간 마스크 (True=노이즈 구간)
+
+ Note:
+ - 속도가 0인 구간(정차 시)에서는 필터링을 적용하지 않고 원본 값 사용
+ - all_zero_mask가 True인 구간은 통신 에러로 간주하여 이전 값 유지
+ """
+ filtered = []
+ last_valid = raw_data[0] if raw_data else 0
+ noise_count = 0
+
+ for i, val in enumerate(raw_data):
+ # 1. 모든 신호가 0인 구간 (통신 에러) - 이전 값 유지
+ if all_zero_mask and i < len(all_zero_mask) and all_zero_mask[i]:
+ filtered.append(last_valid)
+ continue
+
+ # 2. 정차 상태(속도=0)에서도 동일한 노이즈 필터링 적용
+ # 정차 중에도 TASC/ATC가 0과 특정값 사이를 반복할 수 있음
+ # 따라서 정차 여부와 관계없이 동일한 Hold Last Value 필터 적용
+
+ # 3. 일반 필터링 로직 (정차/주행 모두 동일)
+ if val is None:
+ filtered.append(last_valid)
+ continue
+ if preserve_valid_zero and val == 0:
+ last_valid = 0
+ noise_count = 0
+ filtered.append(0)
+ continue
+ if val >= min_valid: # 유효 범위
+ last_valid = val
+ noise_count = 0
+ filtered.append(val)
+ else:
+ noise_count += 1
+ if noise_count > threshold:
+ filtered.append(0) # 실제 0으로 인정
+ last_valid = 0
+ else:
+ filtered.append(last_valid) # 노이즈 구간은 이전 값 유지
+ return filtered
+
+ def filter_speed_noise(self, raw_speed, all_zero_mask):
+ """
+ 속도 데이터 필터링 (모든 신호가 0인 통신 에러 구간에서는 이전 값 유지)
+
+ Args:
+ raw_speed: 원본 속도 데이터 리스트
+ all_zero_mask: 모든 신호가 0인 구간 마스크 (True=통신 에러 구간)
+
+ Returns:
+ filtered: 필터링된 속도 데이터
+ """
+ if not raw_speed:
+ return []
+
+ filtered = []
+ last_valid = raw_speed[0] if raw_speed else 0
+
+ for i, val in enumerate(raw_speed):
+ # 모든 신호가 0인 구간 (통신 에러) - 이전 값 유지
+ if all_zero_mask and i < len(all_zero_mask) and all_zero_mask[i]:
+ filtered.append(last_valid)
+ else:
+ if val is None:
+ filtered.append(last_valid)
+ else:
+ filtered.append(val)
+ last_valid = val
+
+ return filtered
+
+ def detect_all_zero_frames(self, vals_dict):
+ """
+ 모든 주요 신호가 동시에 0이 되는 구간을 감지 (통신 에러로 추정)
+
+ Returns:
+ all_zero_mask: 각 프레임별 True/False 리스트 (True=모든 신호가 0인 노이즈 구간)
+
+ Note:
+ - 속도, PWM, TASC, ATC, DTG가 모두 0인 경우: 통신 에러로 추정 → 이전 값 유지
+ - 속도가 0이 아닌데 나머지가 모두 0인 경우: 통신 에러로 추정 → 이전 값 유지
+ """
+ if not vals_dict:
+ return []
+
+ # 프레임 수
+ frame_count = len(list(vals_dict.values())[0])
+ all_zero_mask = []
+
+ for i in range(frame_count):
+ speed = vals_dict.get('speed', [0])[i] if i < len(vals_dict.get('speed', [])) else 0
+ pwm = vals_dict.get('pwm', [0])[i] if i < len(vals_dict.get('pwm', [])) else 0
+ tasc = vals_dict.get('tasc', [0])[i] if i < len(vals_dict.get('tasc', [])) else 0
+ atc = vals_dict.get('atc', [0])[i] if i < len(vals_dict.get('atc', [])) else 0
+ dtg = vals_dict.get('dtg', [0])[i] if i < len(vals_dict.get('dtg', [])) else 0
+
+ # Case 1: 속도를 포함한 모든 신호가 0이면 통신 에러로 추정
+ if speed == 0 and pwm == 0 and tasc == 0 and atc == 0 and dtg == 0:
+ all_zero_mask.append(True)
+ # Case 2: 속도가 0이 아닌데 다른 모든 신호가 0이면 통신 에러로 추정
+ elif speed != 0 and pwm == 0 and tasc == 0 and atc == 0 and dtg == 0:
+ all_zero_mask.append(True)
+ else:
+ all_zero_mask.append(False)
+
+ return all_zero_mask
+
+ def _infer_valid_sources(self, data_list, signal_attr_map, zero_ratio_threshold=0.98, min_samples=50):
+ """
+ 소스별로 '거의 항상 0'인 신호를 찾아 유효 소스를 추정한다.
+ - 특정 소스가 해당 신호에서 0 비율이 매우 높고, 다른 소스는 그렇지 않으면 해당 소스를 제외.
+ """
+ stats = {k: {} for k in signal_attr_map.keys()}
+ sources_seen = set()
+ for d in data_list:
+ src = getattr(d, "source", None)
+ if src is None:
+ continue
+ sources_seen.add(src)
+ for key, attr in signal_attr_map.items():
+ val = getattr(d, attr, None)
+ if val is None:
+ continue
+ s = stats[key].setdefault(src, {"n": 0, "zero": 0})
+ s["n"] += 1
+ if val == 0:
+ s["zero"] += 1
+
+ valid_sources_map = {}
+ for key, by_src in stats.items():
+ if not by_src or len(by_src) <= 1:
+ valid_sources_map[key] = None
+ continue
+ ratios = {}
+ for src, s in by_src.items():
+ if s["n"] < min_samples:
+ continue
+ ratios[src] = (s["zero"] / s["n"]) if s["n"] else 0
+ if not ratios:
+ valid_sources_map[key] = None
+ continue
+ low_zero = [src for src, r in ratios.items() if r < zero_ratio_threshold]
+ high_zero = [src for src, r in ratios.items() if r >= zero_ratio_threshold]
+ if low_zero and high_zero:
+ valid_sources_map[key] = set(low_zero)
+ else:
+ valid_sources_map[key] = None
+ return valid_sources_map
+
+ def _apply_source_filter(self, raw_values, valid_mask):
+ """유효하지 않은 소스의 값은 직전 유효값으로 대체"""
+ filtered = []
+ last_valid = None
+ for val, is_valid in zip(raw_values, valid_mask):
+ if is_valid:
+ last_valid = val
+ filtered.append(val)
+ else:
+ if last_valid is None:
+ filtered.append(val if val is not None else 0)
+ else:
+ filtered.append(last_valid)
+ return filtered
+
+ def _filter_pg_markers(self, raw_markers, threshold=3):
+ """
+ PG 마커 노이즈 필터링 (시간 정확성 유지)
+
+ 문제: PG 신호가 1초마다 ON/OFF를 반복 (깜박임)
+ 해결: 깜박이는 구간을 병합하되, 실제 첫 시작과 마지막 끝 시간을 유지
+
+ 원리:
+ - 짧은 OFF 구간(threshold 이하) 다음에 다시 ON이 오면 → 연속된 신호로 간주
+ - 짧은 OFF 구간 다음에 충분히 긴 OFF가 오면 → 마지막 ON 시점이 실제 종료
+
+ Args:
+ raw_markers: 원본 마커 리스트
+ threshold: 깜박임으로 간주할 최대 OFF 연속 횟수
+
+ Returns:
+ filtered_markers: 깜박임이 병합된 마커 리스트 (실제 시간 유지)
+ """
+ if not raw_markers:
+ return []
+
+ n = len(raw_markers)
+ filtered = list(raw_markers) # 복사본 생성
+
+ i = 0
+ while i < n:
+ if raw_markers[i] != "-":
+ # PG 신호 시작 발견
+ marker_type = raw_markers[i]
+ start_idx = i
+ last_on_idx = i
+
+ # 해당 마커의 끝을 찾음 (깜박임 병합)
+ j = i + 1
+ off_count = 0
+
+ while j < n:
+ if raw_markers[j] == marker_type:
+ # 같은 마커 다시 발견 - 깜박임 중간을 채움
+ for k in range(last_on_idx + 1, j):
+ filtered[k] = marker_type
+ last_on_idx = j
+ off_count = 0
+ j += 1
+ elif raw_markers[j] == "-":
+ off_count += 1
+ if off_count > threshold:
+ # 충분히 긴 OFF - 실제 종료
+ # last_on_idx가 마지막 ON 위치, 그 다음부터는 "-"로 유지
+ break
+ j += 1
+ else:
+ # 다른 마커 발견 - 현재 마커 종료
+ break
+
+ # last_on_idx까지만 마커로 채우고, 그 이후는 원본 유지
+ i = j
+ else:
+ i += 1
+
+ return filtered
+
+ def set_data(self, data_list):
+ """데이터 로드 및 시각화"""
+ t0 = time.perf_counter()
+ self.data_list = data_list
+ if not data_list: return
+
+ # 1. 시작 시간 설정 (첫 데이터 기준)
+ # 데이터에 'time'이 있으면 해당 값을 기준으로 시작 시간을 설정 (기본: 1초 간격)
+ first_time_str = getattr(data_list[0], "time", "") or ""
+ first_dt = QDateTime.fromString(first_time_str, "yyyy.MM.dd HH:mm:ss")
+ if first_dt.isValid():
+ self.start_timestamp = first_dt.toMSecsSinceEpoch()
+ else:
+ # 파싱 실패 시 현재시간으로 폴백
+ self.start_timestamp = QDateTime.currentDateTime().toMSecsSinceEpoch()
+ first_dt = QDateTime.fromMSecsSinceEpoch(self.start_timestamp)
+
+ # 상단 시간 라벨/파일정보(짧게 표시: HH:mm:ss만)
+ time_str = first_dt.toString("HH:mm:ss")
+ full_dt_str = first_dt.toString("yyyy-MM-dd HH:mm:ss")
+ self.lbl_time_top.setText(f"⏱️ {time_str}")
+ self.info_box.setItemText(0, f"[{len(data_list)}] {time_str}")
+ self.info_box.setToolTip(full_dt_str)
+
+ # 날짜/열번 목록 구성은 아래 메인 루프(2. 데이터 순회)에서 같이 수집하여 1회 순회로 처리
+ old_dates = self._available_qdates
+ self._available_qdates = set()
+ self._date_to_first_idx = {}
+ self._trainno_to_first_idx = {}
+ t1 = time.perf_counter()
+
+ signal_attr_map = {
+ "speed": "trainspeed",
+ "pwm": "pwm_value",
+ "tasc": "tasc_value",
+ "atc": "atc_code_val",
+ "dtg": "dtg",
+ }
+ source_rules = self._infer_valid_sources(data_list, signal_attr_map)
+ self._signal_valid_sources = source_rules
+
+ vals_raw = {k: [] for k in self.signals_map.keys()}
+ source_valid_mask = {k: [] for k in self.signals_map.keys()}
+ markers_pg = []
+ markers_twc = []
+ markers_station = []
+
+ self.cb_stations.clear()
+ self.station_timestamp_map = {}
+ self.chart_view.twc_regions = [] # TWC 영역 초기화
+ self.chart_view.pg_regions = [] # PG 영역 초기화
+
+ last_pstn = -1
+ last_twc = False
+ twc_start_ts = None # TWC 시작 시간 추적
+
+ # PG 마커별 색상 정의
+ PG_COLORS = {
+ "PG1": QColor("#FF6600"), # 오렌지
+ "PG2": QColor("#FF9900"), # 밝은 오렌지
+ "PG3-2": QColor("#FFCC00"), # 노란색
+ "PGX": QColor("#FF3300"), # 빨간 오렌지
+ "ATS": QColor("#9900FF"), # 보라색
+ }
+
+ # 2-1. 먼저 marker 데이터를 수집하여 노이즈 필터링 적용
+ raw_markers = [getattr(d, 'marker', '-') for d in data_list]
+ filtered_markers = self._filter_pg_markers(raw_markers, threshold=3)
+
+ # 2. 데이터 순회 및 추출
+ pg_start_ts = None
+ pg_start_label = None
+ last_marker = "-"
+
+ for i, d in enumerate(data_list):
+ t_ms = self.start_timestamp + (i * 1000)
+
+ # 날짜/열번 인덱싱 (문자열 파싱 대신 t_ms 기반으로 날짜 계산)
+ qdate = QDateTime.fromMSecsSinceEpoch(int(t_ms)).date()
+ if qdate not in self._date_to_first_idx:
+ self._date_to_first_idx[qdate] = i
+ self._available_qdates.add(qdate)
+
+ trainno = getattr(d, "trainno", None)
+ if trainno and trainno != "0000" and trainno not in self._trainno_to_first_idx:
+ self._trainno_to_first_idx[trainno] = i
+
+ # 필드 존재 여부 확인 (getattr) + 소스 기반 필터링 (가짜 0 제거)
+ src = getattr(d, "source", None)
+ for key, attr in signal_attr_map.items():
+ val = getattr(d, attr, 0)
+ valid_sources = source_rules.get(key)
+ is_valid = (valid_sources is None) or (src in valid_sources)
+ vals_raw[key].append(val if is_valid else None)
+ source_valid_mask[key].append(is_valid)
+
+ curr_pstn = getattr(d, 'pstn', 0)
+
+ # (A) 역 정보 (100 이상)
+ if curr_pstn >= 100:
+ if curr_pstn != last_pstn:
+ stn_name = STATION_CODE_MAP.get(curr_pstn, f"STN-{curr_pstn}")
+
+ if self.cb_stations.count() == 0 or \
+ stn_name not in self.cb_stations.itemText(self.cb_stations.count()-1):
+ markers_station.append((t_ms, stn_name))
+ time_str = QDateTime.fromMSecsSinceEpoch(int(t_ms)).toString("HH:mm:ss")
+ self.cb_stations.addItem(f"[{time_str}] {stn_name}")
+ self.station_timestamp_map[self.cb_stations.count()-1] = t_ms
+
+ last_pstn = curr_pstn
+
+ # (B) PG 마커 - 노이즈 필터링된 데이터 사용
+ curr_marker = filtered_markers[i] if i < len(filtered_markers) else "-"
+
+ if curr_marker != last_marker:
+ if curr_marker != "-" and last_marker == "-":
+ # PG 시작 (Rx만 표시)
+ markers_pg.append((t_ms, curr_marker))
+ pg_start_ts = t_ms
+ pg_start_label = curr_marker
+ elif curr_marker == "-" and last_marker != "-":
+ # PG 종료 - End 마커는 표시하지 않음, 영역 데이터만 추가
+ if pg_start_ts is not None:
+ self.chart_view.pg_regions.append({
+ 'start': (pg_start_ts, pg_start_label),
+ 'end': (t_ms, pg_start_label),
+ 'show_region': True,
+ 'region_color': PG_COLORS.get(pg_start_label, QColor("#FF6600")),
+ 'region_opacity': 40,
+ 'pg_type': pg_start_label
+ })
+ pg_start_ts = None
+ pg_start_label = None
+ elif curr_marker != "-" and last_marker != "-" and curr_marker != last_marker:
+ # PG 전환 (이전 영역 저장 + 새로 시작, End 마커 없음)
+ if pg_start_ts is not None:
+ self.chart_view.pg_regions.append({
+ 'start': (pg_start_ts, pg_start_label),
+ 'end': (t_ms, pg_start_label),
+ 'show_region': True,
+ 'region_color': PG_COLORS.get(pg_start_label, QColor("#FF6600")),
+ 'region_opacity': 40,
+ 'pg_type': pg_start_label
+ })
+ markers_pg.append((t_ms, curr_marker)) # Rx만 표시
+ pg_start_ts = t_ms
+ pg_start_label = curr_marker
+ last_marker = curr_marker
+
+ # (C) TWC 수신 여부 - End 마커는 표시하지 않음
+ curr_twc = getattr(d, 'twct_enable', False)
+ if curr_twc and not last_twc:
+ markers_twc.append((t_ms, "TWC")) # Rx만 표시 (End 없음)
+ twc_start_ts = t_ms
+ elif not curr_twc and last_twc:
+ # TWC 영역 데이터만 추가 (End 마커는 표시하지 않음)
+ if twc_start_ts is not None:
+ self.chart_view.twc_regions.append({
+ 'start': (twc_start_ts, "TWC"),
+ 'end': (t_ms, "TWC"),
+ 'show_region': True,
+ 'region_color': QColor("#00FF00"),
+ 'region_opacity': 50
+ })
+ twc_start_ts = None
+ last_twc = curr_twc
+
+ # 날짜 picker / 열번 드롭다운 갱신
+ self._refresh_date_picker(old_dates=old_dates)
+ self._refresh_trainno_combo()
+ t2 = time.perf_counter()
+
+ # 3. 데이터 후처리 (노이즈 제거 및 최적화)
+ # 3-0. 소스 기반 필터 적용 (유효하지 않은 소스는 직전 유효값 유지)
+ vals = {
+ k: self._apply_source_filter(vals_raw[k], source_valid_mask[k])
+ for k in vals_raw.keys()
+ }
+
+ # 3-1. 모든 신호가 동시에 0이 되는 구간 감지 (통신 에러)
+ all_zero_mask = self.detect_all_zero_frames(vals)
+ # AI/디버그용으로 저장
+ self._all_zero_mask = all_zero_mask
+
+ # 3-2. 속도 필터링 (모든 신호가 0인 구간에서는 이전 값 유지)
+ vals['speed'] = self.filter_speed_noise(vals['speed'], all_zero_mask)
+
+ # 3-3. 속도 데이터를 참조하여 정차 시에는 필터링 비활성화
+ speed_data = vals['speed']
+ pwm_preserve_zero = source_rules.get("pwm") is not None
+ dtg_preserve_zero = source_rules.get("dtg") is not None
+ atc_preserve_zero = source_rules.get("atc") is not None
+ tasc_preserve_zero = source_rules.get("tasc") is not None
+
+ vals['pwm'] = self.filter_noise(vals['pwm'], min_valid=15, threshold=3, speed_data=speed_data, all_zero_mask=all_zero_mask, preserve_valid_zero=pwm_preserve_zero) # PWM 필터링
+ vals['dtg'] = self.filter_noise(vals['dtg'], min_valid=1, threshold=2, speed_data=speed_data, all_zero_mask=all_zero_mask, preserve_valid_zero=dtg_preserve_zero) # DTG 필터링 (1m 이상만 유효)
+ vals['atc'] = self.filter_noise(vals['atc'], min_valid=1, threshold=3, speed_data=speed_data, all_zero_mask=all_zero_mask, preserve_valid_zero=atc_preserve_zero) # ATC 코드 필터링 (0 깜박임 제거)
+ vals['tasc'] = self.filter_noise(vals['tasc'], min_valid=1, threshold=3, speed_data=speed_data, all_zero_mask=all_zero_mask, preserve_valid_zero=tasc_preserve_zero) # TASC 필터링 (0 깜박임 제거)
+
+ # 4. 차트에 데이터 적용 (QPointF 변환)
+ for k in self.signals_map:
+ values = vals[k]
+ points = []
+ if values:
+ # 데이터가 너무 많으면 다운샘플링 고려 가능
+ for i, v in enumerate(values):
+ t = self.start_timestamp + (i * 1000)
+ points.append(QPointF(t, v))
+
+ self.signals_map[k]['series'].replace(points)
+ self.signals_map[k]['data'] = values
+ t3 = time.perf_counter()
+
+ # 5. 마커 업데이트
+ self.chart_view.event_markers["PG"] = markers_pg
+ self.chart_view.event_markers["TWC"] = markers_twc
+ self.chart_view.event_markers["STATION"] = markers_station
+ self.chart_view.scene().update()
+
+ # 6. 초기 뷰 설정 (기본 600초 = 10분)
+ # 첫 속도 상승 시점 (0 → 1 이상) 찾기
+ first_speed_rise_idx = 0
+ for i in range(1, len(data_list)):
+ prev_speed = getattr(data_list[i-1], 'trainspeed', 0) or 0
+ curr_speed = getattr(data_list[i], 'trainspeed', 0) or 0
+ if prev_speed == 0 and curr_speed >= 1:
+ first_speed_rise_idx = i
+ break
+
+ # 첫 속도 상승 시점을 중앙에 배치
+ scale_secs = 600 # 기본 스케일
+ first_speed_rise_ts = self.start_timestamp + (first_speed_rise_idx * 1000)
+ center_ts = first_speed_rise_ts
+ half_scale = (scale_secs * 1000) // 2
+
+ view_start_ts = max(self.start_timestamp, center_ts - half_scale)
+ view_end_ts = view_start_ts + (scale_secs * 1000)
+
+ self.axis_x.setRange(
+ QDateTime.fromMSecsSinceEpoch(int(view_start_ts)),
+ QDateTime.fromMSecsSinceEpoch(int(view_end_ts))
+ )
+ self.spin_x_scale.blockSignals(True)
+ self.spin_x_scale.setValue(scale_secs)
+ self.spin_x_scale.blockSignals(False)
+
+ # DTG 축 범위 고정 (0~1500)
+ self.axis_y_right.setRange(0, 1500)
+ t4 = time.perf_counter()
+
+ if self.perf_debug:
+ print(
+ f"[GraphView PERF] n={len(data_list)} "
+ f"setup={t1-t0:.3f}s "
+ f"loop+index+ui={t2-t1:.3f}s "
+ f"series_replace={t3-t2:.3f}s "
+ f"view={t4-t3:.3f}s "
+ f"total={t4-t0:.3f}s"
+ )
+
+ def on_cursor_moved(self, timestamp_ms, _):
+ """커서 이동 시 텍스트 업데이트 및 동기화 요청"""
+ if not self.data_list or self.start_timestamp == 0:
+ return
+ # 상단 시간 라벨 업데이트 (HH:mm:ss만)
+ try:
+ self.lbl_time_top.setText(
+ f"⏱️ {QDateTime.fromMSecsSinceEpoch(int(timestamp_ms)).toString('HH:mm:ss')}"
+ )
+ except Exception:
+ pass
+ idx = int((timestamp_ms - self.start_timestamp) / 1000)
+
+ if 0 <= idx < len(self.data_list):
+ # 우측 패널 값 업데이트
+ for key, info in self.signals_map.items():
+ if idx < len(info['data']):
+ val = info['data'][idx]
+ if key in ['speed', 'dtg']:
+ info['label'].setText(f"{info['base']} ({val:.1f})")
+ else:
+ info['label'].setText(f"{info['base']} ({int(val)})")
+
+ # 동기화 매니저 호출
+ sync_manager.request_sync(idx, self.data_list[idx], self.panel_id)
+
+ def get_ai_context(self, cursor_index: int | None = None):
+ """
+ AI 프롬프트에 넘길 그래프 컨텍스트 생성
+ - 현재 표시 구간(axis_x)
+ - 현재 표시(선택)된 신호들(+ 커서 시점 값)
+ - 사용자 추가 타임라인/도형(drawing_objects)
+ - 커서 시점 전후 2개역(Station Jump 기반) 정보 + 해당 시점 스냅샷
+ """
+ ctx = {}
+ try:
+ mn = self.axis_x.min().toMSecsSinceEpoch()
+ mx = self.axis_x.max().toMSecsSinceEpoch()
+ ctx["visible_range_ms"] = {
+ "min": int(mn),
+ "max": int(mx),
+ "seconds": (mx - mn) / 1000.0,
+ }
+ except Exception:
+ pass
+
+ # 선택(표시)된 신호
+ visible = []
+ try:
+ for k, info in self.signals_map.items():
+ series = info.get("series")
+ if series is not None and series.isVisible():
+ item = {"key": k, "name": info.get("base", k)}
+ # 커서 시점 값 포함
+ if cursor_index is not None:
+ try:
+ data = info.get("data") or []
+ if 0 <= cursor_index < len(data):
+ item["value"] = data[cursor_index]
+ except Exception:
+ pass
+ visible.append(item)
+ except Exception:
+ pass
+ ctx["visible_signals"] = visible
+
+ # 커서 정보
+ try:
+ if cursor_index is not None and getattr(self, "start_timestamp", 0):
+ ts = int(self.start_timestamp + (cursor_index * 1000))
+ ctx["cursor"] = {
+ "index": int(cursor_index),
+ "timestamp_ms": ts,
+ "time": QDateTime.fromMSecsSinceEpoch(ts).toString("yyyy-MM-dd HH:mm:ss"),
+ }
+ except Exception:
+ pass
+
+ # 타임라인/텍스트/도형
+ objs = []
+ try:
+ for o in getattr(self, "drawing_objects", []) or []:
+ if not isinstance(o, dict):
+ continue
+ t = o.get("type")
+ if t not in ("timeline", "text", "rect", "circle", "line"):
+ continue
+ item = dict(o)
+ # QColor 직렬화
+ col = item.get("color")
+ try:
+ if hasattr(col, "name"):
+ item["color"] = col.name()
+ except Exception:
+ pass
+ # PenStyle 직렬화
+ style = item.get("style")
+ try:
+ if hasattr(style, "value"):
+ item["style"] = style.value
+ except Exception:
+ pass
+ # 레인지 안의 타임라인만 우선 포함
+ if t == "timeline":
+ ts = item.get("timestamp")
+ if ts is None:
+ continue
+ objs.append(item)
+ except Exception:
+ pass
+ ctx["drawing_objects"] = objs
+
+ # 타임라인(세로선) 전후 2개(커서 기준)
+ try:
+ if cursor_index is not None and "cursor" in ctx:
+ cur_ts = ctx["cursor"]["timestamp_ms"]
+ timelines = [o for o in objs if o.get("type") == "timeline" and isinstance(o.get("timestamp"), (int, float))]
+ timelines.sort(key=lambda x: x.get("timestamp"))
+ prev_ = [t for t in timelines if t["timestamp"] < cur_ts][-2:]
+ next_ = [t for t in timelines if t["timestamp"] >= cur_ts][:2]
+ ctx["timelines_near_cursor"] = prev_ + next_
+ except Exception:
+ pass
+
+ # 역(Station Jump) 전후 2개 + 스냅샷
+ try:
+ if cursor_index is not None and "cursor" in ctx:
+ cur_ts = ctx["cursor"]["timestamp_ms"]
+ station_events = []
+ for combo_idx, t_ms in (getattr(self, "station_timestamp_map", {}) or {}).items():
+ try:
+ label = self.cb_stations.itemText(combo_idx)
+ except Exception:
+ label = ""
+ station_events.append({"timestamp_ms": int(t_ms), "label": label})
+ station_events.sort(key=lambda x: x["timestamp_ms"])
+
+ # split
+ prev_e = [e for e in station_events if e["timestamp_ms"] < cur_ts][-2:]
+ next_e = [e for e in station_events if e["timestamp_ms"] >= cur_ts][:2]
+ neighbors = prev_e + next_e
+
+ # 스냅샷 부착 (해당 시점 근처 인덱스)
+ snapshots = []
+ for e in neighbors:
+ idx = None
+ if getattr(self, "start_timestamp", 0):
+ idx = int(round((e["timestamp_ms"] - self.start_timestamp) / 1000.0))
+ idx = max(0, min(len(self.data_list) - 1, idx)) if self.data_list else None
+ snap = {"event": e, "index": idx}
+ if idx is not None and self.data_list:
+ d = self.data_list[idx]
+ snap["record"] = {
+ "time": getattr(d, "time", ""),
+ "trainno": getattr(d, "trainno", ""),
+ "trainspeed": getattr(d, "trainspeed", None),
+ "limitspeed": getattr(d, "limitspeed", None),
+ "dtg": getattr(d, "dtg", None),
+ "atc_status": getattr(d, "atc_status", ""),
+ "atc_code": getattr(d, "atc_code", ""),
+ "tasc": getattr(d, "tasc", False),
+ "tasc_value": getattr(d, "tasc_value", None),
+ "over_spd_warning": getattr(d, "over_spd_warning", False),
+ "door_open": getattr(d, "door_open", False),
+ "door_close": getattr(d, "door_close", False),
+ "psd_open": getattr(d, "psd_open", False),
+ "psd_close": getattr(d, "psd_close", False),
+ "pstn": getattr(d, "pstn", None),
+ "nstn": getattr(d, "nstn", None),
+ "dstn": getattr(d, "dstn", None),
+ }
+ snapshots.append(snap)
+ ctx["station_neighbors"] = snapshots
+ except Exception:
+ pass
+
+ # 신호카드 성격의 "현재 상태 요약"(커서 레코드 + 필터링 값 동시 포함)
+ try:
+ if cursor_index is not None and self.data_list and 0 <= cursor_index < len(self.data_list):
+ d = self.data_list[cursor_index]
+ # filtered(그래프 표시) 값
+ filt = {}
+ try:
+ for k, info in self.signals_map.items():
+ arr = info.get("data") or []
+ if 0 <= cursor_index < len(arr):
+ filt[k] = arr[cursor_index]
+ except Exception:
+ pass
+
+ # all-zero 프레임 여부
+ is_all_zero = None
+ try:
+ if hasattr(self, "_all_zero_mask") and self._all_zero_mask and 0 <= cursor_index < len(self._all_zero_mask):
+ is_all_zero = bool(self._all_zero_mask[cursor_index])
+ except Exception:
+ pass
+
+ ctx["signal_cards_like"] = {
+ "raw": {
+ "speed": getattr(d, "trainspeed", None),
+ "pwm": getattr(d, "pwm_value", None),
+ "dtg": getattr(d, "dtg", None),
+ "tasc_value": getattr(d, "tasc_value", None),
+ "atc_code_val": getattr(d, "atc_code_val", None),
+ "atc_status": getattr(d, "atc_status", ""),
+ "marker": getattr(d, "marker", ""),
+ "doormod": getattr(d, "doormod", ""),
+ "tcmsdoor": getattr(d, "tcmsdoor", ""),
+ "nextdoor": getattr(d, "nextdoor", ""),
+ },
+ "filtered": {
+ "speed": filt.get("speed"),
+ "pwm": filt.get("pwm"),
+ "dtg": filt.get("dtg"),
+ "tasc": filt.get("tasc"),
+ "atc": filt.get("atc"),
+ },
+ "all_zero_frame": is_all_zero,
+ "flags": {
+ "system_active": getattr(d, "system_active", False),
+ "tcr": getattr(d, "tcr", False),
+ "hcr": getattr(d, "hcr", False),
+ "fa": getattr(d, "fa", False),
+ "auto": getattr(d, "auto", False),
+ "mcs": getattr(d, "mcs", False),
+ "yard": getattr(d, "yard", False),
+ "fmc": getattr(d, "fmc", False),
+ "over_spd_warning": getattr(d, "over_spd_warning", False),
+ "twct_enable": getattr(d, "twct_enable", False),
+ "tasc": getattr(d, "tasc", False),
+ "door_open": getattr(d, "door_open", False),
+ "door_close": getattr(d, "door_close", False),
+ "psd_open": getattr(d, "psd_open", False),
+ "psd_close": getattr(d, "psd_close", False),
+ },
+ }
+ except Exception:
+ pass
+ return ctx
+
+if __name__ == "__main__":
+ # 테스트용 더미 데이터
+ class Dummy:
+ def __init__(self, i):
+ import math
+ self.trainspeed = abs(50 + 40 * math.sin(i / 20))
+ self.pwm_value = 50 if (i // 10) % 2 == 0 else 0
+ self.tasc_value = i % 80
+ self.atc_code_val = 75
+ self.dtg = 1000 - i
+
+ self.pstn = 0
+ if 10 <= i < 15: self.pstn = 1
+ elif 30 <= i < 35: self.pstn = 32
+ elif 60 <= i < 80: self.pstn = 134
+
+ self.twct_enable = (60 <= i < 70)
+
+ app = QApplication(sys.argv)
+ w = GraphView(1)
+ w.set_data([Dummy(i) for i in range(5000)])
+ w.resize(1200, 600)
+ w.show()
+ sys.exit(app.exec())
\ No newline at end of file
diff --git a/app/ui/views/log_list_view.py b/app/ui/views/log_list_view.py
new file mode 100644
index 0000000..535d1b1
--- /dev/null
+++ b/app/ui/views/log_list_view.py
@@ -0,0 +1 @@
+# Data Grid View
diff --git a/app/ui/widgets/__init__.py b/app/ui/widgets/__init__.py
new file mode 100644
index 0000000..ab16f8a
--- /dev/null
+++ b/app/ui/widgets/__init__.py
@@ -0,0 +1,5 @@
+from .ai_floating_chat import AIFloatingButton, AIChatDialog, animate_open
+
+__all__ = ["AIFloatingButton", "AIChatDialog", "animate_open"]
+
+
diff --git a/app/ui/widgets/__pycache__/__init__.cpython-311.pyc b/app/ui/widgets/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..dcd46cc
Binary files /dev/null and b/app/ui/widgets/__pycache__/__init__.cpython-311.pyc differ
diff --git a/app/ui/widgets/__pycache__/ai_floating_chat.cpython-311.pyc b/app/ui/widgets/__pycache__/ai_floating_chat.cpython-311.pyc
new file mode 100644
index 0000000..87e8e96
Binary files /dev/null and b/app/ui/widgets/__pycache__/ai_floating_chat.cpython-311.pyc differ
diff --git a/app/ui/widgets/ai_floating_chat.py b/app/ui/widgets/ai_floating_chat.py
new file mode 100644
index 0000000..57e1b3d
--- /dev/null
+++ b/app/ui/widgets/ai_floating_chat.py
@@ -0,0 +1,494 @@
+"""
+플로팅 AI 버튼 + 확장(애니메이션) 채팅 모달
+
+요구사항:
+- 웹의 고객응대/AI assistant처럼 우하단 플로팅 버튼
+- 클릭 시 작은 패널에서 큰 채팅 모달로 확장(애니메이션)
+- 탭 전환 없이 현재 화면 컨텍스트(날짜/열번/편성/호차/타임라인/표시신호/역 전후2개 등) 포함
+"""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QRect, QObject, QThread, Signal, QDateTime
+from PySide6.QtGui import QColor
+from PySide6.QtWidgets import (
+ QWidget,
+ QPushButton,
+ QDialog,
+ QVBoxLayout,
+ QHBoxLayout,
+ QLabel,
+ QComboBox,
+ QLineEdit,
+ QTextEdit,
+ QMessageBox,
+ QFrame,
+ QDialogButtonBox,
+)
+
+from app.ai import get_ai_client
+from app.ai.maintenance_kb import get_maintenance_kb
+from app.ui.dialogs.api_key_dialog import APIKeyDialog
+
+
+def _make_json_safe(obj):
+ """json.dumps에서 안전하도록 dict/list/QColor 등을 변환"""
+ if obj is None:
+ return None
+ if isinstance(obj, dict):
+ return {str(k): _make_json_safe(v) for k, v in obj.items()}
+ if isinstance(obj, (list, tuple, set)):
+ return [_make_json_safe(v) for v in obj]
+ if isinstance(obj, QDateTime):
+ try:
+ return obj.toString("yyyy-MM-dd HH:mm:ss")
+ except Exception:
+ return str(obj)
+ # QColor 등 name()이 있는 객체 처리
+ try:
+ if hasattr(obj, "name") and callable(obj.name):
+ return obj.name()
+ except Exception:
+ pass
+ # Enum 등 value가 있는 경우
+ try:
+ if hasattr(obj, "value"):
+ return getattr(obj, "value")
+ except Exception:
+ pass
+ return obj
+
+
+class _AIWorker(QObject):
+ finished = Signal(str)
+ failed = Signal(str)
+
+ def __init__(self, prompt: str):
+ super().__init__()
+ self.prompt = prompt
+
+ def run(self):
+ try:
+ client = get_ai_client()
+ if not client.is_ready():
+ raise RuntimeError("AI가 준비되지 않았습니다. 설정에서 API 키를 입력하세요.")
+ resp = client.chat(self.prompt, temperature=0.2)
+ self.finished.emit(resp.content or "")
+ except Exception as e:
+ self.failed.emit(str(e))
+
+
+class AIFloatingButton(QPushButton):
+ """우하단 플로팅 버튼"""
+
+ def __init__(self, parent: QWidget):
+ super().__init__("AI", parent)
+ self.setFixedSize(56, 56)
+ self.setCursor(Qt.PointingHandCursor)
+ self.setToolTip("AI Assistant 열기")
+ self.setStyleSheet(
+ """
+ QPushButton {
+ background-color: #0078d4;
+ color: white;
+ border: none;
+ border-radius: 28px;
+ font-weight: bold;
+ font-size: 14px;
+ }
+ QPushButton:hover { background-color: #1084d8; }
+ QPushButton:pressed { background-color: #006cc1; }
+ """
+ )
+ self._is_open = False
+
+ def set_open_state(self, is_open: bool):
+ self._is_open = bool(is_open)
+ if self._is_open:
+ self.setText("×")
+ self.setToolTip("AI Assistant 닫기")
+ self.setStyleSheet(
+ """
+ QPushButton {
+ background-color: #2a2a2a;
+ color: #ddd;
+ border: 1px solid #4a4a4a;
+ border-radius: 28px;
+ font-weight: bold;
+ font-size: 18px;
+ }
+ QPushButton:hover { background-color: #333; }
+ QPushButton:pressed { background-color: #222; }
+ """
+ )
+ else:
+ self.setText("AI")
+ self.setToolTip("AI Assistant 열기")
+ self.setStyleSheet(
+ """
+ QPushButton {
+ background-color: #0078d4;
+ color: white;
+ border: none;
+ border-radius: 28px;
+ font-weight: bold;
+ font-size: 14px;
+ }
+ QPushButton:hover { background-color: #1084d8; }
+ QPushButton:pressed { background-color: #006cc1; }
+ """
+ )
+
+ def move_to_bottom_right(self, margin: int = 18):
+ p = self.parentWidget()
+ if not p:
+ return
+ x = p.width() - self.width() - margin
+ y = p.height() - self.height() - margin
+ self.move(max(0, x), max(0, y))
+
+
+class AIChatDialog(QDialog):
+ """확장되는 채팅 모달(프레임리스)"""
+
+ def __init__(self, main_window: QWidget, get_panel_ctx_callable):
+ super().__init__(main_window)
+ self._get_panel_ctx = get_panel_ctx_callable
+
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
+ self.setModal(False)
+ self.setAttribute(Qt.WA_TranslucentBackground, True)
+
+ self._busy = False
+ self._thread: Optional[QThread] = None
+ self._worker: Optional[_AIWorker] = None
+ self._last_payload: Optional[Dict[str, Any]] = None
+
+ self._build_ui()
+ self.refresh_header()
+
+ def _build_ui(self):
+ root = QVBoxLayout(self)
+ root.setContentsMargins(0, 0, 0, 0)
+
+ # 실제 카드(둥근 배경)
+ self.card = QFrame()
+ self.card.setStyleSheet(
+ """
+ QFrame {
+ background: #1f1f1f;
+ border: 1px solid #3a3a3a;
+ border-radius: 14px;
+ }
+ QLabel { color: #ddd; }
+ QLineEdit, QTextEdit {
+ background: #2a2a2a;
+ border: 1px solid #4a4a4a;
+ border-radius: 8px;
+ padding: 8px;
+ color: #eee;
+ }
+ QTextEdit { font-size: 12px; }
+ """
+ )
+ card_layout = QVBoxLayout(self.card)
+ card_layout.setContentsMargins(12, 12, 12, 12)
+ card_layout.setSpacing(10)
+
+ # 헤더
+ header = QHBoxLayout()
+ self.lbl_title = QLabel("AI Assistant")
+ self.lbl_title.setStyleSheet("font-weight: bold; font-size: 14px; color: #fff;")
+
+ self.cmb_panel = QComboBox()
+ self.cmb_panel.addItem("Left Panel", "left")
+ self.cmb_panel.addItem("Right Panel", "right")
+ self.cmb_panel.currentIndexChanged.connect(self.refresh_header)
+
+ self.lbl_ai_status = QLabel("AI: 미준비")
+ self.lbl_ai_status.setStyleSheet("color: #FFAB00;")
+
+ btn_settings = QPushButton("설정")
+ btn_settings.setFixedHeight(30)
+ btn_settings.clicked.connect(self._open_settings)
+ btn_settings.setStyleSheet(
+ "QPushButton{background:#2a2a2a;color:#ddd;border:1px solid #4a4a4a;border-radius:8px;padding:6px 10px;} "
+ "QPushButton:hover{background:#333;}"
+ )
+
+ btn_close = QPushButton("×")
+ btn_close.setFixedSize(30, 30)
+ btn_close.clicked.connect(self.close)
+ btn_close.setStyleSheet(
+ "QPushButton{background:#2a2a2a;color:#ddd;border:1px solid #4a4a4a;border-radius:8px;font-size:16px;} "
+ "QPushButton:hover{background:#333;}"
+ )
+
+ btn_payload = QPushButton("JSON")
+ btn_payload.setFixedHeight(30)
+ btn_payload.clicked.connect(self.show_last_payload)
+ btn_payload.setStyleSheet(
+ "QPushButton{background:#2a2a2a;color:#ddd;border:1px solid #4a4a4a;border-radius:8px;padding:6px 10px;} "
+ "QPushButton:hover{background:#333;}"
+ )
+
+ header.addWidget(self.lbl_title)
+ header.addStretch()
+ header.addWidget(self.cmb_panel)
+ header.addWidget(self.lbl_ai_status)
+ header.addWidget(btn_payload)
+ header.addWidget(btn_settings)
+ header.addWidget(btn_close)
+ card_layout.addLayout(header)
+
+ # 기본정보(자동입력 + 수정가능)
+ info_row = QHBoxLayout()
+ self.in_date = QLineEdit()
+ self.in_date.setPlaceholderText("날짜")
+ self.in_trainno = QLineEdit()
+ self.in_trainno.setPlaceholderText("열번")
+ self.in_car = QLineEdit()
+ self.in_car.setPlaceholderText("호차")
+ self.in_formation = QLineEdit()
+ self.in_formation.setPlaceholderText("편성")
+ for w in [self.in_date, self.in_trainno, self.in_car, self.in_formation]:
+ w.setFixedHeight(32)
+ info_row.addWidget(QLabel("날짜"))
+ info_row.addWidget(self.in_date)
+ info_row.addWidget(QLabel("열번"))
+ info_row.addWidget(self.in_trainno)
+ info_row.addWidget(QLabel("호차"))
+ info_row.addWidget(self.in_car)
+ info_row.addWidget(QLabel("편성"))
+ info_row.addWidget(self.in_formation)
+ card_layout.addLayout(info_row)
+
+ # 추가정보(가이드/툴팁)
+ self.in_extra = QTextEdit()
+ self.in_extra.setFixedHeight(90)
+ self.in_extra.setPlaceholderText(
+ "추가정보 작성 가이드(필요한 것만 기입)\n"
+ "- 발생역: \n"
+ "- 발생시간: \n"
+ "- 발생증상: \n"
+ "- 발생장치: \n"
+ "- 참고: (정상/고장 비교, 다른 열번 같은 역 비교, 특정 조건 구간 등)\n"
+ )
+ self.in_extra.setToolTip(
+ "예시:\n"
+ "발생역=서면, 발생시간=18:09:12, 발생증상=정위치 불가 추정,\n"
+ "발생장치=ATO/TASC, 참고=타임라인으로 표시한 구간 중심으로 원인 후보 추론"
+ )
+ card_layout.addWidget(QLabel("추가정보"))
+ card_layout.addWidget(self.in_extra)
+
+ # 채팅 히스토리
+ self.txt_chat = QTextEdit()
+ self.txt_chat.setReadOnly(True)
+ self.txt_chat.setPlaceholderText("대화 내용이 여기에 표시됩니다.")
+ self.txt_chat.setMinimumHeight(240)
+ card_layout.addWidget(self.txt_chat, stretch=1)
+
+ # 입력 + 전송
+ bottom = QHBoxLayout()
+ self.in_msg = QLineEdit()
+ self.in_msg.setPlaceholderText("요청을 입력하세요. (예: 유사기록 검색 / 특정 조건 구간 분석 / 역명 기준 비교 등)")
+ self.in_msg.setFixedHeight(34)
+
+ btn_send = QPushButton("전송")
+ btn_send.setFixedHeight(34)
+ btn_send.clicked.connect(self.send_message)
+ btn_send.setStyleSheet(
+ "QPushButton{background:#0078d4;color:white;border:none;border-radius:8px;padding:6px 14px;font-weight:bold;} "
+ "QPushButton:hover{background:#1084d8;} "
+ "QPushButton:pressed{background:#006cc1;}"
+ )
+ bottom.addWidget(self.in_msg, stretch=1)
+ bottom.addWidget(btn_send)
+ card_layout.addLayout(bottom)
+
+ root.addWidget(self.card)
+
+ def _open_settings(self):
+ dlg = APIKeyDialog(self)
+ dlg.settings_changed.connect(self.refresh_header)
+ dlg.exec()
+
+ def refresh_header(self):
+ # AI 상태
+ st = get_ai_client().get_status()
+ if st.get("ready"):
+ self.lbl_ai_status.setText(f"AI: {st.get('provider')} / {st.get('model')}")
+ self.lbl_ai_status.setStyleSheet("color: #00D084;")
+ else:
+ self.lbl_ai_status.setText("AI: 미준비")
+ self.lbl_ai_status.setStyleSheet("color: #FFAB00;")
+
+ # 패널 컨텍스트에서 기본정보 자동 채움(비어있을 때만)
+ ctx = self._get_panel_ctx(self.cmb_panel.currentData(), None)
+ date = (ctx.get("data_date") or "") if isinstance(ctx, dict) else ""
+ record = (ctx.get("record") or {}) if isinstance(ctx, dict) else {}
+ trainno = str(record.get("trainno") or "")
+
+ if date and not self.in_date.text().strip():
+ self.in_date.setText(date)
+ if trainno and not self.in_trainno.text().strip():
+ self.in_trainno.setText(trainno)
+
+ def _append(self, who: str, text: str):
+ self.txt_chat.append(f"[{who}]\n{text}\n")
+ sb = self.txt_chat.verticalScrollBar()
+ sb.setValue(sb.maximum())
+
+ def _build_prompt(self, user_text: str) -> str:
+ panel_id = self.cmb_panel.currentData()
+ ctx = self._get_panel_ctx(panel_id, user_text) or {}
+
+ basic = {
+ "date": self.in_date.text().strip(),
+ "trainno": self.in_trainno.text().strip(),
+ "car_no": self.in_car.text().strip(),
+ "formation": self.in_formation.text().strip(),
+ }
+ extra = (self.in_extra.toPlainText() or "").strip()
+
+ # 정비지침서 발췌(유사기록/조건분석 요청에도 도움)
+ kb = get_maintenance_kb()
+ kb_hits = kb.search(" ".join([user_text, extra]), top_k=3)
+
+ payload = {
+ "basic": basic,
+ "extra": extra,
+ "panel_id": panel_id,
+ "ctx": ctx,
+ "kb_hits": kb_hits,
+ "user_text": user_text,
+ }
+ safe_payload = _make_json_safe(payload)
+ self._last_payload = safe_payload
+ self._auto_save_payload(safe_payload)
+
+ return (
+ "당신은 철도 차량 MMI 로그를 분석하는 AI 어시스턴트입니다.\n"
+ "사용자 요청에 대해, 가능한 경우 데이터 근거(필드명/타임라인/역 전후2개)를 들어 설명하세요.\n"
+ "불확실하면 추가로 필요한 데이터/확인 포인트를 질문 형태로 제시하세요.\n\n"
+ f"[기본정보]\n{json.dumps(_make_json_safe(basic), ensure_ascii=False, indent=2)}\n"
+ f"\n[추가정보]\n{extra}\n"
+ f"\n[현재 화면 컨텍스트]\n{json.dumps(_make_json_safe(ctx), ensure_ascii=False, indent=2)}\n"
+ + (f"\n[정비지침서/지식 발췌]\n{json.dumps(_make_json_safe(kb_hits), ensure_ascii=False, indent=2)}\n" if kb_hits else "")
+ + f"\n[사용자 요청]\n{user_text}\n"
+ )
+
+ def _auto_save_payload(self, payload: Dict[str, Any]):
+ try:
+ out_dir = Path.home() / ".mmi_analyzer" / "ai_debug"
+ out_dir.mkdir(parents=True, exist_ok=True)
+ path = out_dir / "last_payload.json"
+ path.write_text(json.dumps(_make_json_safe(payload), ensure_ascii=False, indent=2), encoding="utf-8")
+ except Exception:
+ pass
+
+ def show_last_payload(self):
+ if not self._last_payload:
+ QMessageBox.information(self, "AI", "아직 전송된 payload가 없습니다.")
+ return
+ dlg = QDialog(self)
+ dlg.setWindowTitle("AI Payload (JSON)")
+ dlg.resize(720, 520)
+ lay = QVBoxLayout(dlg)
+ txt = QTextEdit()
+ txt.setReadOnly(True)
+ txt.setText(json.dumps(_make_json_safe(self._last_payload), ensure_ascii=False, indent=2))
+ lay.addWidget(txt, stretch=1)
+ btns = QDialogButtonBox(QDialogButtonBox.Close)
+ btns.rejected.connect(dlg.reject)
+ btns.accepted.connect(dlg.accept)
+ lay.addWidget(btns)
+ dlg.exec()
+
+ def send_message(self):
+ text = (self.in_msg.text() or "").strip()
+ if not text:
+ return
+ if self._busy:
+ QMessageBox.information(self, "AI", "이미 요청이 실행 중입니다. 완료 후 다시 시도하세요.")
+ return
+
+ self.in_msg.clear()
+ self._append("사용자", text)
+
+ prompt = self._build_prompt(text)
+ self._run(prompt)
+
+ def _run(self, prompt: str):
+ self._busy = True
+ self._append("AI", "(응답 생성 중...)")
+
+ self._thread = QThread(self)
+ self._worker = _AIWorker(prompt)
+ self._worker.moveToThread(self._thread)
+ self._thread.started.connect(self._worker.run)
+ self._worker.finished.connect(self._on_done)
+ self._worker.failed.connect(self._on_fail)
+ self._worker.finished.connect(self._thread.quit)
+ self._worker.failed.connect(self._thread.quit)
+ self._thread.finished.connect(self._on_thread_finished)
+ self._thread.start()
+
+ def _on_done(self, text: str):
+ # 마지막 "(응답 생성 중...)" 줄은 단순히 구분만 하고 새 답변을 추가
+ self._append("AI", text.strip())
+
+ def _on_fail(self, err: str):
+ self._append("AI", f"[오류] {err}")
+
+ def _on_thread_finished(self):
+ self._thread = None
+ self._worker = None
+ self._busy = False
+
+
+def animate_open(dialog: QDialog, start_rect: QRect, end_rect: QRect, duration_ms: int = 220):
+ dialog.setGeometry(start_rect)
+ dialog.setWindowOpacity(0.0)
+ dialog.show()
+ anim = QPropertyAnimation(dialog, b"geometry", dialog)
+ anim.setDuration(duration_ms)
+ anim.setStartValue(start_rect)
+ anim.setEndValue(end_rect)
+ anim.setEasingCurve(QEasingCurve.OutCubic)
+ anim.start()
+ dialog._open_anim = anim # GC 방지
+
+ fade = QPropertyAnimation(dialog, b"windowOpacity", dialog)
+ fade.setDuration(max(120, duration_ms))
+ fade.setStartValue(0.0)
+ fade.setEndValue(1.0)
+ fade.setEasingCurve(QEasingCurve.OutCubic)
+ fade.start()
+ dialog._open_fade = fade
+
+
+def animate_close(dialog: QDialog, start_rect: QRect, end_rect: QRect, duration_ms: int = 180):
+ anim = QPropertyAnimation(dialog, b"geometry", dialog)
+ anim.setDuration(duration_ms)
+ anim.setStartValue(start_rect)
+ anim.setEndValue(end_rect)
+ anim.setEasingCurve(QEasingCurve.InCubic)
+ anim.finished.connect(dialog.close)
+ anim.start()
+ dialog._close_anim = anim
+
+ fade = QPropertyAnimation(dialog, b"windowOpacity", dialog)
+ fade.setDuration(max(100, duration_ms))
+ fade.setStartValue(dialog.windowOpacity())
+ fade.setEndValue(0.0)
+ fade.setEasingCurve(QEasingCurve.InCubic)
+ fade.start()
+ dialog._close_fade = fade
+
+
diff --git a/c_Sharp_Code/doordirection.cs b/c_Sharp_Code/doordirection.cs
new file mode 100644
index 0000000..d521e55
--- /dev/null
+++ b/c_Sharp_Code/doordirection.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace SL200_RTLogViewer
+{
+ // Token: 0x02000006 RID: 6
+ public enum DoorDirection
+ {
+ // Token: 0x0400001E RID: 30
+ DE,
+ // Token: 0x0400001F RID: 31
+ DW
+ }
+}
diff --git a/c_Sharp_Code/dpi.cs b/c_Sharp_Code/dpi.cs
new file mode 100644
index 0000000..14c376a
--- /dev/null
+++ b/c_Sharp_Code/dpi.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace SL200_RTLogViewer
+{
+ // Token: 0x02000005 RID: 5
+ public static class Dpi
+ {
+ // Token: 0x06000021 RID: 33
+ [DllImport("Shcore.dll")]
+ public static extern int SetProcessDpiAwareness(int processDpiAwareness);
+
+ // Token: 0x02000055 RID: 85
+ public enum DpiAwareness
+ {
+ // Token: 0x0400041C RID: 1052
+ None,
+ // Token: 0x0400041D RID: 1053
+ SystemAware,
+ // Token: 0x0400041E RID: 1054
+ PerMonitorAware
+ }
+ }
+}
diff --git a/c_Sharp_Code/form1.cs b/c_Sharp_Code/form1.cs
new file mode 100644
index 0000000..b730735
--- /dev/null
+++ b/c_Sharp_Code/form1.cs
@@ -0,0 +1,301 @@
+using System;
+using System.ComponentModel;
+using System.Drawing;
+using System.Windows.Forms;
+using Microsoft.Win32;
+using SL200_RealtimeLogViewer.RealtimeLog;
+using SL200_RTLogViewer.Properties;
+using SL200_RTLogViewer.subForm;
+
+namespace SL200_RTLogViewer
+{
+ // Token: 0x02000003 RID: 3
+ public class Form1 : Form
+ {
+ // Token: 0x06000014 RID: 20 RVA: 0x00003A80 File Offset: 0x00001C80
+ public Form1()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x06000015 RID: 21 RVA: 0x00003AB0 File Offset: 0x00001CB0
+ private void button1_Click_1(object sender, EventArgs e)
+ {
+ try
+ {
+ RealtimelogForm realtimelogForm = new RealtimelogForm();
+ Form1.logOn = false;
+ realtimelogForm.FormBorderStyle = FormBorderStyle.None;
+ realtimelogForm.TopLevel = false;
+ realtimelogForm.TopMost = true;
+ this.mainpanel.Controls.Add(realtimelogForm);
+ realtimelogForm.Dock = DockStyle.Fill;
+ this.ShowChildForm(realtimelogForm);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("실시간 뷰어 기능을 사용하려면 프로그램 폴더내에 \n WinPcap을 설치해야 합니다. ", " 알림 ");
+ }
+ }
+
+ // Token: 0x06000016 RID: 22 RVA: 0x00003B30 File Offset: 0x00001D30
+ private void button2_Click(object sender, EventArgs e)
+ {
+ Form1.logOn = true;
+ this.savedlogForm.FormBorderStyle = FormBorderStyle.None;
+ this.savedlogForm.TopLevel = false;
+ this.savedlogForm.TopMost = true;
+ this.mainpanel.Controls.Add(this.savedlogForm);
+ this.savedlogForm.Dock = DockStyle.Fill;
+ this.ShowChildForm(this.savedlogForm);
+ }
+
+ // Token: 0x06000017 RID: 23 RVA: 0x00003B9C File Offset: 0x00001D9C
+ private void ShowChildForm(Form childForm)
+ {
+ bool flag = childForm.Equals(this._activeForm);
+ if (!flag)
+ {
+ bool flag2 = this._activeForm != null;
+ if (flag2)
+ {
+ this._activeForm.Hide();
+ }
+ this._activeForm = childForm;
+ this._activeForm.Show();
+ }
+ }
+
+ // Token: 0x06000018 RID: 24 RVA: 0x00003BE8 File Offset: 0x00001DE8
+ private void Form1_FormClosed(object sender, FormClosedEventArgs e)
+ {
+ this.savedlogForm.Close();
+ base.Dispose();
+ }
+
+ // Token: 0x06000019 RID: 25 RVA: 0x00003BFE File Offset: 0x00001DFE
+ private void Form1_Load(object sender, EventArgs e)
+ {
+ TrackInfo.SetInfo();
+ this.button2_Click(sender, e);
+ }
+
+ // Token: 0x0600001A RID: 26 RVA: 0x00003C10 File Offset: 0x00001E10
+ public void ReSetFontSize(Control.ControlCollection ctrls)
+ {
+ float num = 1f;
+ using (RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop\\WindowMetrics"))
+ {
+ bool flag = registryKey != null;
+ if (flag)
+ {
+ object value = registryKey.GetValue("AppliedDPI");
+ bool flag2 = value != null;
+ if (flag2)
+ {
+ int num2 = (int)value;
+ num = (float)num2 / 96f;
+ }
+ }
+ }
+ bool flag3 = (double)num == 1.25;
+ if (flag3)
+ {
+ this.SetAllControlsFontSize(base.Controls, 1.27f);
+ }
+ }
+
+ // Token: 0x0600001B RID: 27 RVA: 0x00003CB0 File Offset: 0x00001EB0
+ private void SetAllControlsFontSize(Control.ControlCollection ctrls, float scale)
+ {
+ foreach (object obj in ctrls)
+ {
+ Control control = (Control)obj;
+ bool flag = control.Controls != null;
+ if (flag)
+ {
+ this.SetAllControlsFontSize(control.Controls, scale);
+ }
+ bool flag2 = control != null;
+ if (flag2)
+ {
+ Font font = control.Font;
+ control.Font = new Font(font.Name, font.Size / scale);
+ }
+ }
+ }
+
+ // Token: 0x0600001C RID: 28 RVA: 0x00003D50 File Offset: 0x00001F50
+ private void button3_Click(object sender, EventArgs e)
+ {
+ this.mmiMsgForm.FormBorderStyle = FormBorderStyle.None;
+ this.mmiMsgForm.TopLevel = false;
+ this.mmiMsgForm.TopMost = true;
+ this.mainpanel.Controls.Add(this.mmiMsgForm);
+ this.mmiMsgForm.Dock = DockStyle.Fill;
+ this.ShowChildForm(this.mmiMsgForm);
+ }
+
+ // Token: 0x0600001D RID: 29 RVA: 0x00003DB8 File Offset: 0x00001FB8
+ private void button4_Click(object sender, EventArgs e)
+ {
+ TermsDefinitionForm termsDefinitionForm = new TermsDefinitionForm();
+ termsDefinitionForm.Show();
+ }
+
+ // Token: 0x0600001E RID: 30 RVA: 0x00003DD4 File Offset: 0x00001FD4
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x0600001F RID: 31 RVA: 0x00003E0C File Offset: 0x0000200C
+ private void InitializeComponent()
+ {
+ ComponentResourceManager componentResourceManager = new ComponentResourceManager(typeof(Form1));
+ this.panel1 = new Panel();
+ this.button4 = new Button();
+ this.button3 = new Button();
+ this.button1 = new Button();
+ this.button2 = new Button();
+ this.mainpanel = new Panel();
+ this.panel1.SuspendLayout();
+ base.SuspendLayout();
+ this.panel1.AutoSize = true;
+ this.panel1.Controls.Add(this.button4);
+ this.panel1.Controls.Add(this.button3);
+ this.panel1.Controls.Add(this.button1);
+ this.panel1.Controls.Add(this.button2);
+ this.panel1.Dock = DockStyle.Top;
+ this.panel1.Location = new Point(0, 0);
+ this.panel1.Margin = new Padding(3, 4, 3, 4);
+ this.panel1.Name = "panel1";
+ this.panel1.Size = new Size(1530, 70);
+ this.panel1.TabIndex = 2;
+ this.button4.AutoSize = true;
+ this.button4.BackColor = Color.Transparent;
+ this.button4.Font = new Font("나눔고딕 ExtraBold", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button4.Image = (Image)componentResourceManager.GetObject("button4.Image");
+ this.button4.ImageAlign = ContentAlignment.MiddleLeft;
+ this.button4.Location = new Point(591, 4);
+ this.button4.Margin = new Padding(3, 4, 3, 4);
+ this.button4.Name = "button4";
+ this.button4.Size = new Size(168, 61);
+ this.button4.TabIndex = 4;
+ this.button4.Text = "약어 설명";
+ this.button4.TextAlign = ContentAlignment.MiddleRight;
+ this.button4.UseVisualStyleBackColor = false;
+ this.button4.Click += this.button4_Click;
+ this.button3.AutoSize = true;
+ this.button3.BackColor = Color.Transparent;
+ this.button3.Font = new Font("나눔고딕 ExtraBold", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button3.Image = (Image)componentResourceManager.GetObject("button3.Image");
+ this.button3.ImageAlign = ContentAlignment.MiddleLeft;
+ this.button3.Location = new Point(396, 4);
+ this.button3.Margin = new Padding(3, 4, 3, 4);
+ this.button3.Name = "button3";
+ this.button3.Size = new Size(192, 61);
+ this.button3.TabIndex = 3;
+ this.button3.Text = "메세지 뷰어";
+ this.button3.TextAlign = ContentAlignment.MiddleRight;
+ this.button3.UseVisualStyleBackColor = false;
+ this.button3.Click += this.button3_Click;
+ this.button1.AutoSize = true;
+ this.button1.BackColor = Color.Transparent;
+ this.button1.Font = new Font("맑은 고딕", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button1.Image = Resources.realtime;
+ this.button1.ImageAlign = ContentAlignment.MiddleLeft;
+ this.button1.Location = new Point(3, 4);
+ this.button1.Margin = new Padding(3, 4, 3, 4);
+ this.button1.Name = "button1";
+ this.button1.Size = new Size(195, 62);
+ this.button1.TabIndex = 1;
+ this.button1.Text = "실시간 뷰어";
+ this.button1.TextAlign = ContentAlignment.MiddleRight;
+ this.button1.UseVisualStyleBackColor = false;
+ this.button1.Click += this.button1_Click_1;
+ this.button2.AutoSize = true;
+ this.button2.BackColor = Color.Transparent;
+ this.button2.Font = new Font("맑은 고딕", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button2.Image = Resources.saved;
+ this.button2.ImageAlign = ContentAlignment.MiddleLeft;
+ this.button2.Location = new Point(201, 4);
+ this.button2.Margin = new Padding(3, 4, 3, 4);
+ this.button2.Name = "button2";
+ this.button2.Size = new Size(192, 61);
+ this.button2.TabIndex = 2;
+ this.button2.Text = "LOG 뷰어";
+ this.button2.TextAlign = ContentAlignment.MiddleRight;
+ this.button2.UseVisualStyleBackColor = false;
+ this.button2.Click += this.button2_Click;
+ this.mainpanel.AutoSize = true;
+ this.mainpanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ this.mainpanel.Dock = DockStyle.Fill;
+ this.mainpanel.Location = new Point(0, 70);
+ this.mainpanel.Margin = new Padding(3, 4, 3, 4);
+ this.mainpanel.Name = "mainpanel";
+ this.mainpanel.Size = new Size(1530, 852);
+ this.mainpanel.TabIndex = 0;
+ base.AutoScaleMode = AutoScaleMode.None;
+ this.AutoScroll = true;
+ base.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ base.ClientSize = new Size(1530, 922);
+ base.Controls.Add(this.mainpanel);
+ base.Controls.Add(this.panel1);
+ base.FormBorderStyle = FormBorderStyle.Fixed3D;
+ base.Icon = (Icon)componentResourceManager.GetObject("$this.Icon");
+ base.Margin = new Padding(3, 4, 3, 4);
+ base.MaximizeBox = false;
+ this.MaximumSize = new Size(1550, 965);
+ this.MinimumSize = new Size(1550, 965);
+ base.Name = "Form1";
+ base.SizeGripStyle = SizeGripStyle.Hide;
+ base.StartPosition = FormStartPosition.CenterScreen;
+ this.Text = "SL200_LogViewer Ver 1.0.1_25.10";
+ base.FormClosed += this.Form1_FormClosed;
+ base.Load += this.Form1_Load;
+ this.panel1.ResumeLayout(false);
+ this.panel1.PerformLayout();
+ base.ResumeLayout(false);
+ base.PerformLayout();
+ }
+
+ // Token: 0x04000012 RID: 18
+ private SavedlogForm savedlogForm = new SavedlogForm();
+
+ // Token: 0x04000013 RID: 19
+ private mmiMsgForm mmiMsgForm = new mmiMsgForm();
+
+ // Token: 0x04000014 RID: 20
+ private Form _activeForm;
+
+ // Token: 0x04000015 RID: 21
+ public static bool logOn;
+
+ // Token: 0x04000016 RID: 22
+ private IContainer components = null;
+
+ // Token: 0x04000017 RID: 23
+ private Button button1;
+
+ // Token: 0x04000018 RID: 24
+ private Panel panel1;
+
+ // Token: 0x04000019 RID: 25
+ private Button button2;
+
+ // Token: 0x0400001A RID: 26
+ private Panel mainpanel;
+
+ // Token: 0x0400001B RID: 27
+ private Button button3;
+
+ // Token: 0x0400001C RID: 28
+ private Button button4;
+ }
+}
diff --git a/c_Sharp_Code/lib/acpu200.cs b/c_Sharp_Code/lib/acpu200.cs
new file mode 100644
index 0000000..6a1e26c
--- /dev/null
+++ b/c_Sharp_Code/lib/acpu200.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000021 RID: 33
+ public class ACPU200
+ {
+ // Token: 0x06000139 RID: 313 RVA: 0x000240A0 File Offset: 0x000222A0
+ public static ACPU200Class SetData(byte[] data)
+ {
+ return new ACPU200Class
+ {
+ seq = Convert.ToInt32(data[0]),
+ ver = Convert.ToInt32((int)(data[1] & 252)),
+ boardStat = Convert.ToInt32((int)(data[1] & 3)),
+ source = Convert.ToInt32((int)(data[2] & 240)),
+ destination = Convert.ToInt32((int)(data[2] & 15)),
+ pwm = Convert.ToInt32(data[3]),
+ dr = ((data[4] & 128) > 0),
+ br = ((data[4] & 64) > 0),
+ cs = ((data[4] & 32) > 0),
+ ador = ((data[4] & 16) > 0),
+ adol = ((data[4] & 8) > 0),
+ adc = ((data[4] & 4) > 0),
+ recovery = ((data[4] & 2) > 0),
+ nomal = ((data[4] & 1) > 0),
+ aa = ((data[5] & 128) > 0),
+ am = ((data[5] & 64) > 0),
+ mm = ((data[5] & 32) > 0),
+ ats = ((data[5] & 16) > 0),
+ pgx = ((data[5] & 8) > 0),
+ pg32 = ((data[5] & 4) > 0),
+ pg2 = ((data[5] & 2) > 0),
+ pg1 = ((data[5] & 1) > 0),
+ distance = Convert.ToDouble((int)data[6] << 8 | (int)data[7])
+ };
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/acpu200class.cs b/c_Sharp_Code/lib/acpu200class.cs
new file mode 100644
index 0000000..84f6f3f
--- /dev/null
+++ b/c_Sharp_Code/lib/acpu200class.cs
@@ -0,0 +1,77 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000020 RID: 32
+ public class ACPU200Class
+ {
+ // Token: 0x04000213 RID: 531
+ public int seq;
+
+ // Token: 0x04000214 RID: 532
+ public int ver;
+
+ // Token: 0x04000215 RID: 533
+ public int boardStat;
+
+ // Token: 0x04000216 RID: 534
+ public int source;
+
+ // Token: 0x04000217 RID: 535
+ public int destination;
+
+ // Token: 0x04000218 RID: 536
+ public int pwm;
+
+ // Token: 0x04000219 RID: 537
+ public bool dr;
+
+ // Token: 0x0400021A RID: 538
+ public bool br;
+
+ // Token: 0x0400021B RID: 539
+ public bool cs;
+
+ // Token: 0x0400021C RID: 540
+ public bool adol;
+
+ // Token: 0x0400021D RID: 541
+ public bool ador;
+
+ // Token: 0x0400021E RID: 542
+ public bool adc;
+
+ // Token: 0x0400021F RID: 543
+ public bool recovery;
+
+ // Token: 0x04000220 RID: 544
+ public bool nomal;
+
+ // Token: 0x04000221 RID: 545
+ public bool aa;
+
+ // Token: 0x04000222 RID: 546
+ public bool am;
+
+ // Token: 0x04000223 RID: 547
+ public bool mm;
+
+ // Token: 0x04000224 RID: 548
+ public bool ats;
+
+ // Token: 0x04000225 RID: 549
+ public bool pgx;
+
+ // Token: 0x04000226 RID: 550
+ public bool pg32;
+
+ // Token: 0x04000227 RID: 551
+ public bool pg2;
+
+ // Token: 0x04000228 RID: 552
+ public bool pg1;
+
+ // Token: 0x04000229 RID: 553
+ public double distance;
+ }
+}
diff --git a/c_Sharp_Code/lib/acpu201.cs b/c_Sharp_Code/lib/acpu201.cs
new file mode 100644
index 0000000..ba3ca6c
--- /dev/null
+++ b/c_Sharp_Code/lib/acpu201.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000023 RID: 35
+ public class ACPU201
+ {
+ // Token: 0x0600013C RID: 316 RVA: 0x00024274 File Offset: 0x00022474
+ public static ACPU201Class SetData(byte[] data)
+ {
+ return new ACPU201Class
+ {
+ seq = Convert.ToInt32(data[0]),
+ atolimit = Convert.ToInt32(data[1]),
+ ebreq = ((data[2] & 128) > 0),
+ stationvalid = ((data[2] & 32) > 0),
+ dcwvalid = ((data[2] & 16) > 0),
+ dcw = ((data[2] & 8) > 0),
+ wrongdoor = ((data[2] & 4) > 0),
+ nextStationDr = ((data[2] & 2) > 0),
+ prestation = Convert.ToInt32(data[3]),
+ nextstation = Convert.ToInt32(data[4]),
+ deststation = Convert.ToInt32(data[5]),
+ trainnumber = Convert.ToString((int)data[6] << 8 | (int)data[7], 16)
+ };
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/acpu201class.cs b/c_Sharp_Code/lib/acpu201class.cs
new file mode 100644
index 0000000..4f841a7
--- /dev/null
+++ b/c_Sharp_Code/lib/acpu201class.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000022 RID: 34
+ public class ACPU201Class
+ {
+ // Token: 0x0400022A RID: 554
+ public int seq;
+
+ // Token: 0x0400022B RID: 555
+ public int atolimit;
+
+ // Token: 0x0400022C RID: 556
+ public bool ebreq;
+
+ // Token: 0x0400022D RID: 557
+ public bool stationvalid;
+
+ // Token: 0x0400022E RID: 558
+ public bool dcwvalid;
+
+ // Token: 0x0400022F RID: 559
+ public bool dcw;
+
+ // Token: 0x04000230 RID: 560
+ public bool wrongdoor;
+
+ // Token: 0x04000231 RID: 561
+ public bool nextStationDr;
+
+ // Token: 0x04000232 RID: 562
+ public int prestation;
+
+ // Token: 0x04000233 RID: 563
+ public int nextstation;
+
+ // Token: 0x04000234 RID: 564
+ public int deststation;
+
+ // Token: 0x04000235 RID: 565
+ public string trainnumber;
+ }
+}
diff --git a/c_Sharp_Code/lib/acpu202.cs b/c_Sharp_Code/lib/acpu202.cs
new file mode 100644
index 0000000..a282285
--- /dev/null
+++ b/c_Sharp_Code/lib/acpu202.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000025 RID: 37
+ public class ACPU202
+ {
+ // Token: 0x0600013F RID: 319 RVA: 0x00024370 File Offset: 0x00022570
+ public static ACPU202Class SetData(byte[] data)
+ {
+ ACPU202Class acpu202Class = new ACPU202Class();
+ acpu202Class.seq = Convert.ToInt32(data[0]);
+ acpu202Class.sel = ((data[1] & 64) > 0);
+ acpu202Class.kur = ((data[1] & 32) > 0);
+ acpu202Class.osc = ((data[1] & 16) > 0);
+ acpu202Class.twctxe = ((data[1] & 8) > 0);
+ acpu202Class.inching = ((data[1] & 4) > 0);
+ acpu202Class.tasc = ((data[1] & 2) > 0);
+ acpu202Class.trainberth = ((data[1] & 1) > 0);
+ acpu202Class.rlyfbe = ((data[2] & 128) > 0);
+ acpu202Class.drcle = ((data[2] & 64) > 0);
+ acpu202Class.drope = ((data[2] & 32) > 0);
+ acpu202Class.stncodee = ((data[2] & 16) > 0);
+ acpu202Class.majorover = ((data[2] & 8) > 0);
+ acpu202Class.minorover = ((data[2] & 4) > 0);
+ acpu202Class.majorbelow = ((data[2] & 2) > 0);
+ acpu202Class.minorbelow = ((data[2] & 1) > 0);
+ acpu202Class.atcdxl = ((data[3] & 128) > 0);
+ acpu202Class.atcar = ((data[3] & 64) > 0);
+ acpu202Class.prebr = ((data[3] & 128) > 0);
+ acpu202Class.limitdr = ((data[3] & 64) > 0);
+ acpu202Class.tascdb = ((data[3] & 32) > 0);
+ acpu202Class.pg32miss = ((data[3] & 16) > 0);
+ acpu202Class.pg2miss = ((data[3] & 8) > 0);
+ acpu202Class.pg1miss = ((data[3] & 4) > 0);
+ acpu202Class.atoante = ((data[3] & 2) > 0);
+ acpu202Class.atostope = ((data[3] & 1) > 0);
+ bool tasc = acpu202Class.tasc;
+ if (tasc)
+ {
+ acpu202Class.dtg = Convert.ToDouble((int)data[4] << 8 | (int)data[5]) / 100.0;
+ }
+ else
+ {
+ acpu202Class.dtg = Convert.ToDouble((int)data[4] << 8 | (int)data[5]) / 10.0;
+ }
+ acpu202Class.liveoscf = Convert.ToDouble((int)(data[6] ^ 99) << 8 | (int)(data[7] ^ 100)) / 10.0;
+ return acpu202Class;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/acpu202class.cs b/c_Sharp_Code/lib/acpu202class.cs
new file mode 100644
index 0000000..d90c861
--- /dev/null
+++ b/c_Sharp_Code/lib/acpu202class.cs
@@ -0,0 +1,92 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000024 RID: 36
+ public class ACPU202Class
+ {
+ // Token: 0x04000236 RID: 566
+ public int seq;
+
+ // Token: 0x04000237 RID: 567
+ public bool sel;
+
+ // Token: 0x04000238 RID: 568
+ public bool kur;
+
+ // Token: 0x04000239 RID: 569
+ public bool osc;
+
+ // Token: 0x0400023A RID: 570
+ public bool twctxe;
+
+ // Token: 0x0400023B RID: 571
+ public bool inching;
+
+ // Token: 0x0400023C RID: 572
+ public bool tasc;
+
+ // Token: 0x0400023D RID: 573
+ public bool trainberth;
+
+ // Token: 0x0400023E RID: 574
+ public bool rlyfbe;
+
+ // Token: 0x0400023F RID: 575
+ public bool drcle;
+
+ // Token: 0x04000240 RID: 576
+ public bool drope;
+
+ // Token: 0x04000241 RID: 577
+ public bool stncodee;
+
+ // Token: 0x04000242 RID: 578
+ public bool majorover;
+
+ // Token: 0x04000243 RID: 579
+ public bool minorover;
+
+ // Token: 0x04000244 RID: 580
+ public bool majorbelow;
+
+ // Token: 0x04000245 RID: 581
+ public bool minorbelow;
+
+ // Token: 0x04000246 RID: 582
+ public bool atcdxl;
+
+ // Token: 0x04000247 RID: 583
+ public bool atcar;
+
+ // Token: 0x04000248 RID: 584
+ public bool prebr;
+
+ // Token: 0x04000249 RID: 585
+ public bool limitdr;
+
+ // Token: 0x0400024A RID: 586
+ public bool tascdb;
+
+ // Token: 0x0400024B RID: 587
+ public bool pg32miss;
+
+ // Token: 0x0400024C RID: 588
+ public bool pg2miss;
+
+ // Token: 0x0400024D RID: 589
+ public bool pg1miss;
+
+ // Token: 0x0400024E RID: 590
+ public bool atoante;
+
+ // Token: 0x0400024F RID: 591
+ public bool atostope;
+
+ // Token: 0x04000250 RID: 592
+ public double dtg;
+
+ // Token: 0x04000251 RID: 593
+ public double liveoscf;
+ }
+}
diff --git a/c_Sharp_Code/lib/atcr250.cs b/c_Sharp_Code/lib/atcr250.cs
new file mode 100644
index 0000000..dc3e834
--- /dev/null
+++ b/c_Sharp_Code/lib/atcr250.cs
@@ -0,0 +1,216 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000027 RID: 39
+ public class ATCR250
+ {
+ // Token: 0x06000142 RID: 322 RVA: 0x000245F0 File Offset: 0x000227F0
+ public static ATCR250Class SetData(byte[] data)
+ {
+ ATCR250Class atcr250Class = new ATCR250Class();
+ atcr250Class.seq = Convert.ToInt32(data[0]);
+ atcr250Class.swver = string.Format("{0}.{1}", (data[1] & 192) >> 6, (data[1] & 28) >> 2);
+ atcr250Class.boardstat = Convert.ToInt32((int)(data[1] & 3));
+ bool flag = (data[2] & 240) == 0;
+ if (flag)
+ {
+ atcr250Class.atcmodfq = 0;
+ }
+ else
+ {
+ bool flag2 = (data[2] & 240) == 16;
+ if (flag2)
+ {
+ atcr250Class.atcmodfq = 38;
+ }
+ else
+ {
+ bool flag3 = (data[2] & 240) == 32;
+ if (flag3)
+ {
+ atcr250Class.atcmodfq = 34;
+ }
+ else
+ {
+ bool flag4 = (data[2] & 240) == 48;
+ if (flag4)
+ {
+ atcr250Class.atcmodfq = 22;
+ }
+ else
+ {
+ bool flag5 = (data[2] & 240) == 64;
+ if (flag5)
+ {
+ atcr250Class.atcmodfq = 18;
+ }
+ else
+ {
+ bool flag6 = (data[2] & 240) == 80;
+ if (flag6)
+ {
+ atcr250Class.atcmodfq = 16;
+ }
+ else
+ {
+ bool flag7 = (data[2] & 240) == 96;
+ if (flag7)
+ {
+ atcr250Class.atcmodfq = 14;
+ }
+ else
+ {
+ bool flag8 = (data[2] & 240) == 112;
+ if (flag8)
+ {
+ atcr250Class.atcmodfq = 100;
+ }
+ else
+ {
+ bool flag9 = (data[2] & 240) == 128;
+ if (flag9)
+ {
+ atcr250Class.atcmodfq = 12;
+ }
+ else
+ {
+ bool flag10 = (data[2] & 240) == 144;
+ if (flag10)
+ {
+ atcr250Class.atcmodfq = 10;
+ }
+ else
+ {
+ bool flag11 = (data[2] & 240) == 160;
+ if (flag11)
+ {
+ atcr250Class.atcmodfq = 26;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ bool flag12 = (data[2] & 15) == 0;
+ if (flag12)
+ {
+ atcr250Class.atccode = "02";
+ }
+ else
+ {
+ bool flag13 = (data[2] & 15) == 1;
+ if (flag13)
+ {
+ atcr250Class.atccode = "01";
+ }
+ else
+ {
+ bool flag14 = (data[2] & 15) == 2;
+ if (flag14)
+ {
+ atcr250Class.atccode = "25";
+ }
+ else
+ {
+ bool flag15 = (data[2] & 15) == 3;
+ if (flag15)
+ {
+ atcr250Class.atccode = "40";
+ }
+ else
+ {
+ bool flag16 = (data[2] & 15) == 4;
+ if (flag16)
+ {
+ atcr250Class.atccode = "55";
+ }
+ else
+ {
+ bool flag17 = (data[2] & 15) == 5;
+ if (flag17)
+ {
+ atcr250Class.atccode = "65";
+ }
+ else
+ {
+ bool flag18 = (data[2] & 15) == 6;
+ if (flag18)
+ {
+ atcr250Class.atccode = "75";
+ }
+ else
+ {
+ bool flag19 = (data[2] & 15) == 7;
+ if (flag19)
+ {
+ atcr250Class.atccode = "DE";
+ }
+ else
+ {
+ bool flag20 = (data[2] & 15) == 8;
+ if (flag20)
+ {
+ atcr250Class.atccode = "DW";
+ }
+ else
+ {
+ atcr250Class.atccode = "-";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ atcr250Class.pdtprogress = ((data[3] & 128) > 0);
+ atcr250Class.pdtng = ((data[3] & 64) > 0);
+ atcr250Class.pdtok = ((data[3] & 32) > 0);
+ bool flag21 = (data[3] & 7) == 0;
+ if (flag21)
+ {
+ atcr250Class.atcfofq = 0;
+ }
+ else
+ {
+ bool flag22 = (data[3] & 7) == 1;
+ if (flag22)
+ {
+ atcr250Class.atcfofq = 8460;
+ }
+ else
+ {
+ bool flag23 = (data[3] & 7) == 2;
+ if (flag23)
+ {
+ atcr250Class.atcfofq = 9180;
+ }
+ else
+ {
+ bool flag24 = (data[3] & 7) == 3;
+ if (flag24)
+ {
+ atcr250Class.atcfofq = 10260;
+ }
+ else
+ {
+ bool flag25 = (data[3] & 7) == 4;
+ if (flag25)
+ {
+ atcr250Class.atcfofq = 10980;
+ }
+ }
+ }
+ }
+ }
+ return atcr250Class;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/atcr250class.cs b/c_Sharp_Code/lib/atcr250class.cs
new file mode 100644
index 0000000..a187b17
--- /dev/null
+++ b/c_Sharp_Code/lib/atcr250class.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000026 RID: 38
+ public class ATCR250Class
+ {
+ // Token: 0x04000252 RID: 594
+ public int seq;
+
+ // Token: 0x04000253 RID: 595
+ public string swver;
+
+ // Token: 0x04000254 RID: 596
+ public int boardstat;
+
+ // Token: 0x04000255 RID: 597
+ public int atcmodfq;
+
+ // Token: 0x04000256 RID: 598
+ public string atccode;
+
+ // Token: 0x04000257 RID: 599
+ public bool pdtprogress;
+
+ // Token: 0x04000258 RID: 600
+ public bool pdtng;
+
+ // Token: 0x04000259 RID: 601
+ public bool pdtok;
+
+ // Token: 0x0400025A RID: 602
+ public int atcfofq;
+ }
+}
diff --git a/c_Sharp_Code/lib/atcr501.cs b/c_Sharp_Code/lib/atcr501.cs
new file mode 100644
index 0000000..7d22484
--- /dev/null
+++ b/c_Sharp_Code/lib/atcr501.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000029 RID: 41
+ public class ATCR501
+ {
+ // Token: 0x06000145 RID: 325 RVA: 0x00024988 File Offset: 0x00022B88
+ public static ATCR501Class SetData(byte[] data)
+ {
+ return new ATCR501Class
+ {
+ seq = Convert.ToInt32(data[0]),
+ tc2 = ((data[1] & 128) > 0),
+ tc1 = ((data[1] & 64) > 0),
+ active = ((data[1] & 32) > 0),
+ standby = ((data[1] & 16) > 0),
+ hcr = ((data[1] & 8) > 0),
+ manualpdt = ((data[1] & 1) > 0),
+ mpseqn = Convert.ToInt32(data[2])
+ };
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/atcr501class.cs b/c_Sharp_Code/lib/atcr501class.cs
new file mode 100644
index 0000000..44175cc
--- /dev/null
+++ b/c_Sharp_Code/lib/atcr501class.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000028 RID: 40
+ public class ATCR501Class
+ {
+ // Token: 0x0400025B RID: 603
+ public int seq;
+
+ // Token: 0x0400025C RID: 604
+ public bool tc2;
+
+ // Token: 0x0400025D RID: 605
+ public bool tc1;
+
+ // Token: 0x0400025E RID: 606
+ public bool active;
+
+ // Token: 0x0400025F RID: 607
+ public bool standby;
+
+ // Token: 0x04000260 RID: 608
+ public bool hcr;
+
+ // Token: 0x04000261 RID: 609
+ public bool manualpdt;
+
+ // Token: 0x04000262 RID: 610
+ public int mpseqn;
+ }
+}
diff --git a/c_Sharp_Code/lib/ator160.cs b/c_Sharp_Code/lib/ator160.cs
new file mode 100644
index 0000000..8eee06d
--- /dev/null
+++ b/c_Sharp_Code/lib/ator160.cs
@@ -0,0 +1,80 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200002B RID: 43
+ public class ATOR160
+ {
+ // Token: 0x06000148 RID: 328 RVA: 0x00024A44 File Offset: 0x00022C44
+ public static ATOR160Class SetData(byte[] data)
+ {
+ ATOR160Class ator160Class = new ATOR160Class();
+ ator160Class.seq = Convert.ToInt32(data[0]);
+ ator160Class.swVer = Convert.ToInt32((int)(data[1] & 252));
+ ator160Class.bdStat = Convert.ToInt32((int)(data[1] & 3));
+ bool flag = (data[2] & 7) == 0;
+ if (flag)
+ {
+ ator160Class.marker = "FqOSC";
+ }
+ else
+ {
+ bool flag2 = (data[2] & 7) == 1;
+ if (flag2)
+ {
+ ator160Class.marker = "FqPG1";
+ }
+ else
+ {
+ bool flag3 = (data[2] & 7) == 2;
+ if (flag3)
+ {
+ ator160Class.marker = "FqPG2";
+ }
+ else
+ {
+ bool flag4 = (data[2] & 7) == 3;
+ if (flag4)
+ {
+ ator160Class.marker = "FqPG31";
+ }
+ else
+ {
+ bool flag5 = (data[2] & 7) == 4;
+ if (flag5)
+ {
+ ator160Class.marker = "FqPG32";
+ }
+ else
+ {
+ bool flag6 = (data[2] & 7) == 5;
+ if (flag6)
+ {
+ ator160Class.marker = "FqPG33";
+ }
+ else
+ {
+ bool flag7 = (data[2] & 7) == 6;
+ if (flag7)
+ {
+ ator160Class.marker = "FqPGATS";
+ }
+ else
+ {
+ bool flag8 = (data[2] & 7) == 7;
+ if (flag8)
+ {
+ ator160Class.marker = "FqNONE";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ator160Class.aliveoscf = Convert.ToDouble((int)data[3] << 8 | (int)data[4]);
+ return ator160Class;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/ator160class.cs b/c_Sharp_Code/lib/ator160class.cs
new file mode 100644
index 0000000..be386e5
--- /dev/null
+++ b/c_Sharp_Code/lib/ator160class.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200002A RID: 42
+ public class ATOR160Class
+ {
+ // Token: 0x04000263 RID: 611
+ public int seq;
+
+ // Token: 0x04000264 RID: 612
+ public int swVer;
+
+ // Token: 0x04000265 RID: 613
+ public int bdStat;
+
+ // Token: 0x04000266 RID: 614
+ public string marker;
+
+ // Token: 0x04000267 RID: 615
+ public double aliveoscf;
+ }
+}
diff --git a/c_Sharp_Code/lib/canid.cs b/c_Sharp_Code/lib/canid.cs
new file mode 100644
index 0000000..61fbe2a
--- /dev/null
+++ b/c_Sharp_Code/lib/canid.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000036 RID: 54
+ public static class CanID
+ {
+ // Token: 0x04000390 RID: 912
+ public const uint acpu200 = 512U;
+
+ // Token: 0x04000391 RID: 913
+ public const uint acpu201 = 513U;
+
+ // Token: 0x04000392 RID: 914
+ public const uint acpu202 = 514U;
+
+ // Token: 0x04000393 RID: 915
+ public const uint atcr250 = 592U;
+
+ // Token: 0x04000394 RID: 916
+ public const uint ator160 = 352U;
+
+ // Token: 0x04000395 RID: 917
+ public const uint atcr501 = 1281U;
+
+ // Token: 0x04000396 RID: 918
+ public const uint twct320 = 800U;
+
+ // Token: 0x04000397 RID: 919
+ public const uint ccc150 = 336U;
+
+ // Token: 0x04000398 RID: 920
+ public const uint ccc151 = 337U;
+
+ // Token: 0x04000399 RID: 921
+ public const uint ccc152 = 338U;
+
+ // Token: 0x0400039A RID: 922
+ public const uint mmi = 2048U;
+
+ // Token: 0x0400039B RID: 923
+ public const uint fmmi = 2128U;
+
+ // Token: 0x0400039C RID: 924
+ public const uint tcms = 2304U;
+
+ // Token: 0x0400039D RID: 925
+ public const uint ftcms = 2305U;
+ }
+}
diff --git a/c_Sharp_Code/lib/ccc150.cs b/c_Sharp_Code/lib/ccc150.cs
new file mode 100644
index 0000000..01a023d
--- /dev/null
+++ b/c_Sharp_Code/lib/ccc150.cs
@@ -0,0 +1,95 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000031 RID: 49
+ public class CCC150
+ {
+ // Token: 0x06000151 RID: 337 RVA: 0x00024EB8 File Offset: 0x000230B8
+ public static CCC150Class SetData(byte[] data)
+ {
+ CCC150Class ccc150Class = new CCC150Class();
+ ccc150Class.seq = Convert.ToInt32(data[0]);
+ ccc150Class.source = Convert.ToInt32((int)(data[1] & 240));
+ ccc150Class.destination = Convert.ToInt32((int)(data[1] & 15));
+ bool flag = data[2] == 0;
+ if (flag)
+ {
+ ccc150Class.atccode = "02";
+ }
+ else
+ {
+ bool flag2 = data[2] == 1;
+ if (flag2)
+ {
+ ccc150Class.atccode = "01";
+ }
+ else
+ {
+ bool flag3 = data[2] == 2;
+ if (flag3)
+ {
+ ccc150Class.atccode = "25";
+ }
+ else
+ {
+ bool flag4 = data[2] == 3;
+ if (flag4)
+ {
+ ccc150Class.atccode = "40";
+ }
+ else
+ {
+ bool flag5 = data[2] == 4;
+ if (flag5)
+ {
+ ccc150Class.atccode = "55";
+ }
+ else
+ {
+ bool flag6 = data[2] == 5;
+ if (flag6)
+ {
+ ccc150Class.atccode = "65";
+ }
+ else
+ {
+ bool flag7 = data[2] == 6;
+ if (flag7)
+ {
+ ccc150Class.atccode = "75";
+ }
+ else
+ {
+ bool flag8 = data[2] == 7;
+ if (flag8)
+ {
+ ccc150Class.atccode = "DE";
+ }
+ else
+ {
+ bool flag9 = data[2] == 8;
+ if (flag9)
+ {
+ ccc150Class.atccode = "DW";
+ }
+ else
+ {
+ ccc150Class.atccode = "-";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ccc150Class.trainspeed = Convert.ToInt32(data[3]);
+ ccc150Class.atclimit = Convert.ToInt32(data[4]);
+ ccc150Class.tachopulse = Convert.ToDouble((int)data[5] << 8 | (int)data[6]);
+ ccc150Class.wheeldia = (int)(860U - (Convert.ToUInt32(data[7]) - 1U) * 5U);
+ return ccc150Class;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/ccc150class.cs b/c_Sharp_Code/lib/ccc150class.cs
new file mode 100644
index 0000000..7d5e501
--- /dev/null
+++ b/c_Sharp_Code/lib/ccc150class.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000030 RID: 48
+ public class CCC150Class
+ {
+ // Token: 0x04000287 RID: 647
+ public int seq;
+
+ // Token: 0x04000288 RID: 648
+ public int source;
+
+ // Token: 0x04000289 RID: 649
+ public int destination;
+
+ // Token: 0x0400028A RID: 650
+ public string atccode;
+
+ // Token: 0x0400028B RID: 651
+ public int trainspeed;
+
+ // Token: 0x0400028C RID: 652
+ public int atclimit;
+
+ // Token: 0x0400028D RID: 653
+ public double tachopulse;
+
+ // Token: 0x0400028E RID: 654
+ public int wheeldia;
+ }
+}
diff --git a/c_Sharp_Code/lib/ccc151.cs b/c_Sharp_Code/lib/ccc151.cs
new file mode 100644
index 0000000..11dfcfd
--- /dev/null
+++ b/c_Sharp_Code/lib/ccc151.cs
@@ -0,0 +1,87 @@
+.csusing System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200002F RID: 47
+ public class CCC151
+ {
+ // Token: 0x0600014E RID: 334 RVA: 0x00024BF4 File Offset: 0x00022DF4
+ public static CCC151Class SetData(byte[] data)
+ {
+ CCC151Class ccc151Class = new CCC151Class();
+ ccc151Class.seq = Convert.ToInt32(data[0]);
+ ccc151Class.nomalactive = ((data[1] & 64) > 0);
+ ccc151Class.nomalactive = ((data[1] & 32) > 0);
+ ccc151Class.systemactive = ((data[1] & 16) > 0);
+ ccc151Class.drmclose = ((data[1] & 8) > 0);
+ ccc151Class.recovery = ((data[1] & 4) > 0);
+ ccc151Class.nomal = ((data[1] & 2) > 0);
+ ccc151Class.tachodir = ((data[1] & 1) > 0);
+ ccc151Class.doorcl = ((data[2] & 128) > 0);
+ ccc151Class.doorop = ((data[2] & 64) > 0);
+ ccc151Class.mcneu = ((data[2] & 32) > 0);
+ ccc151Class.mceb = ((data[2] & 16) > 0);
+ ccc151Class.rodfwd = ((data[2] & 1) > 0);
+ bool flag = (data[2] & 14) == 0;
+ if (flag)
+ {
+ ccc151Class.drvmode = "-";
+ }
+ else
+ {
+ bool flag2 = (data[2] & 14) == 1;
+ if (flag2)
+ {
+ ccc151Class.drvmode = "FMC";
+ }
+ else
+ {
+ bool flag3 = (data[2] & 14) == 2;
+ if (flag3)
+ {
+ ccc151Class.drvmode = "YARD";
+ }
+ else
+ {
+ bool flag4 = (data[2] & 14) == 3;
+ if (flag4)
+ {
+ ccc151Class.drvmode = "MCS";
+ }
+ else
+ {
+ bool flag5 = (data[2] & 14) == 4;
+ if (flag5)
+ {
+ ccc151Class.drvmode = "AUTO";
+ }
+ else
+ {
+ bool flag6 = (data[2] & 14) == 5;
+ if (flag6)
+ {
+ ccc151Class.drvmode = "FA";
+ }
+ }
+ }
+ }
+ }
+ }
+ ccc151Class.activeunit = ((data[3] & 128) > 0);
+ ccc151Class.startbutton = ((data[3] & 64) > 0);
+ ccc151Class.hcr = ((data[3] & 16) > 0);
+ ccc151Class.tc2 = ((data[3] & 8) > 0);
+ ccc151Class.tc1 = ((data[3] & 4) > 0);
+ ccc151Class.psdcl = ((data[3] & 2) > 0);
+ ccc151Class.psdop = ((data[3] & 1) > 0);
+ ccc151Class.edl = ((data[4] & 16) > 0);
+ ccc151Class.edr = ((data[4] & 8) > 0);
+ ccc151Class.zvr = ((data[4] & 4) > 0);
+ ccc151Class.fsb = ((data[4] & 2) > 0);
+ ccc151Class.eb = ((data[4] & 1) > 0);
+ ccc151Class.formno = Convert.ToInt32(data[5]);
+ ccc151Class.trainno = Convert.ToString((int)data[6] << 8 | (int)data[7], 16);
+ return ccc151Class;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/ccc151class.cs b/c_Sharp_Code/lib/ccc151class.cs
new file mode 100644
index 0000000..9e926a7
--- /dev/null
+++ b/c_Sharp_Code/lib/ccc151class.cs
@@ -0,0 +1,89 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200002E RID: 46
+ public class CCC151Class
+ {
+ // Token: 0x0400026C RID: 620
+ public int seq;
+
+ // Token: 0x0400026D RID: 621
+ public bool nomalactive;
+
+ // Token: 0x0400026E RID: 622
+ public bool systemactive;
+
+ // Token: 0x0400026F RID: 623
+ public bool drmclose;
+
+ // Token: 0x04000270 RID: 624
+ public bool recovery;
+
+ // Token: 0x04000271 RID: 625
+ public bool nomal;
+
+ // Token: 0x04000272 RID: 626
+ public bool tachodir;
+
+ // Token: 0x04000273 RID: 627
+ public bool doorcl;
+
+ // Token: 0x04000274 RID: 628
+ public bool doorop;
+
+ // Token: 0x04000275 RID: 629
+ public bool mcneu;
+
+ // Token: 0x04000276 RID: 630
+ public bool mceb;
+
+ // Token: 0x04000277 RID: 631
+ public string drvmode;
+
+ // Token: 0x04000278 RID: 632
+ public bool rodfwd;
+
+ // Token: 0x04000279 RID: 633
+ public bool activeunit;
+
+ // Token: 0x0400027A RID: 634
+ public bool startbutton;
+
+ // Token: 0x0400027B RID: 635
+ public bool hcr;
+
+ // Token: 0x0400027C RID: 636
+ public bool tc2;
+
+ // Token: 0x0400027D RID: 637
+ public bool tc1;
+
+ // Token: 0x0400027E RID: 638
+ public bool psdcl;
+
+ // Token: 0x0400027F RID: 639
+ public bool psdop;
+
+ // Token: 0x04000280 RID: 640
+ public bool edl;
+
+ // Token: 0x04000281 RID: 641
+ public bool edr;
+
+ // Token: 0x04000282 RID: 642
+ public bool zvr;
+
+ // Token: 0x04000283 RID: 643
+ public bool fsb;
+
+ // Token: 0x04000284 RID: 644
+ public bool eb;
+
+ // Token: 0x04000285 RID: 645
+ public int formno;
+
+ // Token: 0x04000286 RID: 646
+ public string trainno;
+ }
+}
diff --git a/c_Sharp_Code/lib/ccc152.cs b/c_Sharp_Code/lib/ccc152.cs
new file mode 100644
index 0000000..a810bee
--- /dev/null
+++ b/c_Sharp_Code/lib/ccc152.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200002D RID: 45
+ public class CCC152
+ {
+ // Token: 0x0600014B RID: 331 RVA: 0x00024B90 File Offset: 0x00022D90
+ public static CCC152Class SetData(byte[] data)
+ {
+ return new CCC152Class
+ {
+ seq = Convert.ToInt32(data[0]),
+ prestation = Convert.ToInt32(data[1]),
+ nextstation = Convert.ToInt32(data[2]),
+ deststation = Convert.ToInt32(data[3])
+ };
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/ccc152class.cs b/c_Sharp_Code/lib/ccc152class.cs
new file mode 100644
index 0000000..0ecf71f
--- /dev/null
+++ b/c_Sharp_Code/lib/ccc152class.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200002C RID: 44
+ public class CCC152Class
+ {
+ // Token: 0x04000268 RID: 616
+ public int seq;
+
+ // Token: 0x04000269 RID: 617
+ public int prestation;
+
+ // Token: 0x0400026A RID: 618
+ public int nextstation;
+
+ // Token: 0x0400026B RID: 619
+ public int deststation;
+ }
+}
diff --git a/c_Sharp_Code/lib/ddositommi.cs b/c_Sharp_Code/lib/ddositommi.cs
new file mode 100644
index 0000000..d943076
--- /dev/null
+++ b/c_Sharp_Code/lib/ddositommi.cs
@@ -0,0 +1,1174 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000033 RID: 51
+ public class DDOSItoMMI
+ {
+ // Token: 0x06000154 RID: 340 RVA: 0x00025054 File Offset: 0x00023254
+ public static DOSItoMMIClass SetData(byte[] data)
+ {
+ DOSItoMMIClass dositoMMIClass = new DOSItoMMIClass();
+ dositoMMIClass.seq = Convert.ToUInt32(data[1]);
+ dositoMMIClass.source = Convert.ToUInt32(data[3]);
+ dositoMMIClass.time = "20";
+ DOSItoMMIClass dositoMMIClass2 = dositoMMIClass;
+ dositoMMIClass2.time += string.Format("{0:x2}.", data[4]);
+ DOSItoMMIClass dositoMMIClass3 = dositoMMIClass;
+ dositoMMIClass3.time += string.Format("{0:x2}.", data[5]);
+ DOSItoMMIClass dositoMMIClass4 = dositoMMIClass;
+ dositoMMIClass4.time += string.Format("{0:x2} ", data[6]);
+ DOSItoMMIClass dositoMMIClass5 = dositoMMIClass;
+ dositoMMIClass5.time += string.Format("{0:x2}:", data[7]);
+ DOSItoMMIClass dositoMMIClass6 = dositoMMIClass;
+ dositoMMIClass6.time += string.Format("{0:x2}:", data[8]);
+ DOSItoMMIClass dositoMMIClass7 = dositoMMIClass;
+ dositoMMIClass7.time += string.Format("{0:x2}", data[9]);
+ dositoMMIClass.trainspeed = Convert.ToDouble((int)data[10] << 8 | (int)data[11]) / 10.0;
+ dositoMMIClass.limitspeed = Convert.ToUInt32(data[12]);
+ dositoMMIClass.do_zvr = ((data[13] & 32) > 0);
+ dositoMMIClass.do_edl = ((data[13] & 16) > 0);
+ dositoMMIClass.do_edr = ((data[13] & 8) > 0);
+ dositoMMIClass.do_fsb = ((data[13] & 4) > 0);
+ dositoMMIClass.do_ebm = ((data[13] & 2) > 0);
+ dositoMMIClass.do_ebp = ((data[13] & 1) > 0);
+ dositoMMIClass.system_active = ((data[14] & 128) > 0);
+ dositoMMIClass.over_spd_warning = ((data[14] & 64) > 0);
+ dositoMMIClass.tcr = ((data[14] & 32) > 0);
+ dositoMMIClass.hcr = ((data[14] & 16) > 0);
+ dositoMMIClass.door_open = ((data[14] & 8) > 0);
+ dositoMMIClass.door_close = ((data[14] & 4) > 0);
+ dositoMMIClass.psd_open = ((data[14] & 2) > 0);
+ dositoMMIClass.psd_close = ((data[14] & 1) > 0);
+ dositoMMIClass.fa = ((data[15] & 128) > 0);
+ dositoMMIClass.auto = ((data[15] & 64) > 0);
+ dositoMMIClass.mcs = ((data[15] & 32) > 0);
+ dositoMMIClass.yard = ((data[15] & 16) > 0);
+ dositoMMIClass.fmc = ((data[15] & 8) > 0);
+ dositoMMIClass.reversingrod_rvs = ((data[15] & 4) > 0);
+ dositoMMIClass.reversingrod_fwd = ((data[15] & 2) > 0);
+ dositoMMIClass.reversingrod_neu = ((data[15] & 1) > 0);
+ dositoMMIClass.ato_start_btn = ((data[16] & 128) > 0);
+ dositoMMIClass.ato_eb_req = ((data[16] & 64) > 0);
+ dositoMMIClass.tacho_dir_a = ((data[16] & 32) > 0);
+ dositoMMIClass.tacho_dir_b = ((data[16] & 16) > 0);
+ dositoMMIClass.mascon_neu = ((data[16] & 0) > 0);
+ dositoMMIClass.mascon_dr = ((data[16] & 2) > 0);
+ dositoMMIClass.mascon_br = ((data[16] & 4) > 0);
+ dositoMMIClass.mascon_eb = ((data[16] & 8) > 0);
+ dositoMMIClass.ack_whd1 = 860U - (Convert.ToUInt32(data[17]) - 1U) * 5U;
+ dositoMMIClass.ack_whd2 = 860U - (Convert.ToUInt32(data[18]) - 1U) * 5U;
+ bool flag = (data[19] & 192) == 0;
+ if (flag)
+ {
+ dositoMMIClass.atc_status = "INITIAL PDT";
+ }
+ else
+ {
+ bool flag2 = (data[19] & 192) == 192;
+ if (flag2)
+ {
+ dositoMMIClass.atc_status = "MANUAL PDT";
+ }
+ else
+ {
+ bool flag3 = (data[19] & 192) == 64;
+ if (flag3)
+ {
+ dositoMMIClass.atc_status = "ATC ACTIVE";
+ }
+ else
+ {
+ bool flag4 = (data[19] & 192) == 128;
+ if (flag4)
+ {
+ dositoMMIClass.atc_status = "ATC STANDBY";
+ }
+ else
+ {
+ dositoMMIClass.atc_status = "-";
+ }
+ }
+ }
+ }
+ dositoMMIClass.wheelcheck = ((data[19] & 32) > 0);
+ bool flag5 = (data[19] & 28) == 16;
+ if (flag5)
+ {
+ dositoMMIClass.mpdt = "START";
+ }
+ else
+ {
+ bool flag6 = (data[19] & 28) == 8;
+ if (flag6)
+ {
+ dositoMMIClass.mpdt = "NG";
+ }
+ else
+ {
+ bool flag7 = (data[19] & 28) == 4;
+ if (flag7)
+ {
+ dositoMMIClass.mpdt = "OK";
+ }
+ else
+ {
+ dositoMMIClass.mpdt = "-";
+ }
+ }
+ }
+ bool flag8 = (data[19] & 3) == 2;
+ if (flag8)
+ {
+ dositoMMIClass.ipdt = "NG";
+ }
+ else
+ {
+ bool flag9 = (data[19] & 3) == 1;
+ if (flag9)
+ {
+ dositoMMIClass.ipdt = "OK";
+ }
+ else
+ {
+ dositoMMIClass.ipdt = "-";
+ }
+ }
+ dositoMMIClass.fail_atcr = ((data[20] & 128) > 0);
+ dositoMMIClass.fail_atoc = ((data[20] & 64) > 0);
+ dositoMMIClass.fail_tcms = ((data[20] & 32) > 0);
+ dositoMMIClass.fail_tacho2 = ((data[20] & 2) > 0);
+ dositoMMIClass.fail_tacho1 = ((data[20] & 1) > 0);
+ dositoMMIClass.recovery = ((data[21] & 128) > 0);
+ dositoMMIClass.nomal = ((data[21] & 64) > 0);
+ dositoMMIClass.tasc = ((data[21] & 32) > 0);
+ bool flag10 = (data[21] & 31) == 16;
+ if (flag10)
+ {
+ dositoMMIClass.marker = "ATS";
+ }
+ else
+ {
+ bool flag11 = (data[21] & 31) == 8;
+ if (flag11)
+ {
+ dositoMMIClass.marker = "PGX";
+ }
+ else
+ {
+ bool flag12 = (data[21] & 31) == 4;
+ if (flag12)
+ {
+ dositoMMIClass.marker = "PG3-2";
+ }
+ else
+ {
+ bool flag13 = (data[21] & 31) == 2;
+ if (flag13)
+ {
+ dositoMMIClass.marker = "PG2";
+ }
+ else
+ {
+ bool flag14 = (data[21] & 31) == 1;
+ if (flag14)
+ {
+ dositoMMIClass.marker = "PG1";
+ }
+ else
+ {
+ dositoMMIClass.marker = "-";
+ }
+ }
+ }
+ }
+ }
+ dositoMMIClass.pwm_value = Convert.ToUInt32(data[22]);
+ dositoMMIClass.trac_dr = ((data[23] & 128) > 0);
+ dositoMMIClass.trac_br = ((data[23] & 64) > 0);
+ dositoMMIClass.trac_cs = ((data[23] & 32) > 0);
+ dositoMMIClass.ador = ((data[23] & 16) > 0);
+ dositoMMIClass.adol = ((data[23] & 8) > 0);
+ dositoMMIClass.adc = ((data[23] & 4) > 0);
+ dositoMMIClass.start_enable = ((data[23] & 2) > 0);
+ dositoMMIClass.trainberth = ((data[23] & 1) > 0);
+ dositoMMIClass.tc2 = ((data[24] & 128) > 0);
+ dositoMMIClass.tc1 = ((data[24] & 64) > 0);
+ dositoMMIClass.tascdb = ((data[24] & 32) > 0);
+ bool flag15 = (data[24] & 24) == 16;
+ if (flag15)
+ {
+ dositoMMIClass.tcmsdoor = "LDO";
+ }
+ else
+ {
+ bool flag16 = (data[24] & 24) == 8;
+ if (flag16)
+ {
+ dositoMMIClass.tcmsdoor = "RDO";
+ }
+ else
+ {
+ bool flag17 = (data[24] & 24) == 24;
+ if (flag17)
+ {
+ dositoMMIClass.tcmsdoor = "L,RDO";
+ }
+ else
+ {
+ dositoMMIClass.tcmsdoor = "-";
+ }
+ }
+ }
+ bool flag18 = (data[24] & 7) == 4;
+ if (flag18)
+ {
+ dositoMMIClass.doormod = "A / A";
+ }
+ else
+ {
+ bool flag19 = (data[24] & 7) == 2;
+ if (flag19)
+ {
+ dositoMMIClass.doormod = "A / M";
+ }
+ else
+ {
+ bool flag20 = (data[24] & 7) == 1;
+ if (flag20)
+ {
+ dositoMMIClass.doormod = "M / M";
+ }
+ }
+ }
+ dositoMMIClass.pre_brake = ((data[25] & 128) > 0);
+ dositoMMIClass.limit_drive = ((data[25] & 64) > 0);
+ dositoMMIClass.ov_stop1 = ((data[25] & 8) > 0);
+ dositoMMIClass.ov_stop2 = ((data[25] & 4) > 0);
+ dositoMMIClass.sh_stop1 = ((data[25] & 2) > 0);
+ dositoMMIClass.sh_stop2 = ((data[25] & 1) > 0);
+ dositoMMIClass.trainno = string.Format("{0:x4}", (uint)((int)data[26] << 8 | (int)data[27]));
+ dositoMMIClass.pstn = Convert.ToInt32(data[28]);
+ dositoMMIClass.nstn = Convert.ToInt32(data[29]);
+ dositoMMIClass.dstn = Convert.ToInt32(data[30]);
+ int num = (int)data[31] << 8 | (int)data[32];
+ bool tasc = dositoMMIClass.tasc;
+ if (tasc)
+ {
+ dositoMMIClass.dtg = Convert.ToDouble((float)num / 100f);
+ }
+ else
+ {
+ bool flag21 = dositoMMIClass.trainspeed == 0.0 && !dositoMMIClass.trac_dr;
+ if (flag21)
+ {
+ bool flag22 = (num & 32768) != 0;
+ if (flag22)
+ {
+ num -= 65536;
+ }
+ dositoMMIClass.dtg = Convert.ToDouble((float)num / 100f) * -1.0;
+ }
+ else
+ {
+ dositoMMIClass.dtg = Convert.ToDouble((float)num / 10f);
+ }
+ }
+ dositoMMIClass.osc_f = Convert.ToDouble((int)(data[55] ^ 99) << 8 | (int)(data[58] ^ 100)) / 10.0;
+ dositoMMIClass.ato_limitSpeed = Convert.ToUInt32(data[33]);
+ dositoMMIClass.kur = ((data[34] & 128) > 0);
+ dositoMMIClass.osc = ((data[34] & 64) > 0);
+ dositoMMIClass.inching = ((data[34] & 32) > 0);
+ dositoMMIClass.twct_enable = ((data[34] & 16) > 0);
+ dositoMMIClass.door_close_warning = ((data[34] & 8) > 0);
+ dositoMMIClass.wrongdoor = ((data[34] & 4) > 0);
+ bool flag23 = (data[34] & 3) == 2;
+ if (flag23)
+ {
+ dositoMMIClass.nextdoor = "오른쪽";
+ }
+ else
+ {
+ bool flag24 = (data[34] & 3) == 1;
+ if (flag24)
+ {
+ dositoMMIClass.nextdoor = "왼쪽";
+ }
+ else
+ {
+ dositoMMIClass.nextdoor = "-";
+ }
+ }
+ bool flag25 = (data[35] & 240) >> 4 == 0;
+ if (flag25)
+ {
+ dositoMMIClass.atc_code = "02";
+ }
+ else
+ {
+ bool flag26 = (data[35] & 240) >> 4 == 1;
+ if (flag26)
+ {
+ dositoMMIClass.atc_code = "01";
+ }
+ else
+ {
+ bool flag27 = (data[35] & 240) >> 4 == 2;
+ if (flag27)
+ {
+ dositoMMIClass.atc_code = "25";
+ }
+ else
+ {
+ bool flag28 = (data[35] & 240) >> 4 == 3;
+ if (flag28)
+ {
+ dositoMMIClass.atc_code = "40";
+ }
+ else
+ {
+ bool flag29 = (data[35] & 240) >> 4 == 4;
+ if (flag29)
+ {
+ dositoMMIClass.atc_code = "55";
+ }
+ else
+ {
+ bool flag30 = (data[35] & 240) >> 4 == 5;
+ if (flag30)
+ {
+ dositoMMIClass.atc_code = "65";
+ }
+ else
+ {
+ bool flag31 = (data[35] & 240) >> 4 == 6;
+ if (flag31)
+ {
+ dositoMMIClass.atc_code = "75";
+ }
+ else
+ {
+ bool flag32 = (data[35] & 240) >> 4 == 7;
+ if (flag32)
+ {
+ dositoMMIClass.atc_code = "DE";
+ }
+ else
+ {
+ bool flag33 = (data[35] & 240) >> 4 == 8;
+ if (flag33)
+ {
+ dositoMMIClass.atc_code = "DW";
+ }
+ else
+ {
+ dositoMMIClass.atc_code = "-";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ dositoMMIClass.osc_f0_ok = ((data[35] & 8) > 0);
+ bool flag34 = (data[35] & 7) == 0;
+ if (flag34)
+ {
+ dositoMMIClass.atc_carrier_f = "-";
+ }
+ else
+ {
+ bool flag35 = (data[35] & 7) == 1;
+ if (flag35)
+ {
+ dositoMMIClass.atc_carrier_f = "F1";
+ }
+ else
+ {
+ bool flag36 = (data[35] & 7) == 2;
+ if (flag36)
+ {
+ dositoMMIClass.atc_carrier_f = "F2";
+ }
+ else
+ {
+ bool flag37 = (data[35] & 7) == 3;
+ if (flag37)
+ {
+ dositoMMIClass.atc_carrier_f = "F3";
+ }
+ else
+ {
+ bool flag38 = (data[35] & 7) == 4;
+ if (flag38)
+ {
+ dositoMMIClass.atc_carrier_f = "F4";
+ }
+ }
+ }
+ }
+ }
+ dositoMMIClass.atc_code_carrier_f = Convert.ToDouble((int)(data[36] ^ 65) << 8 | (int)(data[37] ^ 82)) * 10.0;
+ dositoMMIClass.atc_code_f = Convert.ToDouble((int)(data[38] ^ 99) << 8 | (int)(data[39] ^ 116)) / 10.0;
+ dositoMMIClass.messageh = Convert.ToInt32(data[40]);
+ bool flag39 = (data[41] & 128) > 0;
+ if (flag39)
+ {
+ DOSItoMMIClass dositoMMIClass8 = dositoMMIClass;
+ dositoMMIClass8.diagnostic_err_msg += "ATIVSTATUS ERR:";
+ }
+ bool flag40 = (data[41] & 64) > 0;
+ if (flag40)
+ {
+ DOSItoMMIClass dositoMMIClass9 = dositoMMIClass;
+ dositoMMIClass9.diagnostic_err_msg += "VOTING ERR:";
+ }
+ bool flag41 = (data[41] & 32) > 0;
+ if (flag41)
+ {
+ DOSItoMMIClass dositoMMIClass10 = dositoMMIClass;
+ dositoMMIClass10.diagnostic_err_msg += "RUNRTDGET ERR:";
+ }
+ bool flag42 = (data[41] & 16) > 0;
+ if (flag42)
+ {
+ DOSItoMMIClass dositoMMIClass11 = dositoMMIClass;
+ dositoMMIClass11.diagnostic_err_msg += "ATIVSTATUS GET ERR:";
+ }
+ bool flag43 = (data[41] & 8) > 0;
+ if (flag43)
+ {
+ DOSItoMMIClass dositoMMIClass12 = dositoMMIClass;
+ dositoMMIClass12.diagnostic_err_msg = (dositoMMIClass12.diagnostic_err_msg ?? "");
+ }
+ bool flag44 = (data[41] & 4) > 0;
+ if (flag44)
+ {
+ DOSItoMMIClass dositoMMIClass13 = dositoMMIClass;
+ dositoMMIClass13.diagnostic_err_msg = (dositoMMIClass13.diagnostic_err_msg ?? "");
+ }
+ bool flag45 = (data[41] & 2) > 0;
+ if (flag45)
+ {
+ DOSItoMMIClass dositoMMIClass14 = dositoMMIClass;
+ dositoMMIClass14.diagnostic_err_msg = (dositoMMIClass14.diagnostic_err_msg ?? "");
+ }
+ bool flag46 = (data[41] & 1) > 0;
+ if (flag46)
+ {
+ DOSItoMMIClass dositoMMIClass15 = dositoMMIClass;
+ dositoMMIClass15.diagnostic_err_msg = (dositoMMIClass15.diagnostic_err_msg ?? "");
+ }
+ bool flag47 = (data[42] & 128) > 0;
+ if (flag47)
+ {
+ DOSItoMMIClass dositoMMIClass16 = dositoMMIClass;
+ dositoMMIClass16.diagnostic_err_msg += "MDHEALTYG ERR:";
+ }
+ bool flag48 = (data[42] & 32) > 0;
+ if (flag48)
+ {
+ DOSItoMMIClass dositoMMIClass17 = dositoMMIClass;
+ dositoMMIClass17.diagnostic_err_msg += "INPUT ERR:";
+ }
+ bool flag49 = (data[42] & 16) > 0;
+ if (flag49)
+ {
+ DOSItoMMIClass dositoMMIClass18 = dositoMMIClass;
+ dositoMMIClass18.diagnostic_err_msg += "UNHEALTHY ERR:";
+ }
+ bool flag50 = (data[42] & 8) > 0;
+ if (flag50)
+ {
+ DOSItoMMIClass dositoMMIClass19 = dositoMMIClass;
+ dositoMMIClass19.diagnostic_err_msg += "EOT ERR:";
+ }
+ bool flag51 = (data[42] & 4) > 0;
+ if (flag51)
+ {
+ DOSItoMMIClass dositoMMIClass20 = dositoMMIClass;
+ dositoMMIClass20.diagnostic_err_msg += "EOTVALGET ERR:";
+ }
+ bool flag52 = (data[42] & 2) > 0;
+ if (flag52)
+ {
+ DOSItoMMIClass dositoMMIClass21 = dositoMMIClass;
+ dositoMMIClass21.diagnostic_err_msg += "RUN RTD ERR:";
+ }
+ bool flag53 = (data[42] & 1) > 0;
+ if (flag53)
+ {
+ DOSItoMMIClass dositoMMIClass22 = dositoMMIClass;
+ dositoMMIClass22.diagnostic_err_msg += "RUN RTD SET ERR:";
+ }
+ bool flag54 = (data[43] & 128) > 0;
+ if (flag54)
+ {
+ DOSItoMMIClass dositoMMIClass23 = dositoMMIClass;
+ dositoMMIClass23.diagnostic_err_msg = (dositoMMIClass23.diagnostic_err_msg ?? "");
+ }
+ bool flag55 = (data[43] & 64) > 0;
+ if (flag55)
+ {
+ DOSItoMMIClass dositoMMIClass24 = dositoMMIClass;
+ dositoMMIClass24.diagnostic_err_msg = (dositoMMIClass24.diagnostic_err_msg ?? "");
+ }
+ bool flag56 = (data[43] & 32) > 0;
+ if (flag56)
+ {
+ DOSItoMMIClass dositoMMIClass25 = dositoMMIClass;
+ dositoMMIClass25.diagnostic_err_msg += "EB DATA ERR:";
+ }
+ bool flag57 = (data[43] & 16) > 0;
+ if (flag57)
+ {
+ DOSItoMMIClass dositoMMIClass26 = dositoMMIClass;
+ dositoMMIClass26.diagnostic_err_msg += "VDO TEMP ERR:";
+ }
+ bool flag58 = (data[43] & 8) > 0;
+ if (flag58)
+ {
+ DOSItoMMIClass dositoMMIClass27 = dositoMMIClass;
+ dositoMMIClass27.diagnostic_err_msg += "VDI TEMP ERR:";
+ }
+ bool flag59 = (data[43] & 4) > 0;
+ if (flag59)
+ {
+ DOSItoMMIClass dositoMMIClass28 = dositoMMIClass;
+ dositoMMIClass28.diagnostic_err_msg += "OUTGOING ERR:";
+ }
+ bool flag60 = (data[43] & 2) > 0;
+ if (flag60)
+ {
+ DOSItoMMIClass dositoMMIClass29 = dositoMMIClass;
+ dositoMMIClass29.diagnostic_err_msg += "OUTPUT FB ERR:";
+ }
+ bool flag61 = data[41] == 0 && data[42] == 0 && data[43] == 0;
+ if (flag61)
+ {
+ dositoMMIClass.diagnostic_err_msg = "";
+ }
+ bool flag62 = (data[45] & 128) > 0;
+ if (flag62)
+ {
+ DOSItoMMIClass dositoMMIClass30 = dositoMMIClass;
+ dositoMMIClass30.atc_err_msg += "MSCSTATE ERR:";
+ }
+ bool flag63 = (data[45] & 64) > 0;
+ if (flag63)
+ {
+ DOSItoMMIClass dositoMMIClass31 = dositoMMIClass;
+ dositoMMIClass31.atc_err_msg += "DOORSTAT E.ERR:";
+ }
+ bool flag64 = (data[45] & 32) > 0;
+ if (flag64)
+ {
+ DOSItoMMIClass dositoMMIClass32 = dositoMMIClass;
+ dositoMMIClass32.atc_err_msg += "DRIVEMODE ERR:";
+ }
+ bool flag65 = (data[45] & 16) > 0;
+ if (flag65)
+ {
+ DOSItoMMIClass dositoMMIClass33 = dositoMMIClass;
+ dositoMMIClass33.atc_err_msg += "CABMOD ERR:";
+ }
+ bool flag66 = (data[45] & 8) > 0;
+ if (flag66)
+ {
+ DOSItoMMIClass dositoMMIClass34 = dositoMMIClass;
+ dositoMMIClass34.atc_err_msg += "IPDT WHL ERR:";
+ }
+ bool flag67 = (data[45] & 4) > 0;
+ if (flag67)
+ {
+ DOSItoMMIClass dositoMMIClass35 = dositoMMIClass;
+ dositoMMIClass35.atc_err_msg += "IPDT TACHO ERR:";
+ }
+ bool flag68 = (data[45] & 2) > 0;
+ if (flag68)
+ {
+ DOSItoMMIClass dositoMMIClass36 = dositoMMIClass;
+ dositoMMIClass36.atc_err_msg += "IPDT EB ERR:";
+ }
+ bool flag69 = (data[45] & 1) > 0;
+ if (flag69)
+ {
+ DOSItoMMIClass dositoMMIClass37 = dositoMMIClass;
+ dositoMMIClass37.atc_err_msg += "IPDT ATCR ERR:";
+ }
+ bool flag70 = (data[46] & 128) > 0;
+ if (flag70)
+ {
+ DOSItoMMIClass dositoMMIClass38 = dositoMMIClass;
+ dositoMMIClass38.atc_err_msg += "MAXSPEED ERR:";
+ }
+ bool flag71 = (data[46] & 64) > 0;
+ if (flag71)
+ {
+ DOSItoMMIClass dositoMMIClass39 = dositoMMIClass;
+ dositoMMIClass39.atc_err_msg += "WHELLDIA ERR:";
+ }
+ bool flag72 = (data[46] & 32) > 0;
+ if (flag72)
+ {
+ DOSItoMMIClass dositoMMIClass40 = dositoMMIClass;
+ dositoMMIClass40.atc_err_msg += "MANUALPDT ERR:";
+ }
+ bool flag73 = (data[46] & 16) > 0;
+ if (flag73)
+ {
+ DOSItoMMIClass dositoMMIClass41 = dositoMMIClass;
+ dositoMMIClass41.atc_err_msg += "INITPDT ERR:";
+ }
+ bool flag74 = (data[46] & 8) > 0;
+ if (flag74)
+ {
+ DOSItoMMIClass dositoMMIClass42 = dositoMMIClass;
+ dositoMMIClass42.atc_err_msg += "DIRSET ERR:";
+ }
+ bool flag75 = (data[46] & 4) > 0;
+ if (flag75)
+ {
+ DOSItoMMIClass dositoMMIClass43 = dositoMMIClass;
+ dositoMMIClass43.atc_err_msg += "TCSEL ERR:";
+ }
+ bool flag76 = (data[46] & 2) > 0;
+ if (flag76)
+ {
+ DOSItoMMIClass dositoMMIClass44 = dositoMMIClass;
+ dositoMMIClass44.atc_err_msg += "PSDSTATE ERR:";
+ }
+ bool flag77 = (data[46] & 1) > 0;
+ if (flag77)
+ {
+ DOSItoMMIClass dositoMMIClass45 = dositoMMIClass;
+ dositoMMIClass45.atc_err_msg += "REVERSING ERR:";
+ }
+ bool flag78 = (data[47] & 128) > 0;
+ if (flag78)
+ {
+ DOSItoMMIClass dositoMMIClass46 = dositoMMIClass;
+ dositoMMIClass46.atc_err_msg += "TRBSMODE ERR:";
+ }
+ bool flag79 = (data[47] & 64) > 0;
+ if (flag79)
+ {
+ DOSItoMMIClass dositoMMIClass47 = dositoMMIClass;
+ dositoMMIClass47.atc_err_msg += "TDCSMOD E.ERR:";
+ }
+ bool flag80 = (data[47] & 32) > 0;
+ if (flag80)
+ {
+ DOSItoMMIClass dositoMMIClass48 = dositoMMIClass;
+ dositoMMIClass48.atc_err_msg += "TZSSMODE ERR:";
+ }
+ bool flag81 = (data[47] & 16) > 0;
+ if (flag81)
+ {
+ DOSItoMMIClass dositoMMIClass49 = dositoMMIClass;
+ dositoMMIClass49.atc_err_msg += "TRSSMODE ERR:";
+ }
+ bool flag82 = (data[47] & 8) > 0;
+ if (flag82)
+ {
+ DOSItoMMIClass dositoMMIClass50 = dositoMMIClass;
+ dositoMMIClass50.atc_err_msg += "RESTRICTION ERR:";
+ }
+ bool flag83 = (data[47] & 4) > 0;
+ if (flag83)
+ {
+ DOSItoMMIClass dositoMMIClass51 = dositoMMIClass;
+ dositoMMIClass51.atc_err_msg += "ATCCODE ERR:";
+ }
+ bool flag84 = (data[47] & 2) > 0;
+ if (flag84)
+ {
+ DOSItoMMIClass dositoMMIClass52 = dositoMMIClass;
+ dositoMMIClass52.atc_err_msg += "MAXPULSE ERR:";
+ }
+ bool flag85 = (data[47] & 1) > 0;
+ if (flag85)
+ {
+ DOSItoMMIClass dositoMMIClass53 = dositoMMIClass;
+ dositoMMIClass53.atc_err_msg += "DIFFSPEED ERR:";
+ }
+ bool flag86 = (data[48] & 16) > 0;
+ if (flag86)
+ {
+ DOSItoMMIClass dositoMMIClass54 = dositoMMIClass;
+ dositoMMIClass54.atc_err_msg += "PSDSTATE RECOVERY:";
+ }
+ bool flag87 = (data[48] & 8) > 0;
+ if (flag87)
+ {
+ DOSItoMMIClass dositoMMIClass55 = dositoMMIClass;
+ dositoMMIClass55.atc_err_msg += "MNUALSTD BYCLR ERR:";
+ }
+ bool flag88 = (data[48] & 4) > 0;
+ if (flag88)
+ {
+ DOSItoMMIClass dositoMMIClass56 = dositoMMIClass;
+ dositoMMIClass56.atc_err_msg += "MNUALSTD BYSET ERR:";
+ }
+ bool flag89 = (data[48] & 2) > 0;
+ if (flag89)
+ {
+ DOSItoMMIClass dositoMMIClass57 = dositoMMIClass;
+ dositoMMIClass57.atc_err_msg += "ATCMODE ERR:";
+ }
+ bool flag90 = (data[48] & 1) > 0;
+ if (flag90)
+ {
+ DOSItoMMIClass dositoMMIClass58 = dositoMMIClass;
+ dositoMMIClass58.atc_err_msg += "DIRECTION ERR:";
+ }
+ bool flag91 = data[45] == 0 && data[46] == 0 && data[47] == 0 && data[48] == 0;
+ if (flag91)
+ {
+ dositoMMIClass.atc_err_msg = "";
+ }
+ bool flag92 = data[40] == 7;
+ if (flag92)
+ {
+ DOSItoMMIClass dositoMMIClass59 = dositoMMIClass;
+ dositoMMIClass59.atc_err_msg += "TRAIN DECEL WARNING:";
+ }
+ bool flag93 = data[40] == 8;
+ if (flag93)
+ {
+ DOSItoMMIClass dositoMMIClass60 = dositoMMIClass;
+ dositoMMIClass60.atc_err_msg += "REVERSE OVER SPEED WARNING:";
+ }
+ bool flag94 = data[40] == 9;
+ if (flag94)
+ {
+ DOSItoMMIClass dositoMMIClass61 = dositoMMIClass;
+ dositoMMIClass61.atc_err_msg += "DOOR OPEN STATUE EB:";
+ }
+ bool flag95 = data[40] == 10;
+ if (flag95)
+ {
+ DOSItoMMIClass dositoMMIClass62 = dositoMMIClass;
+ dositoMMIClass62.atc_err_msg += "TRAIN ROLL-BACK EB:";
+ }
+ bool flag96 = data[40] == 11;
+ if (flag96)
+ {
+ DOSItoMMIClass dositoMMIClass63 = dositoMMIClass;
+ dositoMMIClass63.atc_err_msg += "TRAIN MOVE EB:";
+ }
+ bool flag97 = data[40] == 11;
+ if (flag97)
+ {
+ DOSItoMMIClass dositoMMIClass64 = dositoMMIClass;
+ dositoMMIClass64.ato_err_msg += "STNCODE ERROR:";
+ }
+ bool flag98 = (data[49] & 128) > 0;
+ if (flag98)
+ {
+ DOSItoMMIClass dositoMMIClass65 = dositoMMIClass;
+ dositoMMIClass65.interface_err_msg += "MMI INIT ERR:";
+ }
+ bool flag99 = (data[49] & 64) > 0;
+ if (flag99)
+ {
+ DOSItoMMIClass dositoMMIClass66 = dositoMMIClass;
+ dositoMMIClass66.interface_err_msg += "MMI W ERR:";
+ }
+ bool flag100 = (data[49] & 32) > 0;
+ if (flag100)
+ {
+ DOSItoMMIClass dositoMMIClass67 = dositoMMIClass;
+ dositoMMIClass67.interface_err_msg += "MMI R ERR:";
+ }
+ bool flag101 = (data[50] & 128) > 0;
+ if (flag101)
+ {
+ DOSItoMMIClass dositoMMIClass68 = dositoMMIClass;
+ dositoMMIClass68.interface_err_msg += "ATCR INIT ERR:";
+ }
+ bool flag102 = (data[50] & 64) > 0;
+ if (flag102)
+ {
+ DOSItoMMIClass dositoMMIClass69 = dositoMMIClass;
+ dositoMMIClass69.interface_err_msg += "ATCR W ERR:";
+ }
+ bool flag103 = (data[50] & 32) > 0;
+ if (flag103)
+ {
+ DOSItoMMIClass dositoMMIClass70 = dositoMMIClass;
+ dositoMMIClass70.interface_err_msg += "ATCR R ERR:";
+ }
+ bool flag104 = (data[50] & 8) > 0;
+ if (flag104)
+ {
+ DOSItoMMIClass dositoMMIClass71 = dositoMMIClass;
+ dositoMMIClass71.interface_err_msg += "TCMS INIT ERR:";
+ }
+ bool flag105 = (data[50] & 4) > 0;
+ if (flag105)
+ {
+ DOSItoMMIClass dositoMMIClass72 = dositoMMIClass;
+ dositoMMIClass72.interface_err_msg += "TCMS W ERR:";
+ }
+ bool flag106 = (data[50] & 2) > 0;
+ if (flag106)
+ {
+ DOSItoMMIClass dositoMMIClass73 = dositoMMIClass;
+ dositoMMIClass73.interface_err_msg += "TCMS R ERR:";
+ }
+ bool flag107 = (data[50] & 1) > 0;
+ if (flag107)
+ {
+ DOSItoMMIClass dositoMMIClass74 = dositoMMIClass;
+ dositoMMIClass74.interface_err_msg = (dositoMMIClass74.interface_err_msg ?? "");
+ }
+ bool flag108 = (data[51] & 128) > 0;
+ if (flag108)
+ {
+ DOSItoMMIClass dositoMMIClass75 = dositoMMIClass;
+ dositoMMIClass75.interface_err_msg += "VDI INIT ERR:";
+ }
+ bool flag109 = (data[51] & 64) > 0;
+ if (flag109)
+ {
+ DOSItoMMIClass dositoMMIClass76 = dositoMMIClass;
+ dositoMMIClass76.interface_err_msg += "VDI W ERR:";
+ }
+ bool flag110 = (data[51] & 32) > 0;
+ if (flag110)
+ {
+ DOSItoMMIClass dositoMMIClass77 = dositoMMIClass;
+ dositoMMIClass77.interface_err_msg += "VDI R ERR:";
+ }
+ bool flag111 = (data[51] & 16) > 0;
+ if (flag111)
+ {
+ DOSItoMMIClass dositoMMIClass78 = dositoMMIClass;
+ dositoMMIClass78.interface_err_msg += "DIO INIT ERR:";
+ }
+ bool flag112 = (data[51] & 8) > 0;
+ if (flag112)
+ {
+ DOSItoMMIClass dositoMMIClass79 = dositoMMIClass;
+ dositoMMIClass79.interface_err_msg += "ATOC INIT ERR:";
+ }
+ bool flag113 = (data[51] & 4) > 0;
+ if (flag113)
+ {
+ DOSItoMMIClass dositoMMIClass80 = dositoMMIClass;
+ dositoMMIClass80.interface_err_msg += "ATOC W ERR:";
+ }
+ bool flag114 = (data[51] & 2) > 0;
+ if (flag114)
+ {
+ DOSItoMMIClass dositoMMIClass81 = dositoMMIClass;
+ dositoMMIClass81.interface_err_msg += "ATOC R ERR_1:";
+ }
+ bool flag115 = (data[51] & 1) > 0;
+ if (flag115)
+ {
+ DOSItoMMIClass dositoMMIClass82 = dositoMMIClass;
+ dositoMMIClass82.interface_err_msg += "ATOC R ERR_2:";
+ }
+ bool flag116 = (data[52] & 128) > 0;
+ if (flag116)
+ {
+ DOSItoMMIClass dositoMMIClass83 = dositoMMIClass;
+ dositoMMIClass83.interface_err_msg = (dositoMMIClass83.interface_err_msg ?? "");
+ }
+ bool flag117 = (data[52] & 64) > 0;
+ if (flag117)
+ {
+ DOSItoMMIClass dositoMMIClass84 = dositoMMIClass;
+ dositoMMIClass84.interface_err_msg = (dositoMMIClass84.interface_err_msg ?? "");
+ }
+ bool flag118 = (data[52] & 32) > 0;
+ if (flag118)
+ {
+ DOSItoMMIClass dositoMMIClass85 = dositoMMIClass;
+ dositoMMIClass85.interface_err_msg = (dositoMMIClass85.interface_err_msg ?? "");
+ }
+ bool flag119 = (data[52] & 16) > 0;
+ if (flag119)
+ {
+ DOSItoMMIClass dositoMMIClass86 = dositoMMIClass;
+ dositoMMIClass86.interface_err_msg = (dositoMMIClass86.interface_err_msg ?? "");
+ }
+ bool flag120 = (data[52] & 8) > 0;
+ if (flag120)
+ {
+ DOSItoMMIClass dositoMMIClass87 = dositoMMIClass;
+ dositoMMIClass87.interface_err_msg += "VDO INIT ERR:";
+ }
+ bool flag121 = (data[52] & 4) > 0;
+ if (flag121)
+ {
+ DOSItoMMIClass dositoMMIClass88 = dositoMMIClass;
+ dositoMMIClass88.interface_err_msg += "VDO W ERR:";
+ }
+ bool flag122 = (data[52] & 2) > 0;
+ if (flag122)
+ {
+ DOSItoMMIClass dositoMMIClass89 = dositoMMIClass;
+ dositoMMIClass89.interface_err_msg += "VDO R ERR:";
+ }
+ bool flag123 = (data[52] & 1) > 0;
+ if (flag123)
+ {
+ DOSItoMMIClass dositoMMIClass90 = dositoMMIClass;
+ dositoMMIClass90.interface_err_msg = (dositoMMIClass90.interface_err_msg ?? "");
+ }
+ bool flag124 = data[49] == 0 && data[50] == 0 && data[51] == 0 && data[52] == 0;
+ if (flag124)
+ {
+ dositoMMIClass.interface_err_msg = "";
+ }
+ bool flag125 = (data[53] & 128) > 0;
+ if (flag125)
+ {
+ DOSItoMMIClass dositoMMIClass91 = dositoMMIClass;
+ dositoMMIClass91.ato_err_msg += "ATO ANT ERR_1:";
+ }
+ bool flag126 = (data[53] & 64) > 0;
+ if (flag126)
+ {
+ DOSItoMMIClass dositoMMIClass92 = dositoMMIClass;
+ dositoMMIClass92.ato_err_msg = (dositoMMIClass92.ato_err_msg ?? "");
+ }
+ bool flag127 = (data[53] & 32) > 0;
+ if (flag127)
+ {
+ DOSItoMMIClass dositoMMIClass93 = dositoMMIClass;
+ dositoMMIClass93.ato_err_msg += "RLY FB ERR:";
+ }
+ bool flag128 = (data[53] & 16) > 0;
+ if (flag128)
+ {
+ DOSItoMMIClass dositoMMIClass94 = dositoMMIClass;
+ dositoMMIClass94.ato_err_msg += "DOORCL ERR:";
+ }
+ bool flag129 = (data[53] & 8) > 0;
+ if (flag129)
+ {
+ DOSItoMMIClass dositoMMIClass95 = dositoMMIClass;
+ dositoMMIClass95.ato_err_msg += "DOOROP ERR:";
+ }
+ bool flag130 = (data[53] & 4) > 0;
+ if (flag130)
+ {
+ DOSItoMMIClass dositoMMIClass96 = dositoMMIClass;
+ dositoMMIClass96.ato_err_msg += "STNCODE ERR:";
+ }
+ bool flag131 = (data[53] & 2) > 0;
+ if (flag131)
+ {
+ DOSItoMMIClass dositoMMIClass97 = dositoMMIClass;
+ dositoMMIClass97.ato_err_msg += "ATO ANT ERR_2:";
+ }
+ bool flag132 = (data[53] & 1) > 0;
+ if (flag132)
+ {
+ DOSItoMMIClass dositoMMIClass98 = dositoMMIClass;
+ dositoMMIClass98.ato_err_msg += "ATO STOP ERR:";
+ }
+ bool flag133 = (data[54] & 128) > 0;
+ if (flag133)
+ {
+ DOSItoMMIClass dositoMMIClass99 = dositoMMIClass;
+ dositoMMIClass99.ato_err_msg = (dositoMMIClass99.ato_err_msg ?? "");
+ }
+ bool flag134 = (data[54] & 64) > 0;
+ if (flag134)
+ {
+ DOSItoMMIClass dositoMMIClass100 = dositoMMIClass;
+ dositoMMIClass100.ato_err_msg = (dositoMMIClass100.ato_err_msg ?? "");
+ }
+ bool flag135 = (data[54] & 32) > 0;
+ if (flag135)
+ {
+ DOSItoMMIClass dositoMMIClass101 = dositoMMIClass;
+ dositoMMIClass101.ato_err_msg = (dositoMMIClass101.ato_err_msg ?? "");
+ }
+ bool flag136 = (data[54] & 16) > 0;
+ if (flag136)
+ {
+ DOSItoMMIClass dositoMMIClass102 = dositoMMIClass;
+ dositoMMIClass102.ato_err_msg = (dositoMMIClass102.ato_err_msg ?? "");
+ }
+ bool flag137 = (data[54] & 8) > 0;
+ if (flag137)
+ {
+ DOSItoMMIClass dositoMMIClass103 = dositoMMIClass;
+ dositoMMIClass103.ato_err_msg = (dositoMMIClass103.ato_err_msg ?? "");
+ }
+ bool flag138 = (data[54] & 4) > 0;
+ if (flag138)
+ {
+ DOSItoMMIClass dositoMMIClass104 = dositoMMIClass;
+ dositoMMIClass104.ato_err_msg += "ATO.PG32 ERR:";
+ }
+ bool flag139 = (data[54] & 2) > 0;
+ if (flag139)
+ {
+ DOSItoMMIClass dositoMMIClass105 = dositoMMIClass;
+ dositoMMIClass105.ato_err_msg += "ATO.PG2 ERR:";
+ }
+ bool flag140 = (data[54] & 1) > 0;
+ if (flag140)
+ {
+ DOSItoMMIClass dositoMMIClass106 = dositoMMIClass;
+ dositoMMIClass106.ato_err_msg += "ATO.PG1 ERR:";
+ }
+ bool flag141 = data[53] == 0 && data[54] == 0;
+ if (flag141)
+ {
+ dositoMMIClass.ato_err_msg = "";
+ }
+ dositoMMIClass.atc_swver = string.Format("V{0}.{1}", Convert.ToString((data[56] & 240) >> 4, 16).ToUpper(), (double)(data[56] & 15) / 10.0);
+ dositoMMIClass.ato_err_detect = Convert.ToString(data[57], 16);
+ string[] array = dositoMMIClass.time.Split(new char[]
+ {
+ '.'
+ });
+ bool flag142 = array[0] == "2024" && int.Parse(array[1]) < 6;
+ if (flag142)
+ {
+ dositoMMIClass.ato_err_msg = "";
+ }
+ else
+ {
+ bool flag143 = (data[57] & 128) > 0;
+ if (flag143)
+ {
+ DOSItoMMIClass dositoMMIClass107 = dositoMMIClass;
+ dositoMMIClass107.ato_err_msg += "ATOCR NG:";
+ }
+ bool flag144 = (data[57] & 64) > 0;
+ if (flag144)
+ {
+ DOSItoMMIClass dositoMMIClass108 = dositoMMIClass;
+ dositoMMIClass108.ato_err_msg += "CCC CommFAIL:";
+ }
+ bool flag145 = (data[57] & 32) > 0;
+ if (flag145)
+ {
+ DOSItoMMIClass dositoMMIClass109 = dositoMMIClass;
+ dositoMMIClass109.ato_err_msg += "ATOR CommFAIL:";
+ }
+ bool flag146 = (data[57] & 16) > 0;
+ if (flag146)
+ {
+ DOSItoMMIClass dositoMMIClass110 = dositoMMIClass;
+ dositoMMIClass110.ato_err_msg += "TWCR CommFAIL:";
+ }
+ bool flag147 = (data[57] & 8) > 0;
+ if (flag147)
+ {
+ DOSItoMMIClass dositoMMIClass111 = dositoMMIClass;
+ dositoMMIClass111.ato_err_msg += "TWCT CommFAIL:";
+ }
+ bool flag148 = (data[57] & 4) > 0;
+ if (flag148)
+ {
+ DOSItoMMIClass dositoMMIClass112 = dositoMMIClass;
+ dositoMMIClass112.ato_err_msg += "PWM_FB ERR:";
+ }
+ bool flag149 = (data[57] & 1) > 0;
+ if (flag149)
+ {
+ DOSItoMMIClass dositoMMIClass113 = dositoMMIClass;
+ dositoMMIClass113.ato_err_msg += "SPD_Diff DETECT:";
+ }
+ }
+ dositoMMIClass.trainspeed_a = Convert.ToDouble((int)data[59] << 8 | (int)data[60]) / 10.0;
+ dositoMMIClass.trainspeed_b = Convert.ToDouble((int)data[61] << 8 | (int)data[62]) / 10.0;
+ dositoMMIClass.vdia_input = Convert.ToString(data[63], 16).ToUpper() + data[64].ToString("X2").ToUpper();
+ dositoMMIClass.vdia_rvs = ((data[63] & 128) > 0);
+ dositoMMIClass.vdia_neu = ((data[63] & 64) > 0);
+ dositoMMIClass.vdia_fwd = ((data[63] & 32) > 0);
+ dositoMMIClass.vdia_mascondr = ((data[63] & 16) > 0);
+ dositoMMIClass.vdia_masconbr = ((data[63] & 8) > 0);
+ dositoMMIClass.vdia_masconeb = ((data[63] & 4) > 0);
+ dositoMMIClass.vdia_doorclose = ((data[63] & 2) > 0);
+ dositoMMIClass.vdia_dooropen = ((data[63] & 1) > 0);
+ dositoMMIClass.vdia_fmc = ((data[64] & 64) > 0);
+ dositoMMIClass.vdia_yard = ((data[64] & 32) > 0);
+ dositoMMIClass.vdia_mcs = ((data[64] & 16) > 0);
+ dositoMMIClass.vdia_auto = ((data[64] & 8) > 0);
+ dositoMMIClass.vdia_fa = ((data[64] & 4) > 0);
+ dositoMMIClass.vdia_tcr = ((data[64] & 2) > 0);
+ dositoMMIClass.vdia_hcr = ((data[64] & 1) > 0);
+ dositoMMIClass.vdia_stat = Convert.ToString(data[65], 16).ToUpper();
+ dositoMMIClass.vdib_input = Convert.ToString(data[66], 16).ToUpper() + data[67].ToString("X2").ToUpper();
+ dositoMMIClass.vdib_rvs = ((data[66] & 128) > 0);
+ dositoMMIClass.vdib_neu = ((data[66] & 64) > 0);
+ dositoMMIClass.vdib_fwd = ((data[66] & 32) > 0);
+ dositoMMIClass.vdib_mascondr = ((data[66] & 16) > 0);
+ dositoMMIClass.vdib_masconbr = ((data[66] & 8) > 0);
+ dositoMMIClass.vdib_masconeb = ((data[66] & 4) > 0);
+ dositoMMIClass.vdib_doorclose = ((data[66] & 2) > 0);
+ dositoMMIClass.vdib_dooropen = ((data[66] & 1) > 0);
+ dositoMMIClass.vdib_fmc = ((data[67] & 64) > 0);
+ dositoMMIClass.vdib_yard = ((data[67] & 32) > 0);
+ dositoMMIClass.vdib_mcs = ((data[67] & 16) > 0);
+ dositoMMIClass.vdib_auto = ((data[67] & 8) > 0);
+ dositoMMIClass.vdib_fa = ((data[67] & 4) > 0);
+ dositoMMIClass.vdib_tcr = ((data[67] & 2) > 0);
+ dositoMMIClass.vdib_hcr = ((data[67] & 1) > 0);
+ dositoMMIClass.vdib_stat = Convert.ToString(data[68], 16).ToUpper();
+ dositoMMIClass.vdic_input = Convert.ToString(data[69], 16).ToUpper() + data[70].ToString("X2").ToUpper();
+ dositoMMIClass.vdic_tc2 = ((data[69] & 128) > 0);
+ dositoMMIClass.vdic_tc1 = ((data[69] & 64) > 0);
+ dositoMMIClass.vdic_edlfb = ((data[69] & 32) > 0);
+ dositoMMIClass.vdic_edrfb = ((data[69] & 16) > 0);
+ dositoMMIClass.vdic_zvrfb = ((data[69] & 8) > 0);
+ dositoMMIClass.vdic_fsbfb = ((data[69] & 4) > 0);
+ dositoMMIClass.vdic_ebmfb = ((data[69] & 2) > 0);
+ dositoMMIClass.vdic_ebpfb = ((data[69] & 1) > 0);
+ dositoMMIClass.vdic_unit1 = ((data[70] & 128) > 0);
+ dositoMMIClass.vdic_startbtn = ((data[70] & 4) > 0);
+ dositoMMIClass.vdic_psdclose = ((data[70] & 2) > 0);
+ dositoMMIClass.vdic_psdopen = ((data[70] & 1) > 0);
+ dositoMMIClass.vdic_stat = Convert.ToString(data[71], 16).ToUpper();
+ dositoMMIClass.vdid_input = Convert.ToString(data[72], 16).ToUpper() + data[73].ToString("X2").ToUpper();
+ dositoMMIClass.vdid_tc2 = ((data[72] & 128) > 0);
+ dositoMMIClass.vdid_tc1 = ((data[72] & 64) > 0);
+ dositoMMIClass.vdid_edlfb = ((data[72] & 32) > 0);
+ dositoMMIClass.vdid_edrfb = ((data[72] & 16) > 0);
+ dositoMMIClass.vdid_zvrfb = ((data[72] & 8) > 0);
+ dositoMMIClass.vdid_fsbfb = ((data[72] & 4) > 0);
+ dositoMMIClass.vdid_ebmfb = ((data[72] & 2) > 0);
+ dositoMMIClass.vdid_ebpfb = ((data[72] & 1) > 0);
+ dositoMMIClass.vdid_unit1 = ((data[73] & 128) > 0);
+ dositoMMIClass.vdid_startbtn = ((data[73] & 4) > 0);
+ dositoMMIClass.vdid_psdclose = ((data[73] & 2) > 0);
+ dositoMMIClass.vdid_psdopen = ((data[73] & 1) > 0);
+ dositoMMIClass.vdid_stat = Convert.ToString(data[74], 16).ToUpper();
+ dositoMMIClass.vdoa_feedback = data[75].ToString("X2").ToUpper();
+ dositoMMIClass.vdoa_edl = ((data[75] & 32) > 0);
+ dositoMMIClass.vdoa_edr = ((data[75] & 16) > 0);
+ dositoMMIClass.vdoa_zvr = ((data[75] & 8) > 0);
+ dositoMMIClass.vdoa_fsb = ((data[75] & 4) > 0);
+ dositoMMIClass.vdoa_ebm = ((data[75] & 2) > 0);
+ dositoMMIClass.vdoa_ebp = ((data[75] & 1) > 0);
+ dositoMMIClass.vdoa_stat = Convert.ToString(data[76], 16).ToUpper();
+ dositoMMIClass.vdob_feedback = data[77].ToString("X2").ToUpper();
+ dositoMMIClass.vdob_edl = ((data[77] & 32) > 0);
+ dositoMMIClass.vdob_edr = ((data[77] & 16) > 0);
+ dositoMMIClass.vdob_zvr = ((data[77] & 8) > 0);
+ dositoMMIClass.vdob_fsb = ((data[77] & 4) > 0);
+ dositoMMIClass.vdob_ebm = ((data[77] & 2) > 0);
+ dositoMMIClass.vdob_ebp = ((data[77] & 1) > 0);
+ dositoMMIClass.vdob_stat = Convert.ToString(data[78], 16).ToUpper();
+ dositoMMIClass.UserCRC = (uint)((uint)(data[60] & byte.MaxValue) << 24);
+ dositoMMIClass.UserCRC |= (uint)((uint)(data[61] & byte.MaxValue) << 16);
+ dositoMMIClass.UserCRC |= (uint)((uint)(data[62] & byte.MaxValue) << 8);
+ dositoMMIClass.UserCRC |= (uint)(data[63] & byte.MaxValue);
+ dositoMMIClass.text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ");
+ return dositoMMIClass;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/dositommiclass.cs b/c_Sharp_Code/lib/dositommiclass.cs
new file mode 100644
index 0000000..79e3395
--- /dev/null
+++ b/c_Sharp_Code/lib/dositommiclass.cs
@@ -0,0 +1,575 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000032 RID: 50
+ public class DOSItoMMIClass
+ {
+ // Token: 0x0400028F RID: 655
+ public uint ack_whd1;
+
+ // Token: 0x04000290 RID: 656
+ public uint ack_whd2;
+
+ // Token: 0x04000291 RID: 657
+ public bool adc;
+
+ // Token: 0x04000292 RID: 658
+ public bool adol;
+
+ // Token: 0x04000293 RID: 659
+ public bool ador;
+
+ // Token: 0x04000294 RID: 660
+ public bool ato_eb_req;
+
+ // Token: 0x04000295 RID: 661
+ public uint ato_limitSpeed;
+
+ // Token: 0x04000296 RID: 662
+ public string ato_err_msg;
+
+ // Token: 0x04000297 RID: 663
+ public string ato_err_detect;
+
+ // Token: 0x04000298 RID: 664
+ public bool ato_start_btn;
+
+ // Token: 0x04000299 RID: 665
+ public string atc_carrier_f;
+
+ // Token: 0x0400029A RID: 666
+ public string atc_code;
+
+ // Token: 0x0400029B RID: 667
+ public double atc_code_carrier_f;
+
+ // Token: 0x0400029C RID: 668
+ public double atc_code_f;
+
+ // Token: 0x0400029D RID: 669
+ public string atc_err_msg;
+
+ // Token: 0x0400029E RID: 670
+ public string atc_status;
+
+ // Token: 0x0400029F RID: 671
+ public string atc_swver;
+
+ // Token: 0x040002A0 RID: 672
+ public bool auto;
+
+ // Token: 0x040002A1 RID: 673
+ public string diagnostic_err_msg;
+
+ // Token: 0x040002A2 RID: 674
+ public string doormod;
+
+ // Token: 0x040002A3 RID: 675
+ public bool door_close;
+
+ // Token: 0x040002A4 RID: 676
+ public bool door_close_warning;
+
+ // Token: 0x040002A5 RID: 677
+ public bool door_open;
+
+ // Token: 0x040002A6 RID: 678
+ public bool do_zvr;
+
+ // Token: 0x040002A7 RID: 679
+ public bool do_edl;
+
+ // Token: 0x040002A8 RID: 680
+ public bool do_edr;
+
+ // Token: 0x040002A9 RID: 681
+ public bool do_fsb;
+
+ // Token: 0x040002AA RID: 682
+ public bool do_ebp;
+
+ // Token: 0x040002AB RID: 683
+ public bool do_ebm;
+
+ // Token: 0x040002AC RID: 684
+ public int dstn;
+
+ // Token: 0x040002AD RID: 685
+ public double dtg;
+
+ // Token: 0x040002AE RID: 686
+ public bool fa;
+
+ // Token: 0x040002AF RID: 687
+ public bool f1;
+
+ // Token: 0x040002B0 RID: 688
+ public bool f2;
+
+ // Token: 0x040002B1 RID: 689
+ public bool f3;
+
+ // Token: 0x040002B2 RID: 690
+ public bool f4;
+
+ // Token: 0x040002B3 RID: 691
+ public bool fail_atcr;
+
+ // Token: 0x040002B4 RID: 692
+ public bool fail_atoc;
+
+ // Token: 0x040002B5 RID: 693
+ public bool fail_tacho1;
+
+ // Token: 0x040002B6 RID: 694
+ public bool fail_tacho2;
+
+ // Token: 0x040002B7 RID: 695
+ public bool fail_tcms;
+
+ // Token: 0x040002B8 RID: 696
+ public uint formno;
+
+ // Token: 0x040002B9 RID: 697
+ public bool fmc;
+
+ // Token: 0x040002BA RID: 698
+ public bool hcr;
+
+ // Token: 0x040002BB RID: 699
+ public string interface_err_msg;
+
+ // Token: 0x040002BC RID: 700
+ public bool inching;
+
+ // Token: 0x040002BD RID: 701
+ public bool ipdt_ng;
+
+ // Token: 0x040002BE RID: 702
+ public bool ipdt_ok;
+
+ // Token: 0x040002BF RID: 703
+ public string ipdt;
+
+ // Token: 0x040002C0 RID: 704
+ public bool kur;
+
+ // Token: 0x040002C1 RID: 705
+ public uint limitspeed;
+
+ // Token: 0x040002C2 RID: 706
+ public bool limit_drive;
+
+ // Token: 0x040002C3 RID: 707
+ public string marker;
+
+ // Token: 0x040002C4 RID: 708
+ public string mascon;
+
+ // Token: 0x040002C5 RID: 709
+ public bool mascon_br;
+
+ // Token: 0x040002C6 RID: 710
+ public bool mascon_dr;
+
+ // Token: 0x040002C7 RID: 711
+ public bool mascon_eb;
+
+ // Token: 0x040002C8 RID: 712
+ public bool mascon_neu;
+
+ // Token: 0x040002C9 RID: 713
+ public bool mcs;
+
+ // Token: 0x040002CA RID: 714
+ public bool mpdt_ng;
+
+ // Token: 0x040002CB RID: 715
+ public bool mpdt_ok;
+
+ // Token: 0x040002CC RID: 716
+ public bool mpdt_start;
+
+ // Token: 0x040002CD RID: 717
+ public string mpdt;
+
+ // Token: 0x040002CE RID: 718
+ public string nextdoor;
+
+ // Token: 0x040002CF RID: 719
+ public bool nomal;
+
+ // Token: 0x040002D0 RID: 720
+ public int nstn;
+
+ // Token: 0x040002D1 RID: 721
+ public bool over_spd_warning;
+
+ // Token: 0x040002D2 RID: 722
+ public bool osc;
+
+ // Token: 0x040002D3 RID: 723
+ public bool osc_f0_ok;
+
+ // Token: 0x040002D4 RID: 724
+ public double osc_f;
+
+ // Token: 0x040002D5 RID: 725
+ public bool ov_stop2;
+
+ // Token: 0x040002D6 RID: 726
+ public bool pre_brake;
+
+ // Token: 0x040002D7 RID: 727
+ public uint pwm_value;
+
+ // Token: 0x040002D8 RID: 728
+ public bool psd_open;
+
+ // Token: 0x040002D9 RID: 729
+ public bool psd_close;
+
+ // Token: 0x040002DA RID: 730
+ public int pstn;
+
+ // Token: 0x040002DB RID: 731
+ public bool recovery;
+
+ // Token: 0x040002DC RID: 732
+ public bool reversingrod_fwd;
+
+ // Token: 0x040002DD RID: 733
+ public bool reversingrod_neu;
+
+ // Token: 0x040002DE RID: 734
+ public bool reversingrod_rvs;
+
+ // Token: 0x040002DF RID: 735
+ public uint seq;
+
+ // Token: 0x040002E0 RID: 736
+ public bool sh_stop2;
+
+ // Token: 0x040002E1 RID: 737
+ public bool start_enable;
+
+ // Token: 0x040002E2 RID: 738
+ public bool system_active;
+
+ // Token: 0x040002E3 RID: 739
+ public double trainspeed;
+
+ // Token: 0x040002E4 RID: 740
+ public double trainspeed_a;
+
+ // Token: 0x040002E5 RID: 741
+ public double trainspeed_b;
+
+ // Token: 0x040002E6 RID: 742
+ public string trainno;
+
+ // Token: 0x040002E7 RID: 743
+ public bool tasc;
+
+ // Token: 0x040002E8 RID: 744
+ public bool tascdb;
+
+ // Token: 0x040002E9 RID: 745
+ public bool tacho_dir_a;
+
+ // Token: 0x040002EA RID: 746
+ public bool tacho_dir_b;
+
+ // Token: 0x040002EB RID: 747
+ public string tcmsdoor;
+
+ // Token: 0x040002EC RID: 748
+ public bool tc1;
+
+ // Token: 0x040002ED RID: 749
+ public bool tc2;
+
+ // Token: 0x040002EE RID: 750
+ public uint UserCRC;
+
+ // Token: 0x040002EF RID: 751
+ public bool trac_br;
+
+ // Token: 0x040002F0 RID: 752
+ public bool trac_cs;
+
+ // Token: 0x040002F1 RID: 753
+ public bool trac_dr;
+
+ // Token: 0x040002F2 RID: 754
+ public bool trainberth;
+
+ // Token: 0x040002F3 RID: 755
+ public bool tcr;
+
+ // Token: 0x040002F4 RID: 756
+ public string time;
+
+ // Token: 0x040002F5 RID: 757
+ public bool twct_enable;
+
+ // Token: 0x040002F6 RID: 758
+ public bool wheelcheck;
+
+ // Token: 0x040002F7 RID: 759
+ public bool wrongdoor;
+
+ // Token: 0x040002F8 RID: 760
+ public bool vdia_auto;
+
+ // Token: 0x040002F9 RID: 761
+ public bool vdia_doorclose;
+
+ // Token: 0x040002FA RID: 762
+ public bool vdia_dooropen;
+
+ // Token: 0x040002FB RID: 763
+ public bool vdia_fa;
+
+ // Token: 0x040002FC RID: 764
+ public bool vdia_fmc;
+
+ // Token: 0x040002FD RID: 765
+ public bool vdia_fwd;
+
+ // Token: 0x040002FE RID: 766
+ public bool vdia_hcr;
+
+ // Token: 0x040002FF RID: 767
+ public bool vdia_masconbr;
+
+ // Token: 0x04000300 RID: 768
+ public bool vdia_mascondr;
+
+ // Token: 0x04000301 RID: 769
+ public bool vdia_masconeb;
+
+ // Token: 0x04000302 RID: 770
+ public bool vdia_mcs;
+
+ // Token: 0x04000303 RID: 771
+ public bool vdia_neu;
+
+ // Token: 0x04000304 RID: 772
+ public bool vdia_rvs;
+
+ // Token: 0x04000305 RID: 773
+ public bool vdia_tcr;
+
+ // Token: 0x04000306 RID: 774
+ public bool vdia_yard;
+
+ // Token: 0x04000307 RID: 775
+ public bool vdib_auto;
+
+ // Token: 0x04000308 RID: 776
+ public bool vdib_doorclose;
+
+ // Token: 0x04000309 RID: 777
+ public bool vdib_dooropen;
+
+ // Token: 0x0400030A RID: 778
+ public bool vdib_fa;
+
+ // Token: 0x0400030B RID: 779
+ public bool vdib_fmc;
+
+ // Token: 0x0400030C RID: 780
+ public bool vdib_fwd;
+
+ // Token: 0x0400030D RID: 781
+ public bool vdib_hcr;
+
+ // Token: 0x0400030E RID: 782
+ public bool vdib_masconbr;
+
+ // Token: 0x0400030F RID: 783
+ public bool vdib_mascondr;
+
+ // Token: 0x04000310 RID: 784
+ public bool vdib_masconeb;
+
+ // Token: 0x04000311 RID: 785
+ public bool vdib_mcs;
+
+ // Token: 0x04000312 RID: 786
+ public bool vdib_neu;
+
+ // Token: 0x04000313 RID: 787
+ public bool vdib_rvs;
+
+ // Token: 0x04000314 RID: 788
+ public bool vdib_tcr;
+
+ // Token: 0x04000315 RID: 789
+ public bool vdib_yard;
+
+ // Token: 0x04000316 RID: 790
+ public bool vdic_edlfb;
+
+ // Token: 0x04000317 RID: 791
+ public bool vdic_edrfb;
+
+ // Token: 0x04000318 RID: 792
+ public bool vdic_ebmfb;
+
+ // Token: 0x04000319 RID: 793
+ public bool vdic_ebpfb;
+
+ // Token: 0x0400031A RID: 794
+ public bool vdic_fsbfb;
+
+ // Token: 0x0400031B RID: 795
+ public bool vdic_psdclose;
+
+ // Token: 0x0400031C RID: 796
+ public bool vdic_psdopen;
+
+ // Token: 0x0400031D RID: 797
+ public bool vdic_startbtn;
+
+ // Token: 0x0400031E RID: 798
+ public bool vdic_tc1;
+
+ // Token: 0x0400031F RID: 799
+ public bool vdic_tc2;
+
+ // Token: 0x04000320 RID: 800
+ public bool vdic_unit1;
+
+ // Token: 0x04000321 RID: 801
+ public bool vdic_zvrfb;
+
+ // Token: 0x04000322 RID: 802
+ public bool vdid_edlfb;
+
+ // Token: 0x04000323 RID: 803
+ public bool vdid_edrfb;
+
+ // Token: 0x04000324 RID: 804
+ public bool vdid_ebmfb;
+
+ // Token: 0x04000325 RID: 805
+ public bool vdid_ebpfb;
+
+ // Token: 0x04000326 RID: 806
+ public bool vdid_fsbfb;
+
+ // Token: 0x04000327 RID: 807
+ public bool vdid_psdclose;
+
+ // Token: 0x04000328 RID: 808
+ public bool vdid_psdopen;
+
+ // Token: 0x04000329 RID: 809
+ public bool vdid_startbtn;
+
+ // Token: 0x0400032A RID: 810
+ public bool vdid_tc1;
+
+ // Token: 0x0400032B RID: 811
+ public bool vdid_tc2;
+
+ // Token: 0x0400032C RID: 812
+ public bool vdid_unit1;
+
+ // Token: 0x0400032D RID: 813
+ public bool vdid_zvrfb;
+
+ // Token: 0x0400032E RID: 814
+ public bool vdoa_ebm;
+
+ // Token: 0x0400032F RID: 815
+ public bool vdoa_ebp;
+
+ // Token: 0x04000330 RID: 816
+ public bool vdoa_edl;
+
+ // Token: 0x04000331 RID: 817
+ public bool vdoa_edr;
+
+ // Token: 0x04000332 RID: 818
+ public bool vdoa_fsb;
+
+ // Token: 0x04000333 RID: 819
+ public bool vdoa_zvr;
+
+ // Token: 0x04000334 RID: 820
+ public bool vdob_ebm;
+
+ // Token: 0x04000335 RID: 821
+ public bool vdob_ebp;
+
+ // Token: 0x04000336 RID: 822
+ public bool vdob_edl;
+
+ // Token: 0x04000337 RID: 823
+ public bool vdob_edr;
+
+ // Token: 0x04000338 RID: 824
+ public bool vdob_fsb;
+
+ // Token: 0x04000339 RID: 825
+ public bool vdob_zvr;
+
+ // Token: 0x0400033A RID: 826
+ public bool yard;
+
+ // Token: 0x0400033B RID: 827
+ public int messageh;
+
+ // Token: 0x0400033C RID: 828
+ internal bool ov_stop1;
+
+ // Token: 0x0400033D RID: 829
+ internal bool sh_stop1;
+
+ // Token: 0x0400033E RID: 830
+ public uint source;
+
+ // Token: 0x0400033F RID: 831
+ public string text;
+
+ // Token: 0x04000340 RID: 832
+ internal string vdia_input;
+
+ // Token: 0x04000341 RID: 833
+ public string vdia_stat;
+
+ // Token: 0x04000342 RID: 834
+ internal string vdib_input;
+
+ // Token: 0x04000343 RID: 835
+ public string vdib_stat;
+
+ // Token: 0x04000344 RID: 836
+ internal string vdic_input;
+
+ // Token: 0x04000345 RID: 837
+ public string vdic_stat;
+
+ // Token: 0x04000346 RID: 838
+ internal string vdid_input;
+
+ // Token: 0x04000347 RID: 839
+ public string vdid_stat;
+
+ // Token: 0x04000348 RID: 840
+ internal string vdoa_feedback;
+
+ // Token: 0x04000349 RID: 841
+ public string vdoa_stat;
+
+ // Token: 0x0400034A RID: 842
+ internal string vdob_feedback;
+
+ // Token: 0x0400034B RID: 843
+ public string vdob_stat;
+ }
+}
diff --git a/c_Sharp_Code/lib/dositotcms.cs b/c_Sharp_Code/lib/dositotcms.cs
new file mode 100644
index 0000000..dfff264
--- /dev/null
+++ b/c_Sharp_Code/lib/dositotcms.cs
@@ -0,0 +1,585 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000035 RID: 53
+ public class DOSItoTCMS
+ {
+ // Token: 0x06000157 RID: 343 RVA: 0x00027520 File Offset: 0x00025720
+ public static DOSItoTCMSClass SetData(byte[] data)
+ {
+ DOSItoTCMSClass dositoTCMSClass = new DOSItoTCMSClass();
+ dositoTCMSClass.destID = Convert.ToUInt32(data[1]);
+ dositoTCMSClass.sourID = Convert.ToUInt32(data[2]);
+ dositoTCMSClass.frameSeq = Convert.ToUInt32(data[4]);
+ dositoTCMSClass.formNo = Convert.ToUInt32(data[6]);
+ dositoTCMSClass.trNo = string.Format("{0:x4}", (uint)((int)data[7] << 8 | (int)data[8]));
+ dositoTCMSClass.trainSpeed = Convert.ToUInt32(data[9]);
+ dositoTCMSClass.atc1_Ok = (((data[10] & 128) > 0) ? "GOOD" : "BAD");
+ dositoTCMSClass.atc1_Act = (((data[10] & 64) > 0) ? "ACTIVE" : "STANDBY");
+ dositoTCMSClass.atc2_Ok = (((data[10] & 32) > 0) ? "GOOD" : "BAD");
+ dositoTCMSClass.atc2_Act = (((data[10] & 16) > 0) ? "ACTIVE" : "STANDBY");
+ bool flag = (data[10] & 12) == 8;
+ if (flag)
+ {
+ dositoTCMSClass.trSize = "10량";
+ }
+ else
+ {
+ bool flag2 = (data[10] & 12) == 4;
+ if (flag2)
+ {
+ dositoTCMSClass.trSize = "8량";
+ }
+ else
+ {
+ dositoTCMSClass.trSize = "6량";
+ }
+ }
+ dositoTCMSClass.adc = ((data[10] & 2) > 0);
+ dositoTCMSClass.eb_status = ((data[10] & 1) > 0);
+ dositoTCMSClass.wheels1 = 860 - ((data[11] & 248) >> 3) * 5;
+ bool flag3 = (data[11] & 7) == 0;
+ if (flag3)
+ {
+ dositoTCMSClass.drvmode = "Invalid";
+ }
+ else
+ {
+ bool flag4 = (data[11] & 7) == 1;
+ if (flag4)
+ {
+ dositoTCMSClass.drvmode = "FMC";
+ }
+ else
+ {
+ bool flag5 = (data[11] & 7) == 2;
+ if (flag5)
+ {
+ dositoTCMSClass.drvmode = "YARD";
+ }
+ else
+ {
+ bool flag6 = (data[11] & 7) == 3;
+ if (flag6)
+ {
+ dositoTCMSClass.drvmode = "MCS";
+ }
+ else
+ {
+ bool flag7 = (data[11] & 7) == 4;
+ if (flag7)
+ {
+ dositoTCMSClass.drvmode = "AUTO";
+ }
+ else
+ {
+ bool flag8 = (data[11] & 7) == 5;
+ if (flag8)
+ {
+ dositoTCMSClass.drvmode = "FA";
+ }
+ else
+ {
+ dositoTCMSClass.drvmode = "None";
+ }
+ }
+ }
+ }
+ }
+ }
+ bool flag9 = (data[12] & 240) == 0;
+ if (flag9)
+ {
+ dositoTCMSClass.atcCode = "02";
+ }
+ else
+ {
+ bool flag10 = (data[12] & 240) == 16;
+ if (flag10)
+ {
+ dositoTCMSClass.atcCode = "01";
+ }
+ else
+ {
+ bool flag11 = (data[12] & 240) == 32;
+ if (flag11)
+ {
+ dositoTCMSClass.atcCode = "25";
+ }
+ else
+ {
+ bool flag12 = (data[12] & 240) == 48;
+ if (flag12)
+ {
+ dositoTCMSClass.atcCode = "40";
+ }
+ else
+ {
+ bool flag13 = (data[12] & 240) == 64;
+ if (flag13)
+ {
+ dositoTCMSClass.atcCode = "55";
+ }
+ else
+ {
+ bool flag14 = (data[12] & 240) == 80;
+ if (flag14)
+ {
+ dositoTCMSClass.atcCode = "65";
+ }
+ else
+ {
+ bool flag15 = (data[12] & 240) == 96;
+ if (flag15)
+ {
+ dositoTCMSClass.atcCode = "75";
+ }
+ else
+ {
+ bool flag16 = (data[12] & 240) == 112;
+ if (flag16)
+ {
+ dositoTCMSClass.atcCode = "D";
+ }
+ else
+ {
+ bool flag17 = (data[12] & 240) == 128;
+ if (flag17)
+ {
+ dositoTCMSClass.atcCode = "DE";
+ }
+ else
+ {
+ bool flag18 = (data[12] & 240) == 144;
+ if (flag18)
+ {
+ dositoTCMSClass.atcCode = "DW";
+ }
+ else
+ {
+ dositoTCMSClass.atcCode = "None";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ bool flag19 = (data[12] & 3) == 0;
+ if (flag19)
+ {
+ dositoTCMSClass.dr_Mod = "None";
+ }
+ else
+ {
+ bool flag20 = (data[12] & 3) == 1;
+ if (flag20)
+ {
+ dositoTCMSClass.dr_Mod = "M/M";
+ }
+ else
+ {
+ bool flag21 = (data[12] & 3) == 2;
+ if (flag21)
+ {
+ dositoTCMSClass.dr_Mod = "A/M";
+ }
+ else
+ {
+ bool flag22 = (data[12] & 3) == 3;
+ if (flag22)
+ {
+ dositoTCMSClass.dr_Mod = "A/A";
+ }
+ }
+ }
+ }
+ bool flag23 = (data[13] & 240) == 0;
+ if (flag23)
+ {
+ dositoTCMSClass.marker = "None";
+ }
+ else
+ {
+ bool flag24 = (data[13] & 240) == 16;
+ if (flag24)
+ {
+ dositoTCMSClass.marker = "PG1";
+ }
+ else
+ {
+ bool flag25 = (data[13] & 240) == 32;
+ if (flag25)
+ {
+ dositoTCMSClass.marker = "PG2";
+ }
+ else
+ {
+ bool flag26 = (data[13] & 240) == 48;
+ if (flag26)
+ {
+ dositoTCMSClass.marker = "PG3-1";
+ }
+ else
+ {
+ bool flag27 = (data[13] & 240) == 64;
+ if (flag27)
+ {
+ dositoTCMSClass.marker = "PG3-2";
+ }
+ else
+ {
+ bool flag28 = (data[13] & 240) == 80;
+ if (flag28)
+ {
+ dositoTCMSClass.marker = "PG3-3";
+ }
+ else
+ {
+ bool flag29 = (data[13] & 240) == 96;
+ if (flag29)
+ {
+ dositoTCMSClass.marker = "ATS";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ bool flag30 = (data[13] & 12) == 12;
+ if (flag30)
+ {
+ dositoTCMSClass.pdt_RLT = "Progress";
+ }
+ else
+ {
+ bool flag31 = (data[13] & 12) == 8;
+ if (flag31)
+ {
+ dositoTCMSClass.pdt_RLT = "NG";
+ }
+ else
+ {
+ bool flag32 = (data[13] & 12) == 4;
+ if (flag32)
+ {
+ dositoTCMSClass.pdt_RLT = "PASS";
+ }
+ else
+ {
+ bool flag33 = (data[13] & 12) == 0;
+ if (flag33)
+ {
+ dositoTCMSClass.pdt_RLT = "No Test";
+ }
+ }
+ }
+ }
+ dositoTCMSClass.active = ((data[13] & 1) > 0);
+ dositoTCMSClass.dc = ((data[14] & 128) > 0);
+ dositoTCMSClass.b = ((data[14] & 64) > 0);
+ dositoTCMSClass.p = ((data[14] & 32) > 0);
+ dositoTCMSClass.pb_PSD = ((data[14] & 2) > 0);
+ dositoTCMSClass.ovsw = ((data[14] & 1) > 0);
+ dositoTCMSClass.ldo = ((data[15] & 128) > 0);
+ dositoTCMSClass.rdo = ((data[15] & 64) > 0);
+ dositoTCMSClass.lde = ((data[15] & 32) > 0);
+ dositoTCMSClass.rde = ((data[15] & 16) > 0);
+ dositoTCMSClass.dblp = ((data[15] & 8) > 0);
+ dositoTCMSClass.zv = ((data[15] & 4) > 0);
+ dositoTCMSClass.fsb = ((data[15] & 2) > 0);
+ dositoTCMSClass.eb = ((data[15] & 1) > 0);
+ dositoTCMSClass.pwmn = Convert.ToUInt32(data[16]);
+ dositoTCMSClass.twctxen = ((data[17] & 32) > 0);
+ dositoTCMSClass.berth = ((data[17] & 16) > 0);
+ bool flag34 = (data[17] & 14) == 0;
+ if (flag34)
+ {
+ dositoTCMSClass.carrier = "-";
+ }
+ else
+ {
+ bool flag35 = (data[17] & 14) == 2;
+ if (flag35)
+ {
+ dositoTCMSClass.carrier = "F1";
+ }
+ else
+ {
+ bool flag36 = (data[17] & 14) == 4;
+ if (flag36)
+ {
+ dositoTCMSClass.carrier = "F2";
+ }
+ else
+ {
+ bool flag37 = (data[17] & 14) == 6;
+ if (flag37)
+ {
+ dositoTCMSClass.carrier = "F3";
+ }
+ else
+ {
+ bool flag38 = (data[17] & 14) == 8;
+ if (flag38)
+ {
+ dositoTCMSClass.carrier = "F4";
+ }
+ }
+ }
+ }
+ }
+ dositoTCMSClass.tacho_R = ((data[17] & 1) > 0);
+ dositoTCMSClass.romver = string.Format("Ver{0}.{1}", (data[18] & 240) >> 4, (int)(data[18] & 15));
+ dositoTCMSClass.missDoor = ((data[19] & 128) > 0);
+ bool flag39 = (data[19] & 24) == 16;
+ if (flag39)
+ {
+ dositoTCMSClass.next_DR = "오른쪽";
+ }
+ else
+ {
+ bool flag40 = (data[19] & 24) == 8;
+ if (flag40)
+ {
+ dositoTCMSClass.next_DR = "왼쪽";
+ }
+ else
+ {
+ dositoTCMSClass.next_DR = "-";
+ }
+ }
+ dositoTCMSClass.st_VAL = ((data[19] & 4) > 0);
+ dositoTCMSClass.dcw_VAL = ((data[19] & 2) > 0);
+ dositoTCMSClass.dcw = ((data[19] & 1) > 0);
+ dositoTCMSClass.pre_CODE = Convert.ToUInt32(data[20]);
+ dositoTCMSClass.nxt_CODE = Convert.ToUInt32(data[21]);
+ dositoTCMSClass.dst_CODE = Convert.ToUInt32(data[22]);
+ dositoTCMSClass.ato_DTG = Convert.ToDouble((int)data[23] << 8 | (int)data[24]);
+ dositoTCMSClass.wheels2 = 860 - ((data[25] & 248) >> 3) * 5;
+ dositoTCMSClass.man_EB = ((data[25] & 1) > 0);
+ dositoTCMSClass.limitSpeed = Convert.ToUInt32(data[26]);
+ bool flag41 = (data[27] & 128) > 0;
+ if (flag41)
+ {
+ DOSItoTCMSClass dositoTCMSClass2 = dositoTCMSClass;
+ dositoTCMSClass2.errM += "TACHOF:";
+ }
+ else
+ {
+ bool flag42 = (data[27] & 64) > 0;
+ if (flag42)
+ {
+ DOSItoTCMSClass dositoTCMSClass3 = dositoTCMSClass;
+ dositoTCMSClass3.errM += "ATCWSF:";
+ }
+ else
+ {
+ bool flag43 = (data[27] & 32) > 0;
+ if (flag43)
+ {
+ DOSItoTCMSClass dositoTCMSClass4 = dositoTCMSClass;
+ dositoTCMSClass4.errM += "INCHING:";
+ }
+ else
+ {
+ bool flag44 = (data[27] & 16) > 0;
+ if (flag44)
+ {
+ DOSItoTCMSClass dositoTCMSClass5 = dositoTCMSClass;
+ dositoTCMSClass5.errM += "OV_STOP2:";
+ }
+ else
+ {
+ bool flag45 = (data[27] & 8) > 0;
+ if (flag45)
+ {
+ DOSItoTCMSClass dositoTCMSClass6 = dositoTCMSClass;
+ dositoTCMSClass6.errM += "OV_STOP1:";
+ }
+ else
+ {
+ bool flag46 = (data[27] & 4) > 0;
+ if (flag46)
+ {
+ DOSItoTCMSClass dositoTCMSClass7 = dositoTCMSClass;
+ dositoTCMSClass7.errM += "SH_STOP2:";
+ }
+ else
+ {
+ bool flag47 = (data[27] & 2) > 0;
+ if (flag47)
+ {
+ DOSItoTCMSClass dositoTCMSClass8 = dositoTCMSClass;
+ dositoTCMSClass8.errM += "SH_STOP1:";
+ }
+ else
+ {
+ bool flag48 = (data[27] & 1) > 0;
+ if (flag48)
+ {
+ DOSItoTCMSClass dositoTCMSClass9 = dositoTCMSClass;
+ dositoTCMSClass9.errM = (dositoTCMSClass9.errM ?? "");
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ bool flag49 = (data[28] & 128) > 0;
+ if (flag49)
+ {
+ DOSItoTCMSClass dositoTCMSClass10 = dositoTCMSClass;
+ dositoTCMSClass10.errM += "STCODEF:";
+ }
+ else
+ {
+ bool flag50 = (data[28] & 64) > 0;
+ if (flag50)
+ {
+ DOSItoTCMSClass dositoTCMSClass11 = dositoTCMSClass;
+ dositoTCMSClass11.errM += "DOF:";
+ }
+ else
+ {
+ bool flag51 = (data[28] & 32) > 0;
+ if (flag51)
+ {
+ DOSItoTCMSClass dositoTCMSClass12 = dositoTCMSClass;
+ dositoTCMSClass12.errM += "DCF:";
+ }
+ else
+ {
+ bool flag52 = (data[28] & 16) > 0;
+ if (flag52)
+ {
+ DOSItoTCMSClass dositoTCMSClass13 = dositoTCMSClass;
+ dositoTCMSClass13.errM += "ATOPSF:";
+ }
+ else
+ {
+ bool flag53 = (data[28] & 8) > 0;
+ if (flag53)
+ {
+ DOSItoTCMSClass dositoTCMSClass14 = dositoTCMSClass;
+ dositoTCMSClass14.errM += "DRVCAMF:";
+ }
+ else
+ {
+ bool flag54 = (data[28] & 4) > 0;
+ if (flag54)
+ {
+ DOSItoTCMSClass dositoTCMSClass15 = dositoTCMSClass;
+ dositoTCMSClass15.errM += "HCRTCRF:";
+ }
+ else
+ {
+ bool flag55 = (data[28] & 2) > 0;
+ if (flag55)
+ {
+ DOSItoTCMSClass dositoTCMSClass16 = dositoTCMSClass;
+ dositoTCMSClass16.errM += "MASNCF:";
+ }
+ else
+ {
+ bool flag56 = (data[28] & 1) > 0;
+ if (flag56)
+ {
+ DOSItoTCMSClass dositoTCMSClass17 = dositoTCMSClass;
+ dositoTCMSClass17.errM += "BALISEAF:";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ bool flag57 = (data[29] & 128) > 0;
+ if (flag57)
+ {
+ DOSItoTCMSClass dositoTCMSClass18 = dositoTCMSClass;
+ dositoTCMSClass18.errM = (dositoTCMSClass18.errM ?? "");
+ }
+ else
+ {
+ bool flag58 = (data[29] & 64) > 0;
+ if (flag58)
+ {
+ DOSItoTCMSClass dositoTCMSClass19 = dositoTCMSClass;
+ dositoTCMSClass19.errM = (dositoTCMSClass19.errM ?? "");
+ }
+ else
+ {
+ bool flag59 = (data[29] & 32) > 0;
+ if (flag59)
+ {
+ DOSItoTCMSClass dositoTCMSClass20 = dositoTCMSClass;
+ dositoTCMSClass20.errM = (dositoTCMSClass20.errM ?? "");
+ }
+ else
+ {
+ bool flag60 = (data[29] & 16) > 0;
+ if (flag60)
+ {
+ DOSItoTCMSClass dositoTCMSClass21 = dositoTCMSClass;
+ dositoTCMSClass21.errM = (dositoTCMSClass21.errM ?? "");
+ }
+ else
+ {
+ bool flag61 = (data[29] & 8) > 0;
+ if (flag61)
+ {
+ DOSItoTCMSClass dositoTCMSClass22 = dositoTCMSClass;
+ dositoTCMSClass22.errM = (dositoTCMSClass22.errM ?? "");
+ }
+ else
+ {
+ bool flag62 = (data[29] & 4) > 0;
+ if (flag62)
+ {
+ DOSItoTCMSClass dositoTCMSClass23 = dositoTCMSClass;
+ dositoTCMSClass23.errM += "PG32F:";
+ }
+ else
+ {
+ bool flag63 = (data[29] & 2) > 0;
+ if (flag63)
+ {
+ DOSItoTCMSClass dositoTCMSClass24 = dositoTCMSClass;
+ dositoTCMSClass24.errM += "PG2F:";
+ }
+ else
+ {
+ bool flag64 = (data[29] & 1) > 0;
+ if (flag64)
+ {
+ DOSItoTCMSClass dositoTCMSClass25 = dositoTCMSClass;
+ dositoTCMSClass25.errM += "PG1F:";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ bool flag65 = data[27] == 0 && data[28] == 0 && data[29] == 0;
+ if (flag65)
+ {
+ dositoTCMSClass.errM = "";
+ }
+ dositoTCMSClass.UserBCC = (uint)((uint)(data[31] & byte.MaxValue) << 8);
+ dositoTCMSClass.UserBCC |= (uint)(data[32] & byte.MaxValue);
+ dositoTCMSClass.text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ");
+ return dositoTCMSClass;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/dositotcmsclass.cs b/c_Sharp_Code/lib/dositotcmsclass.cs
new file mode 100644
index 0000000..3a8100b
--- /dev/null
+++ b/c_Sharp_Code/lib/dositotcmsclass.cs
@@ -0,0 +1,212 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000034 RID: 52
+ public class DOSItoTCMSClass
+ {
+ // Token: 0x0400034C RID: 844
+ public uint frameSeq;
+
+ // Token: 0x0400034D RID: 845
+ public uint destID;
+
+ // Token: 0x0400034E RID: 846
+ public uint sourID;
+
+ // Token: 0x0400034F RID: 847
+ public uint trainSpeed;
+
+ // Token: 0x04000350 RID: 848
+ public uint limitSpeed;
+
+ // Token: 0x04000351 RID: 849
+ public bool zvr;
+
+ // Token: 0x04000352 RID: 850
+ public bool edl;
+
+ // Token: 0x04000353 RID: 851
+ public bool edr;
+
+ // Token: 0x04000354 RID: 852
+ public bool fsb;
+
+ // Token: 0x04000355 RID: 853
+ public bool eb;
+
+ // Token: 0x04000356 RID: 854
+ public bool manPdtStart;
+
+ // Token: 0x04000357 RID: 855
+ public bool manPdtNg;
+
+ // Token: 0x04000358 RID: 856
+ public bool manPdtOk;
+
+ // Token: 0x04000359 RID: 857
+ public bool initPdtNg;
+
+ // Token: 0x0400035A RID: 858
+ public bool initPdtOk;
+
+ // Token: 0x0400035B RID: 859
+ public bool failAtcr;
+
+ // Token: 0x0400035C RID: 860
+ public bool failAtoc;
+
+ // Token: 0x0400035D RID: 861
+ public bool failHmi;
+
+ // Token: 0x0400035E RID: 862
+ public bool failTacho2;
+
+ // Token: 0x0400035F RID: 863
+ public bool failTacho1;
+
+ // Token: 0x04000360 RID: 864
+ public uint UserBCC;
+
+ // Token: 0x04000361 RID: 865
+ public uint formNo;
+
+ // Token: 0x04000362 RID: 866
+ public string trNo;
+
+ // Token: 0x04000363 RID: 867
+ public string atcStat;
+
+ // Token: 0x04000364 RID: 868
+ public string trSize;
+
+ // Token: 0x04000365 RID: 869
+ public bool adc;
+
+ // Token: 0x04000366 RID: 870
+ public bool eb_status;
+
+ // Token: 0x04000367 RID: 871
+ public int wheels1;
+
+ // Token: 0x04000368 RID: 872
+ public int wheels2;
+
+ // Token: 0x04000369 RID: 873
+ public string drvmode;
+
+ // Token: 0x0400036A RID: 874
+ public string atcCode;
+
+ // Token: 0x0400036B RID: 875
+ public string dr_Mod;
+
+ // Token: 0x0400036C RID: 876
+ public string marker;
+
+ // Token: 0x0400036D RID: 877
+ public string pdt_RLT;
+
+ // Token: 0x0400036E RID: 878
+ public bool active;
+
+ // Token: 0x0400036F RID: 879
+ public bool dc;
+
+ // Token: 0x04000370 RID: 880
+ public bool b;
+
+ // Token: 0x04000371 RID: 881
+ public bool p;
+
+ // Token: 0x04000372 RID: 882
+ public bool pb_PSD;
+
+ // Token: 0x04000373 RID: 883
+ public bool ovsw;
+
+ // Token: 0x04000374 RID: 884
+ public bool ldo;
+
+ // Token: 0x04000375 RID: 885
+ public bool rdo;
+
+ // Token: 0x04000376 RID: 886
+ public bool lde;
+
+ // Token: 0x04000377 RID: 887
+ public bool rde;
+
+ // Token: 0x04000378 RID: 888
+ public bool dblp;
+
+ // Token: 0x04000379 RID: 889
+ public bool zv;
+
+ // Token: 0x0400037A RID: 890
+ public uint pwmn;
+
+ // Token: 0x0400037B RID: 891
+ public bool twctxen;
+
+ // Token: 0x0400037C RID: 892
+ public bool berth;
+
+ // Token: 0x0400037D RID: 893
+ public string carrier;
+
+ // Token: 0x0400037E RID: 894
+ public bool tacho_R;
+
+ // Token: 0x0400037F RID: 895
+ public string romver;
+
+ // Token: 0x04000380 RID: 896
+ public bool missDoor;
+
+ // Token: 0x04000381 RID: 897
+ public string next_DR;
+
+ // Token: 0x04000382 RID: 898
+ public bool st_VAL;
+
+ // Token: 0x04000383 RID: 899
+ public bool dcw_VAL;
+
+ // Token: 0x04000384 RID: 900
+ public bool dcw;
+
+ // Token: 0x04000385 RID: 901
+ public uint pre_CODE;
+
+ // Token: 0x04000386 RID: 902
+ public uint nxt_CODE;
+
+ // Token: 0x04000387 RID: 903
+ public uint dst_CODE;
+
+ // Token: 0x04000388 RID: 904
+ public double ato_DTG;
+
+ // Token: 0x04000389 RID: 905
+ public bool man_EB;
+
+ // Token: 0x0400038A RID: 906
+ public string errM;
+
+ // Token: 0x0400038B RID: 907
+ public string atc1_Ok;
+
+ // Token: 0x0400038C RID: 908
+ public string atc2_Ok;
+
+ // Token: 0x0400038D RID: 909
+ public string atc1_Act;
+
+ // Token: 0x0400038E RID: 910
+ public string atc2_Act;
+
+ // Token: 0x0400038F RID: 911
+ public string text;
+ }
+}
diff --git a/c_Sharp_Code/lib/mergeddata.cs b/c_Sharp_Code/lib/mergeddata.cs
new file mode 100644
index 0000000..a06832b
--- /dev/null
+++ b/c_Sharp_Code/lib/mergeddata.cs
@@ -0,0 +1,95 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000037 RID: 55
+ public class MergedData : ICloneable
+ {
+ // Token: 0x06000159 RID: 345 RVA: 0x00028204 File Offset: 0x00026404
+ public MergedData()
+ {
+ this.acpu200Data = new ACPU200Class();
+ this.acpu201Data = new ACPU201Class();
+ this.acpu202Data = new ACPU202Class();
+ this.atcr250Data = new ATCR250Class();
+ this.atcr501Data = new ATCR501Class();
+ this.twct320Data = new TWCT320Class();
+ this.ator300Data = new ATOR160Class();
+ this.ccc150Data = new CCC150Class();
+ this.ccc151Data = new CCC151Class();
+ this.ccc152Data = new CCC152Class();
+ this.mmiData = new DOSItoMMIClass();
+ this.fmmiDaTa = new MMItoDOSIClass();
+ this.tcmsData = new DOSItoTCMSClass();
+ this.ftcmsData = new TCMStoDOSIClass();
+ }
+
+ // Token: 0x0600015A RID: 346 RVA: 0x000282B4 File Offset: 0x000264B4
+ public object Clone()
+ {
+ return new MergedData
+ {
+ acpu200Data = this.acpu200Data,
+ acpu201Data = this.acpu201Data,
+ acpu202Data = this.acpu202Data,
+ atcr250Data = this.atcr250Data,
+ atcr501Data = this.atcr501Data,
+ twct320Data = this.twct320Data,
+ ator300Data = this.ator300Data,
+ ccc150Data = this.ccc150Data,
+ ccc151Data = this.ccc151Data,
+ ccc152Data = this.ccc152Data,
+ mmiData = this.mmiData,
+ fmmiDaTa = this.fmmiDaTa,
+ tcmsData = this.tcmsData,
+ ftcmsData = this.ftcmsData,
+ Time = this.Time
+ };
+ }
+
+ // Token: 0x0400039E RID: 926
+ public DateTime Time;
+
+ // Token: 0x0400039F RID: 927
+ public ACPU200Class acpu200Data;
+
+ // Token: 0x040003A0 RID: 928
+ public ACPU201Class acpu201Data;
+
+ // Token: 0x040003A1 RID: 929
+ public ACPU202Class acpu202Data;
+
+ // Token: 0x040003A2 RID: 930
+ public ATCR250Class atcr250Data;
+
+ // Token: 0x040003A3 RID: 931
+ public TWCT320Class twct320Data;
+
+ // Token: 0x040003A4 RID: 932
+ public ATCR501Class atcr501Data;
+
+ // Token: 0x040003A5 RID: 933
+ public ATOR160Class ator300Data;
+
+ // Token: 0x040003A6 RID: 934
+ public CCC150Class ccc150Data;
+
+ // Token: 0x040003A7 RID: 935
+ public CCC151Class ccc151Data;
+
+ // Token: 0x040003A8 RID: 936
+ public CCC152Class ccc152Data;
+
+ // Token: 0x040003A9 RID: 937
+ public DOSItoMMIClass mmiData;
+
+ // Token: 0x040003AA RID: 938
+ public MMItoDOSIClass fmmiDaTa;
+
+ // Token: 0x040003AB RID: 939
+ public DOSItoTCMSClass tcmsData;
+
+ // Token: 0x040003AC RID: 940
+ public TCMStoDOSIClass ftcmsData;
+ }
+}
diff --git a/c_Sharp_Code/lib/mmitodosi.cs b/c_Sharp_Code/lib/mmitodosi.cs
new file mode 100644
index 0000000..1560851
--- /dev/null
+++ b/c_Sharp_Code/lib/mmitodosi.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000039 RID: 57
+ public class MMItoDOSI
+ {
+ // Token: 0x0600015C RID: 348 RVA: 0x0002838C File Offset: 0x0002658C
+ public static MMItoDOSIClass SetData(byte[] data)
+ {
+ return new MMItoDOSIClass
+ {
+ seq = Convert.ToInt32(data[1]),
+ wheelDIA1 = Convert.ToInt32(data[2]),
+ wheelDIA2 = Convert.ToInt32(data[3]),
+ manualPDTRequest = ((data[6] & 128) > 0),
+ confirmWheelDIA = ((data[6] & 16) > 0),
+ recovery = ((data[6] & 2) > 0),
+ recoveryNomal = ((data[6] & 1) > 0),
+ formNo = Convert.ToInt32(data[7]),
+ ackMh = Convert.ToInt32(data[9]),
+ text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ")
+ };
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/mmotodosiclass.cs b/c_Sharp_Code/lib/mmotodosiclass.cs
new file mode 100644
index 0000000..9d8604f
--- /dev/null
+++ b/c_Sharp_Code/lib/mmotodosiclass.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000038 RID: 56
+ public class MMItoDOSIClass
+ {
+ // Token: 0x040003AD RID: 941
+ public int seq;
+
+ // Token: 0x040003AE RID: 942
+ public int wheelDIA1;
+
+ // Token: 0x040003AF RID: 943
+ public int wheelDIA2;
+
+ // Token: 0x040003B0 RID: 944
+ public bool manualPDTRequest;
+
+ // Token: 0x040003B1 RID: 945
+ public bool confirmWheelDIA;
+
+ // Token: 0x040003B2 RID: 946
+ public bool recovery;
+
+ // Token: 0x040003B3 RID: 947
+ public bool recoveryNomal;
+
+ // Token: 0x040003B4 RID: 948
+ public int formNo;
+
+ // Token: 0x040003B5 RID: 949
+ public int ackMh;
+
+ // Token: 0x040003B6 RID: 950
+ public string text;
+ }
+}
diff --git a/c_Sharp_Code/lib/tcmstodosi.cs b/c_Sharp_Code/lib/tcmstodosi.cs
new file mode 100644
index 0000000..52fffe7
--- /dev/null
+++ b/c_Sharp_Code/lib/tcmstodosi.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200003B RID: 59
+ public class TCMStoDOSI
+ {
+ // Token: 0x0600015F RID: 351 RVA: 0x0002846C File Offset: 0x0002666C
+ public static TCMStoDOSIClass SetData(byte[] data)
+ {
+ TCMStoDOSIClass tcmstoDOSIClass = new TCMStoDOSIClass();
+ tcmstoDOSIClass.seq = Convert.ToInt32(data[4]);
+ tcmstoDOSIClass.time = "20";
+ TCMStoDOSIClass tcmstoDOSIClass2 = tcmstoDOSIClass;
+ tcmstoDOSIClass2.time += string.Format("{0:x2}.", data[5]);
+ TCMStoDOSIClass tcmstoDOSIClass3 = tcmstoDOSIClass;
+ tcmstoDOSIClass3.time += string.Format("{0:x2}.", data[6]);
+ TCMStoDOSIClass tcmstoDOSIClass4 = tcmstoDOSIClass;
+ tcmstoDOSIClass4.time += string.Format("{0:x2} ", data[7]);
+ TCMStoDOSIClass tcmstoDOSIClass5 = tcmstoDOSIClass;
+ tcmstoDOSIClass5.time += string.Format("{0:x2}:", data[8]);
+ TCMStoDOSIClass tcmstoDOSIClass6 = tcmstoDOSIClass;
+ tcmstoDOSIClass6.time += string.Format("{0:x2}:", data[9]);
+ TCMStoDOSIClass tcmstoDOSIClass7 = tcmstoDOSIClass;
+ tcmstoDOSIClass7.time += string.Format("{0:x2}", data[10]);
+ tcmstoDOSIClass.trainNo = string.Format("{0:x4}", (uint)((int)data[11] << 8 | (int)data[12]));
+ tcmstoDOSIClass.cabno = ((data[13] & 128) > 0);
+ tcmstoDOSIClass.dcpb = ((data[13] & 64) > 0);
+ tcmstoDOSIClass.trn_eb = ((data[13] & 32) > 0);
+ tcmstoDOSIClass.rdop = ((data[13] & 8) > 0);
+ tcmstoDOSIClass.ldop = ((data[13] & 4) > 0);
+ tcmstoDOSIClass.pdt_cancel = ((data[13] & 2) > 0);
+ tcmstoDOSIClass.pdt_request = ((data[13] & 1) > 0);
+ tcmstoDOSIClass.pre_stn = Convert.ToInt32(data[14]);
+ tcmstoDOSIClass.next_stn = Convert.ToInt32(data[15]);
+ tcmstoDOSIClass.dest_stn = Convert.ToInt32(data[16]);
+ tcmstoDOSIClass.text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ");
+ return tcmstoDOSIClass;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/tcmstodosiclass.cs b/c_Sharp_Code/lib/tcmstodosiclass.cs
new file mode 100644
index 0000000..b7f3dda
--- /dev/null
+++ b/c_Sharp_Code/lib/tcmstodosiclass.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200003A RID: 58
+ public class TCMStoDOSIClass
+ {
+ // Token: 0x040003B7 RID: 951
+ public int seq;
+
+ // Token: 0x040003B8 RID: 952
+ public string time;
+
+ // Token: 0x040003B9 RID: 953
+ public bool cabno;
+
+ // Token: 0x040003BA RID: 954
+ public string trainNo;
+
+ // Token: 0x040003BB RID: 955
+ public bool rdop;
+
+ // Token: 0x040003BC RID: 956
+ public bool ldop;
+
+ // Token: 0x040003BD RID: 957
+ public bool trn_eb;
+
+ // Token: 0x040003BE RID: 958
+ public bool dcpb;
+
+ // Token: 0x040003BF RID: 959
+ public bool pdt_cancel;
+
+ // Token: 0x040003C0 RID: 960
+ public bool pdt_request;
+
+ // Token: 0x040003C1 RID: 961
+ public int pre_stn;
+
+ // Token: 0x040003C2 RID: 962
+ public int next_stn;
+
+ // Token: 0x040003C3 RID: 963
+ public int dest_stn;
+
+ // Token: 0x040003C4 RID: 964
+ public string text;
+ }
+}
diff --git a/c_Sharp_Code/lib/twc320.cs b/c_Sharp_Code/lib/twc320.cs
new file mode 100644
index 0000000..7268b53
--- /dev/null
+++ b/c_Sharp_Code/lib/twc320.cs
@@ -0,0 +1,97 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200003D RID: 61
+ public class TWCT320
+ {
+ // Token: 0x06000162 RID: 354 RVA: 0x0002867C File Offset: 0x0002687C
+ public static TWCT320Class SetData(byte[] data)
+ {
+ TWCT320Class twct320Class = new TWCT320Class();
+ twct320Class.seq = Convert.ToInt32(data[0]);
+ string text = Convert.ToString((int)data[1] << 8 | (int)data[2], 16);
+ twct320Class.trainNumber = text.Substring(3) + text.Substring(0, 3);
+ twct320Class.destNumber = Convert.ToInt32(data[3]);
+ twct320Class.formNumber = Convert.ToInt32(data[4]);
+ twct320Class.dopr = ((data[5] & 128) > 0);
+ twct320Class.zvr = ((data[5] & 64) > 0);
+ twct320Class.pg3r = ((data[5] & 32) > 0);
+ twct320Class.atocr = ((data[5] & 16) > 0);
+ twct320Class.hcr = ((data[5] & 8) > 0);
+ twct320Class.txen = ((data[5] & 4) > 0);
+ bool flag = (data[5] & 3) == 0;
+ if (flag)
+ {
+ twct320Class.trainR = 6;
+ }
+ else
+ {
+ bool flag2 = (data[5] & 3) == 1;
+ if (flag2)
+ {
+ twct320Class.trainR = 8;
+ }
+ else
+ {
+ bool flag3 = (data[5] & 3) == 2;
+ if (flag3)
+ {
+ twct320Class.trainR = 10;
+ }
+ }
+ }
+ twct320Class.oldnew = ((data[6] & 8) > 0);
+ bool flag4 = (data[6] & 28) == 0;
+ if (flag4)
+ {
+ twct320Class.drvmode = "Invalid";
+ }
+ else
+ {
+ bool flag5 = (data[6] & 28) == 1;
+ if (flag5)
+ {
+ twct320Class.drvmode = "FMC";
+ }
+ else
+ {
+ bool flag6 = (data[6] & 28) == 2;
+ if (flag6)
+ {
+ twct320Class.drvmode = "YARD";
+ }
+ else
+ {
+ bool flag7 = (data[6] & 28) == 3;
+ if (flag7)
+ {
+ twct320Class.drvmode = "MCS";
+ }
+ else
+ {
+ bool flag8 = (data[6] & 28) == 4;
+ if (flag8)
+ {
+ twct320Class.drvmode = "AUTO";
+ }
+ else
+ {
+ bool flag9 = (data[6] & 28) == 5;
+ if (flag9)
+ {
+ twct320Class.drvmode = "FA";
+ }
+ else
+ {
+ twct320Class.drvmode = "None";
+ }
+ }
+ }
+ }
+ }
+ }
+ return twct320Class;
+ }
+ }
+}
diff --git a/c_Sharp_Code/lib/twc320class.cs b/c_Sharp_Code/lib/twc320class.cs
new file mode 100644
index 0000000..b8d4500
--- /dev/null
+++ b/c_Sharp_Code/lib/twc320class.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200003C RID: 60
+ public class TWCT320Class
+ {
+ // Token: 0x040003C5 RID: 965
+ public int seq;
+
+ // Token: 0x040003C6 RID: 966
+ public string trainNumber;
+
+ // Token: 0x040003C7 RID: 967
+ public int destNumber;
+
+ // Token: 0x040003C8 RID: 968
+ public int formNumber;
+
+ // Token: 0x040003C9 RID: 969
+ public bool dopr;
+
+ // Token: 0x040003CA RID: 970
+ public bool zvr;
+
+ // Token: 0x040003CB RID: 971
+ public bool atocr;
+
+ // Token: 0x040003CC RID: 972
+ public bool pg3r;
+
+ // Token: 0x040003CD RID: 973
+ public bool hcr;
+
+ // Token: 0x040003CE RID: 974
+ public bool txen;
+
+ // Token: 0x040003CF RID: 975
+ public int trainR;
+
+ // Token: 0x040003D0 RID: 976
+ public string drvmode;
+
+ // Token: 0x040003D1 RID: 977
+ public bool oldnew;
+
+ // Token: 0x040003D2 RID: 978
+ public bool tbSet;
+ }
+}
diff --git a/c_Sharp_Code/lib/vdiab.cs b/c_Sharp_Code/lib/vdiab.cs
new file mode 100644
index 0000000..c387563
--- /dev/null
+++ b/c_Sharp_Code/lib/vdiab.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200003E RID: 62
+ public class VDIABClass
+ {
+ // Token: 0x040003D3 RID: 979
+ public bool zvr;
+
+ // Token: 0x040003D4 RID: 980
+ public bool edl;
+
+ // Token: 0x040003D5 RID: 981
+ public bool edr;
+
+ // Token: 0x040003D6 RID: 982
+ public bool fsb;
+
+ // Token: 0x040003D7 RID: 983
+ public bool ebp;
+
+ // Token: 0x040003D8 RID: 984
+ public bool ebm;
+
+ // Token: 0x040003D9 RID: 985
+ public bool neu;
+
+ // Token: 0x040003DA RID: 986
+ public bool fwd;
+
+ // Token: 0x040003DB RID: 987
+ public bool rvs;
+
+ // Token: 0x040003DC RID: 988
+ public bool hcr;
+
+ // Token: 0x040003DD RID: 989
+ public bool tcr;
+ }
+}
diff --git a/c_Sharp_Code/lib/vdicdclass.cs b/c_Sharp_Code/lib/vdicdclass.cs
new file mode 100644
index 0000000..3c6e54d
--- /dev/null
+++ b/c_Sharp_Code/lib/vdicdclass.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x0200003F RID: 63
+ public class VDICDClass
+ {
+ }
+}
diff --git a/c_Sharp_Code/lib/vdoclass.cs b/c_Sharp_Code/lib/vdoclass.cs
new file mode 100644
index 0000000..1e8642b
--- /dev/null
+++ b/c_Sharp_Code/lib/vdoclass.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace SL200_RTLogViewer.lib
+{
+ // Token: 0x02000040 RID: 64
+ public class VDOClass
+ {
+ }
+}
diff --git a/c_Sharp_Code/program.cs b/c_Sharp_Code/program.cs
new file mode 100644
index 0000000..9e3c7e7
--- /dev/null
+++ b/c_Sharp_Code/program.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer
+{
+ // Token: 0x02000004 RID: 4
+ internal static class Program
+ {
+ // Token: 0x06000020 RID: 32 RVA: 0x0000452D File Offset: 0x0000272D
+ [STAThread]
+ private static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new Form1());
+ }
+ }
+}
diff --git a/c_Sharp_Code/properties/resources.cs b/c_Sharp_Code/properties/resources.cs
new file mode 100644
index 0000000..b007dd7
--- /dev/null
+++ b/c_Sharp_Code/properties/resources.cs
@@ -0,0 +1,117 @@
+using System;
+using System.CodeDom.Compiler;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Globalization;
+using System.Resources;
+using System.Runtime.CompilerServices;
+
+namespace SL200_RTLogViewer.Properties
+{
+ // Token: 0x02000009 RID: 9
+ [GeneratedCode("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [DebuggerNonUserCode]
+ [CompilerGenerated]
+ internal class Resources
+ {
+ // Token: 0x06000035 RID: 53 RVA: 0x000056D2 File Offset: 0x000038D2
+ internal Resources()
+ {
+ }
+
+ // Token: 0x17000006 RID: 6
+ // (get) Token: 0x06000036 RID: 54 RVA: 0x000056DC File Offset: 0x000038DC
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ internal static ResourceManager ResourceManager
+ {
+ get
+ {
+ bool flag = Resources.resourceMan == null;
+ if (flag)
+ {
+ ResourceManager resourceManager = new ResourceManager("SL200_RTLogViewer.Properties.Resources", typeof(Resources).Assembly);
+ Resources.resourceMan = resourceManager;
+ }
+ return Resources.resourceMan;
+ }
+ }
+
+ // Token: 0x17000007 RID: 7
+ // (get) Token: 0x06000037 RID: 55 RVA: 0x00005724 File Offset: 0x00003924
+ // (set) Token: 0x06000038 RID: 56 RVA: 0x0000573B File Offset: 0x0000393B
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ internal static CultureInfo Culture
+ {
+ get
+ {
+ return Resources.resourceCulture;
+ }
+ set
+ {
+ Resources.resourceCulture = value;
+ }
+ }
+
+ // Token: 0x17000008 RID: 8
+ // (get) Token: 0x06000039 RID: 57 RVA: 0x00005744 File Offset: 0x00003944
+ internal static Bitmap minus
+ {
+ get
+ {
+ object @object = Resources.ResourceManager.GetObject("minus", Resources.resourceCulture);
+ return (Bitmap)@object;
+ }
+ }
+
+ // Token: 0x17000009 RID: 9
+ // (get) Token: 0x0600003A RID: 58 RVA: 0x00005774 File Offset: 0x00003974
+ internal static Bitmap plus_
+ {
+ get
+ {
+ object @object = Resources.ResourceManager.GetObject("plus_", Resources.resourceCulture);
+ return (Bitmap)@object;
+ }
+ }
+
+ // Token: 0x1700000A RID: 10
+ // (get) Token: 0x0600003B RID: 59 RVA: 0x000057A4 File Offset: 0x000039A4
+ internal static Bitmap realtime
+ {
+ get
+ {
+ object @object = Resources.ResourceManager.GetObject("realtime", Resources.resourceCulture);
+ return (Bitmap)@object;
+ }
+ }
+
+ // Token: 0x1700000B RID: 11
+ // (get) Token: 0x0600003C RID: 60 RVA: 0x000057D4 File Offset: 0x000039D4
+ internal static Bitmap saved
+ {
+ get
+ {
+ object @object = Resources.ResourceManager.GetObject("saved", Resources.resourceCulture);
+ return (Bitmap)@object;
+ }
+ }
+
+ // Token: 0x1700000C RID: 12
+ // (get) Token: 0x0600003D RID: 61 RVA: 0x00005804 File Offset: 0x00003A04
+ internal static Bitmap zoom_last
+ {
+ get
+ {
+ object @object = Resources.ResourceManager.GetObject("zoom-last", Resources.resourceCulture);
+ return (Bitmap)@object;
+ }
+ }
+
+ // Token: 0x04000027 RID: 39
+ private static ResourceManager resourceMan;
+
+ // Token: 0x04000028 RID: 40
+ private static CultureInfo resourceCulture;
+ }
+}
diff --git a/c_Sharp_Code/properties/settings.cs b/c_Sharp_Code/properties/settings.cs
new file mode 100644
index 0000000..2f3f191
--- /dev/null
+++ b/c_Sharp_Code/properties/settings.cs
@@ -0,0 +1,26 @@
+using System;
+using System.CodeDom.Compiler;
+using System.Configuration;
+using System.Runtime.CompilerServices;
+
+namespace SL200_RTLogViewer.Properties
+{
+ // Token: 0x0200000A RID: 10
+ [CompilerGenerated]
+ [GeneratedCode("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed class Settings : ApplicationSettingsBase
+ {
+ // Token: 0x1700000D RID: 13
+ // (get) Token: 0x0600003E RID: 62 RVA: 0x00005834 File Offset: 0x00003A34
+ public static Settings Default
+ {
+ get
+ {
+ return Settings.defaultInstance;
+ }
+ }
+
+ // Token: 0x04000029 RID: 41
+ private static Settings defaultInstance = (Settings)SettingsBase.Synchronized(new Settings());
+ }
+}
diff --git a/c_Sharp_Code/stationinfo.cs b/c_Sharp_Code/stationinfo.cs
new file mode 100644
index 0000000..52c1980
--- /dev/null
+++ b/c_Sharp_Code/stationinfo.cs
@@ -0,0 +1,83 @@
+using System;
+
+namespace SL200_RTLogViewer
+{
+ // Token: 0x02000007 RID: 7
+ public class StationInfo
+ {
+ // Token: 0x17000001 RID: 1
+ // (get) Token: 0x06000022 RID: 34 RVA: 0x00004548 File Offset: 0x00002748
+ public double Pos
+ {
+ get
+ {
+ return this.pos;
+ }
+ }
+
+ // Token: 0x17000002 RID: 2
+ // (get) Token: 0x06000023 RID: 35 RVA: 0x00004560 File Offset: 0x00002760
+ public int AtcCode
+ {
+ get
+ {
+ return this.atcCode;
+ }
+ }
+
+ // Token: 0x17000003 RID: 3
+ // (get) Token: 0x06000024 RID: 36 RVA: 0x00004578 File Offset: 0x00002778
+ public string StationName
+ {
+ get
+ {
+ return this.stationName;
+ }
+ }
+
+ // Token: 0x17000004 RID: 4
+ // (get) Token: 0x06000025 RID: 37 RVA: 0x00004590 File Offset: 0x00002790
+ public int StationCode
+ {
+ get
+ {
+ return this.stationCode;
+ }
+ }
+
+ // Token: 0x17000005 RID: 5
+ // (get) Token: 0x06000026 RID: 38 RVA: 0x000045A8 File Offset: 0x000027A8
+ public DoorDirection NextDoorDir
+ {
+ get
+ {
+ return this.nextDoorDir;
+ }
+ }
+
+ // Token: 0x06000027 RID: 39 RVA: 0x000045C0 File Offset: 0x000027C0
+ public StationInfo(double xPos, int atc, string name, DoorDirection nextDoordir, int stationNo)
+ {
+ this.pos = xPos;
+ this.atcCode = atc;
+ this.stationName = name;
+ this.nextDoorDir = nextDoordir;
+ this.stationCode = stationNo;
+ }
+
+ // Token: 0x04000020 RID: 32
+ private double pos;
+
+ // Token: 0x04000021 RID: 33
+ private int atcCode;
+
+ // Token: 0x04000022 RID: 34
+ private string stationName;
+
+ // Token: 0x04000023 RID: 35
+ private int stationCode;
+
+ // Token: 0x04000024 RID: 36
+ private DoorDirection nextDoorDir;
+ }
+}
diff --git a/c_Sharp_Code/subFrom/SavedForm.cs b/c_Sharp_Code/subFrom/SavedForm.cs
new file mode 100644
index 0000000..dccb7b2
--- /dev/null
+++ b/c_Sharp_Code/subFrom/SavedForm.cs
@@ -0,0 +1,1374 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Windows.Forms;
+using SL200_RTLogViewer.lib;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000016 RID: 22
+ public class SavedlogForm : Form
+ {
+ // Token: 0x060000F9 RID: 249 RVA: 0x00020208 File Offset: 0x0001E408
+ public SavedlogForm()
+ {
+ this.InitializeComponent();
+ base.AutoScaleMode = AutoScaleMode.Font;
+ }
+
+ // Token: 0x060000FA RID: 250 RVA: 0x000202A0 File Offset: 0x0001E4A0
+ private void SavedlogForm_Load(object sender, EventArgs e)
+ {
+ this._analysisID = new uint[]
+ {
+ 512U,
+ 513U,
+ 514U,
+ 592U,
+ 1281U,
+ 336U,
+ 337U,
+ 338U,
+ 2048U,
+ 2128U,
+ 2304U,
+ 2305U
+ };
+ this.mmiUI = new mmiUI();
+ this.mmiUI.FormBorderStyle = FormBorderStyle.None;
+ this.mmiUI.TopLevel = false;
+ this.mmiUI.TopMost = true;
+ this.panel1.Controls.Add(this.mmiUI);
+ this.mmiUI.SenderEvent += this.idxSetter;
+ this.mmiUI.Show();
+ this.mmiUI.Dock = DockStyle.Fill;
+ this.mmiUI.cb_TWCR.Visible = true;
+ this.isUp = true;
+ }
+
+ // Token: 0x060000FB RID: 251 RVA: 0x00020359 File Offset: 0x0001E559
+ public void idxSetter(int idx)
+ {
+ this.lblDataIdx.Text = idx.ToString();
+ this._dataIndex = idx;
+ }
+
+ // Token: 0x060000FC RID: 252 RVA: 0x00020378 File Offset: 0x0001E578
+ private void bt_fileOpen_Click(object sender, EventArgs e)
+ {
+ OpenFileDialog openFileDialog = new OpenFileDialog();
+ bool flag = DialogResult.OK != openFileDialog.ShowDialog();
+ if (!flag)
+ {
+ this.Cursor = Cursors.WaitCursor;
+ this.stationList = this.setBasicTwcStation();
+ bool flag2 = this.OpenLogFile(openFileDialog.FileName);
+ if (flag2)
+ {
+ this.lblDataCount.Enabled = true;
+ this.lblDataIdx.Enabled = true;
+ this.lblDataCount.Text = this._dataSource1.Count.ToString();
+ this.lblDataIdx.Text = this._dataIndex.ToString();
+ this.lbl_STATUS.Text = Path.GetFileName(openFileDialog.FileName);
+ this.isUp = (Path.GetExtension(openFileDialog.FileName)[3] == '1');
+ bool flag3 = Path.GetExtension(openFileDialog.FileName)[3] == 't';
+ if (flag3)
+ {
+ this.isUp = this._dataSource[10].mmiData.tc1;
+ }
+ this.addTwcrStation(this.stationList);
+ string directoryName = Path.GetDirectoryName(openFileDialog.FileName);
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(openFileDialog.FileName);
+ this.lbl_formNo.Text = fileNameWithoutExtension;
+ this.lbl_LOGFILE.Enabled = true;
+ this.lbl_LOGFILE.BackColor = Color.Green;
+ bool flag4 = this.lbl_STATUS.Text.Length > 16;
+ if (flag4)
+ {
+ this.lbl_formNo.Text = this.lbl_STATUS.Text.Substring(16, 2) + " 편성";
+ }
+ else
+ {
+ this.lbl_formNo.Text = this._dataSource[100].tcmsData.formNo.ToString() + " 편성";
+ }
+ this.buttonPREV_FAST.Enabled = true;
+ this.buttonPREV.Enabled = true;
+ this.buttonNEXT.Enabled = true;
+ this.buttonNEXT_FAST.Enabled = true;
+ this.checkBox1.Checked = true;
+ this.ShowChart();
+ }
+ this.Cursor = Cursors.Default;
+ }
+ }
+
+ // Token: 0x060000FD RID: 253 RVA: 0x000205B0 File Offset: 0x0001E7B0
+ public List setBasicTwcStation()
+ {
+ return new List
+ {
+ "다대포해",
+ "다대포항",
+ "낫개",
+ "신장림",
+ "장림",
+ "동매",
+ "신평",
+ "하단",
+ "당리",
+ "사하",
+ "괴정",
+ "대티",
+ "서대신",
+ "동대신",
+ "토성",
+ "자갈치",
+ "남포",
+ "중앙",
+ "부산역",
+ "초량",
+ "부산진",
+ "좌천",
+ "범일",
+ "범내골",
+ "서면",
+ "부전",
+ "양정",
+ "시청",
+ "연산",
+ "교대",
+ "동래",
+ "명륜",
+ "온천장",
+ "부산대",
+ "장전",
+ "구서",
+ "두실",
+ "남산",
+ "범어사",
+ "노포"
+ };
+ }
+
+ // Token: 0x060000FE RID: 254 RVA: 0x000207AC File Offset: 0x0001E9AC
+ public void addTwcrStation(List stationList)
+ {
+ this.mmiUI.cb_TWCR.Items.Clear();
+ this._stationInfoList = this.FindStationInfo(this.isUp, true);
+ bool flag = this._stationInfoList == null;
+ if (!flag)
+ {
+ foreach (StationInfo stationInfo in this._stationInfoList)
+ {
+ for (int i = 0; i < stationList.Count; i++)
+ {
+ bool flag2 = stationList[i] == stationInfo.StationName;
+ if (flag2)
+ {
+ stationList.RemoveAt(i);
+ }
+ }
+ this.mmiUI.cb_TWCR.Items.Add(stationInfo);
+ }
+ this.mmiUI._mstationInfoList = this._stationInfoList;
+ this.mmiUI.cb_TWCR.Text = "- STATION -" + this.mmiUI.cb_TWCR.Items.Count.ToString();
+ bool flag3 = this.mmiUI.cb_TWCR.Items.Count < 1;
+ if (flag3)
+ {
+ this.mmiUI.cb_TWCR.Text = "-TWC 미수신 상태- ";
+ }
+ else
+ {
+ bool flag4 = this.mmiUI.cb_TWCR.Items.Count < 7;
+ if (flag4)
+ {
+ this.mmiUI.cb_TWCR.Text = "-TWC(" + this.mmiUI.cb_TWCR.Items.Count.ToString() + ")|출고 및 회송|";
+ }
+ else
+ {
+ bool flag5 = stationList.Count > 0 && stationList.Count < 40;
+ if (flag5)
+ {
+ string str = string.Join("l", stationList);
+ this.mmiUI.cb_TWCR.Text = "-TWC(" + this.mmiUI.cb_TWCR.Items.Count.ToString() + ") 미수신역:" + str;
+ }
+ else
+ {
+ this.mmiUI.cb_TWCR.Text = "- TWC 모든역수신 -" + this.mmiUI.cb_TWCR.Items.Count.ToString();
+ }
+ }
+ }
+ this.mmiUI.tt.SetToolTip(this.mmiUI.cb_TWCR, this.mmiUI.cb_TWCR.Text);
+ }
+ }
+
+ // Token: 0x060000FF RID: 255 RVA: 0x00020A48 File Offset: 0x0001EC48
+ private bool OpenLogFile(string fullPath)
+ {
+ this.lblDataIdx.Text = "0";
+ this.clearData();
+ this._canLogDataSource.Clear();
+ this.mmiUI.cb_TWCR.Items.Clear();
+ this._prevBuff = new Dictionary();
+ foreach (uint key in this._analysisID)
+ {
+ this._prevBuff.Add(key, new byte[0]);
+ }
+ bool result = true;
+ FileStream fileStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read);
+ BinaryReader binaryReader = new BinaryReader(fileStream);
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPath);
+ string extension = Path.GetExtension(fullPath);
+ try
+ {
+ bool flag = extension == ".dat";
+ DateTime dateTime;
+ if (flag)
+ {
+ this.formNo = int.Parse(fileNameWithoutExtension.Substring(16, 2));
+ dateTime = DateTime.Parse(string.Format("{0}-{1}-{2} {3}:{4}:{5}", new object[]
+ {
+ fileNameWithoutExtension.Substring(2, 2),
+ fileNameWithoutExtension.Substring(4, 2),
+ fileNameWithoutExtension.Substring(6, 2),
+ fileNameWithoutExtension.Substring(9, 2),
+ fileNameWithoutExtension.Substring(11, 2),
+ fileNameWithoutExtension.Substring(13, 2)
+ }));
+ }
+ else
+ {
+ string text = extension.Substring(1, 2);
+ dateTime = DateTime.Parse(string.Format("{0}-{1}-{2} {3}:{4}", new object[]
+ {
+ text,
+ fileNameWithoutExtension.Substring(0, 2),
+ fileNameWithoutExtension.Substring(2, 2),
+ fileNameWithoutExtension.Substring(4, 2),
+ fileNameWithoutExtension.Substring(6, 2)
+ }));
+ }
+ DateTime dateTime2 = dateTime.AddMinutes(1.0);
+ long length = binaryReader.BaseStream.Length;
+ byte[] array = new byte[1];
+ byte[] array2 = new byte[6];
+ byte[] array3 = new byte[2];
+ byte[] array4 = new byte[1];
+ int num = 0;
+ while (fileStream.Position < length)
+ {
+ bool flag2 = fileStream.Position + (long)num < length;
+ if (flag2)
+ {
+ bool flag3 = true;
+ DateTime dateTime3 = dateTime;
+ bool flag4 = flag3 && dateTime3 >= dateTime;
+ if (flag4)
+ {
+ dateTime = dateTime3;
+ dateTime2 = dateTime3.AddDays(1.0);
+ CanLogData item = new CanLogData(dateTime3);
+ this._canLogDataSource.Add(item);
+ this._allCanData.Add(new CanLogData(dateTime3));
+ }
+ else
+ {
+ bool flag5 = flag3;
+ if (flag5)
+ {
+ dateTime = dateTime3;
+ dateTime2 = dateTime3.AddDays(1.0);
+ CanLogData item2 = new CanLogData(dateTime3);
+ this._canLogDataSource.Add(item2);
+ this._allCanData.Add(new CanLogData(dateTime3));
+ }
+ else
+ {
+ fileStream.Seek((long)(-(long)num), SeekOrigin.Current);
+ }
+ }
+ }
+ bool flag6 = extension == ".dat";
+ if (flag6)
+ {
+ int num2 = 84;
+ uint num3 = 2048U;
+ bool flag7 = num2 > 0;
+ if (flag7)
+ {
+ byte[] array5 = new byte[num2];
+ array5 = binaryReader.ReadBytes(array5.Length);
+ uint num4 = num3;
+ bool flag8 = this._analysisID.Contains(num4);
+ if (flag8)
+ {
+ CanData item3 = new CanData(num4, array5);
+ this._canLogDataSource[this._canLogDataSource.Count - 1].DataList.Add(item3);
+ }
+ this._allCanData[this._allCanData.Count - 1].DataList.Add(new CanData(num4, array5));
+ }
+ }
+ else
+ {
+ array4 = binaryReader.ReadBytes(array4.Length);
+ array3 = binaryReader.ReadBytes(array3.Length);
+ int num5 = Convert.ToInt32(array4[0]);
+ int num6 = ((int)(array3[0] & 15) << 8) + (int)array3[1];
+ bool flag9 = num5 > 0;
+ if (flag9)
+ {
+ byte[] array6 = new byte[num5];
+ array6 = binaryReader.ReadBytes(array6.Length);
+ uint num7 = (uint)num6;
+ bool @checked = this.cbMMIOnly.Checked;
+ if (@checked)
+ {
+ bool flag10 = num7 == 2048U;
+ if (flag10)
+ {
+ bool flag11 = this._analysisID.Contains(num7);
+ if (flag11)
+ {
+ CanData item4 = new CanData(num7, array6);
+ this._canLogDataSource[this._canLogDataSource.Count - 1].DataList.Add(item4);
+ }
+ this._allCanData[this._allCanData.Count - 1].DataList.Add(new CanData(num7, array6));
+ }
+ }
+ else
+ {
+ bool flag12 = this._analysisID.Contains(num7);
+ if (flag12)
+ {
+ CanData item5 = new CanData(num7, array6);
+ this._canLogDataSource[this._canLogDataSource.Count - 1].DataList.Add(item5);
+ }
+ this._allCanData[this._allCanData.Count - 1].DataList.Add(new CanData(num7, array6));
+ }
+ }
+ }
+ }
+ bool flag13 = this._canLogDataSource.Count == 0;
+ if (flag13)
+ {
+ result = false;
+ }
+ else
+ {
+ result = this.MergeData(this._canLogDataSource, out this._dataSource);
+ this._dataIndex = 0;
+ int count = this._dataSource.Count;
+ for (int j = 0; j < count; j++)
+ {
+ bool flag14 = this._dataSource[j].mmiData.source == 16U || this._dataSource[j].mmiData.source == 80U;
+ if (flag14)
+ {
+ this._dataSource1.Add(this._dataSource[j]);
+ }
+ else
+ {
+ this._dataSource2.Add(this._dataSource[j]);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ result = false;
+ string message = ex.Message;
+ }
+ finally
+ {
+ bool flag15 = binaryReader != null;
+ if (flag15)
+ {
+ binaryReader.Close();
+ }
+ bool flag16 = fileStream != null;
+ if (flag16)
+ {
+ fileStream.Close();
+ }
+ }
+ return result;
+ }
+
+ // Token: 0x06000100 RID: 256 RVA: 0x00021080 File Offset: 0x0001F280
+ private bool MergeData(List source, out List result)
+ {
+ bool result2 = true;
+ result = new List();
+ MergedData mergedData = new MergedData();
+ DateTime logTime = source[0].LogTime;
+ source.RemoveRange(source.Count - 1, 1);
+ foreach (CanLogData canLogData in source)
+ {
+ DateTime logTime2 = canLogData.LogTime;
+ int count = canLogData.DataList.Count;
+ bool flag = count == 0;
+ if (!flag)
+ {
+ int num = 1000 / count;
+ List dataList = canLogData.DataList;
+ int count2 = dataList.Count;
+ for (int i = 0; i < count2; i++)
+ {
+ CanData canData = dataList[i];
+ mergedData.Time = logTime2;
+ bool flag2 = mergedData.mmiData.time != null && DateTime.TryParse(mergedData.mmiData.time, out mergedData.Time);
+ if (flag2)
+ {
+ mergedData.Time = Convert.ToDateTime(mergedData.mmiData.time);
+ }
+ bool flag3 = !BitConverter.ToString(this._prevBuff[canData.Identifier]).Equals(BitConverter.ToString(canData.Data));
+ if (flag3)
+ {
+ this._prevBuff[canData.Identifier] = canData.Data;
+ uint identifier = canData.Identifier;
+ uint num2 = identifier;
+ if (num2 <= 800U)
+ {
+ if (num2 <= 514U)
+ {
+ switch (num2)
+ {
+ case 336U:
+ mergedData.ccc150Data = CCC150.SetData(canData.Data);
+ break;
+ case 337U:
+ mergedData.ccc151Data = CCC151.SetData(canData.Data);
+ break;
+ case 338U:
+ mergedData.ccc152Data = CCC152.SetData(canData.Data);
+ break;
+ default:
+ switch (num2)
+ {
+ case 512U:
+ mergedData.acpu200Data = ACPU200.SetData(canData.Data);
+ break;
+ case 513U:
+ mergedData.acpu201Data = ACPU201.SetData(canData.Data);
+ break;
+ case 514U:
+ mergedData.acpu202Data = ACPU202.SetData(canData.Data);
+ break;
+ default:
+ goto IL_31F;
+ }
+ break;
+ }
+ }
+ else if (num2 != 592U)
+ {
+ if (num2 != 800U)
+ {
+ goto IL_31F;
+ }
+ mergedData.twct320Data = TWCT320.SetData(canData.Data);
+ }
+ else
+ {
+ mergedData.atcr250Data = ATCR250.SetData(canData.Data);
+ }
+ }
+ else if (num2 <= 2048U)
+ {
+ if (num2 != 1281U)
+ {
+ if (num2 != 2048U)
+ {
+ goto IL_31F;
+ }
+ mergedData.mmiData = DDOSItoMMI.SetData(canData.Data);
+ }
+ else
+ {
+ mergedData.atcr501Data = ATCR501.SetData(canData.Data);
+ }
+ }
+ else if (num2 != 2128U)
+ {
+ if (num2 != 2304U)
+ {
+ if (num2 != 2305U)
+ {
+ goto IL_31F;
+ }
+ mergedData.ftcmsData = TCMStoDOSI.SetData(canData.Data);
+ }
+ else
+ {
+ mergedData.tcmsData = DOSItoTCMS.SetData(canData.Data);
+ }
+ }
+ else
+ {
+ mergedData.fmmiDaTa = MMItoDOSI.SetData(canData.Data);
+ }
+ result.Add((MergedData)mergedData.Clone());
+ goto IL_353;
+ IL_31F:
+ result2 = false;
+ MessageBox.Show(string.Format("데이터 분석 오류. CAN ID: 0x{0:X3} 은 처리되지 않음.", canData.Identifier));
+ return result2;
+ }
+ IL_353:;
+ }
+ }
+ }
+ return result2;
+ }
+
+ // Token: 0x06000101 RID: 257 RVA: 0x0002143C File Offset: 0x0001F63C
+ private List FindStationInfo(bool isUp, bool isCtrl1)
+ {
+ List result = new List();
+ List list = Enum.GetValues(typeof(StationCode)).Cast().ToList();
+ list.Remove(StationCode.신평_TC2);
+ list.Remove(StationCode.신평_TC1);
+ Array array = list.ToArray();
+ bool flag = !isUp;
+ if (flag)
+ {
+ Array.Reverse(array);
+ }
+ List source = new List();
+ int num = 0;
+ bool @checked = this.checkBox1.Checked;
+ if (@checked)
+ {
+ for (int i = 0; i < this._dataSource1.Count; i++)
+ {
+ int pstn = this._dataSource1[i].mmiData.pstn;
+ bool door_open = this._dataSource1[i].mmiData.door_open;
+ bool flag2 = pstn != 0 && num != pstn && door_open && this._dataSource1[i].mmiData.time != "2000.00.00 00:00:00";
+ if (flag2)
+ {
+ num = pstn;
+ int num2 = Array.IndexOf(array, (StationCode)pstn);
+ List result3 = result;
+ StationCode stationCode = (StationCode)pstn;
+ result3.Add(new StationInfo(stationCode.ToString(), Convert.ToDateTime(this._dataSource1[i].mmiData.time), i, pstn));
+ }
+ }
+ }
+ else
+ {
+ bool checked2 = this.checkBox2.Checked;
+ if (checked2)
+ {
+ for (int j = 0; j < this._dataSource2.Count; j++)
+ {
+ int pstn2 = this._dataSource2[j].mmiData.pstn;
+ bool door_open2 = this._dataSource2[j].mmiData.door_open;
+ bool flag3 = pstn2 != 0 && num != pstn2 && door_open2 && this._dataSource2[j].mmiData.time != "2000.00.00 00:00:00";
+ if (flag3)
+ {
+ num = pstn2;
+ int num3 = Array.IndexOf(array, (StationCode)pstn2);
+ List result2 = result;
+ StationCode stationCode = (StationCode)pstn2;
+ result2.Add(new StationInfo(stationCode.ToString(), Convert.ToDateTime(this._dataSource2[j].mmiData.time), j, pstn2));
+ }
+ }
+ }
+ }
+ IEnumerable enumerable = from x in source
+ where !(from o in result
+ select o.StationNumber).Contains(x + 1)
+ select x;
+ StringBuilder stringBuilder = new StringBuilder();
+ foreach (int num4 in enumerable)
+ {
+ stringBuilder.Append("\r\n");
+ if (isUp)
+ {
+ bool flag4 = num4 != 0;
+ if (flag4)
+ {
+ StringBuilder stringBuilder2 = stringBuilder;
+ string str = string.Format("[{0}] ", num4 + 94);
+ object value = array.GetValue(num4);
+ stringBuilder2.Append(str + ((value != null) ? value.ToString() : null));
+ }
+ }
+ else
+ {
+ StringBuilder stringBuilder3 = stringBuilder;
+ string str2 = string.Format("[{0}] ", 134 - num4);
+ object value2 = array.GetValue(num4);
+ stringBuilder3.Append(str2 + ((value2 != null) ? value2.ToString() : null));
+ }
+ }
+ return result;
+ }
+
+ // Token: 0x06000102 RID: 258 RVA: 0x000217BC File Offset: 0x0001F9BC
+ private void clearData()
+ {
+ this._dataSource1.Clear();
+ this._dataSource2.Clear();
+ this._dataSource.Clear();
+ }
+
+ // Token: 0x06000103 RID: 259 RVA: 0x000217E4 File Offset: 0x0001F9E4
+ private void ShowChart()
+ {
+ this.mmiUI.chartClear();
+ bool flag = this.checkBox1.Checked && this._dataSource1 != null;
+ if (flag)
+ {
+ this.mmiUI.initChart(this._dataSource1, 0);
+ }
+ else
+ {
+ bool flag2 = this.checkBox2.Checked && this._dataSource2 != null;
+ if (flag2)
+ {
+ this.mmiUI.initChart(this._dataSource2, 0);
+ }
+ }
+ this.mmiUI.chart1.ChartAreas[0].CursorX.Position = 0.0;
+ this.mmiUI.chart1.ChartAreas.ResumeUpdates();
+ }
+
+ // Token: 0x06000104 RID: 260 RVA: 0x000218A4 File Offset: 0x0001FAA4
+ private void button1_Click(object sender, EventArgs e)
+ {
+ this.mmiUI.isFormShow();
+ }
+
+ // Token: 0x06000105 RID: 261 RVA: 0x000218B4 File Offset: 0x0001FAB4
+ private void lblDataIdx_KeyDown(object sender, KeyEventArgs e)
+ {
+ bool flag = e.KeyCode == Keys.Return;
+ if (flag)
+ {
+ this._dataIndex = int.Parse(this.lblDataIdx.Text);
+ this.mmiUI.showChart(this._dataIndex);
+ }
+ }
+
+ // Token: 0x06000106 RID: 262 RVA: 0x000218FC File Offset: 0x0001FAFC
+ private void buttonPREV_FAST_MouseDown(object sender, MouseEventArgs e)
+ {
+ string name = ((Button)sender).Name;
+ string a = name;
+ if (!(a == "buttonPREV_FAST"))
+ {
+ if (!(a == "buttonPREV"))
+ {
+ if (!(a == "buttonNEXT"))
+ {
+ if (a == "buttonNEXT_FAST")
+ {
+ this._scrollMode = ScrollBtn.FwdL;
+ this.tmr_AutoScroll.Start();
+ }
+ }
+ else
+ {
+ this._scrollMode = ScrollBtn.FwdS;
+ this.tmr_AutoScroll.Start();
+ }
+ }
+ else
+ {
+ this._scrollMode = ScrollBtn.PrvS;
+ this.tmr_AutoScroll.Start();
+ }
+ }
+ else
+ {
+ this._scrollMode = ScrollBtn.PrvL;
+ this.tmr_AutoScroll.Start();
+ }
+ }
+
+ // Token: 0x06000107 RID: 263 RVA: 0x000219A4 File Offset: 0x0001FBA4
+ private void tmr_AutoScroll_Tick(object sender, EventArgs e)
+ {
+ bool @checked = this.checkBox1.Checked;
+ if (@checked)
+ {
+ bool flag = this._dataIndex >= 0 && this._dataIndex <= this._dataSource1.Count;
+ if (flag)
+ {
+ switch (this._scrollMode)
+ {
+ case ScrollBtn.PrvL:
+ this._dataIndex -= 24;
+ break;
+ case ScrollBtn.PrvS:
+ this._dataIndex--;
+ break;
+ case ScrollBtn.FwdS:
+ this._dataIndex++;
+ break;
+ case ScrollBtn.FwdL:
+ this._dataIndex += 24;
+ break;
+ }
+ bool flag2 = this._dataIndex < 0;
+ if (flag2)
+ {
+ this._dataIndex = 0;
+ }
+ bool flag3 = this._dataIndex > this._dataSource1.Count;
+ if (flag3)
+ {
+ this._dataIndex = this._dataSource1.Count;
+ }
+ this.lblDataIdx.Text = this._dataIndex.ToString();
+ this.mmiUI.showChart(this._dataIndex);
+ }
+ }
+ else
+ {
+ bool flag4 = this._dataIndex >= 0 && this._dataIndex <= this._dataSource2.Count;
+ if (flag4)
+ {
+ switch (this._scrollMode)
+ {
+ case ScrollBtn.PrvL:
+ this._dataIndex -= 24;
+ break;
+ case ScrollBtn.PrvS:
+ this._dataIndex--;
+ break;
+ case ScrollBtn.FwdS:
+ this._dataIndex++;
+ break;
+ case ScrollBtn.FwdL:
+ this._dataIndex += 24;
+ break;
+ }
+ bool flag5 = this._dataIndex < 0;
+ if (flag5)
+ {
+ this._dataIndex = 0;
+ }
+ bool flag6 = this._dataIndex > this._dataSource2.Count;
+ if (flag6)
+ {
+ this._dataIndex = this._dataSource2.Count;
+ }
+ this.lblDataIdx.Text = this._dataIndex.ToString();
+ this.mmiUI.showChart(this._dataIndex);
+ }
+ }
+ }
+
+ // Token: 0x06000108 RID: 264 RVA: 0x00021BC5 File Offset: 0x0001FDC5
+ private void buttonPREV_FAST_MouseUp(object sender, MouseEventArgs e)
+ {
+ this.tmr_AutoScroll.Stop();
+ }
+
+ // Token: 0x06000109 RID: 265 RVA: 0x00021BD4 File Offset: 0x0001FDD4
+ private void SavedlogForm_DragDrop(object sender, DragEventArgs e)
+ {
+ this.Cursor = Cursors.WaitCursor;
+ string[] array = (string[])e.Data.GetData(DataFormats.FileDrop);
+ bool flag = array[0].Contains("_MSG");
+ if (flag)
+ {
+ mmiMsgForm mmiMsgForm = new mmiMsgForm();
+ mmiMsgForm.Show();
+ mmiMsgForm.fileOpen(array[0]);
+ }
+ else
+ {
+ List list = new List();
+ list.Add("다대포해");
+ list.Add("다대포항");
+ list.Add("낫개");
+ list.Add("신장림");
+ list.Add("장림");
+ list.Add("동매");
+ list.Add("신평");
+ list.Add("하단");
+ list.Add("당리");
+ list.Add("사하");
+ list.Add("괴정");
+ list.Add("대티");
+ list.Add("서대신");
+ list.Add("동대신");
+ list.Add("토성");
+ list.Add("자갈치");
+ list.Add("남포");
+ list.Add("중앙");
+ list.Add("부산역");
+ list.Add("초량");
+ list.Add("부산진");
+ list.Add("좌천");
+ list.Add("범일");
+ list.Add("범내골");
+ list.Add("서면");
+ list.Add("부전");
+ list.Add("양정");
+ list.Add("시청");
+ list.Add("연산");
+ list.Add("교대");
+ list.Add("동래");
+ list.Add("명륜");
+ list.Add("온천장");
+ list.Add("부산대");
+ list.Add("장전");
+ list.Add("구서");
+ list.Add("두실");
+ list.Add("남산");
+ list.Add("범어사");
+ list.Add("노포");
+ bool flag2 = this.OpenLogFile(array[0]);
+ if (flag2)
+ {
+ this.lblDataCount.Enabled = true;
+ this.lblDataIdx.Enabled = true;
+ this.lblDataCount.Text = this._dataSource1.Count.ToString();
+ this.lblDataIdx.Text = this._dataIndex.ToString();
+ this.lbl_STATUS.Text = Path.GetFileName(array[0]);
+ bool flag3 = this.lbl_STATUS.Text.Length > 16;
+ if (flag3)
+ {
+ this.lbl_formNo.Text = this.lbl_STATUS.Text.Substring(16, 2) + " 편성";
+ }
+ else
+ {
+ this.lbl_formNo.Text = this._dataSource[100].tcmsData.formNo.ToString() + " 편성";
+ }
+ string directoryName = Path.GetDirectoryName(array[0]);
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(array[0]);
+ this.lbl_LOGFILE.Enabled = true;
+ this.lbl_LOGFILE.BackColor = Color.Green;
+ this.buttonPREV_FAST.Enabled = true;
+ this.buttonPREV.Enabled = true;
+ this.buttonNEXT.Enabled = true;
+ this.buttonNEXT_FAST.Enabled = true;
+ bool flag4 = Path.GetExtension(array[0])[3] == '1';
+ bool flag5 = Path.GetExtension(array[0])[3] == 't';
+ if (flag5)
+ {
+ flag4 = this._dataSource[10].mmiData.tc1;
+ }
+ this._stationInfoList = this.FindStationInfo(flag4, true);
+ foreach (StationInfo stationInfo in this._stationInfoList)
+ {
+ for (int i = 0; i < list.Count; i++)
+ {
+ bool flag6 = list[i] == stationInfo.StationName;
+ if (flag6)
+ {
+ list.RemoveAt(i);
+ }
+ }
+ this.mmiUI.cb_TWCR.Items.Add(stationInfo);
+ }
+ this.mmiUI._mstationInfoList = this._stationInfoList;
+ this.mmiUI.cb_TWCR.Text = "- STATION -" + this.mmiUI.cb_TWCR.Items.Count.ToString();
+ bool flag7 = this.mmiUI.cb_TWCR.Items.Count < 1;
+ if (flag7)
+ {
+ this.mmiUI.cb_TWCR.Text = "-TWC 미수신 상태- ";
+ }
+ else
+ {
+ bool flag8 = this.mmiUI.cb_TWCR.Items.Count < 7;
+ if (flag8)
+ {
+ this.mmiUI.cb_TWCR.Text = "-TWC(" + this.mmiUI.cb_TWCR.Items.Count.ToString() + ")|출고 및 회송|";
+ }
+ else
+ {
+ bool flag9 = list.Count > 0 && list.Count < 40;
+ if (flag9)
+ {
+ string str = string.Join("l", list);
+ this.mmiUI.cb_TWCR.Text = "-TWC(" + this.mmiUI.cb_TWCR.Items.Count.ToString() + ") 미수신역:" + str;
+ }
+ else
+ {
+ this.mmiUI.cb_TWCR.Text = "- TWC 모든역수신 -" + this.mmiUI.cb_TWCR.Items.Count.ToString();
+ }
+ }
+ }
+ this.mmiUI.tt.SetToolTip(this.mmiUI.cb_TWCR, this.mmiUI.cb_TWCR.Text);
+ this.ShowChart();
+ }
+ }
+ this.Cursor = Cursors.Default;
+ }
+
+ // Token: 0x0600010A RID: 266 RVA: 0x00022238 File Offset: 0x00020438
+ private void SavedlogForm_DragEnter(object sender, DragEventArgs e)
+ {
+ e.Effect = DragDropEffects.Copy;
+ }
+
+ // Token: 0x0600010B RID: 267 RVA: 0x00022244 File Offset: 0x00020444
+ private void checkBox1_Click(object sender, EventArgs e)
+ {
+ this.checkBox2.Checked = false;
+ this.mmiUI.chartClear();
+ this.clearLabel();
+ bool flag = this._dataSource1 == null || this._dataSource1.Count == 0;
+ if (!flag)
+ {
+ this.FindStationInfo(this.isUp, true);
+ this.ShowChart();
+ this._dataIndex = int.Parse(this.lblDataIdx.Text);
+ this.mmiUI.showChart(this._dataIndex);
+ this.lblDataCount.Text = this._dataSource1.Count.ToString();
+ this.stationList = this.setBasicTwcStation();
+ this.addTwcrStation(this.stationList);
+ }
+ }
+
+ // Token: 0x0600010C RID: 268 RVA: 0x00022308 File Offset: 0x00020508
+ private void checkBox2_Click(object sender, EventArgs e)
+ {
+ this.checkBox1.Checked = false;
+ this.mmiUI.chartClear();
+ this.clearLabel();
+ bool flag = this._dataSource2.Count == 0;
+ if (!flag)
+ {
+ this.FindStationInfo(this.isUp, false);
+ this.ShowChart();
+ this._dataIndex = int.Parse(this.lblDataIdx.Text);
+ this.mmiUI.showChart(this._dataIndex);
+ this.lblDataCount.Text = this._dataSource2.Count.ToString();
+ this.stationList = this.setBasicTwcStation();
+ this.addTwcrStation(this.stationList);
+ }
+ }
+
+ // Token: 0x0600010D RID: 269 RVA: 0x000223BF File Offset: 0x000205BF
+ private void clearLabel()
+ {
+ this.mmiUI.clearLabelMMIUI();
+ }
+
+ // Token: 0x0600010E RID: 270 RVA: 0x000223CE File Offset: 0x000205CE
+ private void SavedlogForm_FormClosed(object sender, FormClosedEventArgs e)
+ {
+ this.mmiUI.Close();
+ }
+
+ // Token: 0x0600010F RID: 271 RVA: 0x000223E0 File Offset: 0x000205E0
+ private void btn_markerCalc_Click(object sender, EventArgs e)
+ {
+ bool flag = this._dataSource == null || this._dataSource.Count == 0;
+ if (flag)
+ {
+ MessageBox.Show("[추가데이터 선택] 에서 아이템을 추가 한 후 EXCEL 버튼을 클릭해주세요.");
+ }
+ else
+ {
+ bool @checked = this.checkBox1.Checked;
+ if (@checked)
+ {
+ this.mmiUI.stopPositionWriter(this._dataSource1, this._dataIndex);
+ }
+ else
+ {
+ this.mmiUI.stopPositionWriter(this._dataSource2, this._dataIndex);
+ }
+ }
+ }
+
+ // Token: 0x06000110 RID: 272 RVA: 0x0002245A File Offset: 0x0002065A
+ private void checkBox3_Click(object sender, EventArgs e)
+ {
+ }
+
+ // Token: 0x06000111 RID: 273 RVA: 0x0002245D File Offset: 0x0002065D
+ private void checkBox3_CheckedChanged(object sender, EventArgs e)
+ {
+ this.mmiUI.panel_cVDIO.Visible = this.checkBox3.Checked;
+ }
+
+ // Token: 0x06000112 RID: 274 RVA: 0x0002247C File Offset: 0x0002067C
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x06000113 RID: 275 RVA: 0x000224B4 File Offset: 0x000206B4
+ private void InitializeComponent()
+ {
+ this.components = new Container();
+ this.label1 = new Label();
+ this.button1 = new Button();
+ this.lblDataCount = new Label();
+ this.label5 = new Label();
+ this.lbl_LOGFILE = new Label();
+ this.lbl_STATUS = new Label();
+ this.buttonNEXT_FAST = new Button();
+ this.buttonNEXT = new Button();
+ this.buttonPREV = new Button();
+ this.buttonPREV_FAST = new Button();
+ this.bt_fileOpen = new Button();
+ this.lblDataIdx = new TextBox();
+ this.tmr_AutoScroll = new Timer(this.components);
+ this.checkBox1 = new CheckBox();
+ this.checkBox2 = new CheckBox();
+ this.lbl_formNo = new Label();
+ this.button2 = new Button();
+ this.checkBox3 = new CheckBox();
+ this.cbMMIOnly = new CheckBox();
+ this.panel1 = new Panel();
+ this.panel1.SuspendLayout();
+ base.SuspendLayout();
+ this.label1.BackColor = Color.DarkOrange;
+ this.label1.Dock = DockStyle.Top;
+ this.label1.Font = new Font("맑은 고딕", 18f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.label1.ForeColor = Color.Black;
+ this.label1.Location = new Point(0, 0);
+ this.label1.Name = "label1";
+ this.label1.Size = new Size(1442, 55);
+ this.label1.TabIndex = 92;
+ this.label1.Text = "로그 뷰어";
+ this.label1.TextAlign = ContentAlignment.MiddleLeft;
+ this.button1.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold);
+ this.button1.Location = new Point(1015, 10);
+ this.button1.Margin = new Padding(3, 2, 3, 2);
+ this.button1.Name = "button1";
+ this.button1.Size = new Size(126, 34);
+ this.button1.TabIndex = 97;
+ this.button1.Text = "추가 데이터 선택";
+ this.button1.UseVisualStyleBackColor = true;
+ this.button1.Click += this.button1_Click;
+ this.lblDataCount.Enabled = false;
+ this.lblDataCount.Font = new Font("Verdana", 12f, FontStyle.Bold, GraphicsUnit.Point, 0);
+ this.lblDataCount.Location = new Point(718, 14);
+ this.lblDataCount.Name = "lblDataCount";
+ this.lblDataCount.Size = new Size(100, 27);
+ this.lblDataCount.TabIndex = 104;
+ this.lblDataCount.Text = "-";
+ this.lblDataCount.TextAlign = ContentAlignment.MiddleLeft;
+ this.label5.AutoSize = true;
+ this.label5.BackColor = Color.DarkOrange;
+ this.label5.Font = new Font("Verdana", 15.75f, FontStyle.Regular, GraphicsUnit.Point, 0);
+ this.label5.Location = new Point(699, 15);
+ this.label5.Name = "label5";
+ this.label5.Size = new Size(22, 25);
+ this.label5.TabIndex = 103;
+ this.label5.Text = "/";
+ this.label5.TextAlign = ContentAlignment.MiddleCenter;
+ this.lbl_LOGFILE.BackColor = Color.DarkGray;
+ this.lbl_LOGFILE.BorderStyle = BorderStyle.FixedSingle;
+ this.lbl_LOGFILE.Font = new Font("맑은 고딕", 11.25f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.lbl_LOGFILE.ForeColor = Color.White;
+ this.lbl_LOGFILE.Location = new Point(235, 8);
+ this.lbl_LOGFILE.Name = "lbl_LOGFILE";
+ this.lbl_LOGFILE.Size = new Size(106, 37);
+ this.lbl_LOGFILE.TabIndex = 101;
+ this.lbl_LOGFILE.Text = "로그 파일";
+ this.lbl_LOGFILE.TextAlign = ContentAlignment.MiddleCenter;
+ this.lbl_STATUS.BackColor = Color.DarkBlue;
+ this.lbl_STATUS.BorderStyle = BorderStyle.FixedSingle;
+ this.lbl_STATUS.Font = new Font("맑은 고딕", 9.75f, FontStyle.Regular, GraphicsUnit.Point, 129);
+ this.lbl_STATUS.ForeColor = Color.White;
+ this.lbl_STATUS.Location = new Point(347, 9);
+ this.lbl_STATUS.Name = "lbl_STATUS";
+ this.lbl_STATUS.Size = new Size(131, 36);
+ this.lbl_STATUS.TabIndex = 100;
+ this.lbl_STATUS.Text = "-";
+ this.lbl_STATUS.TextAlign = ContentAlignment.MiddleCenter;
+ this.buttonNEXT_FAST.BackColor = SystemColors.Control;
+ this.buttonNEXT_FAST.Enabled = false;
+ this.buttonNEXT_FAST.FlatStyle = FlatStyle.Flat;
+ this.buttonNEXT_FAST.Font = new Font("Verdana", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 0);
+ this.buttonNEXT_FAST.Location = new Point(967, 10);
+ this.buttonNEXT_FAST.Name = "buttonNEXT_FAST";
+ this.buttonNEXT_FAST.Size = new Size(42, 33);
+ this.buttonNEXT_FAST.TabIndex = 99;
+ this.buttonNEXT_FAST.Text = ">>";
+ this.buttonNEXT_FAST.UseVisualStyleBackColor = false;
+ this.buttonNEXT_FAST.MouseDown += this.buttonPREV_FAST_MouseDown;
+ this.buttonNEXT_FAST.MouseUp += this.buttonPREV_FAST_MouseUp;
+ this.buttonNEXT.BackColor = SystemColors.Control;
+ this.buttonNEXT.Enabled = false;
+ this.buttonNEXT.FlatStyle = FlatStyle.Flat;
+ this.buttonNEXT.Font = new Font("Verdana", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 0);
+ this.buttonNEXT.Location = new Point(919, 10);
+ this.buttonNEXT.Name = "buttonNEXT";
+ this.buttonNEXT.Size = new Size(42, 33);
+ this.buttonNEXT.TabIndex = 98;
+ this.buttonNEXT.Text = ">";
+ this.buttonNEXT.UseVisualStyleBackColor = false;
+ this.buttonNEXT.MouseDown += this.buttonPREV_FAST_MouseDown;
+ this.buttonNEXT.MouseUp += this.buttonPREV_FAST_MouseUp;
+ this.buttonPREV.BackColor = SystemColors.Control;
+ this.buttonPREV.Enabled = false;
+ this.buttonPREV.FlatStyle = FlatStyle.Flat;
+ this.buttonPREV.Font = new Font("Verdana", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 0);
+ this.buttonPREV.Location = new Point(871, 10);
+ this.buttonPREV.Name = "buttonPREV";
+ this.buttonPREV.Size = new Size(42, 33);
+ this.buttonPREV.TabIndex = 97;
+ this.buttonPREV.Text = "<";
+ this.buttonPREV.UseVisualStyleBackColor = false;
+ this.buttonPREV.MouseDown += this.buttonPREV_FAST_MouseDown;
+ this.buttonPREV.MouseUp += this.buttonPREV_FAST_MouseUp;
+ this.buttonPREV_FAST.BackColor = SystemColors.Control;
+ this.buttonPREV_FAST.Enabled = false;
+ this.buttonPREV_FAST.FlatStyle = FlatStyle.Flat;
+ this.buttonPREV_FAST.Font = new Font("Verdana", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 0);
+ this.buttonPREV_FAST.Location = new Point(823, 10);
+ this.buttonPREV_FAST.Name = "buttonPREV_FAST";
+ this.buttonPREV_FAST.Size = new Size(42, 33);
+ this.buttonPREV_FAST.TabIndex = 96;
+ this.buttonPREV_FAST.Text = "<<";
+ this.buttonPREV_FAST.UseVisualStyleBackColor = false;
+ this.buttonPREV_FAST.MouseDown += this.buttonPREV_FAST_MouseDown;
+ this.buttonPREV_FAST.MouseUp += this.buttonPREV_FAST_MouseUp;
+ this.bt_fileOpen.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.bt_fileOpen.Location = new Point(120, 8);
+ this.bt_fileOpen.Margin = new Padding(3, 2, 3, 2);
+ this.bt_fileOpen.Name = "bt_fileOpen";
+ this.bt_fileOpen.Size = new Size(109, 37);
+ this.bt_fileOpen.TabIndex = 105;
+ this.bt_fileOpen.Text = "FILE OPEN";
+ this.bt_fileOpen.UseVisualStyleBackColor = true;
+ this.bt_fileOpen.Click += this.bt_fileOpen_Click;
+ this.lblDataIdx.Enabled = false;
+ this.lblDataIdx.Font = new Font("Verdana", 12f, FontStyle.Regular, GraphicsUnit.Point, 0);
+ this.lblDataIdx.Location = new Point(598, 14);
+ this.lblDataIdx.Name = "lblDataIdx";
+ this.lblDataIdx.Size = new Size(100, 27);
+ this.lblDataIdx.TabIndex = 102;
+ this.lblDataIdx.Text = "-";
+ this.lblDataIdx.TextAlign = HorizontalAlignment.Right;
+ this.lblDataIdx.KeyDown += this.lblDataIdx_KeyDown;
+ this.tmr_AutoScroll.Tick += this.tmr_AutoScroll_Tick;
+ this.checkBox1.Appearance = Appearance.Button;
+ this.checkBox1.Checked = true;
+ this.checkBox1.CheckState = CheckState.Checked;
+ this.checkBox1.Font = new Font("나눔고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.checkBox1.Location = new Point(1147, 10);
+ this.checkBox1.Name = "checkBox1";
+ this.checkBox1.Size = new Size(61, 34);
+ this.checkBox1.TabIndex = 106;
+ this.checkBox1.Text = "1계";
+ this.checkBox1.TextAlign = ContentAlignment.MiddleCenter;
+ this.checkBox1.UseVisualStyleBackColor = true;
+ this.checkBox1.Click += this.checkBox1_Click;
+ this.checkBox2.Appearance = Appearance.Button;
+ this.checkBox2.Font = new Font("나눔고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.checkBox2.Location = new Point(1214, 10);
+ this.checkBox2.Name = "checkBox2";
+ this.checkBox2.Size = new Size(61, 34);
+ this.checkBox2.TabIndex = 107;
+ this.checkBox2.Text = "2계";
+ this.checkBox2.TextAlign = ContentAlignment.MiddleCenter;
+ this.checkBox2.UseVisualStyleBackColor = true;
+ this.checkBox2.Click += this.checkBox2_Click;
+ this.lbl_formNo.BackColor = Color.DarkBlue;
+ this.lbl_formNo.BorderStyle = BorderStyle.FixedSingle;
+ this.lbl_formNo.Font = new Font("맑은 고딕", 14.25f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.lbl_formNo.ForeColor = Color.White;
+ this.lbl_formNo.Location = new Point(484, 9);
+ this.lbl_formNo.Name = "lbl_formNo";
+ this.lbl_formNo.Size = new Size(106, 36);
+ this.lbl_formNo.TabIndex = 113;
+ this.lbl_formNo.Text = "-";
+ this.lbl_formNo.TextAlign = ContentAlignment.MiddleCenter;
+ this.button2.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button2.Location = new Point(1370, 10);
+ this.button2.Margin = new Padding(3, 2, 3, 2);
+ this.button2.Name = "button2";
+ this.button2.Size = new Size(60, 35);
+ this.button2.TabIndex = 114;
+ this.button2.Text = "EXCEL";
+ this.button2.UseVisualStyleBackColor = true;
+ this.button2.Visible = false;
+ this.button2.Click += this.btn_markerCalc_Click;
+ this.checkBox3.Appearance = Appearance.Button;
+ this.checkBox3.Font = new Font("나눔고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.checkBox3.Location = new Point(1281, 10);
+ this.checkBox3.Name = "checkBox3";
+ this.checkBox3.Size = new Size(83, 34);
+ this.checkBox3.TabIndex = 115;
+ this.checkBox3.Text = "DIO";
+ this.checkBox3.TextAlign = ContentAlignment.MiddleCenter;
+ this.checkBox3.UseVisualStyleBackColor = true;
+ this.checkBox3.CheckedChanged += this.checkBox3_CheckedChanged;
+ this.cbMMIOnly.BackColor = Color.SandyBrown;
+ this.cbMMIOnly.FlatAppearance.CheckedBackColor = Color.BlueViolet;
+ this.cbMMIOnly.FlatAppearance.MouseDownBackColor = Color.MediumPurple;
+ this.cbMMIOnly.FlatStyle = FlatStyle.Popup;
+ this.cbMMIOnly.Font = new Font("나눔고딕", 11.25f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.cbMMIOnly.Location = new Point(12, 20);
+ this.cbMMIOnly.Name = "cbMMIOnly";
+ this.cbMMIOnly.Size = new Size(103, 38);
+ this.cbMMIOnly.TabIndex = 112;
+ this.cbMMIOnly.Text = "ONLY MMI";
+ this.cbMMIOnly.UseVisualStyleBackColor = false;
+ this.cbMMIOnly.Visible = false;
+ this.panel1.Controls.Add(this.cbMMIOnly);
+ this.panel1.Dock = DockStyle.Fill;
+ this.panel1.Location = new Point(0, 55);
+ this.panel1.Name = "panel1";
+ this.panel1.Size = new Size(1442, 624);
+ this.panel1.TabIndex = 95;
+ this.panel1.DragDrop += this.SavedlogForm_DragDrop;
+ this.panel1.DragEnter += this.SavedlogForm_DragEnter;
+ this.AllowDrop = true;
+ base.AutoScaleMode = AutoScaleMode.None;
+ base.ClientSize = new Size(1442, 679);
+ base.Controls.Add(this.checkBox3);
+ base.Controls.Add(this.button2);
+ base.Controls.Add(this.lbl_formNo);
+ base.Controls.Add(this.checkBox2);
+ base.Controls.Add(this.checkBox1);
+ base.Controls.Add(this.bt_fileOpen);
+ base.Controls.Add(this.button1);
+ base.Controls.Add(this.lblDataCount);
+ base.Controls.Add(this.label5);
+ base.Controls.Add(this.lblDataIdx);
+ base.Controls.Add(this.lbl_LOGFILE);
+ base.Controls.Add(this.lbl_STATUS);
+ base.Controls.Add(this.buttonNEXT_FAST);
+ base.Controls.Add(this.buttonNEXT);
+ base.Controls.Add(this.buttonPREV);
+ base.Controls.Add(this.buttonPREV_FAST);
+ base.Controls.Add(this.panel1);
+ base.Controls.Add(this.label1);
+ this.Font = new Font("굴림", 9f, FontStyle.Regular, GraphicsUnit.Point, 129);
+ base.Name = "SavedlogForm";
+ base.FormClosed += this.SavedlogForm_FormClosed;
+ base.Load += this.SavedlogForm_Load;
+ base.DragDrop += this.SavedlogForm_DragDrop;
+ base.DragEnter += this.SavedlogForm_DragEnter;
+ this.panel1.ResumeLayout(false);
+ base.ResumeLayout(false);
+ base.PerformLayout();
+ }
+
+ // Token: 0x040001A1 RID: 417
+ private List _canLogDataSource = new List();
+
+ // Token: 0x040001A2 RID: 418
+ private List _canLogDataSource1 = new List();
+
+ // Token: 0x040001A3 RID: 419
+ private List _canLogDataSource2 = new List();
+
+ // Token: 0x040001A4 RID: 420
+ private List _allCanData = new List();
+
+ // Token: 0x040001A5 RID: 421
+ private List _dataSource = new List();
+
+ // Token: 0x040001A6 RID: 422
+ private List _dataSource1 = new List();
+
+ // Token: 0x040001A7 RID: 423
+ private List _dataSource2 = new List();
+
+ // Token: 0x040001A8 RID: 424
+ private List _dataSource_mmi = new List();
+
+ // Token: 0x040001A9 RID: 425
+ private int _dataIndex;
+
+ // Token: 0x040001AA RID: 426
+ private List _stationInfoList = new List();
+
+ // Token: 0x040001AB RID: 427
+ private mmiUI mmiUI;
+
+ // Token: 0x040001AC RID: 428
+ private Dictionary _prevBuff;
+
+ // Token: 0x040001AD RID: 429
+ private uint[] _analysisID;
+
+ // Token: 0x040001AE RID: 430
+ private int formNo;
+
+ // Token: 0x040001AF RID: 431
+ private bool isUp;
+
+ // Token: 0x040001B0 RID: 432
+ private List stationList;
+
+ // Token: 0x040001B1 RID: 433
+ private ScrollBtn _scrollMode = ScrollBtn.FwdS;
+
+ // Token: 0x040001B2 RID: 434
+ private IContainer components = null;
+
+ // Token: 0x040001B3 RID: 435
+ private Label label1;
+
+ // Token: 0x040001B4 RID: 436
+ private Button button1;
+
+ // Token: 0x040001B5 RID: 437
+ private Label label5;
+
+ // Token: 0x040001B6 RID: 438
+ private Label lbl_LOGFILE;
+
+ // Token: 0x040001B7 RID: 439
+ private Label lbl_STATUS;
+
+ // Token: 0x040001B8 RID: 440
+ private Button buttonNEXT_FAST;
+
+ // Token: 0x040001B9 RID: 441
+ private Button buttonNEXT;
+
+ // Token: 0x040001BA RID: 442
+ private Button buttonPREV;
+
+ // Token: 0x040001BB RID: 443
+ private Button buttonPREV_FAST;
+
+ // Token: 0x040001BC RID: 444
+ private Button bt_fileOpen;
+
+ // Token: 0x040001BD RID: 445
+ public Label lblDataCount;
+
+ // Token: 0x040001BE RID: 446
+ public TextBox lblDataIdx;
+
+ // Token: 0x040001BF RID: 447
+ private Timer tmr_AutoScroll;
+
+ // Token: 0x040001C0 RID: 448
+ public CheckBox checkBox1;
+
+ // Token: 0x040001C1 RID: 449
+ public CheckBox checkBox2;
+
+ // Token: 0x040001C2 RID: 450
+ private Label lbl_formNo;
+
+ // Token: 0x040001C3 RID: 451
+ private Button button2;
+
+ // Token: 0x040001C4 RID: 452
+ public CheckBox checkBox3;
+
+ // Token: 0x040001C5 RID: 453
+ private CheckBox cbMMIOnly;
+
+ // Token: 0x040001C6 RID: 454
+ private Panel panel1;
+ }
+}
diff --git a/c_Sharp_Code/subFrom/atccodeselform.cs b/c_Sharp_Code/subFrom/atccodeselform.cs
new file mode 100644
index 0000000..24b6f29
--- /dev/null
+++ b/c_Sharp_Code/subFrom/atccodeselform.cs
@@ -0,0 +1,235 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x0200000B RID: 11
+ public class atcCodeSelForm : Form
+ {
+ // Token: 0x14000002 RID: 2
+ // (add) Token: 0x06000041 RID: 65 RVA: 0x0000586C File Offset: 0x00003A6C
+ // (remove) Token: 0x06000042 RID: 66 RVA: 0x000058A4 File Offset: 0x00003AA4
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event atcCodeSelForm.DataSender CloseEvent;
+
+ // Token: 0x06000043 RID: 67 RVA: 0x000058D9 File Offset: 0x00003AD9
+ public atcCodeSelForm()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x06000044 RID: 68 RVA: 0x000058F1 File Offset: 0x00003AF1
+ private void button_02_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000045 RID: 69 RVA: 0x00005912 File Offset: 0x00003B12
+ private void button_01_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000046 RID: 70 RVA: 0x00005933 File Offset: 0x00003B33
+ private void button_25_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000047 RID: 71 RVA: 0x00005954 File Offset: 0x00003B54
+ private void button_40_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000048 RID: 72 RVA: 0x00005975 File Offset: 0x00003B75
+ private void button_55_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000049 RID: 73 RVA: 0x00005996 File Offset: 0x00003B96
+ private void button_65_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600004A RID: 74 RVA: 0x000059B7 File Offset: 0x00003BB7
+ private void button_75_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600004B RID: 75 RVA: 0x000059D8 File Offset: 0x00003BD8
+ private void button_DE_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600004C RID: 76 RVA: 0x000059F9 File Offset: 0x00003BF9
+ private void button_DW_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600004D RID: 77 RVA: 0x00005A1C File Offset: 0x00003C1C
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x0600004E RID: 78 RVA: 0x00005A54 File Offset: 0x00003C54
+ private void InitializeComponent()
+ {
+ this.button_40 = new Button();
+ this.button_25 = new Button();
+ this.button_01 = new Button();
+ this.button_02 = new Button();
+ this.button_DW = new Button();
+ this.button_DE = new Button();
+ this.button_75 = new Button();
+ this.button_65 = new Button();
+ this.button_55 = new Button();
+ base.SuspendLayout();
+ this.button_40.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_40.Location = new Point(225, 3);
+ this.button_40.Name = "button_40";
+ this.button_40.Size = new Size(75, 42);
+ this.button_40.TabIndex = 9;
+ this.button_40.Text = "40";
+ this.button_40.UseVisualStyleBackColor = true;
+ this.button_40.Click += this.button_40_Click;
+ this.button_25.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_25.Location = new Point(151, 3);
+ this.button_25.Name = "button_25";
+ this.button_25.Size = new Size(75, 42);
+ this.button_25.TabIndex = 8;
+ this.button_25.Text = "25";
+ this.button_25.UseVisualStyleBackColor = true;
+ this.button_25.Click += this.button_25_Click;
+ this.button_01.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_01.Location = new Point(77, 3);
+ this.button_01.Name = "button_01";
+ this.button_01.Size = new Size(75, 42);
+ this.button_01.TabIndex = 7;
+ this.button_01.Text = "01";
+ this.button_01.UseVisualStyleBackColor = true;
+ this.button_01.Click += this.button_01_Click;
+ this.button_02.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_02.Location = new Point(3, 3);
+ this.button_02.Name = "button_02";
+ this.button_02.Size = new Size(75, 42);
+ this.button_02.TabIndex = 6;
+ this.button_02.Text = "02";
+ this.button_02.UseVisualStyleBackColor = true;
+ this.button_02.Click += this.button_02_Click;
+ this.button_DW.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_DW.Location = new Point(225, 47);
+ this.button_DW.Name = "button_DW";
+ this.button_DW.Size = new Size(75, 42);
+ this.button_DW.TabIndex = 13;
+ this.button_DW.Text = "DW";
+ this.button_DW.UseVisualStyleBackColor = true;
+ this.button_DW.Click += this.button_DW_Click;
+ this.button_DE.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_DE.Location = new Point(151, 47);
+ this.button_DE.Name = "button_DE";
+ this.button_DE.Size = new Size(75, 42);
+ this.button_DE.TabIndex = 12;
+ this.button_DE.Text = "DE";
+ this.button_DE.UseVisualStyleBackColor = true;
+ this.button_DE.Click += this.button_DE_Click;
+ this.button_75.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_75.Location = new Point(77, 47);
+ this.button_75.Name = "button_75";
+ this.button_75.Size = new Size(75, 42);
+ this.button_75.TabIndex = 11;
+ this.button_75.Text = "75";
+ this.button_75.UseVisualStyleBackColor = true;
+ this.button_75.Click += this.button_75_Click;
+ this.button_65.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_65.Location = new Point(3, 47);
+ this.button_65.Name = "button_65";
+ this.button_65.Size = new Size(75, 42);
+ this.button_65.TabIndex = 10;
+ this.button_65.Text = "65";
+ this.button_65.UseVisualStyleBackColor = true;
+ this.button_65.Click += this.button_65_Click;
+ this.button_55.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_55.Location = new Point(300, 3);
+ this.button_55.Name = "button_55";
+ this.button_55.Size = new Size(75, 42);
+ this.button_55.TabIndex = 14;
+ this.button_55.Text = "55";
+ this.button_55.UseVisualStyleBackColor = true;
+ this.button_55.Click += this.button_55_Click;
+ base.AutoScaleMode = AutoScaleMode.None;
+ this.BackColor = SystemColors.HotTrack;
+ base.ClientSize = new Size(378, 93);
+ base.Controls.Add(this.button_55);
+ base.Controls.Add(this.button_DW);
+ base.Controls.Add(this.button_DE);
+ base.Controls.Add(this.button_75);
+ base.Controls.Add(this.button_65);
+ base.Controls.Add(this.button_40);
+ base.Controls.Add(this.button_25);
+ base.Controls.Add(this.button_01);
+ base.Controls.Add(this.button_02);
+ base.FormBorderStyle = FormBorderStyle.None;
+ base.Name = "atcCodeSelForm";
+ this.Text = "atcCodeSelForm";
+ base.ResumeLayout(false);
+ }
+
+ // Token: 0x0400002B RID: 43
+ private IContainer components = null;
+
+ // Token: 0x0400002C RID: 44
+ private Button button_40;
+
+ // Token: 0x0400002D RID: 45
+ private Button button_25;
+
+ // Token: 0x0400002E RID: 46
+ private Button button_01;
+
+ // Token: 0x0400002F RID: 47
+ private Button button_02;
+
+ // Token: 0x04000030 RID: 48
+ private Button button_DW;
+
+ // Token: 0x04000031 RID: 49
+ private Button button_DE;
+
+ // Token: 0x04000032 RID: 50
+ private Button button_75;
+
+ // Token: 0x04000033 RID: 51
+ private Button button_65;
+
+ // Token: 0x04000034 RID: 52
+ private Button button_55;
+
+ // Token: 0x02000059 RID: 89
+ // (Invoke) Token: 0x06000198 RID: 408
+ public delegate void DataSender(string value);
+ }
+}
diff --git a/c_Sharp_Code/subFrom/atcstatselform.cs b/c_Sharp_Code/subFrom/atcstatselform.cs
new file mode 100644
index 0000000..4e0e67d
--- /dev/null
+++ b/c_Sharp_Code/subFrom/atcstatselform.cs
@@ -0,0 +1,135 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x0200000C RID: 12
+ public class atcStatSelForm : Form
+ {
+ // Token: 0x14000003 RID: 3
+ // (add) Token: 0x0600004F RID: 79 RVA: 0x0000616C File Offset: 0x0000436C
+ // (remove) Token: 0x06000050 RID: 80 RVA: 0x000061A4 File Offset: 0x000043A4
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event atcStatSelForm.DataSender CloseEvent;
+
+ // Token: 0x06000051 RID: 81 RVA: 0x000061D9 File Offset: 0x000043D9
+ public atcStatSelForm()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x06000052 RID: 82 RVA: 0x000061F1 File Offset: 0x000043F1
+ private void button_ipdt_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000053 RID: 83 RVA: 0x00006212 File Offset: 0x00004412
+ private void button_mpdt_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000054 RID: 84 RVA: 0x00006233 File Offset: 0x00004433
+ private void button_active_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000055 RID: 85 RVA: 0x00006254 File Offset: 0x00004454
+ private void button_standby_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000056 RID: 86 RVA: 0x00006278 File Offset: 0x00004478
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x06000057 RID: 87 RVA: 0x000062B0 File Offset: 0x000044B0
+ private void InitializeComponent()
+ {
+ this.button_mpdt = new Button();
+ this.button_ipdt = new Button();
+ this.button_standby = new Button();
+ this.button_active = new Button();
+ base.SuspendLayout();
+ this.button_mpdt.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_mpdt.Location = new Point(75, 1);
+ this.button_mpdt.Name = "button_mpdt";
+ this.button_mpdt.Size = new Size(75, 42);
+ this.button_mpdt.TabIndex = 3;
+ this.button_mpdt.Text = "Manual PDT";
+ this.button_mpdt.UseVisualStyleBackColor = true;
+ this.button_mpdt.Click += this.button_mpdt_Click;
+ this.button_ipdt.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_ipdt.Location = new Point(1, 1);
+ this.button_ipdt.Name = "button_ipdt";
+ this.button_ipdt.Size = new Size(75, 42);
+ this.button_ipdt.TabIndex = 2;
+ this.button_ipdt.Text = "Initial \r\nPDT";
+ this.button_ipdt.UseVisualStyleBackColor = true;
+ this.button_ipdt.Click += this.button_ipdt_Click;
+ this.button_standby.Font = new Font("맑은 고딕", 9f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_standby.Location = new Point(223, 1);
+ this.button_standby.Name = "button_standby";
+ this.button_standby.Size = new Size(75, 42);
+ this.button_standby.TabIndex = 5;
+ this.button_standby.Text = "ATC STANDBY";
+ this.button_standby.UseVisualStyleBackColor = true;
+ this.button_standby.Click += this.button_standby_Click;
+ this.button_active.Font = new Font("맑은 고딕", 9f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_active.Location = new Point(149, 1);
+ this.button_active.Name = "button_active";
+ this.button_active.Size = new Size(75, 42);
+ this.button_active.TabIndex = 4;
+ this.button_active.Text = "ATC ACTIVE";
+ this.button_active.UseVisualStyleBackColor = true;
+ this.button_active.Click += this.button_active_Click;
+ base.AutoScaleMode = AutoScaleMode.None;
+ this.BackColor = SystemColors.HotTrack;
+ base.ClientSize = new Size(300, 46);
+ base.Controls.Add(this.button_standby);
+ base.Controls.Add(this.button_active);
+ base.Controls.Add(this.button_mpdt);
+ base.Controls.Add(this.button_ipdt);
+ base.FormBorderStyle = FormBorderStyle.None;
+ base.Name = "atcStatSelForm";
+ this.Text = "atcStatSelForm";
+ base.ResumeLayout(false);
+ }
+
+ // Token: 0x04000036 RID: 54
+ private IContainer components = null;
+
+ // Token: 0x04000037 RID: 55
+ private Button button_mpdt;
+
+ // Token: 0x04000038 RID: 56
+ private Button button_ipdt;
+
+ // Token: 0x04000039 RID: 57
+ private Button button_standby;
+
+ // Token: 0x0400003A RID: 58
+ private Button button_active;
+
+ // Token: 0x0200005A RID: 90
+ // (Invoke) Token: 0x0600019C RID: 412
+ public delegate void DataSender(string value);
+ }
+}
diff --git a/c_Sharp_Code/subFrom/atomarkerselform.cs b/c_Sharp_Code/subFrom/atomarkerselform.cs
new file mode 100644
index 0000000..5059d9e
--- /dev/null
+++ b/c_Sharp_Code/subFrom/atomarkerselform.cs
@@ -0,0 +1,155 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x0200000D RID: 13
+ public class atoMarkerSelForm : Form
+ {
+ // Token: 0x14000004 RID: 4
+ // (add) Token: 0x06000058 RID: 88 RVA: 0x0000660C File Offset: 0x0000480C
+ // (remove) Token: 0x06000059 RID: 89 RVA: 0x00006644 File Offset: 0x00004844
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event atoMarkerSelForm.DataSender CloseEvent;
+
+ // Token: 0x0600005A RID: 90 RVA: 0x00006679 File Offset: 0x00004879
+ public atoMarkerSelForm()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x0600005B RID: 91 RVA: 0x00006691 File Offset: 0x00004891
+ private void button_02_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600005C RID: 92 RVA: 0x000066B2 File Offset: 0x000048B2
+ private void button_01_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600005D RID: 93 RVA: 0x000066D3 File Offset: 0x000048D3
+ private void button_25_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600005E RID: 94 RVA: 0x000066F4 File Offset: 0x000048F4
+ private void button_65_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x0600005F RID: 95 RVA: 0x00006715 File Offset: 0x00004915
+ private void button_75_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000060 RID: 96 RVA: 0x00006738 File Offset: 0x00004938
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x06000061 RID: 97 RVA: 0x00006770 File Offset: 0x00004970
+ private void InitializeComponent()
+ {
+ this.button_75 = new Button();
+ this.button_65 = new Button();
+ this.button_25 = new Button();
+ this.button_01 = new Button();
+ this.button_02 = new Button();
+ base.SuspendLayout();
+ this.button_75.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_75.Location = new Point(79, 48);
+ this.button_75.Name = "button_75";
+ this.button_75.Size = new Size(75, 42);
+ this.button_75.TabIndex = 17;
+ this.button_75.Text = "ATS";
+ this.button_75.UseVisualStyleBackColor = true;
+ this.button_75.Click += this.button_75_Click;
+ this.button_65.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_65.Location = new Point(5, 48);
+ this.button_65.Name = "button_65";
+ this.button_65.Size = new Size(75, 42);
+ this.button_65.TabIndex = 16;
+ this.button_65.Text = "PGX";
+ this.button_65.UseVisualStyleBackColor = true;
+ this.button_65.Click += this.button_65_Click;
+ this.button_25.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_25.Location = new Point(153, 4);
+ this.button_25.Name = "button_25";
+ this.button_25.Size = new Size(75, 42);
+ this.button_25.TabIndex = 15;
+ this.button_25.Text = "PG3-2";
+ this.button_25.UseVisualStyleBackColor = true;
+ this.button_25.Click += this.button_25_Click;
+ this.button_01.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_01.Location = new Point(79, 4);
+ this.button_01.Name = "button_01";
+ this.button_01.Size = new Size(75, 42);
+ this.button_01.TabIndex = 14;
+ this.button_01.Text = "PG2";
+ this.button_01.UseVisualStyleBackColor = true;
+ this.button_01.Click += this.button_01_Click;
+ this.button_02.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_02.Location = new Point(5, 4);
+ this.button_02.Name = "button_02";
+ this.button_02.Size = new Size(75, 42);
+ this.button_02.TabIndex = 13;
+ this.button_02.Text = "PG1";
+ this.button_02.UseVisualStyleBackColor = true;
+ this.button_02.Click += this.button_02_Click;
+ base.AutoScaleMode = AutoScaleMode.None;
+ this.BackColor = SystemColors.HotTrack;
+ base.ClientSize = new Size(233, 92);
+ base.Controls.Add(this.button_75);
+ base.Controls.Add(this.button_65);
+ base.Controls.Add(this.button_25);
+ base.Controls.Add(this.button_01);
+ base.Controls.Add(this.button_02);
+ base.FormBorderStyle = FormBorderStyle.None;
+ base.Name = "atoMarkerSelForm";
+ this.Text = "atoMarkerSelForm";
+ base.ResumeLayout(false);
+ }
+
+ // Token: 0x0400003C RID: 60
+ private IContainer components = null;
+
+ // Token: 0x0400003D RID: 61
+ private Button button_75;
+
+ // Token: 0x0400003E RID: 62
+ private Button button_65;
+
+ // Token: 0x0400003F RID: 63
+ private Button button_25;
+
+ // Token: 0x04000040 RID: 64
+ private Button button_01;
+
+ // Token: 0x04000041 RID: 65
+ private Button button_02;
+
+ // Token: 0x0200005B RID: 91
+ // (Invoke) Token: 0x060001A0 RID: 416
+ public delegate void DataSender(string value);
+ }
+}
diff --git a/c_Sharp_Code/subFrom/candata.cs b/c_Sharp_Code/subFrom/candata.cs
new file mode 100644
index 0000000..d2e06af
--- /dev/null
+++ b/c_Sharp_Code/subFrom/candata.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000017 RID: 23
+ public class CanData
+ {
+ // Token: 0x1700000E RID: 14
+ // (get) Token: 0x06000114 RID: 276 RVA: 0x00023644 File Offset: 0x00021844
+ // (set) Token: 0x06000115 RID: 277 RVA: 0x0002365C File Offset: 0x0002185C
+ public uint Identifier
+ {
+ get
+ {
+ return this.identifier;
+ }
+ set
+ {
+ this.identifier = value;
+ }
+ }
+
+ // Token: 0x1700000F RID: 15
+ // (get) Token: 0x06000116 RID: 278 RVA: 0x00023668 File Offset: 0x00021868
+ // (set) Token: 0x06000117 RID: 279 RVA: 0x00023680 File Offset: 0x00021880
+ public byte[] Data
+ {
+ get
+ {
+ return this.data;
+ }
+ set
+ {
+ this.data = value;
+ }
+ }
+
+ // Token: 0x06000118 RID: 280 RVA: 0x0002368A File Offset: 0x0002188A
+ public CanData(uint identifier, byte[] data)
+ {
+ this.identifier = identifier;
+ this.data = data;
+ }
+
+ // Token: 0x040001C7 RID: 455
+ private uint identifier;
+
+ // Token: 0x040001C8 RID: 456
+ private byte[] data;
+ }
+}
diff --git a/c_Sharp_Code/subFrom/canlogdata.cs b/c_Sharp_Code/subFrom/canlogdata.cs
new file mode 100644
index 0000000..b99ad56
--- /dev/null
+++ b/c_Sharp_Code/subFrom/canlogdata.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000018 RID: 24
+ public class CanLogData
+ {
+ // Token: 0x17000010 RID: 16
+ // (get) Token: 0x06000119 RID: 281 RVA: 0x000236A4 File Offset: 0x000218A4
+ // (set) Token: 0x0600011A RID: 282 RVA: 0x000236BC File Offset: 0x000218BC
+ public DateTime LogTime
+ {
+ get
+ {
+ return this.logTime;
+ }
+ set
+ {
+ this.logTime = value;
+ }
+ }
+
+ // Token: 0x17000011 RID: 17
+ // (get) Token: 0x0600011B RID: 283 RVA: 0x000236C8 File Offset: 0x000218C8
+ // (set) Token: 0x0600011C RID: 284 RVA: 0x000236E0 File Offset: 0x000218E0
+ public List DataList
+ {
+ get
+ {
+ return this.dataList;
+ }
+ set
+ {
+ this.dataList = value;
+ }
+ }
+
+ // Token: 0x0600011D RID: 285 RVA: 0x000236EA File Offset: 0x000218EA
+ public CanLogData()
+ {
+ this.dataList = new List();
+ }
+
+ // Token: 0x0600011E RID: 286 RVA: 0x000236FF File Offset: 0x000218FF
+ public CanLogData(DateTime logTime) : this()
+ {
+ this.logTime = logTime;
+ }
+
+ // Token: 0x040001C9 RID: 457
+ private DateTime logTime;
+
+ // Token: 0x040001CA RID: 458
+ private List dataList;
+ }
+}
diff --git a/c_Sharp_Code/subFrom/doormodeselform.cs b/c_Sharp_Code/subFrom/doormodeselform.cs
new file mode 100644
index 0000000..b37817b
--- /dev/null
+++ b/c_Sharp_Code/subFrom/doormodeselform.cs
@@ -0,0 +1,115 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x0200000E RID: 14
+ public class doorModeSelForm : Form
+ {
+ // Token: 0x14000005 RID: 5
+ // (add) Token: 0x06000062 RID: 98 RVA: 0x00006B88 File Offset: 0x00004D88
+ // (remove) Token: 0x06000063 RID: 99 RVA: 0x00006BC0 File Offset: 0x00004DC0
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event doorModeSelForm.DataSender CloseEvent;
+
+ // Token: 0x06000064 RID: 100 RVA: 0x00006BF5 File Offset: 0x00004DF5
+ public doorModeSelForm()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x06000065 RID: 101 RVA: 0x00006C0D File Offset: 0x00004E0D
+ private void button_aa_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000066 RID: 102 RVA: 0x00006C2E File Offset: 0x00004E2E
+ private void button_am_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000067 RID: 103 RVA: 0x00006C4F File Offset: 0x00004E4F
+ private void button_mm_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(((Button)sender).Text);
+ base.Close();
+ }
+
+ // Token: 0x06000068 RID: 104 RVA: 0x00006C70 File Offset: 0x00004E70
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x06000069 RID: 105 RVA: 0x00006CA8 File Offset: 0x00004EA8
+ private void InitializeComponent()
+ {
+ this.button_mm = new Button();
+ this.button_am = new Button();
+ this.button_aa = new Button();
+ base.SuspendLayout();
+ this.button_mm.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_mm.Location = new Point(151, 4);
+ this.button_mm.Name = "button_mm";
+ this.button_mm.Size = new Size(75, 42);
+ this.button_mm.TabIndex = 18;
+ this.button_mm.Text = "M / M";
+ this.button_mm.UseVisualStyleBackColor = true;
+ this.button_mm.Click += this.button_mm_Click;
+ this.button_am.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_am.Location = new Point(77, 4);
+ this.button_am.Name = "button_am";
+ this.button_am.Size = new Size(75, 42);
+ this.button_am.TabIndex = 17;
+ this.button_am.Text = "A / M";
+ this.button_am.UseVisualStyleBackColor = true;
+ this.button_am.Click += this.button_am_Click;
+ this.button_aa.Font = new Font("맑은 고딕", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_aa.Location = new Point(3, 4);
+ this.button_aa.Name = "button_aa";
+ this.button_aa.Size = new Size(75, 42);
+ this.button_aa.TabIndex = 16;
+ this.button_aa.Text = "A / A";
+ this.button_aa.UseVisualStyleBackColor = true;
+ this.button_aa.Click += this.button_aa_Click;
+ base.AutoScaleMode = AutoScaleMode.None;
+ this.BackColor = SystemColors.HotTrack;
+ base.ClientSize = new Size(230, 48);
+ base.Controls.Add(this.button_mm);
+ base.Controls.Add(this.button_am);
+ base.Controls.Add(this.button_aa);
+ base.FormBorderStyle = FormBorderStyle.None;
+ base.Name = "doorModeSelForm";
+ this.Text = "doorModeSelForm";
+ base.ResumeLayout(false);
+ }
+
+ // Token: 0x04000043 RID: 67
+ private IContainer components = null;
+
+ // Token: 0x04000044 RID: 68
+ private Button button_mm;
+
+ // Token: 0x04000045 RID: 69
+ private Button button_am;
+
+ // Token: 0x04000046 RID: 70
+ private Button button_aa;
+
+ // Token: 0x0200005C RID: 92
+ // (Invoke) Token: 0x060001A4 RID: 420
+ public delegate void DataSender(string value);
+ }
+}
diff --git a/c_Sharp_Code/subFrom/dormdata.cs b/c_Sharp_Code/subFrom/dormdata.cs
new file mode 100644
index 0000000..db4ce10
--- /dev/null
+++ b/c_Sharp_Code/subFrom/dormdata.cs
@@ -0,0 +1,160 @@
+using System;
+using System.ComponentModel;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x0200000F RID: 15
+ public class FormDATA : Form
+ {
+ // Token: 0x0600006A RID: 106 RVA: 0x00006F45 File Offset: 0x00005145
+ public FormDATA()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x0600006B RID: 107 RVA: 0x00006F5D File Offset: 0x0000515D
+ public void displaytcms(byte[] data)
+ {
+ this.tb_tcms.Text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ");
+ }
+
+ // Token: 0x0600006C RID: 108 RVA: 0x00006F85 File Offset: 0x00005185
+ public void displayftcms1(byte[] data)
+ {
+ this.tb_ftcms1.Text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ");
+ }
+
+ // Token: 0x0600006D RID: 109 RVA: 0x00006FAD File Offset: 0x000051AD
+ public void displayftcms2(byte[] data)
+ {
+ this.tb_ftcms1.Text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ");
+ }
+
+ // Token: 0x0600006E RID: 110 RVA: 0x00006FD5 File Offset: 0x000051D5
+ public void displaymmi(byte[] data)
+ {
+ this.tb_mmi.Text = BitConverter.ToString(data, 0, data.Length).Replace("-", " ");
+ }
+
+ // Token: 0x0600006F RID: 111 RVA: 0x00007000 File Offset: 0x00005200
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x06000070 RID: 112 RVA: 0x00007038 File Offset: 0x00005238
+ private void InitializeComponent()
+ {
+ this.label1 = new Label();
+ this.tb_tcms = new TextBox();
+ this.label2 = new Label();
+ this.tb_ftcms1 = new TextBox();
+ this.tb_mmi = new TextBox();
+ this.label3 = new Label();
+ this.tb_ftcms2 = new TextBox();
+ this.label4 = new Label();
+ base.SuspendLayout();
+ this.label1.AutoSize = true;
+ this.label1.Font = new Font("굴림", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.label1.Location = new Point(12, 9);
+ this.label1.Name = "label1";
+ this.label1.Size = new Size(138, 21);
+ this.label1.TabIndex = 0;
+ this.label1.Text = "ATC > TCMS";
+ this.tb_tcms.Location = new Point(12, 42);
+ this.tb_tcms.Multiline = true;
+ this.tb_tcms.Name = "tb_tcms";
+ this.tb_tcms.ReadOnly = true;
+ this.tb_tcms.Size = new Size(779, 60);
+ this.tb_tcms.TabIndex = 1;
+ this.label2.AutoSize = true;
+ this.label2.Font = new Font("굴림", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.label2.Location = new Point(12, 133);
+ this.label2.Name = "label2";
+ this.label2.Size = new Size(162, 21);
+ this.label2.TabIndex = 2;
+ this.label2.Text = "TCMS > ATC-1";
+ this.tb_ftcms1.Location = new Point(12, 168);
+ this.tb_ftcms1.Multiline = true;
+ this.tb_ftcms1.Name = "tb_ftcms1";
+ this.tb_ftcms1.ReadOnly = true;
+ this.tb_ftcms1.Size = new Size(779, 58);
+ this.tb_ftcms1.TabIndex = 3;
+ this.tb_mmi.Location = new Point(12, 451);
+ this.tb_mmi.Multiline = true;
+ this.tb_mmi.Name = "tb_mmi";
+ this.tb_mmi.ReadOnly = true;
+ this.tb_mmi.Size = new Size(779, 60);
+ this.tb_mmi.TabIndex = 5;
+ this.label3.AutoSize = true;
+ this.label3.Font = new Font("굴림", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.label3.Location = new Point(12, 418);
+ this.label3.Name = "label3";
+ this.label3.Size = new Size(117, 21);
+ this.label3.TabIndex = 4;
+ this.label3.Text = "ATC > MMI";
+ this.tb_ftcms2.Location = new Point(12, 300);
+ this.tb_ftcms2.Multiline = true;
+ this.tb_ftcms2.Name = "tb_ftcms2";
+ this.tb_ftcms2.ReadOnly = true;
+ this.tb_ftcms2.Size = new Size(779, 58);
+ this.tb_ftcms2.TabIndex = 7;
+ this.label4.AutoSize = true;
+ this.label4.Font = new Font("굴림", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.label4.Location = new Point(12, 265);
+ this.label4.Name = "label4";
+ this.label4.Size = new Size(162, 21);
+ this.label4.TabIndex = 6;
+ this.label4.Text = "TCMS > ATC-2";
+ base.AutoScaleDimensions = new SizeF(7f, 12f);
+ base.AutoScaleMode = AutoScaleMode.Font;
+ base.ClientSize = new Size(867, 523);
+ base.Controls.Add(this.tb_ftcms2);
+ base.Controls.Add(this.label4);
+ base.Controls.Add(this.tb_mmi);
+ base.Controls.Add(this.label3);
+ base.Controls.Add(this.tb_ftcms1);
+ base.Controls.Add(this.label2);
+ base.Controls.Add(this.tb_tcms);
+ base.Controls.Add(this.label1);
+ base.Name = "FormDATA";
+ this.Text = "FormDATA";
+ base.ResumeLayout(false);
+ base.PerformLayout();
+ }
+
+ // Token: 0x04000047 RID: 71
+ private IContainer components = null;
+
+ // Token: 0x04000048 RID: 72
+ private Label label1;
+
+ // Token: 0x04000049 RID: 73
+ private TextBox tb_tcms;
+
+ // Token: 0x0400004A RID: 74
+ private Label label2;
+
+ // Token: 0x0400004B RID: 75
+ private TextBox tb_ftcms1;
+
+ // Token: 0x0400004C RID: 76
+ private TextBox tb_mmi;
+
+ // Token: 0x0400004D RID: 77
+ private Label label3;
+
+ // Token: 0x0400004E RID: 78
+ private TextBox tb_ftcms2;
+
+ // Token: 0x0400004F RID: 79
+ private Label label4;
+ }
+}
diff --git a/c_Sharp_Code/subFrom/extensions.cs b/c_Sharp_Code/subFrom/extensions.cs
new file mode 100644
index 0000000..e9f7b82
--- /dev/null
+++ b/c_Sharp_Code/subFrom/extensions.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Reflection;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000014 RID: 20
+ public static class Extensions
+ {
+ // Token: 0x060000F0 RID: 240 RVA: 0x0001FE04 File Offset: 0x0001E004
+ public static void DoubleBuffered(this Control control, bool enabled)
+ {
+ PropertyInfo property = control.GetType().GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic);
+ property.SetValue(control, enabled, null);
+ }
+ }
+}
diff --git a/c_Sharp_Code/subFrom/highlowform.cs b/c_Sharp_Code/subFrom/highlowform.cs
new file mode 100644
index 0000000..51915fe
--- /dev/null
+++ b/c_Sharp_Code/subFrom/highlowform.cs
@@ -0,0 +1,95 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000010 RID: 16
+ public class highlowForm : Form
+ {
+ // Token: 0x14000006 RID: 6
+ // (add) Token: 0x06000071 RID: 113 RVA: 0x00007560 File Offset: 0x00005760
+ // (remove) Token: 0x06000072 RID: 114 RVA: 0x00007598 File Offset: 0x00005798
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event highlowForm.DataSender CloseEvent;
+
+ // Token: 0x06000073 RID: 115 RVA: 0x000075CD File Offset: 0x000057CD
+ public highlowForm()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x06000074 RID: 116 RVA: 0x000075E5 File Offset: 0x000057E5
+ private void button_high_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(true);
+ base.Close();
+ }
+
+ // Token: 0x06000075 RID: 117 RVA: 0x000075FC File Offset: 0x000057FC
+ private void button_low_Click(object sender, EventArgs e)
+ {
+ this.CloseEvent(false);
+ base.Close();
+ }
+
+ // Token: 0x06000076 RID: 118 RVA: 0x00007614 File Offset: 0x00005814
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x06000077 RID: 119 RVA: 0x0000764C File Offset: 0x0000584C
+ private void InitializeComponent()
+ {
+ this.button_high = new Button();
+ this.button_low = new Button();
+ base.SuspendLayout();
+ this.button_high.Font = new Font("나눔고딕 ExtraBold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_high.Location = new Point(1, 2);
+ this.button_high.Name = "button_high";
+ this.button_high.Size = new Size(75, 37);
+ this.button_high.TabIndex = 0;
+ this.button_high.Text = "HIGH";
+ this.button_high.UseVisualStyleBackColor = true;
+ this.button_high.Click += this.button_high_Click;
+ this.button_low.Font = new Font("나눔고딕 ExtraBold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button_low.Location = new Point(79, 2);
+ this.button_low.Name = "button_low";
+ this.button_low.Size = new Size(75, 37);
+ this.button_low.TabIndex = 1;
+ this.button_low.Text = "LOW";
+ this.button_low.UseVisualStyleBackColor = true;
+ this.button_low.Click += this.button_low_Click;
+ base.AutoScaleMode = AutoScaleMode.None;
+ this.BackColor = SystemColors.HotTrack;
+ base.ClientSize = new Size(155, 42);
+ base.Controls.Add(this.button_low);
+ base.Controls.Add(this.button_high);
+ base.FormBorderStyle = FormBorderStyle.None;
+ base.Name = "highlowForm";
+ this.Text = "highlowForm";
+ base.ResumeLayout(false);
+ }
+
+ // Token: 0x04000051 RID: 81
+ private IContainer components = null;
+
+ // Token: 0x04000052 RID: 82
+ private Button button_high;
+
+ // Token: 0x04000053 RID: 83
+ private Button button_low;
+
+ // Token: 0x0200005D RID: 93
+ // (Invoke) Token: 0x060001A8 RID: 424
+ public delegate void DataSender(bool value);
+ }
+}
diff --git a/c_Sharp_Code/subFrom/itemselectform.cs b/c_Sharp_Code/subFrom/itemselectform.cs
new file mode 100644
index 0000000..816a9f2
--- /dev/null
+++ b/c_Sharp_Code/subFrom/itemselectform.cs
@@ -0,0 +1,889 @@
+using System;
+using System.Collections;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Reflection;
+using System.Windows.Forms;
+using SL200_RTLogViewer.lib;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000011 RID: 17
+ public class itemSelectForm : Form
+ {
+ // Token: 0x14000007 RID: 7
+ // (add) Token: 0x06000078 RID: 120 RVA: 0x00007828 File Offset: 0x00005A28
+ // (remove) Token: 0x06000079 RID: 121 RVA: 0x00007860 File Offset: 0x00005A60
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event itemSelectForm.DataSender SenderEvent;
+
+ // Token: 0x0600007A RID: 122 RVA: 0x00007895 File Offset: 0x00005A95
+ public itemSelectForm()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x0600007B RID: 123 RVA: 0x000078BF File Offset: 0x00005ABF
+ private void itemSelectForm_Load(object sender, EventArgs e)
+ {
+ this.comboBox1.SelectedIndex = 0;
+ this.listViewADDED.DoubleBuffered(true);
+ this.listViewEx.DoubleBuffered(true);
+ this.listViewEx.ListViewItemSorter = this.lvwColumnSorter;
+ }
+
+ // Token: 0x0600007C RID: 124 RVA: 0x000078FC File Offset: 0x00005AFC
+ private void listViewEx_DoubleClick(object sender, EventArgs e)
+ {
+ string[] items = new string[]
+ {
+ this.listViewEx.FocusedItem.SubItems[0].Text,
+ this.listViewEx.FocusedItem.SubItems[1].Text,
+ this.listViewEx.FocusedItem.SubItems[2].Text
+ };
+ ListViewItem value = new ListViewItem(items);
+ this.listViewADDED.Items.Add(value);
+ this.listViewEx.Items.RemoveAt(this.listViewEx.FocusedItem.Index);
+ this.labelitemCount();
+ }
+
+ // Token: 0x0600007D RID: 125 RVA: 0x000079AC File Offset: 0x00005BAC
+ private void listViewADDED_DoubleClick(object sender, EventArgs e)
+ {
+ bool flag = this.listViewADDED.Items.Count < 40;
+ if (flag)
+ {
+ this.listViewADDED.Items.RemoveAt(this.listViewADDED.FocusedItem.Index);
+ this.comboRefresh();
+ this.labelitemCount();
+ }
+ }
+
+ // Token: 0x0600007E RID: 126 RVA: 0x00007A04 File Offset: 0x00005C04
+ private void buttonOK_Click(object sender, EventArgs e)
+ {
+ ListView lv = new ListView();
+ lv = this.listViewADDED;
+ this.SenderEvent(lv);
+ base.Close();
+ }
+
+ // Token: 0x0600007F RID: 127 RVA: 0x00007A33 File Offset: 0x00005C33
+ private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ this.comboRefresh();
+ }
+
+ // Token: 0x06000080 RID: 128 RVA: 0x00007A40 File Offset: 0x00005C40
+ public void comboRefresh()
+ {
+ string text = this.comboBox1.SelectedItem.ToString();
+ string text2 = text;
+ string text3 = text2;
+ uint num = .ComputeStringHash(text3);
+ if (num <= 1054371910U)
+ {
+ if (num != 323942130U)
+ {
+ if (num != 670519660U)
+ {
+ if (num == 1054371910U)
+ {
+ if (text3 == "ACPU")
+ {
+ this.listViewEx.Items.Clear();
+ FieldInfo[] fields = typeof(ACPU200Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int i = 0; i < fields.Length; i++)
+ {
+ string[] items = new string[]
+ {
+ "ACPU",
+ "200",
+ fields[i].Name
+ };
+ ListViewItem value = new ListViewItem(items);
+ bool flag = this.FindItem(this.listViewADDED, fields[i].Name, "ACPU") == null;
+ if (flag)
+ {
+ this.listViewEx.Items.Add(value);
+ }
+ }
+ fields = typeof(ACPU201Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int j = 0; j < fields.Length; j++)
+ {
+ string[] items2 = new string[]
+ {
+ "ACPU",
+ "201",
+ fields[j].Name
+ };
+ ListViewItem value2 = new ListViewItem(items2);
+ bool flag2 = this.FindItem(this.listViewADDED, fields[j].Name, "ACPU") == null;
+ if (flag2)
+ {
+ this.listViewEx.Items.Add(value2);
+ }
+ }
+ fields = typeof(ACPU202Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int k = 0; k < fields.Length; k++)
+ {
+ string[] items3 = new string[]
+ {
+ "ACPU",
+ "202",
+ fields[k].Name
+ };
+ ListViewItem value3 = new ListViewItem(items3);
+ bool flag3 = this.FindItem(this.listViewADDED, fields[k].Name, "ACPU") == null;
+ if (flag3)
+ {
+ this.listViewEx.Items.Add(value3);
+ }
+ }
+ }
+ }
+ }
+ else if (text3 == "TCMS")
+ {
+ this.listViewEx.Items.Clear();
+ FieldInfo[] fields = typeof(DOSItoTCMSClass).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int l = 0; l < fields.Length; l++)
+ {
+ string[] items4 = new string[]
+ {
+ "TCMS",
+ "UART",
+ fields[l].Name
+ };
+ ListViewItem value4 = new ListViewItem(items4);
+ bool flag4 = this.FindItem(this.listViewADDED, fields[l].Name, "TCMS") == null;
+ if (flag4)
+ {
+ this.listViewEx.Items.Add(value4);
+ }
+ }
+ fields = typeof(TCMStoDOSIClass).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int m = 0; m < fields.Length; m++)
+ {
+ string[] items5 = new string[]
+ {
+ "FTCMS",
+ "UART",
+ fields[m].Name
+ };
+ ListViewItem value5 = new ListViewItem(items5);
+ bool flag5 = this.FindItem(this.listViewADDED, fields[m].Name, "TCMS") == null;
+ if (flag5)
+ {
+ this.listViewEx.Items.Add(value5);
+ }
+ }
+ }
+ }
+ else if (text3 == "MMI")
+ {
+ this.listViewEx.Items.Clear();
+ FieldInfo[] fields = typeof(DOSItoMMIClass).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int n = 0; n < fields.Length; n++)
+ {
+ string[] items6 = new string[]
+ {
+ "MMI",
+ "UART",
+ fields[n].Name.ToUpper()
+ };
+ ListViewItem value6 = new ListViewItem(items6);
+ bool flag6 = this.FindItem(this.listViewADDED, fields[n].Name, "MMI") == null;
+ if (flag6)
+ {
+ this.listViewEx.Items.Add(value6);
+ }
+ }
+ bool @checked = this.checkBox2.Checked;
+ if (@checked)
+ {
+ fields = typeof(MMItoDOSIClass).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num2 = 0; num2 < fields.Length; num2++)
+ {
+ string[] items7 = new string[]
+ {
+ "FMMI",
+ "UART",
+ fields[num2].Name
+ };
+ ListViewItem value7 = new ListViewItem(items7);
+ bool flag7 = this.FindItem(this.listViewADDED, fields[num2].Name, "MMI") == null;
+ if (flag7)
+ {
+ this.listViewEx.Items.Add(value7);
+ }
+ }
+ }
+ }
+ }
+ else if (num <= 3014100859U)
+ {
+ if (num != 1716134816U)
+ {
+ if (num == 3014100859U)
+ {
+ if (text3 == "TWCT")
+ {
+ this.listViewEx.Items.Clear();
+ FieldInfo[] fields = typeof(TWCT320Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num3 = 0; num3 < fields.Length; num3++)
+ {
+ string[] items8 = new string[]
+ {
+ "TWCT",
+ "320",
+ fields[num3].Name
+ };
+ ListViewItem value8 = new ListViewItem(items8);
+ bool flag8 = this.FindItem(this.listViewADDED, fields[num3].Name, "TWCT") == null;
+ if (flag8)
+ {
+ this.listViewEx.Items.Add(value8);
+ }
+ }
+ }
+ }
+ }
+ else if (text3 == "CCC")
+ {
+ this.listViewEx.Items.Clear();
+ FieldInfo[] fields = typeof(CCC150Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num4 = 0; num4 < fields.Length; num4++)
+ {
+ string[] items9 = new string[]
+ {
+ "CCC",
+ "150",
+ fields[num4].Name
+ };
+ ListViewItem value9 = new ListViewItem(items9);
+ bool flag9 = this.FindItem(this.listViewADDED, fields[num4].Name, "CCC") == null;
+ if (flag9)
+ {
+ this.listViewEx.Items.Add(value9);
+ }
+ }
+ fields = typeof(CCC151Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num5 = 0; num5 < fields.Length; num5++)
+ {
+ string[] items10 = new string[]
+ {
+ "CCC",
+ "151",
+ fields[num5].Name
+ };
+ ListViewItem value10 = new ListViewItem(items10);
+ bool flag10 = this.FindItem(this.listViewADDED, fields[num5].Name, "CCC") == null;
+ if (flag10)
+ {
+ this.listViewEx.Items.Add(value10);
+ }
+ }
+ fields = typeof(CCC152Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num6 = 0; num6 < fields.Length; num6++)
+ {
+ string[] items11 = new string[]
+ {
+ "CCC",
+ "152",
+ fields[num6].Name
+ };
+ ListViewItem value11 = new ListViewItem(items11);
+ bool flag11 = this.FindItem(this.listViewADDED, fields[num6].Name, "CCC") == null;
+ if (flag11)
+ {
+ this.listViewEx.Items.Add(value11);
+ }
+ }
+ }
+ }
+ else if (num != 3437617337U)
+ {
+ if (num == 3571249909U)
+ {
+ if (text3 == "ATOR")
+ {
+ this.listViewEx.Items.Clear();
+ FieldInfo[] fields = typeof(ATOR160Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num7 = 0; num7 < fields.Length; num7++)
+ {
+ string[] items12 = new string[]
+ {
+ "ATOR",
+ "160",
+ fields[num7].Name
+ };
+ ListViewItem value12 = new ListViewItem(items12);
+ bool flag12 = this.FindItem(this.listViewADDED, fields[num7].Name, "ATOR") == null;
+ if (flag12)
+ {
+ this.listViewEx.Items.Add(value12);
+ }
+ }
+ }
+ }
+ }
+ else if (text3 == "ATCR")
+ {
+ this.listViewEx.Items.Clear();
+ FieldInfo[] fields = typeof(ATCR250Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num8 = 0; num8 < fields.Length; num8++)
+ {
+ string[] items13 = new string[]
+ {
+ "ATCR",
+ "250",
+ fields[num8].Name
+ };
+ ListViewItem value13 = new ListViewItem(items13);
+ bool flag13 = this.FindItem(this.listViewADDED, fields[num8].Name, "ATCR") == null;
+ if (flag13)
+ {
+ this.listViewEx.Items.Add(value13);
+ }
+ }
+ fields = typeof(ATCR501Class).GetFields(BindingFlags.Instance | BindingFlags.Public);
+ for (int num9 = 0; num9 < fields.Length; num9++)
+ {
+ string[] items14 = new string[]
+ {
+ "ATCR",
+ "501",
+ fields[num9].Name
+ };
+ ListViewItem value14 = new ListViewItem(items14);
+ bool flag14 = this.FindItem(this.listViewADDED, fields[num9].Name, "ATCR") == null;
+ if (flag14)
+ {
+ this.listViewEx.Items.Add(value14);
+ }
+ }
+ }
+ }
+
+ // Token: 0x06000081 RID: 129 RVA: 0x000083F8 File Offset: 0x000065F8
+ public void listViewAdded_Clone(ListView lv)
+ {
+ this.listViewADDED.Items.Clear();
+ for (int i = 0; i < lv.Items.Count; i++)
+ {
+ string[] items = new string[]
+ {
+ lv.Items[i].SubItems[0].Text,
+ lv.Items[i].SubItems[1].Text,
+ lv.Items[i].SubItems[2].Text
+ };
+ ListViewItem value = new ListViewItem(items);
+ this.listViewADDED.Items.Add(value);
+ }
+ }
+
+ // Token: 0x06000082 RID: 130 RVA: 0x000084B8 File Offset: 0x000066B8
+ private ListViewItem FindItem(ListView listview, string keyword, string type)
+ {
+ for (int i = 0; i < listview.Items.Count; i++)
+ {
+ ListViewItem listViewItem = listview.Items[i];
+ bool flag = listViewItem.SubItems[0].Text == type;
+ if (flag)
+ {
+ bool flag2 = listViewItem.SubItems[2].Text.Contains(keyword);
+ bool flag3 = flag2;
+ if (flag3)
+ {
+ return listViewItem;
+ }
+ }
+ }
+ return null;
+ }
+
+ // Token: 0x06000083 RID: 131 RVA: 0x0000853A File Offset: 0x0000673A
+ private void button3_Click(object sender, EventArgs e)
+ {
+ base.Close();
+ }
+
+ // Token: 0x06000084 RID: 132 RVA: 0x00008544 File Offset: 0x00006744
+ private void button4_Click(object sender, EventArgs e)
+ {
+ this.listViewADDED.Items.Clear();
+ this.comboRefresh();
+ this.labelitemCount();
+ }
+
+ // Token: 0x06000085 RID: 133 RVA: 0x00008568 File Offset: 0x00006768
+ public void labelitemCount()
+ {
+ this.lb_itemCount.Text = "선택 항목:" + this.listViewADDED.Items.Count.ToString() + "/40";
+ }
+
+ // Token: 0x06000086 RID: 134 RVA: 0x000085AC File Offset: 0x000067AC
+ private void button1_Click(object sender, EventArgs e)
+ {
+ bool flag = this.listViewEx.FocusedItem != null && this.listViewADDED.Items.Count < 40;
+ if (flag)
+ {
+ string[] items = new string[]
+ {
+ this.listViewEx.FocusedItem.SubItems[0].Text,
+ this.listViewEx.FocusedItem.SubItems[1].Text,
+ this.listViewEx.FocusedItem.SubItems[2].Text
+ };
+ ListViewItem value = new ListViewItem(items);
+ this.listViewADDED.Items.Add(value);
+ this.listViewEx.Items.RemoveAt(this.listViewEx.FocusedItem.Index);
+ this.labelitemCount();
+ this.listViewEx.Items[this.listViewEx.FocusedItem.Index].EnsureVisible();
+ }
+ }
+
+ // Token: 0x06000087 RID: 135 RVA: 0x000086B0 File Offset: 0x000068B0
+ private void button2_Click(object sender, EventArgs e)
+ {
+ bool flag = this.listViewADDED.FocusedItem != null;
+ if (flag)
+ {
+ this.listViewADDED.Items.RemoveAt(this.listViewADDED.FocusedItem.Index);
+ this.comboRefresh();
+ this.labelitemCount();
+ }
+ }
+
+ // Token: 0x06000088 RID: 136 RVA: 0x00008701 File Offset: 0x00006901
+ private void listViewEx_ColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e)
+ {
+ e.NewWidth = this.listViewEx.Columns[e.ColumnIndex].Width;
+ e.Cancel = true;
+ }
+
+ // Token: 0x06000089 RID: 137 RVA: 0x0000872E File Offset: 0x0000692E
+ private void listViewADDED_ColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e)
+ {
+ e.NewWidth = this.listViewADDED.Columns[e.ColumnIndex].Width;
+ e.Cancel = true;
+ }
+
+ // Token: 0x0600008A RID: 138 RVA: 0x0000875C File Offset: 0x0000695C
+ private void checkBox2_CheckedChanged(object sender, EventArgs e)
+ {
+ bool @checked = this.checkBox2.Checked;
+ if (@checked)
+ {
+ this.comboBox1.Items.Add("ACPU");
+ this.comboBox1.Items.Add("ATCR");
+ this.comboBox1.Items.Add("ATOR");
+ this.comboBox1.Items.Add("CCC");
+ this.comboBox1.Items.Add("TCMS");
+ this.comboBox1.Items.Add("TWCT");
+ this.comboBox1.SelectedIndex = 0;
+ this.comboRefresh();
+ }
+ else
+ {
+ this.comboBox1.Items.Clear();
+ this.comboBox1.Items.Add("MMI");
+ this.comboBox1.SelectedIndex = 0;
+ this.comboRefresh();
+ }
+ }
+
+ // Token: 0x0600008B RID: 139 RVA: 0x00008858 File Offset: 0x00006A58
+ private void listViewEx_ColumnClick(object sender, ColumnClickEventArgs e)
+ {
+ bool flag = e.Column == this.lvwColumnSorter.SortColumn;
+ if (flag)
+ {
+ bool flag2 = this.lvwColumnSorter.Order == SortOrder.Ascending;
+ if (flag2)
+ {
+ this.lvwColumnSorter.Order = SortOrder.Descending;
+ }
+ else
+ {
+ this.lvwColumnSorter.Order = SortOrder.Ascending;
+ }
+ }
+ else
+ {
+ this.lvwColumnSorter.SortColumn = e.Column;
+ this.lvwColumnSorter.Order = SortOrder.Ascending;
+ }
+ this.listViewEx.Sort();
+ }
+
+ // Token: 0x0600008C RID: 140 RVA: 0x000088E0 File Offset: 0x00006AE0
+ private void listViewADDED_DragDrop(object sender, DragEventArgs e)
+ {
+ bool dataPresent = e.Data.GetDataPresent(typeof(ListViewItem));
+ if (dataPresent)
+ {
+ Point point = this.listViewADDED.PointToClient(new Point(e.X, e.Y));
+ ListViewItem itemAt = this.listViewADDED.GetItemAt(point.X, point.Y);
+ bool flag = itemAt != null;
+ if (flag)
+ {
+ int index = itemAt.Index;
+ ListViewItem item = this.listViewADDED.Items[this.draggedItemIndex];
+ this.listViewADDED.Items.RemoveAt(this.draggedItemIndex);
+ this.listViewADDED.Items.Insert(index, item);
+ }
+ }
+ }
+
+ // Token: 0x0600008D RID: 141 RVA: 0x00008999 File Offset: 0x00006B99
+ private void listViewADDED_DragOver(object sender, DragEventArgs e)
+ {
+ e.Effect = DragDropEffects.Move;
+ }
+
+ // Token: 0x0600008E RID: 142 RVA: 0x000089A4 File Offset: 0x00006BA4
+ private void listViewADDED_DragEnter(object sender, DragEventArgs e)
+ {
+ bool dataPresent = e.Data.GetDataPresent(typeof(ListViewItem));
+ if (dataPresent)
+ {
+ e.Effect = DragDropEffects.Move;
+ }
+ else
+ {
+ e.Effect = DragDropEffects.None;
+ }
+ }
+
+ // Token: 0x0600008F RID: 143 RVA: 0x000089E1 File Offset: 0x00006BE1
+ private void listViewADDED_ItemDrag(object sender, ItemDragEventArgs e)
+ {
+ this.draggedItemIndex = ((ListViewItem)e.Item).Index;
+ base.DoDragDrop(e.Item, DragDropEffects.Move);
+ }
+
+ // Token: 0x06000090 RID: 144 RVA: 0x00008A08 File Offset: 0x00006C08
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x06000091 RID: 145 RVA: 0x00008A40 File Offset: 0x00006C40
+ private void InitializeComponent()
+ {
+ ComponentResourceManager componentResourceManager = new ComponentResourceManager(typeof(itemSelectForm));
+ this.listViewADDED = new ListView();
+ this.columnHeader1 = new ColumnHeader();
+ this.columnHeader5 = new ColumnHeader();
+ this.columnHeader2 = new ColumnHeader();
+ this.listViewEx = new ListView();
+ this.columnHeader3 = new ColumnHeader();
+ this.columnHeader6 = new ColumnHeader();
+ this.columnHeader4 = new ColumnHeader();
+ this.button1 = new Button();
+ this.button2 = new Button();
+ this.button3 = new Button();
+ this.buttonOK = new Button();
+ this.comboBox1 = new ComboBox();
+ this.button4 = new Button();
+ this.lb_itemCount = new Label();
+ this.checkBox2 = new CheckBox();
+ base.SuspendLayout();
+ this.listViewADDED.AllowColumnReorder = true;
+ this.listViewADDED.AllowDrop = true;
+ this.listViewADDED.BackColor = SystemColors.GradientInactiveCaption;
+ this.listViewADDED.BorderStyle = BorderStyle.None;
+ this.listViewADDED.Columns.AddRange(new ColumnHeader[]
+ {
+ this.columnHeader1,
+ this.columnHeader5,
+ this.columnHeader2
+ });
+ this.listViewADDED.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.listViewADDED.FullRowSelect = true;
+ this.listViewADDED.GridLines = true;
+ this.listViewADDED.HideSelection = false;
+ this.listViewADDED.Location = new Point(12, 45);
+ this.listViewADDED.MultiSelect = false;
+ this.listViewADDED.Name = "listViewADDED";
+ this.listViewADDED.Size = new Size(265, 366);
+ this.listViewADDED.TabIndex = 278;
+ this.listViewADDED.UseCompatibleStateImageBehavior = false;
+ this.listViewADDED.View = View.Details;
+ this.listViewADDED.ColumnWidthChanging += this.listViewADDED_ColumnWidthChanging;
+ this.listViewADDED.ItemDrag += this.listViewADDED_ItemDrag;
+ this.listViewADDED.DragDrop += this.listViewADDED_DragDrop;
+ this.listViewADDED.DragEnter += this.listViewADDED_DragEnter;
+ this.listViewADDED.DragOver += this.listViewADDED_DragOver;
+ this.listViewADDED.DoubleClick += this.listViewADDED_DoubleClick;
+ this.columnHeader1.Text = "Group";
+ this.columnHeader1.Width = 94;
+ this.columnHeader5.Text = "CANID";
+ this.columnHeader5.Width = 0;
+ this.columnHeader2.Text = "Item";
+ this.columnHeader2.Width = 168;
+ this.listViewEx.BackColor = SystemColors.GradientInactiveCaption;
+ this.listViewEx.BorderStyle = BorderStyle.None;
+ this.listViewEx.Columns.AddRange(new ColumnHeader[]
+ {
+ this.columnHeader3,
+ this.columnHeader6,
+ this.columnHeader4
+ });
+ this.listViewEx.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.listViewEx.FullRowSelect = true;
+ this.listViewEx.GridLines = true;
+ this.listViewEx.HideSelection = false;
+ this.listViewEx.Location = new Point(376, 45);
+ this.listViewEx.MultiSelect = false;
+ this.listViewEx.Name = "listViewEx";
+ this.listViewEx.Size = new Size(286, 366);
+ this.listViewEx.Sorting = SortOrder.Ascending;
+ this.listViewEx.TabIndex = 279;
+ this.listViewEx.UseCompatibleStateImageBehavior = false;
+ this.listViewEx.View = View.Details;
+ this.listViewEx.ColumnClick += this.listViewEx_ColumnClick;
+ this.listViewEx.ColumnWidthChanging += this.listViewEx_ColumnWidthChanging;
+ this.listViewEx.DoubleClick += this.listViewEx_DoubleClick;
+ this.columnHeader3.Text = "Group";
+ this.columnHeader3.Width = 87;
+ this.columnHeader6.Text = "CANID";
+ this.columnHeader6.Width = 0;
+ this.columnHeader4.Text = "Item";
+ this.columnHeader4.Width = 198;
+ this.button1.Font = new Font("나눔고딕 ExtraBold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button1.Location = new Point(289, 125);
+ this.button1.Name = "button1";
+ this.button1.Size = new Size(75, 107);
+ this.button1.TabIndex = 280;
+ this.button1.Text = "ADD";
+ this.button1.UseVisualStyleBackColor = true;
+ this.button1.Click += this.button1_Click;
+ this.button2.Font = new Font("나눔고딕 ExtraBold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button2.Location = new Point(289, 234);
+ this.button2.Name = "button2";
+ this.button2.Size = new Size(75, 107);
+ this.button2.TabIndex = 281;
+ this.button2.Text = "DELETE";
+ this.button2.UseVisualStyleBackColor = true;
+ this.button2.Click += this.button2_Click;
+ this.button3.BackColor = Color.OrangeRed;
+ this.button3.Font = new Font("나눔고딕 ExtraBold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.button3.ForeColor = Color.Transparent;
+ this.button3.Location = new Point(587, 10);
+ this.button3.Name = "button3";
+ this.button3.Size = new Size(75, 29);
+ this.button3.TabIndex = 282;
+ this.button3.Text = "CLOSE";
+ this.button3.UseVisualStyleBackColor = false;
+ this.button3.Click += this.button3_Click;
+ this.buttonOK.BackColor = Color.LimeGreen;
+ this.buttonOK.Font = new Font("나눔고딕 ExtraBold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.buttonOK.ForeColor = Color.White;
+ this.buttonOK.Location = new Point(506, 10);
+ this.buttonOK.Name = "buttonOK";
+ this.buttonOK.Size = new Size(75, 29);
+ this.buttonOK.TabIndex = 283;
+ this.buttonOK.Text = "OK";
+ this.buttonOK.UseVisualStyleBackColor = false;
+ this.buttonOK.Click += this.buttonOK_Click;
+ this.comboBox1.FormattingEnabled = true;
+ this.comboBox1.Items.AddRange(new object[]
+ {
+ "MMI"
+ });
+ this.comboBox1.Location = new Point(376, 13);
+ this.comboBox1.Name = "comboBox1";
+ this.comboBox1.Size = new Size(124, 20);
+ this.comboBox1.TabIndex = 284;
+ this.comboBox1.SelectedIndexChanged += this.comboBox1_SelectedIndexChanged;
+ this.button4.Location = new Point(202, 10);
+ this.button4.Name = "button4";
+ this.button4.Size = new Size(75, 29);
+ this.button4.TabIndex = 285;
+ this.button4.Text = "CLEAR";
+ this.button4.UseVisualStyleBackColor = true;
+ this.button4.Click += this.button4_Click;
+ this.lb_itemCount.Font = new Font("Verdana", 12f, FontStyle.Bold, GraphicsUnit.Point, 0);
+ this.lb_itemCount.Location = new Point(14, 13);
+ this.lb_itemCount.Name = "lb_itemCount";
+ this.lb_itemCount.Size = new Size(162, 23);
+ this.lb_itemCount.TabIndex = 286;
+ this.lb_itemCount.Text = "선택 항목: 0/40";
+ this.checkBox2.FlatStyle = FlatStyle.Popup;
+ this.checkBox2.Font = new Font("나눔고딕", 9.75f, FontStyle.Regular, GraphicsUnit.Point, 129);
+ this.checkBox2.Location = new Point(299, 11);
+ this.checkBox2.Name = "checkBox2";
+ this.checkBox2.Size = new Size(75, 25);
+ this.checkBox2.TabIndex = 287;
+ this.checkBox2.Text = "실시간";
+ this.checkBox2.TextAlign = ContentAlignment.MiddleCenter;
+ this.checkBox2.UseVisualStyleBackColor = true;
+ this.checkBox2.CheckedChanged += this.checkBox2_CheckedChanged;
+ base.AutoScaleMode = AutoScaleMode.None;
+ base.ClientSize = new Size(674, 423);
+ base.Controls.Add(this.checkBox2);
+ base.Controls.Add(this.lb_itemCount);
+ base.Controls.Add(this.button4);
+ base.Controls.Add(this.comboBox1);
+ base.Controls.Add(this.buttonOK);
+ base.Controls.Add(this.button3);
+ base.Controls.Add(this.button2);
+ base.Controls.Add(this.button1);
+ base.Controls.Add(this.listViewEx);
+ base.Controls.Add(this.listViewADDED);
+ base.FormBorderStyle = FormBorderStyle.FixedSingle;
+ base.Icon = (Icon)componentResourceManager.GetObject("$this.Icon");
+ base.Name = "itemSelectForm";
+ this.Text = "추가 데이터 선택";
+ base.Load += this.itemSelectForm_Load;
+ base.ResumeLayout(false);
+ }
+
+ // Token: 0x04000055 RID: 85
+ private itemSelectForm.ListViewColumnSorter lvwColumnSorter = new itemSelectForm.ListViewColumnSorter();
+
+ // Token: 0x04000056 RID: 86
+ private int draggedItemIndex = -1;
+
+ // Token: 0x04000057 RID: 87
+ private IContainer components = null;
+
+ // Token: 0x04000058 RID: 88
+ private ColumnHeader columnHeader1;
+
+ // Token: 0x04000059 RID: 89
+ private ColumnHeader columnHeader2;
+
+ // Token: 0x0400005A RID: 90
+ private ColumnHeader columnHeader3;
+
+ // Token: 0x0400005B RID: 91
+ private ColumnHeader columnHeader4;
+
+ // Token: 0x0400005C RID: 92
+ private Button button1;
+
+ // Token: 0x0400005D RID: 93
+ private Button button2;
+
+ // Token: 0x0400005E RID: 94
+ private Button button3;
+
+ // Token: 0x0400005F RID: 95
+ private Button buttonOK;
+
+ // Token: 0x04000060 RID: 96
+ private ColumnHeader columnHeader5;
+
+ // Token: 0x04000061 RID: 97
+ private ColumnHeader columnHeader6;
+
+ // Token: 0x04000062 RID: 98
+ private ListView listViewADDED;
+
+ // Token: 0x04000063 RID: 99
+ public ListView listViewEx;
+
+ // Token: 0x04000064 RID: 100
+ public ComboBox comboBox1;
+
+ // Token: 0x04000065 RID: 101
+ private Button button4;
+
+ // Token: 0x04000066 RID: 102
+ private Label lb_itemCount;
+
+ // Token: 0x04000067 RID: 103
+ public CheckBox checkBox2;
+
+ // Token: 0x0200005E RID: 94
+ // (Invoke) Token: 0x060001AC RID: 428
+ public delegate void DataSender(ListView lv);
+
+ // Token: 0x0200005F RID: 95
+ public class ListViewColumnSorter : IComparer
+ {
+ // Token: 0x060001AF RID: 431 RVA: 0x000288C6 File Offset: 0x00026AC6
+ public ListViewColumnSorter()
+ {
+ this.ColumnToSort = 0;
+ this.OrderOfSort = SortOrder.Ascending;
+ this.ObjectCompare = new CaseInsensitiveComparer();
+ }
+
+ // Token: 0x060001B0 RID: 432 RVA: 0x000288EC File Offset: 0x00026AEC
+ public int Compare(object x, object y)
+ {
+ ListViewItem listViewItem = (ListViewItem)x;
+ ListViewItem listViewItem2 = (ListViewItem)y;
+ int num = this.ObjectCompare.Compare(listViewItem.SubItems[this.ColumnToSort].Text, listViewItem2.SubItems[this.ColumnToSort].Text);
+ bool flag = this.OrderOfSort == SortOrder.Ascending;
+ int result;
+ if (flag)
+ {
+ result = num;
+ }
+ else
+ {
+ bool flag2 = this.OrderOfSort == SortOrder.Descending;
+ if (flag2)
+ {
+ result = -num;
+ }
+ else
+ {
+ result = 0;
+ }
+ }
+ return result;
+ }
+
+ // Token: 0x17000023 RID: 35
+ // (get) Token: 0x060001B2 RID: 434 RVA: 0x0002897C File Offset: 0x00026B7C
+ // (set) Token: 0x060001B1 RID: 433 RVA: 0x00028971 File Offset: 0x00026B71
+ public int SortColumn
+ {
+ get
+ {
+ return this.ColumnToSort;
+ }
+ set
+ {
+ this.ColumnToSort = value;
+ }
+ }
+
+ // Token: 0x17000024 RID: 36
+ // (get) Token: 0x060001B4 RID: 436 RVA: 0x000289A0 File Offset: 0x00026BA0
+ // (set) Token: 0x060001B3 RID: 435 RVA: 0x00028994 File Offset: 0x00026B94
+ public SortOrder Order
+ {
+ get
+ {
+ return this.OrderOfSort;
+ }
+ set
+ {
+ this.OrderOfSort = value;
+ }
+ }
+
+ // Token: 0x04000438 RID: 1080
+ private int ColumnToSort;
+
+ // Token: 0x04000439 RID: 1081
+ private SortOrder OrderOfSort;
+
+ // Token: 0x0400043A RID: 1082
+ private CaseInsensitiveComparer ObjectCompare;
+ }
+ }
+}
diff --git a/c_Sharp_Code/subFrom/mainchartvalue.cs b/c_Sharp_Code/subFrom/mainchartvalue.cs
new file mode 100644
index 0000000..668bad3
--- /dev/null
+++ b/c_Sharp_Code/subFrom/mainchartvalue.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x0200001A RID: 26
+ public class MainChartValue
+ {
+ // Token: 0x040001F7 RID: 503
+ public int atc;
+
+ // Token: 0x040001F8 RID: 504
+ public int tasc;
+
+ // Token: 0x040001F9 RID: 505
+ public double speed;
+
+ // Token: 0x040001FA RID: 506
+ public int pwm;
+ }
+}
diff --git a/c_Sharp_Code/subFrom/mmimsgform.cs b/c_Sharp_Code/subFrom/mmimsgform.cs
new file mode 100644
index 0000000..79fa567
--- /dev/null
+++ b/c_Sharp_Code/subFrom/mmimsgform.cs
@@ -0,0 +1,379 @@
+using System;
+using System.ComponentModel;
+using System.Drawing;
+using System.IO;
+using System.Windows.Forms;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000012 RID: 18
+ public class mmiMsgForm : Form
+ {
+ // Token: 0x06000092 RID: 146 RVA: 0x00009558 File Offset: 0x00007758
+ public mmiMsgForm()
+ {
+ this.InitializeComponent();
+ }
+
+ // Token: 0x06000093 RID: 147 RVA: 0x00009570 File Offset: 0x00007770
+ private void mmiMsgForm_DragDrop(object sender, DragEventArgs e)
+ {
+ string[] array = (string[])e.Data.GetData(DataFormats.FileDrop);
+ string text = array[0];
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text);
+ string extension = Path.GetExtension(text);
+ bool flag = text.Contains("MSG");
+ if (flag)
+ {
+ this.Cursor = Cursors.WaitCursor;
+ this.fileOpen(text);
+ this.Cursor = Cursors.Default;
+ this.lbl_filename.Text = fileNameWithoutExtension + extension;
+ }
+ }
+
+ // Token: 0x06000094 RID: 148 RVA: 0x000095EC File Offset: 0x000077EC
+ private static void CopySelectedItems(ListView source, ListView target)
+ {
+ foreach (object obj in source.Items)
+ {
+ ListViewItem listViewItem = (ListViewItem)obj;
+ target.Items.Add((ListViewItem)listViewItem.Clone());
+ }
+ }
+
+ // Token: 0x06000095 RID: 149 RVA: 0x0000965C File Offset: 0x0000785C
+ public void fileOpen(string fileName)
+ {
+ this.sublistView.Items.Clear();
+ bool @checked = this.checkBox_save.Checked;
+ if (@checked)
+ {
+ this.listViewMSG.Items.Clear();
+ }
+ FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
+ StreamReader streamReader = new StreamReader(fileStream);
+ long length = streamReader.BaseStream.Length;
+ try
+ {
+ string text;
+ while ((text = streamReader.ReadLine()) != null)
+ {
+ string text2 = text.Substring(14, text.Length - 14);
+ bool flag = text2.Contains("오류") || text2.Contains("실패") || text2.Contains("고장") || text2.Contains("비정상") || text2.Contains("반대편") || text2.Contains("부족") || text2.Contains("롤백") || text2.Contains("미끌림") || text2.Contains("움직임") || text2.Contains("MSG") || text2.Contains("성공") || text2.Contains("대기");
+ string[] items;
+ if (flag)
+ {
+ items = new string[]
+ {
+ text.Substring(0, 5),
+ text.Substring(6, 8),
+ text.Substring(14, text.Length - 14),
+ "1"
+ };
+ }
+ else
+ {
+ items = new string[]
+ {
+ text.Substring(0, 5),
+ text.Substring(6, 8),
+ text.Substring(14, text.Length - 14),
+ "0"
+ };
+ }
+ ListViewItem value = new ListViewItem(items);
+ this.listViewMSG.Items.Add(value);
+ }
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ finally
+ {
+ bool flag2 = streamReader != null;
+ if (flag2)
+ {
+ streamReader.Close();
+ }
+ bool flag3 = fileStream != null;
+ if (flag3)
+ {
+ fileStream.Close();
+ }
+ }
+ mmiMsgForm.CopySelectedItems(this.listViewMSG, this.sublistView);
+ this.lb_itemCount.Text = "총 메세지 :" + this.listViewMSG.Items.Count.ToString();
+ }
+
+ // Token: 0x06000096 RID: 150 RVA: 0x000098E0 File Offset: 0x00007AE0
+ private void mmiMsgForm_DragEnter(object sender, DragEventArgs e)
+ {
+ e.Effect = DragDropEffects.Copy;
+ }
+
+ // Token: 0x06000097 RID: 151 RVA: 0x000098EC File Offset: 0x00007AEC
+ private void checkBox_event_CheckedChanged(object sender, EventArgs e)
+ {
+ this.listViewMSG.Items.Clear();
+ bool @checked = this.checkBox_event.Checked;
+ if (@checked)
+ {
+ foreach (object obj in this.sublistView.Items)
+ {
+ ListViewItem listViewItem = (ListViewItem)obj;
+ bool flag = listViewItem.SubItems[3].Text == "1";
+ if (flag)
+ {
+ this.listViewMSG.Items.Add((ListViewItem)listViewItem.Clone());
+ }
+ }
+ }
+ else
+ {
+ foreach (object obj2 in this.sublistView.Items)
+ {
+ ListViewItem listViewItem2 = (ListViewItem)obj2;
+ this.listViewMSG.Items.Add((ListViewItem)listViewItem2.Clone());
+ }
+ }
+ this.lb_itemCount.Text = "총 메세지 :" + this.listViewMSG.Items.Count.ToString();
+ }
+
+ // Token: 0x06000098 RID: 152 RVA: 0x00009A50 File Offset: 0x00007C50
+ private void bt_fileOpen_Click(object sender, EventArgs e)
+ {
+ OpenFileDialog openFileDialog = new OpenFileDialog();
+ bool flag = DialogResult.OK != openFileDialog.ShowDialog();
+ if (!flag)
+ {
+ string fileName = openFileDialog.FileName;
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
+ string extension = Path.GetExtension(fileName);
+ bool flag2 = fileName.Contains("MSG");
+ if (flag2)
+ {
+ this.Cursor = Cursors.WaitCursor;
+ this.fileOpen(openFileDialog.FileName);
+ this.Cursor = Cursors.Default;
+ this.lbl_filename.Text = fileNameWithoutExtension + extension;
+ }
+ }
+ }
+
+ // Token: 0x06000099 RID: 153 RVA: 0x00009AD8 File Offset: 0x00007CD8
+ protected override void Dispose(bool disposing)
+ {
+ bool flag = disposing && this.components != null;
+ if (flag)
+ {
+ this.components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ // Token: 0x0600009A RID: 154 RVA: 0x00009B10 File Offset: 0x00007D10
+ private void InitializeComponent()
+ {
+ this.lb_itemCount = new Label();
+ this.listViewMSG = new ListView();
+ this.columnHeader1 = new ColumnHeader();
+ this.columnHeader5 = new ColumnHeader();
+ this.columnHeader2 = new ColumnHeader();
+ this.columnHeader3 = new ColumnHeader();
+ this.checkBox_event = new CheckBox();
+ this.sublistView = new ListView();
+ this.columnHeader4 = new ColumnHeader();
+ this.columnHeader6 = new ColumnHeader();
+ this.columnHeader7 = new ColumnHeader();
+ this.columnHeader8 = new ColumnHeader();
+ this.bt_fileOpen = new Button();
+ this.lbl_filename = new Label();
+ this.label1 = new Label();
+ this.checkBox_save = new CheckBox();
+ base.SuspendLayout();
+ this.lb_itemCount.BackColor = Color.Silver;
+ this.lb_itemCount.Font = new Font("나눔스퀘어 ExtraBold", 15.75f, FontStyle.Bold, GraphicsUnit.Point, 0);
+ this.lb_itemCount.Location = new Point(433, 19);
+ this.lb_itemCount.Name = "lb_itemCount";
+ this.lb_itemCount.Size = new Size(162, 23);
+ this.lb_itemCount.TabIndex = 288;
+ this.lb_itemCount.Text = "총 메세지 : 0";
+ this.listViewMSG.BackColor = SystemColors.GradientInactiveCaption;
+ this.listViewMSG.BorderStyle = BorderStyle.None;
+ this.listViewMSG.Columns.AddRange(new ColumnHeader[]
+ {
+ this.columnHeader1,
+ this.columnHeader5,
+ this.columnHeader2,
+ this.columnHeader3
+ });
+ this.listViewMSG.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.listViewMSG.FullRowSelect = true;
+ this.listViewMSG.GridLines = true;
+ this.listViewMSG.HideSelection = false;
+ this.listViewMSG.Location = new Point(12, 68);
+ this.listViewMSG.MultiSelect = false;
+ this.listViewMSG.Name = "listViewMSG";
+ this.listViewMSG.Size = new Size(1160, 779);
+ this.listViewMSG.TabIndex = 287;
+ this.listViewMSG.UseCompatibleStateImageBehavior = false;
+ this.listViewMSG.View = View.Details;
+ this.columnHeader1.Text = "날짜";
+ this.columnHeader1.Width = 70;
+ this.columnHeader5.Text = "시간";
+ this.columnHeader5.Width = 120;
+ this.columnHeader2.Text = "내용";
+ this.columnHeader2.Width = 260;
+ this.columnHeader3.Text = "분류";
+ this.columnHeader3.Width = 0;
+ this.checkBox_event.Appearance = Appearance.Button;
+ this.checkBox_event.Font = new Font("나눔스퀘어 Bold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.checkBox_event.Location = new Point(1097, 12);
+ this.checkBox_event.Name = "checkBox_event";
+ this.checkBox_event.Size = new Size(75, 33);
+ this.checkBox_event.TabIndex = 289;
+ this.checkBox_event.Text = "이벤트";
+ this.checkBox_event.TextAlign = ContentAlignment.MiddleCenter;
+ this.checkBox_event.UseVisualStyleBackColor = true;
+ this.checkBox_event.CheckedChanged += this.checkBox_event_CheckedChanged;
+ this.sublistView.BackColor = SystemColors.GradientInactiveCaption;
+ this.sublistView.BorderStyle = BorderStyle.None;
+ this.sublistView.Columns.AddRange(new ColumnHeader[]
+ {
+ this.columnHeader4,
+ this.columnHeader6,
+ this.columnHeader7,
+ this.columnHeader8
+ });
+ this.sublistView.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.sublistView.FullRowSelect = true;
+ this.sublistView.GridLines = true;
+ this.sublistView.HideSelection = false;
+ this.sublistView.Location = new Point(12, 356);
+ this.sublistView.MultiSelect = false;
+ this.sublistView.Name = "sublistView";
+ this.sublistView.Size = new Size(143, 127);
+ this.sublistView.TabIndex = 290;
+ this.sublistView.UseCompatibleStateImageBehavior = false;
+ this.sublistView.View = View.Details;
+ this.sublistView.Visible = false;
+ this.columnHeader4.Text = "날짜";
+ this.columnHeader4.Width = 70;
+ this.columnHeader6.Text = "시간";
+ this.columnHeader6.Width = 120;
+ this.columnHeader7.Text = "내용";
+ this.columnHeader7.Width = 260;
+ this.columnHeader8.Text = "분류";
+ this.bt_fileOpen.Font = new Font("맑은 고딕", 9.75f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.bt_fileOpen.Location = new Point(150, 8);
+ this.bt_fileOpen.Margin = new Padding(3, 2, 3, 2);
+ this.bt_fileOpen.Name = "bt_fileOpen";
+ this.bt_fileOpen.Size = new Size(90, 40);
+ this.bt_fileOpen.TabIndex = 292;
+ this.bt_fileOpen.Text = "FILE OPEN";
+ this.bt_fileOpen.UseVisualStyleBackColor = true;
+ this.bt_fileOpen.Click += this.bt_fileOpen_Click;
+ this.lbl_filename.BackColor = Color.DarkBlue;
+ this.lbl_filename.BorderStyle = BorderStyle.FixedSingle;
+ this.lbl_filename.Font = new Font("맑은 고딕", 9.75f, FontStyle.Regular, GraphicsUnit.Point, 129);
+ this.lbl_filename.ForeColor = Color.White;
+ this.lbl_filename.Location = new Point(246, 8);
+ this.lbl_filename.Name = "lbl_filename";
+ this.lbl_filename.Size = new Size(181, 40);
+ this.lbl_filename.TabIndex = 291;
+ this.lbl_filename.Text = "-";
+ this.lbl_filename.TextAlign = ContentAlignment.MiddleCenter;
+ this.label1.BackColor = Color.Silver;
+ this.label1.Dock = DockStyle.Top;
+ this.label1.Font = new Font("맑은 고딕", 18f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.label1.ForeColor = Color.Black;
+ this.label1.Location = new Point(0, 0);
+ this.label1.Name = "label1";
+ this.label1.Size = new Size(1184, 55);
+ this.label1.TabIndex = 293;
+ this.label1.Text = "메세지 뷰어";
+ this.label1.TextAlign = ContentAlignment.MiddleLeft;
+ this.checkBox_save.Appearance = Appearance.Button;
+ this.checkBox_save.Checked = true;
+ this.checkBox_save.CheckState = CheckState.Checked;
+ this.checkBox_save.Font = new Font("나눔스퀘어 Bold", 12f, FontStyle.Bold, GraphicsUnit.Point, 129);
+ this.checkBox_save.Location = new Point(1016, 12);
+ this.checkBox_save.Name = "checkBox_save";
+ this.checkBox_save.Size = new Size(75, 33);
+ this.checkBox_save.TabIndex = 294;
+ this.checkBox_save.Text = "갱 신";
+ this.checkBox_save.TextAlign = ContentAlignment.MiddleCenter;
+ this.checkBox_save.UseVisualStyleBackColor = true;
+ this.AllowDrop = true;
+ base.AutoScaleDimensions = new SizeF(7f, 12f);
+ base.AutoScaleMode = AutoScaleMode.Font;
+ base.ClientSize = new Size(1184, 860);
+ base.Controls.Add(this.checkBox_save);
+ base.Controls.Add(this.bt_fileOpen);
+ base.Controls.Add(this.lbl_filename);
+ base.Controls.Add(this.sublistView);
+ base.Controls.Add(this.checkBox_event);
+ base.Controls.Add(this.lb_itemCount);
+ base.Controls.Add(this.listViewMSG);
+ base.Controls.Add(this.label1);
+ base.Name = "mmiMsgForm";
+ this.Text = "MMI 메세지";
+ base.DragDrop += this.mmiMsgForm_DragDrop;
+ base.DragEnter += this.mmiMsgForm_DragEnter;
+ base.ResumeLayout(false);
+ }
+
+ // Token: 0x04000068 RID: 104
+ private IContainer components = null;
+
+ // Token: 0x04000069 RID: 105
+ private Label lb_itemCount;
+
+ // Token: 0x0400006A RID: 106
+ private ListView listViewMSG;
+
+ // Token: 0x0400006B RID: 107
+ private ColumnHeader columnHeader1;
+
+ // Token: 0x0400006C RID: 108
+ private ColumnHeader columnHeader5;
+
+ // Token: 0x0400006D RID: 109
+ private ColumnHeader columnHeader2;
+
+ // Token: 0x0400006E RID: 110
+ private CheckBox checkBox_event;
+
+ // Token: 0x0400006F RID: 111
+ private ColumnHeader columnHeader3;
+
+ // Token: 0x04000070 RID: 112
+ private ListView sublistView;
+
+ // Token: 0x04000071 RID: 113
+ private ColumnHeader columnHeader4;
+
+ // Token: 0x04000072 RID: 114
+ private ColumnHeader columnHeader6;
+
+ // Token: 0x04000073 RID: 115
+ private ColumnHeader columnHeader7;
+
+ // Token: 0x04000074 RID: 116
+ private ColumnHeader columnHeader8;
+
+ // Token: 0x04000075 RID: 117
+ private Button bt_fileOpen;
+
+ // Token: 0x04000076 RID: 118
+ private Label lbl_filename;
+
+ // Token: 0x04000077 RID: 119
+ private Label label1;
+
+ // Token: 0x04000078 RID: 120
+ private CheckBox checkBox_save;
+ }
+}
diff --git a/c_Sharp_Code/subFrom/mmiui.cs b/c_Sharp_Code/subFrom/mmiui.cs
new file mode 100644
index 0000000..2c66d86
--- /dev/null
+++ b/c_Sharp_Code/subFrom/mmiui.cs
@@ -0,0 +1,6760 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.IO;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Windows.Forms;
+using System.Windows.Forms.DataVisualization.Charting;
+using AquaControls;
+using MetroFramework;
+using MetroFramework.Controls;
+using Microsoft.CSharp.RuntimeBinder;
+using Microsoft.Office.Interop.Excel;
+using Microsoft.Win32;
+using SL200_RealtimeLogViewer.RealtimeLog;
+using SL200_RTLogViewer.lib;
+using SL200_RTLogViewer.Properties;
+
+namespace SL200_RTLogViewer.subForm
+{
+ // Token: 0x02000013 RID: 19
+ public class mmiUI : Form
+ {
+ // Token: 0x14000008 RID: 8
+ // (add) Token: 0x0600009B RID: 155 RVA: 0x0000A46C File Offset: 0x0000866C
+ // (remove) Token: 0x0600009C RID: 156 RVA: 0x0000A4A4 File Offset: 0x000086A4
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event mmiUI.DataSender SenderEvent;
+
+ // Token: 0x14000009 RID: 9
+ // (add) Token: 0x0600009D RID: 157 RVA: 0x0000A4DC File Offset: 0x000086DC
+ // (remove) Token: 0x0600009E RID: 158 RVA: 0x0000A514 File Offset: 0x00008714
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ public event mmiUI.funtionCall CallEvent;
+
+ // Token: 0x0600009F RID: 159 RVA: 0x0000A54C File Offset: 0x0000874C
+ public mmiUI()
+ {
+ this.InitializeComponent();
+ base.AutoScaleMode = AutoScaleMode.None;
+ }
+
+ // Token: 0x060000A0 RID: 160 RVA: 0x0000A61C File Offset: 0x0000881C
+ private void mmiUI_Load(object sender, EventArgs e)
+ {
+ this._mstationInfoList = new List();
+ this._chartSize = 800;
+ this.chart1.ChartAreas[0].AxisX.Interval = 50.0;
+ this.chart1.ChartAreas[0].AxisX.Minimum = 0.0;
+ this.chart1.ChartAreas[0].AxisX.Maximum = (double)this._chartSize;
+ this.chart1.ChartAreas[0].AxisX.ScaleView.Size = (double)this._chartSize;
+ this.chart1.ChartAreas[0].AxisX.MajorGrid.Interval = (double)(this._chartSize / 10);
+ this.chart1.ChartAreas[0].AxisY.MajorGrid.Interval = 10.0;
+ this.chart1.ChartAreas[0].AxisX.LabelStyle.Interval = 300.0;
+ this.chart1.ChartAreas[0].AxisX.LabelStyle.IntervalType = DateTimeIntervalType.NotSet;
+ bool flag = !Form1.logOn;
+ if (flag)
+ {
+ this.timer2.Start();
+ }
+ this.listViewHOL.DoubleBuffered(true);
+ this.listViewJJAC.DoubleBuffered(true);
+ this.chart1.DoubleBuffered(true);
+ this.serATC = this.chart1.Series["atc"];
+ this.serTASC = this.chart1.Series["tasc"];
+ this.serSpeed = this.chart1.Series["speed"];
+ this.serPWM = this.chart1.Series["pwm"];
+ this.serStation = this.chart1.Series["station"];
+ this._dataIndex = 0;
+ this._prevCursorPos = 0;
+ this.btnZoomPrevious.Visible = !Form1.logOn;
+ this.pg1Pt = 0;
+ this.pg2Pt = 0;
+ this.pg33Pt = 0;
+ this.pg1dt = false;
+ this.pg2dt = false;
+ this.pg3dt = false;
+ this.pgXdt = false;
+ this.pgATSdt = false;
+ this.wrpg3 = false;
+ this.twcdt = false;
+ this.addstationmaker = true;
+ this.preStaion = "";
+ this.isTC1 = false;
+ this.serStation.MarkerColor = Color.White;
+ this.sc = Screen.AllScreens;
+ this.tt = new ToolTip();
+ }
+
+ // Token: 0x060000A1 RID: 161 RVA: 0x0000A8E8 File Offset: 0x00008AE8
+ public void StartLog()
+ {
+ this.realtimelog = new RealtimelogForm();
+ DateTime now = DateTime.Now;
+ string text = System.Windows.Forms.Application.StartupPath + "\\Pg\\" + now.ToString("yyyyMMdd");
+ string str = string.Format(now.ToString("MMddHHmm") + ".csv", Array.Empty