Skip to content

Commit 98d4677

Browse files
kjy5Copilot
andauthored
838 sfn 2025 looping demo (#841)
* Add demo setup controls * WIP Build out demo UI * Demo loop * Update Assets/Scripts/Services/EphysLinkService.cs Co-authored-by: Copilot <[email protected]> * Update Assets/Scripts/Services/EphysLinkService.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 835d795 commit 98d4677

File tree

8 files changed

+434
-7
lines changed

8 files changed

+434
-7
lines changed

Assets/Scripts/Models/MainState.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public record MainState
88
{
99
public bool IsAutomationEnabled;
1010

11+
public bool IsDemoEnabled;
12+
1113
public SplitView.State MainSplitViewState;
1214

1315
public int LeftSidePanelTabIndex;

Assets/Scripts/Models/Scene/ManipulatorState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,15 @@ public record ManipulatorState
7070
public int DrivePastDistance;
7171

7272
#endregion
73+
74+
#region Demo
75+
76+
public Vector4 DemoHomeCoordinate;
77+
78+
public Vector4 DemoTargetCoordinate;
79+
80+
public bool IsDemoRunning;
81+
82+
#endregion
7383
}
7484
}

Assets/Scripts/Models/Scene/SceneReducers.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,54 @@ public static SceneState SetManipulatorManualControlEnabledReducer(
680680
return state with { Manipulators = manipulatorsCopy };
681681
}
682682

683+
public static SceneState SetManipulatorDemoHomeCoordinateReducer(
684+
SceneState state,
685+
IAction<(string Id, Vector4 DemoHomeCoordinate)> action
686+
)
687+
{
688+
var index = state.Manipulators.FindIndex(m => m.Id == action.payload.Id);
689+
if (index == -1)
690+
return state;
691+
var manipulatorsCopy = state.Manipulators.ToList();
692+
manipulatorsCopy[index] = manipulatorsCopy[index] with
693+
{
694+
DemoHomeCoordinate = action.payload.DemoHomeCoordinate,
695+
};
696+
return state with { Manipulators = manipulatorsCopy };
697+
}
698+
699+
public static SceneState SetManipulatorDemoTargetCoordinateReducer(
700+
SceneState state,
701+
IAction<(string Id, Vector4 DemoTargetCoordinate)> action
702+
)
703+
{
704+
var index = state.Manipulators.FindIndex(m => m.Id == action.payload.Id);
705+
if (index == -1)
706+
return state;
707+
var manipulatorsCopy = state.Manipulators.ToList();
708+
manipulatorsCopy[index] = manipulatorsCopy[index] with
709+
{
710+
DemoTargetCoordinate = action.payload.DemoTargetCoordinate,
711+
};
712+
return state with { Manipulators = manipulatorsCopy };
713+
}
714+
715+
public static SceneState SetManipulatorDemoRunningReducer(
716+
SceneState state,
717+
IAction<(string Id, bool IsDemoRunning)> action
718+
)
719+
{
720+
var index = state.Manipulators.FindIndex(m => m.Id == action.payload.Id);
721+
if (index == -1)
722+
return state;
723+
var manipulatorsCopy = state.Manipulators.ToList();
724+
manipulatorsCopy[index] = manipulatorsCopy[index] with
725+
{
726+
IsDemoRunning = action.payload.IsDemoRunning,
727+
};
728+
return state with { Manipulators = manipulatorsCopy };
729+
}
730+
683731
#endregion
684732

685733
#region Automation Reducers
@@ -1138,6 +1186,24 @@ bool ManualControlEnabled
11381186
)> SET_MANIPULATOR_MANUAL_CONTROL_ENABLED =
11391187
$"{SliceNames.SCENE_SLICE}/SetManipulatorManualControlEnabled";
11401188

1189+
public static readonly ActionCreator<(
1190+
string Id,
1191+
Vector4 DemoHomeCoordinate
1192+
)> SET_MANIPULATOR_DEMO_HOME_COORDINATE =
1193+
$"{SliceNames.SCENE_SLICE}/SetManipulatorDemoHomeCoordinate";
1194+
1195+
public static readonly ActionCreator<(
1196+
string Id,
1197+
Vector4 DemoTargetCoordinate
1198+
)> SET_MANIPULATOR_DEMO_TARGET_COORDINATE =
1199+
$"{SliceNames.SCENE_SLICE}/SetManipulatorDemoTargetCoordinate";
1200+
1201+
public static readonly ActionCreator<(
1202+
string Id,
1203+
bool IsDemoRunning
1204+
)> SET_MANIPULATOR_DEMO_RUNNING =
1205+
$"{SliceNames.SCENE_SLICE}/SetManipulatorDemoRunning";
1206+
11411207
#endregion
11421208

11431209
#region Automation Actions

Assets/Scripts/Services/EphysLinkService.cs

Lines changed: 200 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ public class EphysLinkService
5050

