@@ -12,13 +12,12 @@ def __init__(self):
1212
1313 self .pidevice = None
1414 self .is_initialized = False
15+ self .is_moving = False
16+ self .move_event = threading .Event ()
1517
1618 # Create lock for device access
1719 self .mutex = threading .Lock ()
1820
19- # Store current positions
20- self .current_positions = {}
21-
2221 def open (self ):
2322 """Initialize the PI device and connect."""
2423 controller_name = self .config .get ('controller_name' , 'E-727.3SDA' )
@@ -33,17 +32,6 @@ def open(self):
3332 'y' : 2
3433 })
3534
36- # Get initial positions from config
37- initial_pos = self .config .get ('initial_position' , None )
38-
39- # only initialized positions when asked, otherwise keep last settings
40- if initial_pos :
41- # Initialize current positions
42- for name , axis_num in self .axis_map .items ():
43- self .current_positions [axis_num ] = initial_pos .get (name , 0.0 )
44-
45- self .log .info (f'Initial positions: { initial_pos } ' )
46-
4735 # Connect to device
4836 self .pidevice = GCSDevice (controller_name )
4937 device_found = False
@@ -61,17 +49,36 @@ def open(self):
6149 # Initialize stages
6250 pitools .startup (self .pidevice )
6351
64- # Move to initial positions
65- with self .mutex :
66- self .pidevice .MOV (self .current_positions )
67- pitools .waitontarget (self .pidevice )
52+ # Get initial positions from config
53+ initial_pos = self .config .get ('initial_position' , None )
6854
69- self .is_initialized = True
55+ # Move to initial positions (if provided)
56+ if initial_pos :
57+ axis_positions = {
58+ self .axis_map [name ]: float (value )
59+ for name , value in initial_pos .items ()
60+ if name in self .axis_map
61+ }
62+
63+ if axis_positions :
64+ with self .mutex :
65+ self .pidevice .MOV (axis_positions )
66+ pitools .waitontarget (self .pidevice )
67+
68+ self .log .info (f'Initial positions: { initial_pos } ' )
7069
71- # Create data streams for telemetry
70+ # Create data streams
7271 num_axes = len (self .axis_map )
7372 self .positions = self .make_data_stream ('positions' , 'float64' , [num_axes ], 20 )
7473 self .target_positions = self .make_data_stream ('target_positions' , 'float64' , [num_axes ], 20 )
74+ # self.target_positions_wait = self.make_data_stream('target_positions_wait', 'float64', [num_axes], 20)
75+
76+ # Precompute reverse map for speed
77+ self .axis_num_to_name = {num : name for name , num in self .axis_map .items ()}
78+
79+ # Start the worker threads
80+ threading .Thread (target = self ._target_positions_worker , daemon = True ).start ()
81+ self .log .info ("Worker Target Positions started with 250ms timeout" )
7582
7683 # Submit initial positions
7784 self ._submit_positions ()
@@ -81,54 +88,74 @@ def open(self):
8188 self ._make_axis_property (name , axis_num )
8289
8390 # Create commands
84- self .make_command ('move_to' , self .move_to )
91+ self .make_command ('move_to' , self .move_to_wrapper )
92+ self .make_command ('move_and_wait' , self .move_and_wait )
8593 self .make_command ('move_relative' , self .move_relative )
94+ self .make_command ('move_relative_and_wait' , self .move_relative_and_wait )
8695 self .make_command ('get_positions' , self .get_positions )
8796 self .make_command ('stop_motion' , self .stop_motion )
97+ self .make_command ('wait_on_target' , self .wait_on_target )
98+
99+ self .is_initialized = True
88100
89101 def _make_axis_property (self , name , axis_num ):
90102 """Create a property for an axis."""
91103 def getter ():
92- return self .current_positions [axis_num ]
104+ with self .mutex :
105+ return self .pidevice .qPOS ()[str (axis_num )]
93106
94107 def setter (value ):
95- self .move_to ({name : float (value )})
108+ with self .mutex :
109+ actual = self .pidevice .qPOS ()
110+ positions = {n : actual [str (num )] for n , num in self .axis_map .items ()}
111+ positions [name ] = float (value )
112+ self .move_to_wrapper (positions )
96113
97114 self .make_property (f'position_{ name } ' , getter , setter )
98115
99116 def _submit_positions (self ):
100- """Submit current positions to telemetry stream."""
101- # Get positions in order of axis numbers
102- pos_array = np .array ([self .current_positions [i ] for i in sorted (self .current_positions .keys ())], dtype = 'float64' )
117+ """Submit current hardware positions to telemetry stream."""
118+ with self .mutex :
119+ actual = self .pidevice .qPOS ()
120+ pos_array = np .array (
121+ [actual [str (num )] for num in sorted (self .axis_map .values ())],
122+ dtype = 'float64'
123+ )
103124 self .positions .submit_data (pos_array )
104125
126+ def _target_positions_worker (self ):
127+ """Worker thread to handle move_to commands from the target_positions data stream."""
128+ while not self .should_shut_down :
129+ try :
130+ frame = self .target_positions .get_next_frame (wait_time_in_ms = 250 )
131+ data = frame .data
132+ positions = {
133+ self .axis_num_to_name [num ]: float (data [idx ])
134+ for idx , num in enumerate (sorted (self .axis_map .values ()))
135+ }
136+ self .move_to (positions )
137+ self .sleep (0 )
138+ except RuntimeError :
139+ pass
140+
105141 def main (self ):
106- """Main loop - monitor positions ."""
142+ """Main loop - polls for move completion and updates telemetry when motion stops ."""
107143 while not self .should_shut_down :
108- if self .is_initialized :
109- try :
110- with self .mutex :
111- # Query actual positions from device
112- actual_positions = self .pidevice .qPOS ()
113-
114- # Update stored positions
115- for axis_num in self .current_positions .keys ():
116- if axis_num in actual_positions :
117- self .current_positions [axis_num ] = actual_positions [axis_num ]
118-
119- # Submit telemetry
144+ if self .is_moving :
145+ with self .mutex :
146+ on_target = all (self .pidevice .qONT ().values ())
147+ if on_target :
148+ self .is_moving = False
149+ self .move_event .clear ()
120150 self ._submit_positions ()
121-
122- except Exception as e :
123- self .log .error (f"Error reading positions: { e } " )
124-
125- self .sleep (0.1 ) # Update at 10 Hz
151+ self .sleep (0 )
126152
127153 def close (self ):
128154 """Close the PI device connection."""
129155 if self .pidevice is not None :
130156 try :
131157 with self .mutex :
158+ self .pidevice .STP ()
132159 self .pidevice .CloseConnection ()
133160 self .log .info ("PI device connection closed" )
134161 except Exception as e :
@@ -137,9 +164,17 @@ def close(self):
137164 self .pidevice = None
138165 self .is_initialized = False
139166
167+ def move_to_wrapper (self , positions ):
168+ """Wrapper for move_to command to submit target positions to the data stream."""
169+ target_move = np .array ([positions [name ] for name in sorted (self .axis_map .keys ())], dtype = np .float64 )
170+ self .target_positions .submit_data (target_move )
171+
140172 def move_to (self , positions ):
141173 """
142- Move to absolute positions.
174+ Move to absolute positions without waiting for completion.
175+ Telemetry is updated asynchronously by the main loop once the stage reaches target.
176+
177+ Keep in mind telemetry will update only when movement has stopped.
143178
144179 Parameters
145180 ----------
@@ -149,7 +184,6 @@ def move_to(self, positions):
149184 if not self .is_initialized :
150185 raise RuntimeError ("Controller not initialized" )
151186
152- # Convert named axes to axis numbers
153187 axis_positions = {}
154188 for name , value in positions .items ():
155189 if name in self .axis_map :
@@ -161,21 +195,50 @@ def move_to(self, positions):
161195 if not axis_positions :
162196 return
163197
164- # Command the device
165198 with self .mutex :
166199 self .pidevice .MOV (axis_positions )
167200
168- # Update stored positions
169- self .current_positions .update (axis_positions )
201+ self .is_moving = True
202+ self .move_event .set ()
203+
204+
205+ def move_and_wait (self , positions ):
206+ """
207+ Move to absolute positions and wait for completion.
208+ Telemetry is updated synchronously before returning.
209+
210+ Parameters
211+ ----------
212+ positions : dict
213+ Dictionary with axis names (e.g., {'x': 10.0, 'y': 20.0})
214+ """
215+ if not self .is_initialized :
216+ raise RuntimeError ("Controller not initialized" )
217+
218+ axis_positions = {}
219+ for name , value in positions .items ():
220+ if name in self .axis_map :
221+ axis_num = self .axis_map [name ]
222+ axis_positions [axis_num ] = float (value )
223+ else :
224+ self .log .warning (f"Unknown axis name: { name } " )
225+
226+ if not axis_positions :
227+ return
228+
229+ with self .mutex :
230+ self .pidevice .MOV (axis_positions )
170231
171- # Submit telemetry
172- target_array = np .array ([axis_positions .get (i , self .current_positions [i ])
173- for i in sorted (self .current_positions .keys ())], dtype = 'float64' )
174- self .target_positions .submit_data (target_array )
232+ self .wait_on_target (timeout = 5 )
233+ self ._submit_positions ()
175234
176235 def move_relative (self , deltas ):
177236 """
178- Move relative to current positions.
237+ Move relative to current positions without waiting for completion.
238+ Queries actual hardware position as the reference point for the relative move.
239+ Telemetry is updated asynchronously by the main loop once the stage reaches target.
240+
241+ Keep in mind telemetry will update only when movement has stopped.
179242
180243 Parameters
181244 ----------
@@ -185,31 +248,70 @@ def move_relative(self, deltas):
185248 if not self .is_initialized :
186249 raise RuntimeError ("Controller not initialized" )
187250
188- # Convert to absolute positions
189251 absolute_positions = {}
252+
253+ with self .mutex :
254+ actual_positions = self .pidevice .qPOS ()
255+
190256 for name , delta in deltas .items ():
191257 if name in self .axis_map :
192258 axis_num = self .axis_map [name ]
193- absolute_positions [name ] = self . current_positions [ axis_num ] + float (delta )
259+ absolute_positions [name ] = actual_positions [ str ( axis_num ) ] + float (delta )
194260 else :
195261 self .log .warning (f"Unknown axis name: { name } " )
196262
197263 if absolute_positions :
198- self .move_to (absolute_positions )
264+ target_move = np .array ([absolute_positions [name ] for name in sorted (self .axis_map .keys ())], dtype = np .float64 )
265+ self .target_positions .submit_data (target_move )
266+
267+ def move_relative_and_wait (self , deltas ):
268+ """
269+ Move relative to current positions and wait for completion.
270+ Queries actual hardware position as the reference point for the relative move.
271+ Telemetry is updated synchronously before returning.
272+
273+ Parameters
274+ ----------
275+ deltas : dict
276+ Dictionary with axis names and relative movements (e.g., {'x': 1.0, 'y': -0.5})
277+ """
278+ if not self .is_initialized :
279+ raise RuntimeError ("Controller not initialized" )
280+
281+ absolute_positions = {}
282+
283+ with self .mutex :
284+ actual_positions = self .pidevice .qPOS ()
285+
286+ for name , delta in deltas .items ():
287+ if name in self .axis_map :
288+ axis_num = self .axis_map [name ]
289+ absolute_positions [name ] = actual_positions [str (axis_num )] + float (delta )
290+ else :
291+ self .log .warning (f"Unknown axis name: { name } " )
292+
293+ if absolute_positions :
294+ self .move_and_wait (absolute_positions )
199295
200296 def get_positions (self ):
201297 """
202- Get current positions.
298+ Get current positions from hardware .
203299
204300 Returns
205301 -------
206302 positions : dict
207- Dictionary with axis names and positions
303+ Dictionary with axis names and actual hardware positions
208304 """
209- positions = {}
210- for name , axis_num in self .axis_map .items ():
211- positions [name ] = self .current_positions [axis_num ]
212- return positions
305+ if not self .is_initialized :
306+ raise RuntimeError ("Controller not initialized" )
307+
308+ with self .mutex :
309+ actual = self .pidevice .qPOS ()
310+
311+ return {
312+ name : actual [str (axis_num )]
313+ for name , axis_num in self .axis_map .items ()
314+ }
213315
214316 def stop_motion (self ):
215317 """Stop all motion immediately."""
@@ -219,13 +321,16 @@ def stop_motion(self):
219321 try :
220322 with self .mutex :
221323 self .pidevice .STP ()
324+ self .is_moving = False
325+ self .move_event .clear ()
222326 self .log .info ("Motion stopped" )
223327 except Exception as e :
224328 self .log .error (f"Error stopping motion: { e } " )
225329
226- def wait_on_target (self , timeout = None ):
330+ def wait_on_target (self , timeout = 5 ):
227331 """
228332 Wait for all axes to reach their target positions.
333+ Default timeout is 5 seconds to prevent hanging indefinitely if something goes wrong.
229334
230335 Parameters
231336 ----------
@@ -235,11 +340,10 @@ def wait_on_target(self, timeout=None):
235340 if not self .is_initialized :
236341 return
237342
238- with self .mutex :
239- if timeout is not None :
240- pitools .waitontarget (self .pidevice , timeout = timeout )
241- else :
242- pitools .waitontarget (self .pidevice )
343+ if timeout is not None :
344+ pitools .waitontarget (self .pidevice , timeout = timeout )
345+ else :
346+ pitools .waitontarget (self .pidevice )
243347
244348
245349if __name__ == '__main__' :
0 commit comments