<feat> 모터 상태 api통신

This commit is contained in:
SOOBEEN HAN
2025-10-24 11:55:43 +09:00
parent 5308a505a5
commit ff8afeb045
25 changed files with 77 additions and 906 deletions

View File

@@ -1,20 +0,0 @@
using UnityEngine;
using System.Collections.Generic;
[System.Serializable]
public class IKData
{
public string robotId;
public Vector3 position;
public List<Vector3> nodesPosition;
public List<Quaternion> nodesRotation;
public float deltaTime;
}
[System.Serializable]
public class IKDataListWrapper
{
public string robotId;
public List<IKData> keyFrameList;
//public List<Vector3> nodesList;
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 1073b77c68490294db77f64de40d82cd

View File

@@ -1,136 +0,0 @@
using UnityEngine;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;
using System;
using System.Text;
using System.Collections.Generic;
using System.Collections.Concurrent;
public class MqttManager : MonoBehaviour
{
public static MqttManager Instance { get; private set; }
[Header("MQTT Broker Settings")]
[SerializeField] private string brokerAddress = "localhost";
[SerializeField] private int brokerPort = 1883;
[Header("Robot Config")]
[SerializeField] private TextAsset robotIDconfig;
[SerializeField] private TextAsset robotActionTopics;
private MqttClient client;
public static event Action<string, string> OnMessageReceived;
private readonly ConcurrentQueue<Tuple<string, string>> receivedMessages = new ConcurrentQueue<Tuple<string, string>>();
List<string> topicList=new List<string>();
private void Awake()
{
// 싱글톤 인스턴스 설정
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
void Start()
{
ConnectToBroker();
}
private void Update()
{
// 매 프레임마다 큐에 쌓인 메시지가 있는지 확인
while (receivedMessages.TryDequeue(out var messageItem))
{
// 메인 스레드에서 안전하게 이벤트를 발생시킴
OnMessageReceived?.Invoke(messageItem.Item1, messageItem.Item2);
}
}
private void ConnectToBroker()
{
if (client != null && client.IsConnected)
{
Debug.LogWarning("Already connected to MQTT Broker");
return;
}
try
{
client = new MqttClient(brokerAddress, brokerPort, false, null, null, MqttSslProtocols.None);
client.MqttMsgPublishReceived += OnMqttMessageReceived;
string clientId = Guid.NewGuid().ToString();
client.Connect(clientId);
if (client.IsConnected)
{
Debug.Log("Connected to MQTT Broker");
SetSubscribeTopics();
}
}
catch (Exception e)
{
Debug.LogError($"Failed to connect to MQTT Broker: {e.Message}");
}
}
// 별도의 스레드에서 실행됨 (MqttManager는 백그라운드에서 네트워크 메시지 받음) -> 메시지 큐 방식 사용
private void OnMqttMessageReceived(object sender, MqttMsgPublishEventArgs e)
{
string message = Encoding.UTF8.GetString(e.Message);
receivedMessages.Enqueue(new Tuple<string, string>(e.Topic, message));
}
public void PublishMessage(string topic, string message)
{
if (string.IsNullOrEmpty(topic))
{
Debug.LogError("MQTT Publish Canceled: Topic is null or empty!");
return;
}
if (client == null || !client.IsConnected) return;
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
client.Publish(topic, messageBytes, MqttMsgBase.QOS_LEVEL_AT_MOST_ONCE, false);
}
void SetSubscribeTopics()
{
var topics = JsonUtility.FromJson<RobotIDConfig>(robotIDconfig.text);
var actions = JsonUtility.FromJson<RobotActionTopics>(robotActionTopics.text);
topicList.Add($"robots/{topics.ID}/{actions.Topic1}");
topicList.Add($"robots/{topics.ID}/{actions.Topic2}");
SubscribeToTopics(topicList.ToArray());
}
public void SubscribeToTopics(string[] topics)
{
if (client == null || !client.IsConnected || topics == null) return;
byte[] qosLevels = new byte[topics.Length];
for (int i = 0; i < topics.Length; i++)
{
qosLevels[i] = MqttMsgBase.QOS_LEVEL_AT_LEAST_ONCE;
}
client.Subscribe(topics, qosLevels);
Debug.Log($"토픽 구독 시작: {string.Join(", ",topics)}");
}
private void OnDestroy()
{
if (client != null && client.IsConnected)
{
client.Disconnect();
Debug.Log("Disconnected from MQTT Broker");
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 5491d87c5326c7640be51aa08dba16a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -200
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
using System;
[Serializable]
public class RobotActionTopics
{
public string Topic1;
public string Topic2;
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8e59e2e5a53eb5846a7c7ac4b9995c16

View File

@@ -4,9 +4,6 @@ using System;
public class RobotController : MonoBehaviour
{
public string robotId = "robot1"; // 로봇 ID
public bool isLocallyControlled = false; // 로컬에서 제어 여부
[Header("Motor State")]
[SerializeField] private GameObject motorStatusIndicator1;
[SerializeField] private GameObject motorStatusIndicator2;
@@ -14,7 +11,7 @@ public class RobotController : MonoBehaviour
[SerializeField] private Material indicatorMaterial1; // ±âº»»ö(ȸ»ö)
[SerializeField] private Material indicatorMaterial2; // ÃÊ·Ï
private bool isMotorOn = false;
private bool isMotorOn;
void Start()
{
@@ -26,14 +23,8 @@ public class RobotController : MonoBehaviour
{
motorStatusIndicator2.GetComponent<MeshRenderer>().material = indicatorMaterial1;
}
SetMotorState(false);
}
/// <summary>
/// 로봇의 모터 상태 설정 (나중에 UI에서 호출할 함수)
/// </summary>
/// <param name="isOn">모터를 켤지 여부</param>
public void SetMotorState(bool isOn)
{
isMotorOn = isOn;

View File

@@ -1,7 +0,0 @@
using System;
[Serializable]
public class RobotIDConfig
{
public string ID;
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 7d96c46e3e183d44bbc30211a6e9821d

View File

@@ -1,239 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RobotManager : MonoBehaviour
{
public static RobotManager Instance { get; private set; }
UIManager uiManager => UIManager.Instance;
[Header("MQTT Settings")]
[SerializeField] private MqttManager mqttManager;
[Header("Robot Config")]
[SerializeField] private TextAsset robotIDconfig;
[SerializeField] private TextAsset robotActionTopics;
[SerializeField] private HybridInverseKinematicsNode hybridIKNode;
[Header("UI Settings")]
[Tooltip("각 로봇에 대해 생성할 UI 프리팹")]
[SerializeField] private GameObject robotUIPrefab;
[Tooltip("생성된 UI들이 위치할 부모 Canvas")]
[SerializeField] private Transform uiParentCanvas;
// --- MQTT Topic 관리 ---
private RobotIDConfig idConfigData;
private RobotActionTopics actionTopicsData;
// --- 로봇 관리 ---
private readonly Dictionary<string, RobotController> robotRegistry = new Dictionary<string, RobotController>();// 로봇 ID를 키로 하여 관리
private string targetRobotId; // 현재 조작 중인 로봇 ID (가상 = 현실)
private Dictionary<string, GameObject> robotUIs = new Dictionary<string, GameObject>();
// --- 로봇 선택 및 기록 관리 ---
private RobotController selectedRobot; // 현재 선택된 로봇
private Transform RecordingHandleTarget; // Handle(잡는 부분)의 Transform
public static event Action<RobotController> OnRobotSelected; // 로봇 선택 이벤트
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
InitializeRobotRegistry();
ManageConfig();
}
// 이벤트 구독 관리
private void OnEnable()
{
MqttManager.OnMessageReceived += HandleMessage; // 메시지 수신 이벤트 구독
UIController.OnTransformSendUIClicked += HandlePublishTransformCommand;
UIController.OnKeyframesSendUIClicked += HandlePublishKeyframeListCommand;
}
private void OnDisable()
{
MqttManager.OnMessageReceived -= HandleMessage;
UIController.OnTransformSendUIClicked -= HandlePublishTransformCommand;
UIController.OnKeyframesSendUIClicked -= HandlePublishKeyframeListCommand;
}
void Start()
{
InstantiateRobotUIs();
}
private void InitializeRobotRegistry()
{
RobotController[] allRobots = FindObjectsByType<RobotController>(FindObjectsSortMode.None); // 비활성화 된 건 빼고 찾기
foreach (var robot in allRobots)
{
if (!robotRegistry.ContainsKey(robot.robotId))
{
robotRegistry.Add(robot.robotId, robot);
}
else
{
Debug.LogWarning($"로봇 ID {robot.robotId}가 중복되었습니다. 첫 번째 로봇만 등록됩니다.");
}
}
}
private void InstantiateRobotUIs()
{
if (robotUIPrefab == null || uiParentCanvas == null)
{
Debug.LogError("Robot UI Prefab 또는 UI Parent Canvas가 설정되지 않았습니다.");
return;
}
foreach (var entry in robotRegistry)
{
RobotController robot = entry.Value;
// '가상 로봇 (트윈)'에 대해서만 UI를 생성
if (!robot.isLocallyControlled)
{
GameObject uiInstance = Instantiate(robotUIPrefab, uiParentCanvas);
uiInstance.name = $"UI_{robot.robotId}";
// 기존 UI Prefab은 비활성화
robotUIPrefab.SetActive(false);
UIController uiController = uiInstance.GetComponent<UIController>();
if (uiController != null)
{
uiController.Initialize(this, uiManager);
robotUIs.Add(robot.robotId, uiInstance);
}
uiInstance.SetActive(false);
}
}
}
public void SelectRobot(string robotId)
{
if (robotRegistry.TryGetValue(robotId, out RobotController robot))
{
selectedRobot = robot;
targetRobotId = robotId; // 현재 조작 중인 로봇 ID 설정
Debug.Log($"로봇 '{robotId}' 선택됨. 현재 조작 중인 로봇 ID로 설정.");
// 기록할 Handle Target 설정
RecordingHandleTarget = GameObject.FindWithTag("Handle")?.transform;
// 모든 UI 비활성화
foreach (var ui in robotUIs.Values) ui.SetActive(false);
// 선택된 로봇의 UI만 활성화
if (robotUIs.TryGetValue(robotId, out GameObject selectedUI))
{
selectedUI.SetActive(true);
}
OnRobotSelected?.Invoke(selectedRobot); // UIManager의 HandleRobotSelected 이벤트 호출
SubscribeToTopic(new string[] { $"robots/{targetRobotId}/ {actionTopicsData.Topic1}" }); // Topic1: state
HandlePublishTransformCommand(); // 선택 시 한 번 동기화
}
}
#region Publishing Logic
private void ManageConfig()
{
idConfigData = JsonUtility.FromJson<RobotIDConfig>(robotIDconfig.text);
actionTopicsData = JsonUtility.FromJson<RobotActionTopics>(robotActionTopics.text);
}
// 가상 로봇을 조작하여 현실 로봇에게 명령 발행 (UI 사용해서)
public void HandlePublishTransformCommand()
{
Debug.Log("TransformSendUI 이벤트 실행되어 HandlePublishTransformCommand 불러옴");
Vector3 commandPosition = RecordingHandleTarget.transform.localPosition;
List<Vector3> commandJoints = hybridIKNode.GetCurrentJointPositions();
List<Quaternion> commandRotations = hybridIKNode.GetCurrentJointRotations();
string topic = $"robots/{targetRobotId}/{actionTopicsData.Topic2}"; // Topic2: command
Debug.Log($"topic: {topic}");
IKData data = new IKData
{
robotId = targetRobotId, // 로봇 ID (가상 = 현실)
position = commandPosition,
nodesPosition = new List<Vector3>(commandJoints),
nodesRotation = new List<Quaternion>(commandRotations)
};
string json = JsonUtility.ToJson(data, true);
mqttManager.PublishMessage(topic, json);
Debug.Log($"위치 동기화 명령 발행: {json}");
}
public void HandlePublishKeyframeListCommand()
{
Debug.Log("ListSendUI 이벤트 실행되어 HandlePublishKeyframeListCommand 불러옴");
IReadOnlyList<IKData> keyframePositions = UIManager.Instance.KeyframePositions;
//List<Vector3> commandJoints = hybridIKNode.GetJointPositionsForKeyframe();
// 리스트를 메시지로 publish
if (keyframePositions.Count > 0)
{
string topic = $"robots/{targetRobotId}/{actionTopicsData.Topic2}"; // Topic2: command
Debug.Log($"topic: {topic}");
IKDataListWrapper wrapper = new IKDataListWrapper
{
robotId = targetRobotId,
keyFrameList = new List<IKData>(keyframePositions),
//nodesList = commandJoints
};
string json = JsonUtility.ToJson(wrapper, true);
mqttManager.PublishMessage(topic, json);
Debug.Log($"키프레임 리스트 명령 발행: {json}");
}
}
private void HandleMessage(string topic, string message)
{
string[] topicLevels = topic.Split('/');
if (topicLevels.Length < 3) return;
string robotId = topicLevels[1];
string messageType = topicLevels[2];
// ID에 해당하는 로봇이 있는지 확인
if (!robotRegistry.TryGetValue(robotId, out RobotController targetRobot))
{
return;
}
//HandleStateUpdate(targetRobot, message); // TODO. 가상 트윈의 위치 업데이트 -> 실제 로봇에서 버튼 클릭하면 동기화 하기로.
}
private void HandleStateUpdate(RobotController twinRobot, string message) // 가상 <- 현실
{
IKData data = JsonUtility.FromJson<IKData>(message);
twinRobot.transform.localPosition = data.position;
}
#endregion
#region Subscribing Logic
private void SubscribeToTopic(string[] topics)
{
// MqttManager에서 구독 관리
mqttManager.SubscribeToTopics(topics);
}
#endregion
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: ec0bbd7b79e224641af1518339a681fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -100
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,27 +0,0 @@
using UnityEngine;
using UnityEngine.EventSystems;
[RequireComponent(typeof(RobotController))]
public class RobotSelector : MonoBehaviour, IPointerClickHandler
{
private RobotController robotController;
private void Start()
{
robotController = GetComponent<RobotController>();
}
public void OnPointerClick(PointerEventData eventData)
{
Debug.Log("RobotSelector OnPointerClick called.");
if (RobotManager.Instance != null && robotController != null)
{
RobotManager.Instance.SelectRobot(robotController.robotId);
Debug.Log(robotController.robotId + " Ŭ¸¯");
}
else
{
Debug.LogError("RobotManager Instance or RobotController is null.");
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 2c7de44cbc958774c8df01a2be9703bb

View File

@@ -1,84 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UIController : MonoBehaviour
{
private RobotManager robotManager;
private UIManager uiManager;
public TextMeshProUGUI keyframeText;
public List<Button> buttonsToDisable;
public static event Action<UIController> OnUIControllerInitialized;
public static event Action OnTransformSendUIClicked;
public static event Action OnKeyframesSendUIClicked;
public static event Action OnRecordUIClicked;
public static event Action OnDeleteUIClicked;
public static event Action OnInitUIClicked;
public static event Action OnRunUIClicked;
public void Initialize(RobotManager r_manager, UIManager u_manager)
{
this.robotManager = r_manager;
this.uiManager = u_manager;
OnUIControllerInitialized?.Invoke(this);
}
public void OnSendTransformClicked()
{
if (robotManager == null)
{
Debug.LogError("RobotManager reference is not set.");
return;
}
//robotManager.HandlePublishTransformCommand();
OnTransformSendUIClicked?.Invoke();
Debug.Log("OnTransformSendUIClicked event invoked.");
}
public void OnSendKeyframesClicked()
{
if (robotManager == null)
{
Debug.LogError("RobotManager reference is not set.");
return;
}
//robotManager.HandlePublishKeyframeListCommand();
OnKeyframesSendUIClicked?.Invoke();
Debug.Log("OnKeyframesSendUIClicked event invoked.");
}
public void OnRecordClicked()
{
if (uiManager == null)
{
Debug.LogError("UIManager reference is not set.");
return;
}
OnRecordUIClicked?.Invoke();
}
public void OnDeleteClicked()
{
if (uiManager == null) return;
OnDeleteUIClicked?.Invoke();
}
public void OnClearClicked()
{
if (uiManager == null) return;
OnInitUIClicked?.Invoke();
}
public void OnRunClicked()
{
if (uiManager == null) return;
OnRunUIClicked?.Invoke();
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 6a2d0ca11794a1b45881a2e55f004d5f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 100
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,318 +0,0 @@
using System.Collections.Generic;
using System.Text;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class UIManager : MonoBehaviour
{
public static UIManager Instance { get; private set; }
#region Inspector Variables
[Header("Settings of Target")]
[SerializeField] private Transform targetToRecord; // endNode 역할을 할 Transform
[SerializeField] private GameObject IKSolver;
[SerializeField] private TextMeshProUGUI keyframeText;
[SerializeField] private List<Button> buttonsToDisable; // 시뮬레이션 실행 중 비활성화할 버튼들
[Header("System References")]
[SerializeField] private RobotManager robotManager;
[SerializeField] private HybridInverseKinematicsNode hybridIKNode;
private Transform originalTransform; // 기록 시작 전의 원래 위치 저장
private List<IKData> keyframePositions = new List<IKData>();
public IReadOnlyList<IKData> KeyframePositions => keyframePositions; // 외부에서 읽기 전용으로 접근 가능하게 함
private float lastRecordingTime = 0f;
private Coroutine simulationCoroutine = null;
private readonly StringBuilder stringBuilder = new StringBuilder();
#endregion
private void Awake()
{
// 싱글톤 인스턴스 설정
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// 이벤트 구독 관리
private void OnEnable()
{
RobotManager.OnRobotSelected += HandleRobotSelected;
UIController.OnUIControllerInitialized += HandleUIInitialize;
UIController.OnRecordUIClicked += RecordPosition;
UIController.OnDeleteUIClicked += DeleteLastRecordedPosition;
UIController.OnInitUIClicked += InitRecordedPositions;
UIController.OnRunUIClicked += RunSimulation;
}
private void OnDisable()
{
RobotManager.OnRobotSelected -= HandleRobotSelected;
UIController.OnUIControllerInitialized -= HandleUIInitialize;
UIController.OnRecordUIClicked -= RecordPosition;
UIController.OnDeleteUIClicked -= DeleteLastRecordedPosition;
UIController.OnInitUIClicked -= InitRecordedPositions;
UIController.OnRunUIClicked -= RunSimulation;
}
// RobotManager로부터 로봇이 선택되었다는 소식을 받았을 때 실행될 함수
private void HandleRobotSelected(RobotController selectedRobot)
{
if (selectedRobot != null)
{
Transform handleTransform = selectedRobot.transform.Find("Handle");
IKSolver = selectedRobot.GetComponentInChildren<HybridInverseKinematicsNode>()?.gameObject;
if (handleTransform != null)
{
this.targetToRecord = handleTransform;
Debug.Log($"UIManager: 기록 대상이 '{selectedRobot.robotId}'의 Handle로 변경.");
originalTransform = targetToRecord;
}
else
{
Debug.LogWarning($"'{selectedRobot.robotId}'에서 Handle을 찾지 못함.");
}
}
}
// UIController가 초기화되었을 때 실행될 함수
private void HandleUIInitialize(UIController controller)
{
Debug.Log("UIManager: UIController 초기화 이벤트 수신됨.");
if (controller != null && controller.keyframeText != null && controller.buttonsToDisable != null)
{
this.keyframeText = controller.keyframeText;
Debug.Log("UIManager: Keyframe Text가 UIController로부터 설정됨.");
this.buttonsToDisable = controller.buttonsToDisable;
Debug.Log("UIManager: Buttons to Disable가 UIController로부터 설정됨.");
}
else
{
Debug.LogWarning("UIController 또는 Keyframe Text/ButtonsToDisable이 null임.");
}
}
#region Public Methods
public void RecordPosition()
{
if (targetToRecord == null)
{
Debug.LogError("Target Transform이 지정되지 않았습니다!");
return;
}
float currentTime = Time.time;
float timeSinceLastFrame = (keyframePositions.Count > 0) ? currentTime - lastRecordingTime : 0f;
IKData newFrame = new IKData
{
position = targetToRecord.localPosition,
nodesPosition = new List<Vector3>(hybridIKNode.GetCurrentJointPositions()),
nodesRotation = new List<Quaternion>(hybridIKNode.GetCurrentJointRotations()),
deltaTime = timeSinceLastFrame,
};
keyframePositions.Add(newFrame);
lastRecordingTime = currentTime;
Debug.Log($"키프레임 기록됨: 위치={newFrame.position}, 시간 간격={newFrame.deltaTime}");
UpdateKeyframeUI();
}
public void DeleteLastRecordedPosition()
{
if (keyframePositions.Count > 0)
{
keyframePositions.RemoveAt(keyframePositions.Count - 1);
UpdateKeyframeUI();
}
else
{
Debug.Log("No recorded positions to delete.");
}
}
public void InitRecordedPositions()
{
StartCoroutine(ResetPositionRoutine());
}
private IEnumerator ResetPositionRoutine()
{
TargetRangeLimiter limiter = targetToRecord.GetComponent<TargetRangeLimiter>();
if (limiter != null) limiter.enabled = false;
keyframePositions.Clear();
UpdateKeyframeUI();
targetToRecord.localPosition = originalTransform.localPosition;
Debug.Log("Recorded positions initialized.");
// 한 프레임만 기다림
yield return null;
// 다시 활성화
if (limiter != null) limiter.enabled = true;
}
public void RunSimulation()
{
if (keyframePositions.Count == 0)
{
Debug.Log("No recorded positions to simulate.");
return;
}
if (simulationCoroutine != null)
{
StopCoroutine(simulationCoroutine);
}
SetButtonsInteractable(false);
IKSolver.SetActive(false);
simulationCoroutine = StartCoroutine(SimulateMovement());
}
#endregion
#region Inner Logic & Coroutines
private void SetButtonsInteractable(bool isInteractable)
{
foreach (Button button in buttonsToDisable)
{
if (button != null)
{
button.interactable = isInteractable;
}
}
}
private void UpdateKeyframeUI()
{
if (keyframeText == null)
{
Debug.LogError("Keyframe Text가 지정되지 않았습니다!");
return;
}
// StringBuilder를 사용하여 불필요한 메모리 할당을 방지
stringBuilder.Clear();
for (int i = 0; i < keyframePositions.Count; i++)
{
// Vector3의 소수점을 2자리까지만 표시
string positionText = keyframePositions[i].position.ToString("F2");
string deltaTimeText = keyframePositions[i].deltaTime.ToString("F2");
stringBuilder.AppendLine($"[{i + 1}] pos:{positionText}, time:{deltaTimeText}");
}
keyframeText.text = stringBuilder.ToString(); // 최종적으로 완성된 문자열을 한 번만 할당
}
private IEnumerator SimulateMovement()
{
//Debug.Log("시뮬레이션 시작.");
//Vector3 startPosition = keyframePositions[0].position;
//foreach (IKData target in keyframePositions)
//{
// Vector3 targetPosition = target.position;
// float moveDuration = target.deltaTime; // 수신된 시간 간격
// if (moveDuration <= 0f)
// {
// targetToRecord.localPosition = targetPosition;
// startPosition = targetPosition;
// continue;
// }
// float elapsedTime = 0f;
// while (elapsedTime < moveDuration)
// {
// elapsedTime += Time.deltaTime;
// float t = elapsedTime / moveDuration; // 이동 진행도 계산 (0.0 ~ 1.0)
// targetToRecord.localPosition = Vector3.Lerp(startPosition, targetPosition, t); // Lerp 함수로 시작점과 목표점 사이의 현재 위치 계산
// yield return null;
// }
// targetToRecord.localPosition = targetPosition; // 오차 없애기 위해 이동이 끝나면 목표 위치로 정확히 설정
// startPosition = targetPosition;
//}
Debug.Log("동기화 시뮬레이션 시작");
if (hybridIKNode == null || keyframePositions == null)
{
Debug.LogWarning("타겟 IK나 키프레임 리스트가 비어있음");
yield break;
}
Transform[] jointTransforms = new Transform[hybridIKNode.nodes.Count];
Debug.Log($"관절 개수: {hybridIKNode.nodes.Count}");
for (int i = 0; i < hybridIKNode.nodes.Count; i++)
{
jointTransforms[i] = hybridIKNode.nodes[i].jointTransform;
}
foreach (IKData targetKeyframe in keyframePositions)
{
float moveDuration = targetKeyframe.deltaTime; // 수신된 시간 간격
Debug.Log($"목표 키프레임 위치: {targetKeyframe.position}, 이동 시간: {moveDuration}");
Vector3[] startPositions = new Vector3[jointTransforms.Length];
Quaternion[] startRotations = new Quaternion[jointTransforms.Length];
for (int i = 0; i < jointTransforms.Length; i++)
{
startPositions[i] = jointTransforms[i].localPosition;
startRotations[i] = jointTransforms[i].localRotation;
}
List<Vector3> targetPositions = targetKeyframe.nodesPosition;
List<Quaternion> targetRotations = targetKeyframe.nodesRotation;
if (moveDuration > 0f)
{
float elapsedTime = 0f;
while (elapsedTime < moveDuration)
{
elapsedTime += Time.deltaTime;
Debug.Log($"로봇 이동 중... 경과 시간: {elapsedTime:F2}s / {moveDuration:F2}s");
float t = elapsedTime / moveDuration; // 이동 진행도 계산 (0.0 ~ 1.0)
for (int i = 0; i < jointTransforms.Length; i++)
{
jointTransforms[i].localPosition = Vector3.Lerp(startPositions[i], targetPositions[i], t);
jointTransforms[i].localRotation = Quaternion.Slerp(startRotations[i], targetRotations[i], t);
Debug.Log($"관절 {i} 위치: {jointTransforms[i].localPosition}, 회전: {jointTransforms[i].localRotation.eulerAngles}");
}
yield return null;
}
}
// 보간이 끝난 뒤 모든 관절을 목표 자세로 정확히 설정
for (int i = 0; i < jointTransforms.Length; i++)
{
jointTransforms[i].localPosition = targetPositions[i];
jointTransforms[i].localRotation = targetRotations[i];
Debug.Log($"로봇 관절 {i} 최종 위치: {jointTransforms[i].localPosition}, 회전: {jointTransforms[i].localRotation.eulerAngles}");
}
}
Debug.Log("시뮬레이션 종료.");
simulationCoroutine = null;
SetButtonsInteractable(true);
IKSolver.SetActive(true);
}
#endregion
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 5ab9a95eb69cb82438fe98f7e4caada1

View File

@@ -90,7 +90,7 @@ MonoBehaviour:
active: 1
postExposure:
m_OverrideState: 1
m_Value: -1.33
m_Value: -0.6
contrast:
m_OverrideState: 1
m_Value: 20

View File

@@ -15389,6 +15389,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
view: {fileID: 1243479632}
robotController: {fileID: 806304512143720359}
motorStatePollInterval: 1
--- !u!4 &1299890571
Transform:
m_ObjectHideFlags: 0
@@ -23658,7 +23659,7 @@ PrefabInstance:
m_Modifications:
- target: {fileID: 585480472500942354, guid: 77e7c27b2c5525e4aa8cc9f99d654486, type: 3}
propertyPath: m_Antialiasing
value: 1
value: 3
objectReference: {fileID: 0}
- target: {fileID: 585480472500942354, guid: 77e7c27b2c5525e4aa8cc9f99d654486, type: 3}
propertyPath: m_RenderPostProcessing
@@ -23864,6 +23865,10 @@ PrefabInstance:
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7917674758326848207, guid: 77e7c27b2c5525e4aa8cc9f99d654486, type: 3}
propertyPath: near clip plane
value: 0.3
objectReference: {fileID: 0}
- target: {fileID: 8806329115733545877, guid: 77e7c27b2c5525e4aa8cc9f99d654486, type: 3}
propertyPath: m_Name
value: XR Origin Hands (XR Rig)

View File

@@ -1,3 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
@@ -7,6 +8,8 @@ public class AppManager : MonoBehaviour
{
[SerializeField] private ProgramView view;
[SerializeField] private RobotController robotController;
[SerializeField] private float motorStatePollInterval = 1.0f;
ProgramPresenter presenter;
private string hostip;
private int tcpPort;
private int udpPort;
@@ -19,10 +22,14 @@ public class AppManager : MonoBehaviour
ProgramModel model = new ProgramModel(hostip, tcpPort);
await model.InitializeAsync();
ProgramPresenter presenter = new ProgramPresenter(model, view);
presenter = new ProgramPresenter(model, view);
presenter.RegisterControlledRobot(robotController);
await presenter.UpdateMotorStateAsync();
view.DisplayProgram(null);
StartCoroutine(PollMotorStateCoroutine());
}
private void LoadConfig()
@@ -116,4 +123,14 @@ public class AppManager : MonoBehaviour
udpPort = defaultPort;
}
}
private IEnumerator PollMotorStateCoroutine()
{
while (true)
{
yield return new WaitForSeconds(motorStatePollInterval);
_ = presenter.UpdateMotorStateAsync();
}
}
}

View File

@@ -120,6 +120,26 @@ public class ProgramModel : IProgramModel
return modelName;
}
public async Task<bool> GetRobotMotorStateAsync()
{
string requestUri = $"{baseUrl}/project/rgen";
HttpResponseMessage result = await httpClient.GetAsync(requestUri);
string jsonResponse = await result.Content.ReadAsStringAsync();
JObject data = JObject.Parse(jsonResponse);
int motorState = (int)data.SelectToken("enable_state");
if (motorState == 2 || motorState == 256)
return true;
else if (motorState == 1)
return false;
else
{
throw new Exception("로봇 상태 API 응답에서 모터 상태를 찾을 수 없습니다.");
}
}
public async Task<bool> LoadProgram(string programId)
{
string requestUri = $"{baseUrl}/file_manager/files?pathname=project/jobs/{programId}&common";
@@ -145,7 +165,7 @@ public class ProgramModel : IProgramModel
try
{
HttpResponseMessage result = await httpClient.GetAsync(new Uri($"{baseUrl}/project/jobs_info"));
HttpResponseMessage result = await httpClient.GetAsync($"{baseUrl}/project/jobs_info");
jsonResponse = await result.Content.ReadAsStringAsync();
wrappedJson = $"{{\"jobs\":{jsonResponse}}}";

View File

@@ -1,5 +1,7 @@
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR.ARSubsystems;
public class ProgramPresenter
{
@@ -7,6 +9,7 @@ public class ProgramPresenter
private IProgramView view;
private RobotController controlledRobot;
private string _programId;
private bool lastKnownMotorState = false;
public ProgramPresenter(ProgramModel model, IProgramView view)
{
@@ -26,12 +29,29 @@ public class ProgramPresenter
this.controlledRobot = robot;
}
public async Task UpdateMotorStateAsync()
{
try
{
bool currentState = await model.GetRobotMotorStateAsync();
if (currentState != lastKnownMotorState)
{
controlledRobot.SetMotorState(currentState);
lastKnownMotorState = currentState;
}
}
catch (Exception e)
{
Debug.LogWarning($"모터 상태 업데이트 실패: {e.Message}");
}
}
public void OnApplicationStart()
{
if (controlledRobot != null)
{
Debug.Log("로봇 모터를 ON 상태로 설정합니다.");
controlledRobot.SetMotorState(true);
Debug.Log("제어할 로봇이 등록되었습니다.");
}
else
{
@@ -43,7 +63,7 @@ public class ProgramPresenter
{
if (await model.CreateNewProgram(programId))
{
view.DisplayProgram(model.CurrentProgram);
view.DisplayProgram(programId);
view.HideProgramSelectPanel();
OnApplicationStart();
}
@@ -68,7 +88,7 @@ public class ProgramPresenter
{
if(_programId != null && await model.LoadProgram(_programId))
{
view.DisplayProgram(model.CurrentProgram);
view.DisplayProgram(_programId);
view.HideProgramSelectPanel();
view.HideProgramList();

View File

@@ -7,7 +7,7 @@ using TMPro;
public interface IProgramView
{
void ShowMessage(string message);
void DisplayProgram(RobotProgram program);
void DisplayProgram(string programId);
void ShowProgramList(List<string> programIds);
void HideProgramList();
void HideProgramSelectPanel();
@@ -107,16 +107,16 @@ public class ProgramView : MonoBehaviour, IProgramView
Debug.LogWarning(message);
}
public void DisplayProgram(RobotProgram program)
public void DisplayProgram(string programId)
{
if (program == null)
if (programId == null)
{
//currentProgramIdText.text = "No Program Loaded";
//endpointListText.text = "";
Debug.Log("No Program Loaded");
return;
}
Debug.Log($"연결된 프로그램: {program.ProgramId}.job");
Debug.Log($"연결된 프로그램: {programId}.job");
//currentProgramIdText.text = "Current: " + program.programId;

View File

@@ -267,7 +267,7 @@ QualitySettings:
terrainMaxTrees: 50
excludedTargetPlatforms: []
- serializedVersion: 4
name: Ultra
name: PC
pixelLightCount: 1
shadows: 2
shadowResolution: 2
@@ -285,7 +285,7 @@ QualitySettings:
antiAliasing: 0
softParticles: 1
softVegetation: 1
realtimeReflectionProbes: 1
realtimeReflectionProbes: 0
billboardsFaceCameraPosition: 1
useLegacyDetailDistribution: 1
adaptiveVsync: 0