292 lines
9.8 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|