1919using Utils ;
2020using Utils . Types ;
2121using Action = System . Action ;
22+ using System . Threading ;
2223
2324namespace Services
2425{
@@ -31,6 +32,9 @@ public class EphysLinkService
3132 // FIXME: This should go into some common constants file (along with copy in probe inspector view model).
3233 private readonly Vector2 _pitchRange = new ( 0 , 90 ) ;
3334
35+ // Visualization loop interval (ms)
36+ private const int VISUALIZATION_UPDATE_INTERVAL_MS = 10 ; // about 60 Hz
37+
3438 #endregion
3539
3640 #region Services
@@ -48,12 +52,15 @@ public class EphysLinkService
4852
4953 public string SocketId => _socket . Id ;
5054
55+ // Cancellation for the visualization update loop
56+ private CancellationTokenSource _visualizationLoopCts ;
57+
5158 #endregion
5259
5360 #region Demo Loop
5461
55- private readonly Dictionary < string , bool > _runningDemoLoops = new ( ) ;
56- private const float DEMO_SPEED = 100f ; // µm /s
62+ private readonly HashSet < string > _runningDemoLoops = new ( ) ;
63+ private const float DEMO_SPEED = 0.1f ; // mm /s
5764 #endregion
5865
5966 public EphysLinkService ( StoreService storeService )
@@ -72,36 +79,31 @@ public EphysLinkService(StoreService storeService)
7279
7380 private async void OnSceneStateChanged ( SceneState sceneState )
7481 {
75- // Apply small delay to prevent overrunning updates (delay for roughly 60 FPS).
76- await Task . Delay ( 10 ) ;
77-
7882 // Handle demo loop state changes.
7983 foreach ( var manipulatorState in sceneState . Manipulators )
8084 {
81- _runningDemoLoops . TryGetValue ( manipulatorState . Id , out var isRunning ) ;
85+ var isRunning = _runningDemoLoops . Contains ( manipulatorState . Id ) ;
8286
8387 switch ( manipulatorState . IsDemoRunning )
8488 {
8589 // Start demo loop if requested and not already running.
8690 case true when ! isRunning :
87- _runningDemoLoops [ manipulatorState . Id ] = true ;
91+ _runningDemoLoops . Add ( manipulatorState . Id ) ;
8892 _ = RunDemoLoop ( manipulatorState . Id ) ;
8993 break ;
9094 // Stop demo loop if requested and currently running.
9195 case false when isRunning :
9296 await Stop ( manipulatorState . Id ) ;
97+ _runningDemoLoops . Remove ( manipulatorState . Id ) ;
9398 break ;
9499 }
95100 }
96-
97- // WARNING: this will create an infinite loop of state updates on purpose.
98- // Update the position of visualization probes.
99- await UpdateVisualizationProbePosition ( sceneState ) ;
100101 }
101102
102103 private void OnShuttingDown ( )
103104 {
104105 _sceneStateSubscription . Dispose ( ) ;
106+ StopVisualizationLoop ( ) ;
105107 App . shuttingDown -= OnShuttingDown ;
106108 }
107109
@@ -125,6 +127,9 @@ public void ConnectToServer(
125127 if ( _socketManager != null && _socketManager . Socket . IsOpen )
126128 _socketManager . Close ( ) ;
127129
130+ // Ensure any previous loop is stopped.
131+ StopVisualizationLoop ( ) ;
132+
128133 // Create new connection.
129134 var options = new SocketOptions { Timeout = new TimeSpan ( 0 , 0 , 2 ) } ;
130135
@@ -135,10 +140,10 @@ public void ConnectToServer(
135140 _socketManager = new SocketManager ( new Uri ( $ "http://{ ip } :{ port } ") , options ) ;
136141 _socket = _socketManager . Socket ;
137142
138- // On successful connection.
139- _socket . Once (
140- "connect" ,
141- async ( ) =>
143+ // Local async handler to run after successful connection.
144+ async Task OnConnectedAsync ( )
145+ {
146+ try
142147 {
143148 // Check version compatibility.
144149 if ( await IsVersionCompatible ( ) )
@@ -153,12 +158,38 @@ public void ConnectToServer(
153158 ( EphysLinkConnectionState . Connected , _socket . Id )
154159 ) ;
155160 onConnected ? . Invoke ( ) ;
161+
162+ // Start visualization update loop until disconnect.
163+ StartVisualizationLoop ( ) ;
156164 }
157165 else
158166 {
159167 HandleError ( GetOutdatedVersionErrorMessage ( ) ) ;
160168 }
161169 }
170+ catch ( Exception ex )
171+ {
172+ HandleError ( $ "{ GetErrorConnectingToServerMessage ( ) } Caused exception: { ex . Message } ") ;
173+ }
174+ }
175+
176+ // When the server disconnects, stop the visualization loop and update state.
177+ _socket . On (
178+ "disconnect" ,
179+ ( ) =>
180+ {
181+ StopVisualizationLoop ( ) ;
182+ _storeService . Store . Dispatch (
183+ SettingsActions . SET_EPHYS_LINK_CONNECTION_STATE ,
184+ ( EphysLinkConnectionState . Disconnected , "" )
185+ ) ;
186+ }
187+ ) ;
188+
189+ // On successful connection, delegate to async task handler.
190+ _socket . Once (
191+ "connect" ,
192+ ( ) => { _ = OnConnectedAsync ( ) ; }
162193 ) ;
163194
164195 // On error.
@@ -202,6 +233,9 @@ void HandleError(string message)
202233 /// <param name="onDisconnected">Post disconnection behavior.</param>
203234 public void Disconnect ( Action onDisconnected = null )
204235 {
236+ // Stop the visualization loop if running.
237+ StopVisualizationLoop ( ) ;
238+
205239 // Close socket connection.
206240 _socketManager ? . Close ( ) ;
207241 _socketManager = null ;
@@ -527,6 +561,61 @@ private static string ToJson<T>(T data)
527561 return JsonUtility . ToJson ( data ) ;
528562 }
529563
564+ // Starts the continuous visualization update loop until the socket disconnects or service disconnects.
565+ private void StartVisualizationLoop ( )
566+ {
567+ StopVisualizationLoop ( ) ; // ensure only one loop runs
568+ _visualizationLoopCts = new CancellationTokenSource ( ) ;
569+ var token = _visualizationLoopCts . Token ;
570+ _ = VisualizationUpdateLoop ( token ) ;
571+ }
572+
573+ // Cancels and disposes the visualization update loop.
574+ private void StopVisualizationLoop ( )
575+ {
576+ if ( _visualizationLoopCts != null )
577+ {
578+ try { _visualizationLoopCts . Cancel ( ) ; }
579+ catch ( ObjectDisposedException ) { /* ignore disposed */ }
580+ catch ( Exception ex )
581+ {
582+ Debug . Log ( $ "Ignored exception during visualization loop cancellation: { ex } ") ;
583+ }
584+ _visualizationLoopCts . Dispose ( ) ;
585+ _visualizationLoopCts = null ;
586+ }
587+ }
588+
589+ // The loop body calling UpdateVisualizationProbePosition at a fixed interval.
590+ private async Task VisualizationUpdateLoop ( CancellationToken token )
591+ {
592+ while ( ! token . IsCancellationRequested )
593+ {
594+ try
595+ {
596+ var sceneState = _storeService . Store . GetState < SceneState > ( SliceNames . SCENE_SLICE ) ;
597+ await UpdateVisualizationProbePosition ( sceneState ) ;
598+ }
599+ catch ( OperationCanceledException )
600+ {
601+ break ;
602+ }
603+ catch ( Exception ex )
604+ {
605+ Debug . LogWarning ( $ "Visualization update loop error: { ex . Message } ") ;
606+ }
607+
608+ try
609+ {
610+ await Task . Delay ( VISUALIZATION_UPDATE_INTERVAL_MS , token ) ;
611+ }
612+ catch ( OperationCanceledException )
613+ {
614+ break ;
615+ }
616+ }
617+ }
618+
530619 #endregion
531620
532621 #region Visualization control
@@ -611,6 +700,13 @@ var manipulatorState in sceneState.Manipulators.Where(state =>
611700 var transformedAPMLDV = BrainAtlasManager . World2T_Vector (
612701 referenceCoordinateAdjustedWorldPosition
613702 ) ;
703+
704+ // Cancel update if the manipulator's position did not change by a lot.
705+ var probeState = sceneState . Probes . FirstOrDefault ( state => state . Name == manipulatorState . VisualizationProbeName ) ;
706+ if ( probeState == null || Vector3 . SqrMagnitude ( transformedAPMLDV - probeState . APMLDV ) < 0.0001f )
707+ {
708+ continue ;
709+ }
614710
615711 // Get the current forward vector of the probe.
616712 var forwardT = BrainAtlasManager . ActiveAtlasTransform . U2T_Vector (
@@ -782,14 +878,14 @@ private async Task RunDemoLoop(string manipulatorId)
782878 // Exit if manipulator not found.
783879 if ( manipulatorState == null )
784880 {
785- _runningDemoLoops [ manipulatorId ] = false ;
881+ _runningDemoLoops . Remove ( manipulatorId ) ;
786882 return ;
787883 }
788884
789885 // Exit if demo is no longer running.
790886 if ( ! manipulatorState . IsDemoRunning )
791887 {
792- _runningDemoLoops [ manipulatorId ] = false ;
888+ _runningDemoLoops . Remove ( manipulatorId ) ;
793889 return ;
794890 }
795891
@@ -804,7 +900,7 @@ private async Task RunDemoLoop(string manipulatorId)
804900 // Exit on error (including stop request).
805901 if ( HasError ( homeResponse . Error ) )
806902 {
807- _runningDemoLoops [ manipulatorId ] = false ;
903+ _runningDemoLoops . Remove ( manipulatorId ) ;
808904 return ;
809905 }
810906
@@ -815,7 +911,7 @@ private async Task RunDemoLoop(string manipulatorId)
815911 ) ;
816912 if ( manipulatorState is not { IsDemoRunning : true } )
817913 {
818- _runningDemoLoops [ manipulatorId ] = false ;
914+ _runningDemoLoops . Remove ( manipulatorId ) ;
819915 return ;
820916 }
821917
@@ -836,7 +932,7 @@ private async Task RunDemoLoop(string manipulatorId)
836932 // Exit on error (including stop request).
837933 if ( HasError ( intermediateResponse . Error ) )
838934 {
839- _runningDemoLoops [ manipulatorId ] = false ;
935+ _runningDemoLoops . Remove ( manipulatorId ) ;
840936 return ;
841937 }
842938
@@ -847,7 +943,7 @@ private async Task RunDemoLoop(string manipulatorId)
847943 ) ;
848944 if ( manipulatorState is not { IsDemoRunning : true } )
849945 {
850- _runningDemoLoops [ manipulatorId ] = false ;
946+ _runningDemoLoops . Remove ( manipulatorId ) ;
851947 return ;
852948 }
853949
@@ -862,7 +958,7 @@ private async Task RunDemoLoop(string manipulatorId)
862958 // Exit on error (including stop request).
863959 if ( HasError ( targetResponse . Error ) )
864960 {
865- _runningDemoLoops [ manipulatorId ] = false ;
961+ _runningDemoLoops . Remove ( manipulatorId ) ;
866962 return ;
867963 }
868964
@@ -873,7 +969,7 @@ private async Task RunDemoLoop(string manipulatorId)
873969 ) ;
874970 if ( manipulatorState is not { IsDemoRunning : true } )
875971 {
876- _runningDemoLoops [ manipulatorId ] = false ;
972+ _runningDemoLoops . Remove ( manipulatorId ) ;
877973 return ;
878974 }
879975
@@ -894,7 +990,7 @@ private async Task RunDemoLoop(string manipulatorId)
894990 // Exit on error (including stop request).
895991 if ( HasError ( returnToIntermediateResponse . Error ) )
896992 {
897- _runningDemoLoops [ manipulatorId ] = false ;
993+ _runningDemoLoops . Remove ( manipulatorId ) ;
898994 return ;
899995 }
900996
@@ -909,7 +1005,7 @@ private async Task RunDemoLoop(string manipulatorId)
9091005 continue ;
9101006 }
9111007
912- _runningDemoLoops [ manipulatorId ] = false ;
1008+ _runningDemoLoops . Remove ( manipulatorId ) ;
9131009 return ;
9141010 }
9151011 }
0 commit comments