/*=============================================================================== Copyright (C) 2024 Immersal - Part of Hexagon. All Rights Reserved. This file is part of the Immersal SDK. The Immersal SDK cannot be copied, distributed, or made available to third-parties for commercial purposes without written permission of Immersal Ltd. Contact sales@immersal.com for licensing requests. ===============================================================================*/ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Events; using UnityEngine.Serialization; using Object = UnityEngine.Object; namespace Immersal.XR { public class ImmersalSession : MonoBehaviour, IImmersalSession { [Tooltip("Start Session at app startup")] [SerializeField] private bool m_AutoStart = true; [Tooltip("Seconds between continuous updates")] [SerializeField] private float m_SessionUpdateInterval = 2.0f; [Tooltip("Try to localize at maximum speed at app startup / resume")] [SerializeField] private bool m_BurstMode = true; [Tooltip("Number of successful localizations to turn off burst mode")] [SerializeField] private int m_BurstSuccessCount = 10; [Tooltip("Time limit for burst mode in seconds")] [SerializeField] private float m_BurstTimeLimit = 15f; [Tooltip("Reset stats and ARSpace filters on application pause")] [SerializeField] private bool m_ResetOnPause = true; [Tooltip("Restart session automatically after a reset")] [SerializeField] private bool m_RestartOnReset = true; [Tooltip("Process component results in a data processing chain")] [SerializeField] private bool m_ProcessData = false; [SerializeField] [Interface(typeof(IDataProcessor))] private Object[] m_SessionDataProcessors; public IDataProcessor[] SessionDataProcessors => m_SessionDataProcessors.OfType>().ToArray(); public bool AutoStart { get => m_AutoStart; set => m_AutoStart = value; } public bool BurstMode { get { return m_BurstMode; } set { SetBurstMode(value); } } public bool ProcessData { get => m_ProcessData; set => m_ProcessData = value; } public float SessionUpdateInterval { get => m_SessionUpdateInterval; set => m_SessionUpdateInterval = value; } public int BurstSuccessCount { get => m_BurstSuccessCount; set => m_BurstSuccessCount = value; } public float BurstTimeLimit { get => m_BurstTimeLimit; set => m_BurstTimeLimit = value; } public bool RestartOnReset { get => m_RestartOnReset; set => m_RestartOnReset = value; } private void SetBurstMode(bool on) { m_BurstStartTime = Time.unscaledTime; m_BurstModeActive = on; } #region Events public UnityEvent OnPause; public UnityEvent OnResume; public UnityEvent OnReset; #endregion private ImmersalSDK sdk; private float m_BurstStartTime = 0.0f; private bool m_BurstModeActive = false; private float m_LastUpdateTime; private bool m_SessionIsRunning = false; private bool m_RunningTasks = false; private bool m_Paused = false; private Task m_CurrentRunningTask; private CancellationTokenSource m_CTS; private IPlatformUpdateResult m_LatestPlatformUpdateResult = null; private ILocalizationResults m_LatestLocalizationResults = null; private DataProcessingChain m_SessionDataProcessingChain; private void Start() { sdk = ImmersalSDK.Instance; m_SessionDataProcessingChain = new DataProcessingChain(SessionDataProcessors); if (!m_AutoStart) return; if (sdk.IsReady) { StartSession(); } else { sdk.OnInitializationComplete.AddListener(StartSession); } } public void StartSession() { if (m_SessionIsRunning) { if (m_Paused) ResumeSession(); // Resume if paused return; } if (!sdk.IsReady) return; ImmersalLogger.Log("Session starting", ImmersalLogger.LoggingLevel.Verbose); m_SessionIsRunning = true; SetBurstMode(BurstMode); } private void Update() { // update data processing chain if (m_ProcessData) m_SessionDataProcessingChain.UpdateChain(); // bail out conditionals if (!m_SessionIsRunning || !sdk.IsReady || !MapManager.HasRegisteredMaps) return; float curTime = Time.unscaledTime; // deactivate burst after enough success or certain time if (sdk.TrackingStatus?.LocalizationSuccessCount >= m_BurstSuccessCount || curTime - m_BurstStartTime >= m_BurstTimeLimit) { SetBurstMode(false); } // check if update interval has passed or if we are bursting bool updateIntervalOrBurst = curTime - m_LastUpdateTime >= m_SessionUpdateInterval || m_BurstModeActive; // update conditional bool doUpdate = updateIntervalOrBurst && !m_RunningTasks && !m_Paused; if (doUpdate) { m_LastUpdateTime = curTime; m_RunningTasks = true; m_CTS = new CancellationTokenSource(); m_CurrentRunningTask = RunTasksAsync(m_ProcessData, m_CTS.Token); } } private async Task RunTasksAsync(bool processData, CancellationToken cancellationToken) { try { // Update platform m_LatestPlatformUpdateResult = await sdk.PlatformSupport.UpdatePlatform(); if (cancellationToken.IsCancellationRequested) { m_RunningTasks = false; return; } if (m_LatestPlatformUpdateResult.Success) { // Localize m_LatestLocalizationResults = await sdk.Localizer.Localize(m_LatestPlatformUpdateResult.CameraData); if (cancellationToken.IsCancellationRequested) { m_RunningTasks = false; return; } foreach (ILocalizationResult result in m_LatestLocalizationResults.Results) { if (result.Success) { // Check if map entry exists if (MapManager.TryGetMapEntry(result.MapId, out MapEntry entry)) { ImmersalLogger.Log($"Localized to map {entry.Map.mapId}-{entry.Map.mapName}", ImmersalLogger.LoggingLevel.Verbose); ILocalizationResult r = result; IPlatformUpdateResult p = m_LatestPlatformUpdateResult; if (processData) { // Push new data to data processing chain await m_SessionDataProcessingChain.ProcessNewData(new SessionData() { Entry = entry, PlatformResult = p, LocalizationResult = r }); SessionData processed = m_SessionDataProcessingChain.GetCurrentData(); entry = processed.Entry; r = processed.LocalizationResult; p = processed.PlatformResult; } // Update SceneUpdater if (cancellationToken.IsCancellationRequested) { m_RunningTasks = false; return; } await sdk.SceneUpdater.UpdateScene(entry, p.CameraData, r); } else { ImmersalLogger.LogError("Localization result does not match with any registered map."); } } } } else { // Localization never ran due to failed platform update // so we need to invalidate previous results. m_LatestLocalizationResults = new LocalizationResults { Results = Array.Empty() }; } // Update tracking status sdk.TrackingAnalyzer.Analyze(m_LatestPlatformUpdateResult.Status, m_LatestLocalizationResults); } catch (Exception e) { ImmersalLogger.LogError($"Session task error: {e.Message}. Stopping session."); m_SessionIsRunning = false; } finally { m_RunningTasks = false; } } // Convenience method public async Task LocalizeOnce() { if (m_CurrentRunningTask != null) await m_CurrentRunningTask; m_LastUpdateTime = Time.unscaledTime; m_RunningTasks = true; m_CTS = new CancellationTokenSource(); try { m_CurrentRunningTask = RunTasksAsync(m_ProcessData, m_CTS.Token); } catch (Exception e) { ImmersalLogger.LogError(e.Message); m_SessionIsRunning = false; } } // Note: MonoBehaviour.OnApplicationPause is called as a GameObject starts. The call is made after Awake. private void OnApplicationPause(bool paused) { if (paused) { PauseSession(); } else { ResumeSession(); } } public async void PauseSession() { if (!m_SessionIsRunning) return; if (m_ResetOnPause) await ResetSession(); m_Paused = true; OnPause?.Invoke(); } public void ResumeSession() { if (!m_SessionIsRunning) return; m_Paused = false; SetBurstMode(BurstMode); OnResume?.Invoke(); } public void TriggerResetSession() { ResetSession(); } public async Task ResetSession() { ImmersalLogger.Log("Resetting session"); await StopSession(); await m_SessionDataProcessingChain.ResetProcessors(); OnReset?.Invoke(); if (m_RestartOnReset) StartSession(); } public async Task StopSession(bool cancelRunningTask = true) { // Send cancellation token to abort running task if (cancelRunningTask) m_CTS?.Cancel(); // Wait for running task to finish if (m_CurrentRunningTask != null) await m_CurrentRunningTask; m_SessionIsRunning = false; m_RunningTasks = false; m_LatestLocalizationResults = null; m_LatestPlatformUpdateResult = null; m_BurstModeActive = false; m_BurstStartTime = 0.0f; m_LastUpdateTime = 0f; m_Paused = false; ImmersalLogger.Log("Session stopped"); } } public class SessionData { public MapEntry Entry; public IPlatformUpdateResult PlatformResult; public ILocalizationResult LocalizationResult; } }