using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading; using UnityEngine; /// /// ¾îÇø®ÄÉÀ̼ÇÀÇ ¿£Æ®¸® Æ÷ÀÎÆ®ÀÌÀÚ Àüü °ü¸®ÀÚ Å¬·¡½º /// ½Ì±ÛÅæ(Singleton) ÆÐÅÏÀ» »ç¿ëÇϸç, Model, View, Presenter °£ÀÇ ¿¬°á(ÀÇÁ¸¼º ÁÖÀÔ)À» ´ã´ç /// ¾Û ½ÃÀÛ ½Ã ¼³Á¤ ÆÄÀÏ ·Îµå, Åë½Å ¸ðµâ ÃʱâÈ­, ¸ÞÀÎ ·çÇÁ ½ÃÀÛ µîÀ» ¼öÇà /// 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(); } /// /// InteractionView(ÄÁÆ®·Ñ·¯)°¡ ÃʱâÈ­µÉ ¶§ È£ÃâÇÏ¿© ÀÚ½ÅÀ» AppManager¿¡ µî·Ï /// public void RegisterView(InteractionView Iview) { if (Iview.handSide == HandSide.Left) { this.leftInteractionView = Iview; } else if (Iview.handSide == HandSide.Right) { this.rightInteractionView = Iview; } // ¾ç¼Õ ÄÁÆ®·Ñ·¯°¡ ÁغñµÇ¸é Presenter »ý¼ºÀ» ´Ù½Ã ½Ãµµ TryCreatePresenter(); } /// /// ¸ðµç ÀÇÁ¸¼º(Model, Static Views, Interaction Views)ÀÌ ÁغñµÇ¾ú´ÂÁö È®ÀÎÇϰí, /// ÁغñµÇ¾ú´Ù¸é Presenter¸¦ »ý¼ºÇÏ¿© MVP ±¸Á¶¸¦ ¿Ï¼º /// 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(); } } /// /// StreamingAssets Æú´õ¿¡¼­ config.cfg ÆÄÀÏÀ» Àоî IP¿Í Port Á¤º¸¸¦ ·Îµå /// ÆÄÀÏÀÌ ¾øÀ¸¸é ±âº»°ª(127.0.0.1:8888)À» »ç¿ë /// 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[] 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; } } /// /// ÁÖ±âÀûÀ¸·Î ·Îº¿ÀÇ ¸ðÅÍ »óÅÂ(ON/OFF)¸¦ È®ÀÎÇÏ¿© UI¸¦ °»½ÅÇÏ´Â ÄÚ·çÆ¾ /// private IEnumerator PollMotorStateCoroutine() { while (true) { yield return new WaitForSeconds(motorStatePollInterval); _ = presenter.UpdateMotorStateAsync(); } } }