Files
HDRobotics/Assets/Scripts/AppManager.cs
2025-12-03 21:19:07 +09:00

292 lines
9.8 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using UnityEngine;
/// <summary>
/// 어플리케이션의 엔트리 포인트이자 전체 관리자 클래스
/// 싱글톤(Singleton) 패턴을 사용하며, Model, View, Presenter 간의 연결(의존성 주입)을 담당
/// 앱 시작 시 설정 파일 로드, 통신 모듈 초기화, 메인 루프 시작 등을 수행
/// </summary>
public class AppManager : MonoBehaviour
{
public static AppManager Instance { get; private set; }
// --- 인스펙터 할당 뷰 (Static Views) ---
[Header("UI Views")]
[SerializeField] private ProgramView view; // 프로그램 관리 UI
[SerializeField] private TCPView tcpView; // 좌표 표시 UI
[SerializeField] private RobotInfoView robotInfoView;// 로봇 상태 UI
[SerializeField] private ProgramInfoView programInfoView; // 실행 제어 UI
[SerializeField] private EnvView envView; // 메인 설정 UI
[SerializeField] private PopupView popupView; // 팝업 UI
[Header("Scene Views & Controllers")]
[SerializeField] private GripperCollide gripperCollide;
[SerializeField] private RobotController robotController; // 3D 로봇 제어
[SerializeField] private PointManagerView pointManagerView; // 포인트 마커 관리
[SerializeField] private PathLineView pathLineView; // 이동 경로 라인
[SerializeField] private AudioSource robotAudio; // 로봇 효과음 소스
// --- 런타임 등록 뷰 (Interaction Views) ---
private InteractionView leftInteractionView;
private InteractionView rightInteractionView;
// --- 설정 및 상태 ---
[SerializeField] private float motorStatePollInterval = 1.0f; // 모터 상태 확인 주기
public CancellationTokenSource cancellationTokenSource; // 비동기 작업 취소용 토큰
private bool isModelAndStaticViewsReady = false; // 초기화 완료 여부
// --- MVP 핵심 모듈 ---
private ProgramModel model; // 데이터 및 통신 로직
private ProgramPresenter presenter; // 뷰와 모델의 중재자
// --- 네트워크 설정 ---
private string hostip;
private int tcpPort;
private int udpPort;
private string configFileName = "config.cfg"; // 설정 파일 이름
void Awake()
{
// 싱글톤 초기화
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
// --- 네트워크 최적화 설정 (RTT 감소) ---
// Nagle 알고리즘 비활성화:
// 작은 데이터라도 버퍼에 모으지 않고 즉시 전송하여 지연 시간(Latency)을 줄임
System.Net.ServicePointManager.UseNagleAlgorithm = false;
// 100-Continue 대기 제거:
// POST 요청 시 헤더를 먼저 보내고 서버 승인을 기다리는 불필요한 왕복 시간을 제거
System.Net.ServicePointManager.Expect100Continue = false;
// 동시 연결 수 증가:
// 기본값을 늘려 HTTP 요청이 병목 현상 없이 병렬 처리되도록 함
System.Net.ServicePointManager.DefaultConnectionLimit = 10;
}
async void Start()
{
// 1. 설정 파일 로드 (IP/Port)
LoadConfig();
// 2. 모델 생성 및 초기화 (네트워크 연결 시작)
model = new ProgramModel(hostip, tcpPort, udpPort, robotController);
await model.InitializeAsync();
// 3. 필수 View 할당 확인
if (view == null || tcpView == null || robotInfoView == null || programInfoView == null || envView == null || robotController == null ||
pointManagerView == null || popupView == null || pathLineView == null)
{
Debug.LogError("AppManager의 인스펙터에 [Static Views]가 모두 할당되지 않음", this);
return;
}
isModelAndStaticViewsReady = true;
// 4. Presenter 생성 시도 (InteractionView가 등록될 때까지 대기할 수도 있음
TryCreatePresenter();
}
/// <summary>
/// InteractionView(컨트롤러)가 초기화될 때 호출하여 자신을 AppManager에 등록
/// </summary>
public void RegisterView(InteractionView Iview)
{
if (Iview.handSide == HandSide.Left)
{
this.leftInteractionView = Iview;
}
else if (Iview.handSide == HandSide.Right)
{
this.rightInteractionView = Iview;
}
// 양손 컨트롤러가 준비되면 Presenter 생성을 다시 시도
TryCreatePresenter();
}
/// <summary>
/// 모든 의존성(Model, Static Views, Interaction Views)이 준비되었는지 확인하고,
/// 준비되었다면 Presenter를 생성하여 MVP 구조를 완성
/// </summary>
private void TryCreatePresenter()
{
// 이미 생성되었거나, 필수 요소가 부족하면 중단
if (presenter != null) return;
if (!isModelAndStaticViewsReady || leftInteractionView == null || rightInteractionView == null)
{
return;
}
cancellationTokenSource = new CancellationTokenSource();
try
{
presenter = new ProgramPresenter(
model,
view,
tcpView,
robotInfoView,
programInfoView,
envView,
gripperCollide,
leftInteractionView, rightInteractionView,
pointManagerView,
popupView,
pathLineView,
robotAudio,
cancellationTokenSource
);
}
catch (Exception e)
{
Debug.LogError($"Presenter 생성자에서 오류 발생: {e.Message}\n{e.StackTrace}");
return; // 생성 실패
}
// 초기화 후속 작업
presenter.RegisterControlledRobot(robotController);
_ = presenter.UpdateMotorStateAsync(); // 모터 상태 초기 확인
// 백그라운드 작업 시작 (위치 동기화, 에러 체크 등)
_ = model.GetTCPAsync(cancellationTokenSource.Token);
_ = model.StartMovementCheckLoopAsync(cancellationTokenSource.Token);
_ = model.GetMovementState(cancellationTokenSource.Token);
view.DisplayProgram(null);
// 주기적인 모터 상태 확인 루틴 시작
StartCoroutine(PollMotorStateCoroutine());
}
private void OnDestroy()
{
if (Instance == this)
{
cancellationTokenSource?.Cancel();
cancellationTokenSource?.Dispose();
}
}
/// <summary>
/// StreamingAssets 폴더에서 config.cfg 파일을 읽어 IP와 Port 정보를 로드
/// 파일이 없으면 기본값(127.0.0.1:8888)을 사용
/// </summary>
private void LoadConfig()
{
// 기본값 설정 (파일을 못 찾을 경우 대비)
string defaultIp = "127.0.0.1";
int defaultPort = 8888;
string path = Path.Combine(Application.streamingAssetsPath, configFileName);
if (File.Exists(path))
{
try
{
var config = new Dictionary<string, string>();
string[] lines = File.ReadAllLines(path);
foreach (string line in lines)
{
if (string.IsNullOrWhiteSpace(line) || line.Trim().StartsWith("#"))
continue;
string[] parts = line.Split('=');
if (parts.Length == 2)
{
config[parts[0].Trim()] = parts[1].Trim();
}
}
if (config.ContainsKey("IP_ADDRESS"))
{
hostip = config["IP_ADDRESS"];
}
else
{
hostip = defaultIp;
Debug.LogWarning($"config 파일에 IP_ADDRESS 키가 없습니다. 기본값({defaultIp}) 사용.");
}
if (config.ContainsKey("TCP_PORT"))
{
if (int.TryParse(config["TCP_PORT"], out int parsedPort))
{
tcpPort = parsedPort;
}
else
{
tcpPort = defaultPort;
Debug.LogWarning($"config 파일의 TCP_PORT 값이 잘못되었습니다. 기본값({defaultPort}) 사용.");
}
}
else
{
tcpPort = defaultPort;
Debug.LogWarning($"config 파일에 TCP_PORT 키가 없습니다. 기본값({defaultPort}) 사용.");
}
if (config.ContainsKey("UDP_PORT"))
{
if (int.TryParse(config["UDP_PORT"], out int parsedPort))
{
udpPort = parsedPort;
}
else
{
udpPort = defaultPort;
Debug.LogWarning($"config 파일의 UDP_PORT 값이 잘못되었습니다. 기본값({defaultPort}) 사용.");
}
}
else
{
udpPort = defaultPort;
Debug.LogWarning($"config 파일에 UDP_PORT 키가 없습니다. 기본값({defaultPort}) 사용.");
}
Debug.Log($"Config 로드 성공: {hostip}:{tcpPort}/{udpPort}");
}
catch (System.Exception e)
{
Debug.LogError($"Config 파일 로드 중 오류 발생: {e.Message}. 기본값 사용.");
hostip = defaultIp;
tcpPort = defaultPort;
udpPort = defaultPort;
}
}
else
{
Debug.LogWarning($"{configFileName} 파일을 찾을 수 없습니다. 기본값({defaultIp}:{defaultPort}) 사용.");
hostip = defaultIp;
tcpPort = defaultPort;
udpPort = defaultPort;
}
}
/// <summary>
/// 주기적으로 로봇의 모터 상태(ON/OFF)를 확인하여 UI를 갱신하는 코루틴
/// </summary>
private IEnumerator PollMotorStateCoroutine()
{
while (true)
{
yield return new WaitForSeconds(motorStatePollInterval);
_ = presenter.UpdateMotorStateAsync();
}
}
}