5151
#endregion
5252

53+
#region Demo Loop
54+
55+
private readonly Dictionary<string, bool> _runningDemoLoops = new();
56+
private const float DEMO_SPEED = 100f; // µm/s
57+
#endregion
58+
5359
public EphysLinkService(StoreService storeService)
5460
{
5561
// Register services.
@@ -69,6 +75,25 @@ private async void OnSceneStateChanged(SceneState sceneState)
6975
// Apply small delay to prevent overrunning updates (delay for roughly 60 FPS).
7076
await Task.Delay(10);
7177

78+
// Handle demo loop state changes.
79+
foreach (var manipulatorState in sceneState.Manipulators)
80+
{
81+
_runningDemoLoops.TryGetValue(manipulatorState.Id, out var isRunning);
82+
83+
switch (manipulatorState.IsDemoRunning)
84+
{
85+
// Start demo loop if requested and not already running.
86+
case true when !isRunning:
87+
_runningDemoLoops[manipulatorState.Id] = true;
88+
_ = RunDemoLoop(manipulatorState.Id);
89+
break;
90+
// Stop demo loop if requested and currently running.
91+
case false when isRunning:
92+
await Stop(manipulatorState.Id);
93+
break;
94+
}
95+
}
96+
7297
// WARNING: this will create an infinite loop of state updates on purpose.
7398
// Update the position of visualization probes.
7499
await UpdateVisualizationProbePosition(sceneState);
@@ -669,13 +694,13 @@ public async Task SetManipulatorDuraOffsetToCurrentDepth(string manipulatorId)
669694
var visualizationProbeState = currentSceneState.Probes.FirstOrDefault(state =>
670695
state.Name == currentSceneState.ActiveManipulatorState.VisualizationProbeName
671696
);
672-
697+
673698
Debug.Log("Collecting info");
674-
699+
675700
// Exit if we cannot find the visualization probe manager or state.
676701
if (visualizationProbeManager == null || visualizationProbeState == null)
677702
return;
678-
703+
679704
Debug.Log("Found viz states and manager");
680705

681706
if (visualizationProbeManager.IsProbeInBrain())
@@ -693,7 +718,7 @@ public async Task SetManipulatorDuraOffsetToCurrentDepth(string manipulatorId)
693718
// We need to calculate the surface coordinate ourselves
694719
var (brainSurfaceCoordinateIdx, _) =
695720
visualizationProbeManager.CalculateEntryCoordinate();
696-
721+
697722
// Exit if not in brain.
698723
if (float.IsNaN(brainSurfaceCoordinateIdx.x))
699724
return;
@@ -716,6 +741,177 @@ public async Task SetManipulatorDuraOffsetToCurrentDepth(string manipulatorId)
716741
);
717742
}
718743

