Skip to content

Commit a3455f2

Browse files
committed
Add arbitrary-point scanning API to Galvo
Introduce arbitrary point scanning support to the Galvo class. Adds methods to start/stop/pause/resume point-based scans (set_arbitrary_points, stop_arbitrary_points, pause_arbitrary_points, resume_arbitrary_points), an override for laser trigger mode (set_trigger_mode), and helpers to build common patterns (generate_circle_points, generate_grid_points). The set_arbitrary_points method validates inputs (x/y in 0-4095, laser_intensity 0-255, dwell_us present) and enforces a 265-point limit, and all actions send payloads to the /galvo_act endpoint. Includes usage examples in docstrings and implements simple stop behavior via a single long-dwell origin point.
1 parent 524ec60 commit a3455f2

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

uc2rest/galvo.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,184 @@ def get_galvo_status(self, timeout=1):
129129

130130
return self._parent.post_json(path, payload, timeout=timeout)
131131

132+
def set_arbitrary_points(self, points, laser_trigger="AUTO", timeout=1):
133+
"""
134+
Start arbitrary point scanning with custom XY coordinate list
135+
136+
Each point in the list specifies an X, Y coordinate, a dwell time,
137+
and optionally a laser intensity.
138+
The scanner will loop through the points continuously.
139+
140+
Args:
141+
points: List of dicts with keys:
142+
- 'x' (0-4095): X DAC coordinate
143+
- 'y' (0-4095): Y DAC coordinate
144+
- 'dwell_us' (int): Dwell time in microseconds
145+
- 'laser_intensity' (0-255, optional): Per-point laser intensity
146+
Maximum 265 points.
147+
laser_trigger: Trigger mode - "AUTO" (HIGH during dwell, LOW during movement),
148+
"HIGH" (force always on), "LOW" (force always off),
149+
"CONTINUOUS" (HIGH during entire scan)
150+
timeout: Request timeout in seconds (default: 1)
151+
152+
Example:
153+
>>> points = [
154+
... {"x": 1024, "y": 2048, "dwell_us": 500, "laser_intensity": 128},
155+
... {"x": 1500, "y": 2100, "dwell_us": 1000, "laser_intensity": 255},
156+
... {"x": 2000, "y": 2500, "dwell_us": 250}
157+
... ]
158+
>>> galvo.set_arbitrary_points(points)
159+
>>> galvo.set_arbitrary_points(points, laser_trigger="CONTINUOUS")
160+
"""
161+
if len(points) > 265:
162+
raise ValueError("Maximum 265 points supported")
163+
if len(points) == 0:
164+
raise ValueError("At least 1 point required")
165+
166+
# Validate points
167+
for i, pt in enumerate(points):
168+
if not all(k in pt for k in ('x', 'y', 'dwell_us')):
169+
raise ValueError(f"Point {i} missing required keys (x, y, dwell_us)")
170+
if pt['x'] < 0 or pt['x'] > 4095:
171+
raise ValueError(f"Point {i} x={pt['x']} out of range (0-4095)")
172+
if pt['y'] < 0 or pt['y'] > 4095:
173+
raise ValueError(f"Point {i} y={pt['y']} out of range (0-4095)")
174+
if 'laser_intensity' in pt:
175+
if pt['laser_intensity'] < 0 or pt['laser_intensity'] > 255:
176+
raise ValueError(f"Point {i} laser_intensity={pt['laser_intensity']} out of range (0-255)")
177+
178+
path = '/galvo_act'
179+
payload = {
180+
"task": path,
181+
#"laser_trigger": laser_trigger,
182+
"points": points
183+
}
184+
185+
return self._parent.post_json(path, payload, timeout=timeout)
186+
187+
def stop_arbitrary_points(self, timeout=1):
188+
"""
189+
Stop arbitrary point scanning
190+
191+
Args:
192+
timeout: Request timeout in seconds (default: 1)
193+
194+
Example:
195+
>>> galvo.stop_arbitrary_points()
196+
"""
197+
path = '/galvo_act'
198+
payload = {"task":"/galvo_act", "points":[{"x":0,"y":0,"dwell_us":1000}]} # Single point at origin with long dwell to effectively stop scanning
199+
200+
return self._parent.post_json(path, payload, timeout=timeout)
201+
202+
def pause_arbitrary_points(self, timeout=1):
203+
"""
204+
Pause arbitrary point scanning (keeps current index)
205+
206+
Args:
207+
timeout: Request timeout in seconds (default: 1)
208+
209+
Example:
210+
>>> galvo.pause_arbitrary_points()
211+
"""
212+
path = '/galvo_act'
213+
payload = {
214+
"task": path,
215+
"pause_points": True
216+
}
217+
218+
return self._parent.post_json(path, payload, timeout=timeout)
219+
220+
def resume_arbitrary_points(self, timeout=1):
221+
"""
222+
Resume arbitrary point scanning from paused position
223+
224+
Args:
225+
timeout: Request timeout in seconds (default: 1)
226+
227+
Example:
228+
>>> galvo.resume_arbitrary_points()
229+
"""
230+
path = '/galvo_act'
231+
payload = {
232+
"task": path,
233+
"resume_points": True
234+
}
235+
236+
return self._parent.post_json(path, payload, timeout=timeout)
237+
238+
def set_trigger_mode(self, mode="AUTO", timeout=1):
239+
"""
240+
Set laser trigger mode override
241+
242+
Args:
243+
mode: Trigger mode - "AUTO", "HIGH", "LOW", "CONTINUOUS"
244+
timeout: Request timeout in seconds (default: 1)
245+
246+
Example:
247+
>>> galvo.set_trigger_mode("HIGH") # Force laser on
248+
>>> galvo.set_trigger_mode("LOW") # Force laser off
249+
>>> galvo.set_trigger_mode("AUTO") # Restore automatic control
250+
"""
251+
path = '/galvo_act'
252+
payload = {
253+
"task": path,
254+
"laser_trigger": mode
255+
}
256+
257+
return self._parent.post_json(path, payload, timeout=timeout)
258+
259+
def generate_circle_points(self, center_x=2048, center_y=2048, radius=1000,
260+
num_points=32, dwell_us=100):
261+
"""
262+
Generate points along a circle for arbitrary point scanning
263+
264+
Args:
265+
center_x: Circle center X (default: 2048)
266+
center_y: Circle center Y (default: 2048)
267+
radius: Circle radius in DAC units (default: 1000)
268+
num_points: Number of points around circle (default: 32)
269+
dwell_us: Dwell time per point in microseconds (default: 100)
270+
271+
Returns:
272+
list: List of point dicts suitable for set_arbitrary_points()
273+
274+
Example:
275+
>>> points = galvo.generate_circle_points(radius=500, num_points=64)
276+
>>> galvo.set_arbitrary_points(points)
277+
"""
278+
points = []
279+
for i in range(num_points):
280+
angle = 2.0 * np.pi * i / num_points
281+
x = int(center_x + radius * np.cos(angle))
282+
y = int(center_y + radius * np.sin(angle))
283+
x = max(0, min(4095, x))
284+
y = max(0, min(4095, y))
285+
points.append({"x": x, "y": y, "dwell_us": dwell_us})
286+
return points
287+
288+
def generate_grid_points(self, x_min=500, x_max=3500, y_min=500, y_max=3500,
289+
nx=4, ny=4, dwell_us=500):
290+
"""
291+
Generate a grid of points for arbitrary point scanning
292+
293+
Args:
294+
x_min, x_max: X range (default: 500-3500)
295+
y_min, y_max: Y range (default: 500-3500)
296+
nx, ny: Number of grid points per axis (default: 4x4)
297+
dwell_us: Dwell time per point in microseconds (default: 500)
298+
299+
Returns:
300+
list: List of point dicts suitable for set_arbitrary_points()
301+
302+
Example:
303+
>>> points = galvo.generate_grid_points(nx=8, ny=8, dwell_us=1000)
304+
>>> galvo.set_arbitrary_points(points)
305+
"""
306+
points = []
307+
for iy in range(ny):
308+
y = int(y_min + (y_max - y_min) * iy / max(1, ny - 1)) if ny > 1 else (y_min + y_max) // 2
309+
for ix in range(nx):
310+
x = int(x_min + (x_max - x_min) * ix / max(1, nx - 1)) if nx > 1 else (x_min + x_max) // 2
311+
points.append({"x": x, "y": y, "dwell_us": dwell_us})
312+
return points

0 commit comments

Comments
 (0)