744+
public async Task SetManipulatorDemoHomeCoordinateToCurrentPosition(string manipulatorId)
745+
{
746+
var currentPositionResponse = await GetPosition(manipulatorId);
747+
if (HasError(currentPositionResponse.Error))
748+
return;
749+
_storeService.Store.Dispatch(
750+
SceneActions.SET_MANIPULATOR_DEMO_HOME_COORDINATE,
751+
(manipulatorId, currentPositionResponse.Position)
752+
);
753+
}
754+
755+
public async Task SetManipulatorDemoTargetCoordinateToCurrentPosition(string manipulatorId)
756+
{
757+
var currentPositionResponse = await GetPosition(manipulatorId);
758+
if (HasError(currentPositionResponse.Error))
759+
return;
760+
_storeService.Store.Dispatch(
761+
SceneActions.SET_MANIPULATOR_DEMO_TARGET_COORDINATE,
762+
(manipulatorId, currentPositionResponse.Position)
763+
);
764+
}
765+
766+
/// <summary>
767+
/// Run the demo loop for a manipulator.
768+
/// Sequence: Home -> Target (X,Z only) -> Target (Y,W only) -> Back to Target (X,Z only) -> repeat indefinitely
769+
/// until IsDemoRunning is set to false or an error occurs.
770+
/// </summary>
771+
/// <param name="manipulatorId">ID of the manipulator to run demo loop for.</param>
772+
private async Task RunDemoLoop(string manipulatorId)
773+
{
774+
while (true)
775+
{
776+
// Get current manipulator state.
777+
var sceneState = _storeService.Store.GetState<SceneState>(SliceNames.SCENE_SLICE);
778+
var manipulatorState = sceneState.Manipulators.FirstOrDefault(m =>
779+
m.Id == manipulatorId
780+
);
781+
782+
// Exit if manipulator not found.
783+
if (manipulatorState == null)
784+
{
785+
_runningDemoLoops[manipulatorId] = false;
786+
return;
787+
}
788+
789+
// Exit if demo is no longer running.
790+
if (!manipulatorState.IsDemoRunning)
791+
{
792+
_runningDemoLoops[manipulatorId] = false;
793+
return;
794+
}
795+
796+
// Step 1: Move to home position.
797+
var homeRequest = new SetPositionRequest(
798+
manipulatorId,
799+
manipulatorState.DemoHomeCoordinate,
800+
DEMO_SPEED
801+
);
802+
var homeResponse = await SetPosition(homeRequest);
803+
804+
// Exit on error (including stop request).
805+
if (HasError(homeResponse.Error))
806+
{
807+
_runningDemoLoops[manipulatorId] = false;
808+
return;
809+
}
810+
811+
// Refresh state and check if demo is still running.
812+
sceneState = _storeService.Store.GetState<SceneState>(SliceNames.SCENE_SLICE);
813+
manipulatorState = sceneState.Manipulators.FirstOrDefault(m =>
814+
m.Id == manipulatorId
815+
);
816+
if (manipulatorState is not { IsDemoRunning: true })
817+
{
818+
_runningDemoLoops[manipulatorId] = false;
819+
return;
820+
}
821+
822+
// Step 2: Move to target position on X and Z axes only (keep Y and W from home).
823+
var intermediatePosition = new Vector4(
824+
manipulatorState.DemoTargetCoordinate.x,
825+
manipulatorState.DemoHomeCoordinate.y,
826+
manipulatorState.DemoTargetCoordinate.z,
827+
manipulatorState.DemoHomeCoordinate.w
828+
);
829+
var intermediateRequest = new SetPositionRequest(
830+
manipulatorId,
831+
intermediatePosition,
832+
DEMO_SPEED
833+
);
834+
var intermediateResponse = await SetPosition(intermediateRequest);
835+
836+
// Exit on error (including stop request).
837+
if (HasError(intermediateResponse.Error))
838+
{
839+
_runningDemoLoops[manipulatorId] = false;
840+
return;
841+
}
842+
843+
// Refresh state and check if demo is still running.
844+
sceneState = _storeService.Store.GetState<SceneState>(SliceNames.SCENE_SLICE);
845+
manipulatorState = sceneState.Manipulators.FirstOrDefault(m =>
846+
m.Id == manipulatorId
847+
);
848+
if (manipulatorState is not { IsDemoRunning: true })
849+
{
850+
_runningDemoLoops[manipulatorId] = false;
851+
return;
852+
}
853+
854+
// Step 3: Move to target position on Y and W axes only (complete movement to target).
855+
var targetRequest = new SetPositionRequest(
856+
manipulatorId,
857+
manipulatorState.DemoTargetCoordinate,
858+
DEMO_SPEED
859+
);
860+
var targetResponse = await SetPosition(targetRequest);
861+
862+
// Exit on error (including stop request).
863+
if (HasError(targetResponse.Error))
864+
{
865+
_runningDemoLoops[manipulatorId] = false;
866+
return;
867+
}
868+
869+
// Refresh state and check if demo is still running.
870+
sceneState = _storeService.Store.GetState<SceneState>(SliceNames.SCENE_SLICE);
871+
manipulatorState = sceneState.Manipulators.FirstOrDefault(m =>
872+
m.Id == manipulatorId
873+
);
874+
if (manipulatorState is not { IsDemoRunning: true })
875+
{
876+
_runningDemoLoops[manipulatorId] = false;
877+
return;
878+
}
879+
880+
// Step 4: Move back to intermediate position (X,Z only, keeping Y,W from home).
881+
var returnToIntermediatePosition = new Vector4(
882+
manipulatorState.DemoTargetCoordinate.x,
883+
manipulatorState.DemoHomeCoordinate.y,
884+
manipulatorState.DemoTargetCoordinate.z,
885+
manipulatorState.DemoHomeCoordinate.w
886+
);
887+
var returnToIntermediateRequest = new SetPositionRequest(
888+
manipulatorId,
889+
returnToIntermediatePosition,
890+
DEMO_SPEED
891+
);
892+
var returnToIntermediateResponse = await SetPosition(returnToIntermediateRequest);
893+
894+
// Exit on error (including stop request).
895+
if (HasError(returnToIntermediateResponse.Error))
896+
{
897+
_runningDemoLoops[manipulatorId] = false;
898+
return;
899+
}
900+
901+
// Refresh state and check if demo is still running before next iteration.
902+
sceneState = _storeService.Store.GetState<SceneState>(SliceNames.SCENE_SLICE);
903+
manipulatorState = sceneState.Manipulators.FirstOrDefault(m =>
904+
m.Id == manipulatorId
905+
);
906+
if (manipulatorState is { IsDemoRunning: true })
907+
{
908+
// Loop continues to next iteration (back to home).
909+
continue;
910+
}
911+
_runningDemoLoops[manipulatorId] = false;
912+
return;
913+
}
914+
719915
#endregion
720916
}
721917
}

0 commit comments

Comments
 (0)